PWN学习笔记vol.2 —— Off by one、Unlink和巅峰极客 2022的smallcontainer
如题,这次写一下unsortedbin
的利用,unsortedbin
的检查比tcachebin
和fastbin
复杂很多,所以一直是(我的)一个很头痛的问题
题目:https://www.nssctf.cn/problem/2513
调试的libc
:https://ftp.gnu.org/gnu/libc/glibc-2.27.tar.gz
漏洞点·
这题的漏洞在deit
函数的check
中

在check
时如果遇到0x11
就会改成0x00
,妥妥的后门了
如果熟悉堆结构的话,会知道0x11
是一个常出现在堆块的size
中的数
看一下题目允许的堆块大小

也就是如果分配0x111
、0x211
或0x311
的堆块的话通过填满物理地址的上一个堆块,就可以把这个堆块的大小改成0x100
、0x200
或0x300
这里如果是把堆块大小改大的话,那么很简单地可以想到可以把物理地址的下一个堆块放到tcachebin
中,然后改fd
指针实现任意写
但这里只能改一字节,而且只能把字节改成0x00
,也就是只能把下一个堆块的大小改小
PS:这种只溢出一字节的,在PWN中叫做Off by one,Off by one
不一定只能溢出0x00
,只是这题做了限制而已
回想一下,在笔记vol.1构造fake_chunk
,如果乱构造的话会触发malloc.c:4280
的条件(源码是这个)

当时并没说这个条件具体是做什么的
其实prev_inuse
拿的就是next_chunk
的size
的最低为,假设next_chunk
的size
是0x211
的话,那么prev_inuse(next_chunk)
就是1
,如果触发Off by one
把大小改成0x200
的话,prev_inuse(next_chunk)
就是0
也就是除了把物理地址的下一个堆块改小外,这个漏洞还可以把当前堆块标记为未被使用的状态,也就是被free
掉的状态
这里直接说结论,如果一个堆块被标记为NOT INUSE
的话,那么在free
掉这个堆块的物理相邻的堆块,而且被free
的堆块不满足tcachebin
或fastbin
的条件时,就会尝试把这个NOT INUSE
的堆块从它所在的bin
中进行unlink
操作,然后将被free
的堆块和这个NOT INUSE
的堆块进行合并(注意是物理地址的合并,不是bin
的合并)
这里听着挺拗口的,可以在后面在慢慢体会这个过程
攻击思路·
所以现在就有了一个可以修改nextchunk
的PREV_INUSE
位把堆块改成NOT INUSE
的功能
然后我的想法是,可以在堆上叠出这样的一个东西
1 | chunk1 <= PREV_INUSE=1 |
然后写满chunk1
进行Off by one
,就会把chunk1
改成NOT INUSE
1 | chunk1 <= PREV_INUSE=1 |
这时如果delete
掉chunk2
的话,就会触发堆块合并,把chunk1
和chunk2
合并,然后再add
一个chunk1
加chunk2
大小的堆块,就可以得到一个新的堆块chunk3
,其中chunk3
的物理位置是原来chunk1
加chunk2
的位置
1 | chunk3 <= PREV_INUSE=1 => chunk1 |
而且很重要的一点是,chunk1
和chunk3
指向同一个物理位置,注意这个过程中,chunk1
都没有被delete
过,所以这里delete
掉chunk3
后,就可以用chunk1
的指针进行uaf
左后用笔记vol.0的打法,写free_hook
即可
下面细说具体的操作
Part.1 泄露heap_base·
这题依然可以利用tcachebin
泄露heap_base
首先add
两个大小一样的堆块,然后依次delete
掉,那么在对应的tcachebin
上就会形成
1 | chunk2 -> chunk1 -> NULL |
这时chunk2
的fd
指针就会指向chunk1
然后再add
一个一样大小的堆块,根据tcache
的机制就可以拿到chunk2
,而且这时chunk2
的fd
指针不会被清零,所以进行依次show
就可以拿到chunk1
的地址,也就泄露出heap_base
Part.2 泄露libc_base·
失败的方法·
泄露libc_base
的话我有想过用类似的方法,就是先把一个堆块delete
到unsortedbin
,这样就可以把main_arena
的地址写到这个堆块的fd
指针上,然后通过add
把这个堆块拿回来,再用show
去读fd
指针
但实际操作下来好像不太行,首先因为题目对add
的大小有限制,所以要到unsortedbin
的话就要先把tcachebin
填满,而且add
的时候也要先把tcachebin
的堆块全拿出来才能拿到那个在unsortedbin
的堆块
然后在malloc
的时候还有一段这个东西

