pwn

how2heap

some questions

Posted by pic4xiu on September 29, 2019

好多大佬们都对how2heap这个项目进行了汇总,我就不班门弄斧了,但是同时大佬对一些问题一笔带过,这里就记一下本人在学 how2heap 中的一些有疑问的点,应该具有一定的代表性.大佬可以帮忙挑错,希望和大家一起进步

first_fit 疑问和拓展

我一开始就有疑问,为什么明明是 smallbins 和 largebins 范围内的 chunk ,它直接去 unsortedbins 呢,事实上只要不是 fastbins 范围内的,经过 free 后都会先进入 unsorted bin 待命.系统在进行 malloc 分配的时候 unsortedbin 算是起到一个缓冲区的作用,测试程序如下

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
    char* a = malloc(512);
    char* b = malloc(256);
    free(a);
    char* c = malloc(500);
}

断在 malloc(c)处(:以后 char* c = malloc(500) 这种直接简写成 malloc(c) )

    4 int main()
    5 {
    6     char* a = malloc(512);
    7     char* b = malloc(256);
    8     free(a);
 ►  9     char* c = malloc(500);
   10 //  char* d = malloc(512); 
   11 }
──────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────
...
────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────
...
──────────────────────────────────────────────────────────────────────────────────────────────────────────────
Breakpoint /home/pic/桌面/te.c:9
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x602000 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602000
smallbins
empty
largebins
empty

之后 malloc(c) 会把 unsortedbin 这个取出(只有这个bin不加s,不是复数,也可以说明只有一个链表,2333),而倘若把程序进行如下的修改

    5 {
    6     char* a = malloc(512);
    7     char* b = malloc(256);
    8     free(a);
    9     char* c = malloc(10);
 ► 10     char* d = malloc(512); 
   11 }
──────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────
...
────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────
...
──────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/32gx 0x602000
0x602000:	0x0000000000000000	0x0000000000000021
0x602010:	0x00007ffff7dd1d78	0x00007ffff7dd1d78
0x602020:	0x0000000000000000	0x00000000000001f1
0x602030:	0x00007ffff7dd1b78	0x00007ffff7dd1b78
0x602040:	0x0000000000000000	0x0000000000000000
0x602050:	0x0000000000000000	0x0000000000000000
0x602060:	0x0000000000000000	0x0000000000000000
0x602070:	0x0000000000000000	0x0000000000000000
0x602080:	0x0000000000000000	0x0000000000000000
0x602090:	0x0000000000000000	0x0000000000000000
0x6020a0:	0x0000000000000000	0x0000000000000000
0x6020b0:	0x0000000000000000	0x0000000000000000
0x6020c0:	0x0000000000000000	0x0000000000000000
0x6020d0:	0x0000000000000000	0x0000000000000000
0x6020e0:	0x0000000000000000	0x0000000000000000
0x6020f0:	0x0000000000000000	0x0000000000000000

可以看到程序仍然是从 unsortedbin 取出的,不过进行了切割, c 只拿走了0x20(1是标志位)个字节,剩下了0x1f0字节(剩下的我们称之为 remainder chunk ,仍留在 unsortedbin 中),而 remainder chunk 完全不够 d 的大小,所以猜想 d 会切割 top chunk ,之后我们再 n 单步运行发现果然是这样,但是同时