就是如果malloc
时是从unsortedbin
中拿堆块的话,就会有一个把unsortedbin
的堆块放到合适的bin
中的过程
其中有一个步骤是,先把这个堆块从unsortedbin
上unlink
出来(malloc.c:3778
),然后再看这个拿出来的堆块victim
是不是满足tcachebin
的条件(malloc.c:3791
),如果是的话就执行tcache_put
把victim
放到tcachebin
中
注意这时如果我要拿到unsortedbin
的话,就要先清空tcachebin
,所以tcache->counts[tc_idx] = 0
,也就会满足malloc.c:3792
的条件
在tcache_put
中会有一句e->next = tcache->entries[tc_idx]

这里的next
指针相当于在unsortedbin
机制中的fd
指针,也就是这一步会把在unsortedbin
中拿出来的堆块的fd
指针改为0
而题目中的show
限制了只能拿到堆块的fd
指针

所以就搞不定了
通过堆块合并泄露·
在攻击思路中有提到,通过Off by one
可以触发堆块的合并,然后可以构造出两个指针指向同一个堆块,这样的话,如果用其中一个指针把堆块delete
到unsortedbin
中,就可以用另一个指针泄露出main_arena
地址了
下面实际操作一波
如果要利用Off by one
的话,就需要被Off by one
的堆块的大小的低字节为0x11
,这里首先排除0x111
,因为这样在Off by one
后需要把0x100
的堆块放到unsortedbin
中,这就需要0x100
的tcachebin
被填满,而题目add
限制了不能分配0x100
的堆块,所以就不太好操作
PS:感觉利用Off by one
修改堆块大小后再delete
也可以,不过这样就太麻烦了
然后因为合并后的堆块要小于0x400
才能被add
,所以分配的堆块越小越好,于是这里就选择使用0x211
的堆块
再然后,被合并的堆块的大小要小于0x400 - 0x210 = 0x1f0
,这里我就随便选一个0x111
好了,也就是add(0x108)
注意在add
的时候,十六进制的最低位要是8
,这样才能在写满堆块后,堆块中的内容和下一个堆块的size
相连
根据攻击思路的话,我现在需要先在堆上叠一个这样的东西
1 | chunk0 |
注意chunk2
下面不能是top_chunk
,不然一会delete
的时候会把chunk2
和top_chunk
合并,而不是放到unsortedbin
中
然后把chunk1
写满触发Off by one
,把chunk2
的size
改为0x200
1 | chunk0 |
最后对chunk2
进行delete
,因为我想把合并后的堆块放到unsortedbin
中,所以在delete
前还要先把大小为0x200
的unsortedbin
填满
绕过free的检查和报错·
理论上这就会触发chunk1
和chunk2
的合并,但事情并没有这么简单
主要就是free
的时候如果不是tcachebin
或fastbin
的堆块的话,就会对物理地址的上下两个堆块的INUSE
状态进行检查,而且调用unlink
的时候如果没构造好的话也会报错,下面细说
绕过double free·
因为这时的chunk2
并不是一个正常的堆块,所以在delete
时需要绕过free
中的一堆检查,不然程序会崩溃
首先第一个是老朋友malloc.c:4280

就是chunk2
的物理地址的下一个堆块的PREV_INUSE
位要是1
,不然就会被认为是对一个NOT INUSE
的堆块进行free
,触发double free
错误
nextchunk
就是上面我叠出来的old_chunk2
,只需要把这个地方改成一个假的堆块头(fake_chunk
)就可以绕过检查
这里我想把堆叠的正常一点,所以我会选择在fake_chunk
下面加个正常的堆块chunk3
,然后让fake_chunk
的下一个堆块和chunk3
的下一个堆块指向相同的堆块
大概就是
1 | chunk0 |
绕过prev_size·
然后下一个要绕的地方是malloc.c:4292
的prevsize