pwndbg> heap
0x602000 FASTBIN {			<------------------c
  prev_size = 0, 
  size = 33, 
  fd = 0x7ffff7dd1d78 <main_arena+600>, 
  bk = 0x7ffff7dd1d78 <main_arena+600>, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x1f1
}
0x602020 PREV_INUSE {			<------------------之前的 remainder chunk
  prev_size = 0, 
  size = 497, 
  fd = 0x7ffff7dd1d58 <main_arena+568>, 
  bk = 0x7ffff7dd1d58 <main_arena+568>, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x602210 {				<------------------b
  prev_size = 496, 
  size = 272, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x602320 PREV_INUSE {			<-------------------d
  prev_size = 0, 
  size = 529, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x602530 PREV_INUSE {			<-------------------top chunk
  prev_size = 0, 
  size = 133841, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
pwndbg> p d
$1 = 0x602330 ""
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
0x1f0: 0x602020 —▸ 0x7ffff7dd1d58 (main_arena+568) ◂— 0x602020 /* '  `' */
largebins
empty

发现我们之前剩下的 chunk 这才被归入了 smallbins .我们可以形象的理解为 unsortedbin 非常强势,试图掌握一切,但是它在能力不足时才会把别人应得的归还,也即我们常说的甩锅,2333

consolidate研究

我在这里直接演示一个比较极端的例子,因为 how2heap 中的程序 fastbin_dup_consolidate 看上去就像是直接整合到了 smallbins 一样

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main() {
  void* p1 = malloc(0x70);
  void* p3 = malloc(0x70);
  void* p4 = malloc(0x70);
  void* p5 = malloc(0x70);
  void* p6 = malloc(0x70);
  void* p7 = malloc(0x70);
  void* p8 = malloc(0x70);
  void* p9 = malloc(0x70);
  void* p10 = malloc(0x70);
  void* p2 = malloc(0x70);
  free(p1);
  free(p3);
  free(p4);
  free(p5);
  free(p6);
  free(p7);
  free(p8);
  free(p9);
  free(p10);
  void* p12 = malloc(0x400);
}

直接在 malloc(p12) 中下断,跑起来

pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x602400 —▸ 0x602380 —▸ 0x602300 —▸ 0x602280 —▸ 0x602200 ◂— ...
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

之前 free 的都跑到了 fastbins 再次 n 单步,发现

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

经过 consolidate 后,我们的 fastbins 全部被摘下来了,同时进入了 unsortedbin ,而且也够 p12 要求的大小.所以整合过的 chunk 就直接分配给 p12 了.

pwndbg> p p12
$2 = (void *) 0x602010

这时候我们再回头看看 fastbin_dup_consolidate ,把之前我们介绍的强势的 unsortedbin 概念拿过来,我们发现,事实上该程序在 void* p3 = malloc(0x400); 时, unsortedbin 的大小并不满足 p3 所要求的大小,所以会进行”甩锅”,把 unsortedbin 中的 chunk 丢到 smallbins 中

大佬们已经总结的很好了,解决这两个问题就完事

  • unlink 过程
  • 最后的赋值修改

unlink过程

FD = P->fd;
BK = P->bk;
FD->bk = BK
BK->fd = FD

我们看一下, FD 和 BK 都是相对 P(fake chunk) 而言的,所以 FD 就是 0x602058 ,同时 BK 是 0x602060 ,所以 FD->bk = 0x602060 , BK->fd = 0x602058 ,由于FD->bkBK->fd这两块都是指向一处地址,后边的有效,改成了

pwndbg> x/32gx 0x602058
0x602058:	0x0000000000000000	0x00007ffff7dd2540
0x602068 <completed.7594>:	0x0000000000000000	0x0000000000602058

要注意 chunk0_ptr 此时地址为 0x602070 ,我们可以修改指针的指向以达到任意写的目的,可以看看如下的小 demo

最后的赋值修改,类似如此

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

uint64_t  *chunk0_ptr;
int main()
{
	char b[8]="aaaa";
	chunk0_ptr=&b;
	chunk0_ptr[0] = 0x4242424242424242LL;
	printf("%s\n",b);
}

程序结果

{13:40}~/桌面 ➭ gcc te.c
te.c: In function ‘main’:
te.c:10:12: warning: assignment from incompatible pointer type [-Wincompatible-pointer-types]
  chunk0_ptr=&b;
            ^
{13:40}~/桌面 ➭ ./a.out 
BBBBBBBB

接上篇.上一篇见这里 (虽然两篇联系不大).这篇出现的小 demo ,都可以直接调试,供大家参考

困惑我的 small bin 的源码

做 house_of_lore 时,我在看源码时我遇到了一个很费解的问题(注释来自 ctf-wiki)

    /*
       If a small request, check regular bin.  Since these "smallbins"
       hold one size each, no searching within bins is necessary.
       (For a large request, we need to wait until unsorted chunks are
       processed to find best fit. But for small ones, fits are exact
       anyway, so we can check now, which is faster.)
     */

    if (in_smallbin_range(nb)) {
        // 获取 small bin 的索引
        idx = smallbin_index(nb);
        // 获取对应 small bin 中的 chunk 指针
        bin = bin_at(av, idx);
        // 先执行 victim= last(bin),获取 small bin 的最后一个 chunk
        // 如果 victim = bin ,那说明该 bin 为空。
        // 如果不相等,那么会有两种情况
        if ((victim = last(bin)) != bin) {
            // 第一种情况,small bin 还没有初始化。
            if (victim == 0) /* initialization check */
                // 执行初始化,将 fast bins 中的 chunk 进行合并
                malloc_consolidate(av);
            // 第二种情况,small bin 中存在空闲的 chunk
            else {
                // 获取 small bin 中倒数第二个 chunk 。
                bck = victim->bk;
                // 检查 bck->fd 是不是 victim,防止伪造
                if (__glibc_unlikely(bck->fd != victim)) {
                    errstr = "malloc(): smallbin double linked list corrupted";
                    goto errout;
                }
                // 设置 victim 对应的 inuse 位
                set_inuse_bit_at_offset(victim, nb);
                // 修改 small bin 链表,将 small bin 的最后一个 chunk 取出来
                bin->bk = bck;
                bck->fd = bin;
                // 如果不是 main_arena,设置对应的标志
                if (av != &main_arena) set_non_main_arena(victim);
                // 细致的检查
                check_malloced_chunk(av, victim, nb);
                // 将申请到的 chunk 转化为对应的 mem 状态
                void *p = chunk2mem(victim);
                // 如果设置了 perturb_type , 则将获取到的chunk初始化为 perturb_type ^ 0xff
                alloc_perturb(p, bytes);
                return p;
            }
        }
    }

其中的bck = victim->bk;把我整蒙了.ei??既然找到最后一个( victim ),那倒数第二个不就应该是 victim->fd 吗??怎么是找 victim->bk 呢.事实上,这就要考虑到 smallbins 的 FIFO (先进先出)原则,在本程序中

pwndbg> x/32gx victim-2
0x602000:	0x0000000000000000	0x0000000000000071
0x602010:	0x00007ffff7dd1bd8	0x00007fffffffddf0
...
pwndbg> p stack_buffer_1
$1 = {0x0, 0x0, 0x602000, 0x7fffffffddd0}
pwndbg> p stack_buffer_2
$2 = {0x0, 0x0, 0x7fffffffddf0}
pwndbg> p &stack_buffer_1
$3 = (intptr_t *(*)[4]) 0x7fffffffddf0
pwndbg> p &stack_buffer_2
$4 = (intptr_t *(*)[3]) 0x7fffffffddd0

通过构造情况我们画一下图(表格)

victim_hdr pre_size size
victim_ptr fd &stack_buffer_1_hdr
stack_buffer_1_hdr pre_size size
stack_buffer_1_ptr &victim_hdr &stack_buffer_2_hdr
stack_buffer_2_hdr pre_size size
stack_buffer_2_ptr &stack_buffer_1_hdr bk

所谓的最后一个其实是 victim_hdr (我们伪造的链是从后向前伪造的),关键满足的 bypass 条件便是 chunk->bk->fd==chunk 即可,程序第一次 malloc(p3) 的时候是最后一个即 0x602010 要验证的便是 victim->bk->fd==victim ,把程序具体变量放进去就是 stack_buffer_1->fd==victim .同理,之后的 malloc(p4) 便是 stack_buffer_2->fd==stack_buffer_1 ,确实成立后自然就 malloc 出来了

关于 poison_null_byte 的思考

做这个的时候我感觉这个 bypass 条件有点过于简单,既然条件为 prev_size(nextchunk(P))==chunksize(P) ,只要能够利用溢出修改 chunksize(P) ,我们的 prev_size(nextchunk(P)) 也是由我们控制的,那我在下方任意一处伪造一个 fake_nextchunk 不就行了吗??测试程序如下

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

int main(void){
	uint8_t* a = (uint8_t*) malloc(1);
	int real_a_size = malloc_usable_size(a);
	void * B = malloc(0x150);
	void * C = malloc(0x150);
	malloc(0x10);
	free(B);
	a[real_a_size] = 0x40;
	*(size_t*)(B+0x130) = 0x140;
	void * D = malloc(0x80);
	void * E = malloc(0x10);
	free(D);
	free(C);
	C = malloc(0x2b0);
	D = malloc(0x90);
}

可以看到,我在a[real_a_size] = 0x40;*(size_t*)(B+0x130) = 0x140;构造了和 poison_null_byte 一样的条件.断在 malloc(D) 之前,让程序跑起来

0x602160:	0x0000000000000140	0x0000000000000000
0x602170:	0x0000000000000000	0x0000000000000000
0x602180:	0x0000000000000160	0x0000000000000160
0x602190:	0x0000000000000000	0x0000000000000000
pwndbg> p C
$3 = (void *) 0x602190

之后在 n 单步运行,发现程序果真修改的是我们伪造的 0x602160 处的地址

0x602160:	0x00000000000000b0	0x0000000000000000
0x602170:	0x0000000000000000	0x0000000000000000
0x602180:	0x0000000000000160	0x0000000000000160

事实上,把 prev_size(nextchunk(P)) 改到 C 的下方也是可以的(修改一下上方程序调试一下).所以结论便是:修改的 0x602160 ( prev_size(nextchunk(P)) )是通过现在的 B 的 size 找到的,然而在 free(D) 和 free(C) 后 chunk 之间的合并竟然利用的是 C 的 pre_size 来找到的之前的 B (虽然听上去很矛盾,但是 pre_size 设计出来的目的确实是如此,参见此处).所以只要不修改 real_c_presize 无论如何都是能够完成 chunk 的合并的.

其实通过这个 bypass 还可以挖掘一些骚操作,比如通过把 chunksize(P) 改大(要求有溢出漏洞)我们甚至可以通过切割 unsortedbin 的方式获得一个新的 ptr_c ,用来完成一个变向的 UAF 等等

about unsortedbin

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

int main() {
  intptr_t stack_buffer[4] = {0};
  intptr_t stack_buffer_1[4] = {0};
  intptr_t* victim = malloc(0x100);
  intptr_t* p1 = malloc(0x100);
  free(victim);
  stack_buffer[1] = 0x110;
  stack_buffer[3] = (intptr_t)stack_buffer_1;
  stack_buffer_1[1] = 0x100 + 0x10;
  stack_buffer_1[3] = (intptr_t)stack_buffer_1;
  victim[-1] = 32;
  victim[1] = (intptr_t)stack_buffer;
  fprintf(stderr, "malloc(0x100): %p\n", malloc(0x100));
}

为了加深理解,我写了一个这样的小 demo ,可以看到,我故意把链加长了,有一个 check 我们需要注意一下,即

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");

大小必须让它和下一个要求的不同,且大于 2*SIZE_SZ ,同时还必须小于已分配内存的大小.好,我们让程序跑起来,断在最后一个 malloc(0x100) 处.尽管 stack_buffer 和 stack_buffer_1 差不多,但是程序在 malloc(0x100) 的时候还是会选择 stack_buffer

pwndbg> x/8gx stack_buffer
0x7fffffffddd0:	0x0000000000000000	0x0000000000000110		----stack_buffer
0x7fffffffdde0:	0x0000000000000000	0x00007fffffffddf0
0x7fffffffddf0:	0x0000000000000000	0x0000000000000110		----stack_buffer_1
0x7fffffffde00:	0x0000000000000000	0x00007fffffffddf0
pwndbg> n
malloc(0x100): 0x7fffffffdde0

然而当把 stack_buffer[1] = 0x110; 中的 0x110 改成别的,这时候再 malloc(0x100) 才会使用我们之后构造的 stack_buffer_1

11	  stack_buffer[1] = 0x100;		--------修改为 0x100
12	  stack_buffer[3] = (intptr_t)stack_buffer_1;
13	  stack_buffer_1[1] = 0x110;
14	  stack_buffer_1[3] = (intptr_t)stack_buffer_1;
pwndbg> x/8gx stack_buffer
0x7fffffffddd0:	0x0000000000000000	0x0000000000000100
0x7fffffffdde0:	0x0000000000000000	0x00007fffffffddf0
0x7fffffffddf0:	0x0000000000000000	0x0000000000000110
0x7fffffffde00:	0x0000000000000000	0x00007fffffffddf0
pwndbg> n
malloc(0x100): 0x7fffffffde00

同时经过遍历的 stack_buffer 和一开始真的 chunk 即 victim 也因为能力不足(上篇中形象的概念),跑到了 smallbins.当然能能不能用另说,2333

pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all [corrupted]
FD: 0x602000 —▸ 0x7ffff7dd1b88 (main_arena+104) ◂— 0x602000
BK: 0x7fffffffddf0 ◂— 0x7fffffffddf0
smallbins
0x20: 0x602000 —▸ 0x7ffff7dd1b88 (main_arena+104) ◂— 0x602000
0x100: 0x7fffffffddd0 —▸ 0x7ffff7dd1c68 (main_arena+328) ◂— 0x7fffffffddd0
largebins
empty

所以 unsortedbin 是从头开始遍历,途中遇到的能力不足的 unsortedbin 都会被安排到对应的 bins 中,而一旦有合适的就停止遍历并使用,为什么说是停止遍历呢??可以调试一下一开始的程序,看程序在最后一个 malloc(0x100) 之后的 bins

pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all [corrupted]
FD: 0x602000 —▸ 0x7ffff7dd1b88 (main_arena+104) ◂— 0x602000
BK: 0x7fffffffddf0 ◂— 0x7fffffffddf0
smallbins
0x20: 0x602000 —▸ 0x7ffff7dd1b88 (main_arena+104) ◂— 0x602000
largebins
empty

相当于只把一开始的 victim 给并入了 smallbins ,而 stack_buffer_1 还是待在 unsortedbin 中

总结

剩下的一些问题,在网上各位师傅的分析中已经很明了了.本篇的初衷就是扣一些易犯的错和问题,越是遇到难的诸如 house of orange 等问题大家就越深入分析,本篇也就不再谈了(而且感觉自己也不能表达的很清楚). how2heap 中的大部分都是欺骗系统的一系列 bypass 和构造,提升的方式就是做题和看源码了