这个地方大概说的是,如果当前堆块PREV_INUSE
位为0
的话(我现在delete
的chunk2
就是),就会把物理地址的上一个堆块从bin
中给unlink
出来,然后再和当前堆块进行合并
而要做这一步,首先需要找到物理地址的上一个堆块,这里的做法是通过当前堆块的地址减去当前堆块的prev_size
chunk2
的prev_size
就是chunk1
的最后8
个字节,如果我的chunk1
是乱搞的话(比如写满字符0
),就是

这样在根据prev_size
去找上一个堆块是就会找到一个不存在的地址,触发SIGSEGV
所以在写chunk1
的时候,要在最后8
字节写上chunk1
的大小,这样chunk_at_offset(p, -((long) prevsize))
拿到的就是chunk1
,是一个正常的堆块

注意这里prev_size
写0x110
绕过unlink·
如无意外到malloc.c:4295
的unlink
也会报SIGSEGV
这里的unlink
是一个宏定义,所以调试追不进去
不妨先看看定义

这里最起码P->fd
和P->bk
是一个可以访问的地址,我两个都是乱写的0x3131313131313131
那就肯定报错了
问题是,这两个地址应该写什么呢
先看看unlink
做了什么,除去一堆检查的话,主要就是一个简单的把P
从双链表上拿下来的过程
1 | FD = P->fd |
PS:CTF Wiki上也有unlink
的内容,可以先看看,虽然个人感觉也讲得不太清晰就是了。。。
这里我并不是想要利用unlink
实现某些攻击,只是想绕过不让它报错而已
现在我能控制的是P->fd
和P->bk
,所以可以想到的一个简单的方法是,设
1 | P->fd = P |
那么就是
1 | FD = P->fd => FD = P |
然后
1 | FD->bk = BK => FD->bk = P |
最终的结果依然还是
1 | P->fd = P |
也就是在什么都没改变的情况下就顺利跑完了unlink
这时的prev_chunk
长这样子

绕过nextchunk的unlink·
除了对上一个堆块进行合并的unlink
外,在malloc.c:4304
还有一处对下一个堆块合并的unlink

这里的nextchunk
即上面叠出来的fake_chunk
,首先根据题目的条件,这里只有0x10
的空间可以控制,不能写到这个fake_chunk
的fd
和bk
,也就不能用chunk2
的方法进行绕过
然后fake_chunk
也不可能为top_chunk
,所以malloc.c:4298
的if
是肯定要进的
最后就只剩绕过malloc.c:4303
的条件了,就是检查netchunk
是否INUSE
检查的方法是用nextchunk
的地址加上nextsize
找到nextnextchunk
(源码中没有这个变量,名字我瞎编的),然后检查nextnextchunk
的PREV_INUSE
位
这里如果是按我现在的方式叠的话,nextnextchunk
就是top_chunk
,而top_chunk
的PREV_INUSE
位固定为1
,所以就不会进这个if
条件
PS:如果不想像我这样叠的话,也可以在chunk3
中写下另一个假的堆头,然后把nextnextchunk
指向这个堆头
泄露libc_base·
如无意外上面绕完后就可以成功delete
掉chunk2
,并且触发chunk1
和chunk2
的合并(令合并后的堆块为chunk4
好了),这时堆上大概长
1 | chunk0 |

注意这时chunk4
已经被delete
到unsortedbin
上,所以chunk4
的fd
和bk
都指向main_arena
虽然我不能直接拿到chunk4
的指针,但是chunk4
原来也是chunk1
的位置,而且chunk1
没被delete
过,所以对chunk1
进行show
就可以拿到chunk4
的fd
,泄露出libc_base
Part.3 写free_hook·
触发堆块合并后,后面的事情就简单很多了
注意0x311
其实是一个tcachebin
的大小,只是因为合并前0x200
的tcachebin
被填满了,所以chunk4
才会到unsortedbin
中
这时需要先把chunk4
挪到tcachebin
中,只需要进行一次add(0x308)
然后再delete
掉就好,这时tcachebin
上就是
1 | chunk4 -> NULL |
然后利用chunk1
的指针,可以把chunk4
的fd
改成free_hook
,这样tcachebin
就是
1 | chunk4 -> free_hook |
进行两次add(0x308)
后就可以把堆块分配到free_hook
最后在free_hook
上写上one_gadget
或者system
然后delete
就搞定了
参考Exp·
1 | from pwn import * |