首页
社区
课程
招聘
The House of Mind
2022-2-18 22:34 21484

The House of Mind

2022-2-18 22:34
21484

"The House of Mind"的学习条件

  其实不光是"The House of Mind",在学习各种堆溢出漏洞的利用方法之前,都必须对glibc malloc()/free()的逻辑,有相当程度的了解,《Glibc内存管理--Ptmalloc2源代码分析》这份文档,通过129页的篇幅,已经分析的非常深刻和详细(如果没有积分下载文档,也可以去看作者的博客:https://www.iteye.com/blog/user/mqzhuang),也可以看看我发过的一个帖子:https://bbs.pediy.com/thread-271331.htm,先从外围了解glibc malloc()/free()的本质和设计目标,瞄一眼宏观的地形,再深入到茫茫的内部实现中,应该可以少迷点路。
  另外,本文是对phrack杂志中一篇神作的总结和补充,所以exploit code和更完整的分析过程,请阅读原文:http://phrack.org/issues/66/10.html

什么是"The House of Mind"?

  "The House of Mind"是一种堆溢出漏洞的利用方法(为什么叫这个名称我目前还不知道),可以通过构造输入数据,让漏洞程序执行攻击者期望的任意代码(不过,不是所有存在堆溢出漏洞的程序,都可以利用这种方法进行攻击,需要漏洞程序满足一定条件,稍后具体说明)。
  再具体一点就是,在应用程序分配到的内存周围,很多都是glibc内部使用的内存,程序存在漏洞,攻击者就有机会通过构造输入数据,溢出glibc内部使用的变量,进一步控制malloc()/free()的执行逻辑,最终借glibc之手,修改某个函数对应的got表项(可以理解为函数指针,感兴趣也可以看看我的另外一个帖子:https://bbs.pediy.com/thread-246373.htm),使其指向一段shell code(同样通过用户输入构造),这样,当漏洞程序后续执行该函数时(比如.dtors()函数,它会在main()函数结束后执行),就会触发shell code执行。

  • "The House of Mind"的目标,就是欺骗_int_free()按如下逻辑执行:

漏洞程序

  为了满足"The House of Mind"的利用条件,作者提供了一个用于演示的漏洞程序(现实中这类漏洞当然会隐蔽的多,几乎不会存在这么饥渴难耐的想被宰割的程序)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
 * K-sPecial's vulnerable program
 */
 
#include <stdio.h>
#include <stdlib.h>
 
int main (void) {
   char *ptr  = malloc(1024);        /* First allocated chunk */
   char *ptr2;                       /* Second chunk          */
   /* ptr & ~(HEAP_MAX_SIZE-1) = 0x08000000 */
   int heap = (int)ptr & 0xFFF00000;
   _Bool found = 0;
 
   printf("ptr found at %p\n", ptr);  /* Print address of first chunk */
 
   // i == 2 because this is my second chunk to allocate
   for (int i = 2; i < 1024; i++) {
     /* Allocate chunks up to 0x08100000 */
     if (!found && (((int)(ptr2 = malloc(1024)) & 0xFFF00000) == \
                                           (heap + 0x100000))) {
       printf("good heap allignment found on malloc() %i (%p)\n", i, ptr2);
          found = 1; /* Go out */
          break;
       }
 
   }
        malloc(1024); /* Request another chunk: (ptr2 != av->top) */
        /* Incorrect input: 1048576 bytes */
        fread (ptr, 1024 * 1024, 1, stdin);
 
        free(ptr);   /* Free first chunk  */
        free(ptr2);  /* The House of Mind */
        return(0);   /* Bye */
}
  • 按照程序逻辑,归纳程序的执行流程如下:
  1. ptr = malloc(1024);
    heap = (int)ptr & 0xFFF00000; // 将ptr值按1M向下取整
  2. 循环执行ptr2 = malloc(1024),直到(ptr2 & 0xFFF00000) == (heap + 0x100000)
  3. 再次执行一次malloc(1024)
  4. fread(ptr, 1024 * 1024, 1, stdin);
    使用fead()函数读取用户输入,是为了减小攻击难度,如果换成strcpy()函数,读到'\0'字符就不会再读了,解决这个问题,要使用的就是另外的技术了,作者为了让大家专注于"The House of Mind",就特地避开了更复杂的情况。
  5. free(ptr);
    free(ptr2);
  • 稍后就会明白为什么这样才能满足"The House of Mind"的利用条件,先看漏洞程序的内存布局(左):
    • 由于glibc在每个chunk头部,都额外安排了4字节的size字段记录其大小,并将整个chunk的大小按8字节对齐,所以每次malloc(1024)实际消耗的是1032字节((1024+4)按8对齐),因此在循环中,ptr2是按1032字节递增的,这样,假设实际运行时ptr=0x804a008(如果/proc/sys/kernel/randomize_va_space文件内容非0,ptr值会是随机的,即使随机化是关闭的,也跟漏洞程序代码段、数据段的长度有关,可以认为攻击者无法精确预测这个值,不过它也不会影响能否攻击成功,定个假设值只是为了后续描述方便),循环第723次时,ptr2=0x81002a0,跳出循环。
    • 蓝色区域对应第一次malloc(1024)占用的内存,灰色区域对应循环中前721次malloc(1024)占用的内存,白色区域对应循环第722次malloc(1024)占用的内存,橙色区域对应循环第723次malloc(1024)占用的内存;
    • 利用size字段,对任意chunk计算其结束位置,也就是next chunk的开始位置,是很容易的,如果只是为了划清各个chunk之间的界线,有size字段其实就够了,但是在释放过程中,如果当前释放chunk的prev chunk为free chunk,这时要是能知道prev chunk的大小,也是很有价值的,因为这样就可以很方便计算prev chunk的起始位置,进而跟当前释放chunk合并,相反,如果prev chunk为inuse chunk,即不需要与当前释放chunk合并,那也就不需要知道prev chunk的大小了,所以,glibc并不是始终为每个chunk安排一个prev_size字段,而是将free chunk的末尾4字节作为其next chunk的prev_size(一方面,既然是free chunk,那就表示业务层不会继续使用user data了,当然就可以被glibc内部使用;另一方面,prev_size相对于各个chunk头部的位置是确定的,向前偏移4字节就是);
    • 每个chunk的size都是0x409(最低3位清0为0x408,表示chunk总大小为1032字节,最低3位二进制值为001,表示A=0、M=0、P=1),是可以根据向malloc()传的大小推测的,在构造溢出数据时,尽量保持各个size的原值,另外,刚分配完时,所有chunk都是inuse状态,所以prev_size字段所占空间,都是用于user data,不管被溢出数据填充成什么内容,都不会影响glibc内部的执行逻辑。

攻击过程分析

  fread(ptr, 1024 * 1024, 1, stdin)这行代码,使攻击者有机会往0x804a008之后的1M内存,写入任意数据,这块内存中,有很多地方保存的是chunk->size,由glibc内部使用,通过构造溢出数据,控制这些地方的值,就可以达到欺骗glibc的效果,甚至还可以进一步欺骗glibc,将部分user data也当作自己内部使用的内存,为此,作者构造出了上述内存布局图(右)中的数据,当漏洞程序执行free(ptr2)时,glibc就会按照攻击者欺骗的流程执行。

  • (1) chunk2->size = 0x40d
    最低3位清0为0x408,表示chunk总大小为1032字节,最低3位二进制值为101,表示A=1、M=0、P=1,这是要欺骗glibc认为chunk2在thread arena中(chunk2实际在main arena中),然后进一步欺骗glibc,使其认为chunk2所属arena的管理结构,在输入数据可以溢出到的某个位置,这样就相当于控制了chunk2所属arena的管理结构的内容了(而main_arena是个变局变量,位于数据段,溢出数据到达不了)。
    1
    2
    3
    4
    #define heap_for_ptr(ptr) \
    ((heap_info *)((unsigned long)(ptr) & ~(HEAP_MAX_SIZE-1)))
    #define arena_for_chunk(ptr) \
    (chunk_non_main_arena(ptr) ? heap_for_ptr(ptr)->ar_ptr : &main_arena)
    通过这段代码可以看出,glibc是将chunk2的地址按1M(HEAP_MAX_SIZE)向下对齐(即0x8100000),将其当作chunk2所属heap的地址,然后再将heap->ar_ptr指向的内存,作为chunk2所属arena的管理信息(这也是漏洞程序中用for循环分配内存,直到ptr2达到一直高度的原因,否则向下对齐为0x8000000,就不在可以溢出的范围了)。
  • (2) fake_heap->ar_ptr = 0x804a014
    由于ar_ptr为heap_info结构的第一个成员,所以按照布局图中的构造地址,glibc会认为arena管理结构位于0x804a014。
  • (3) fake_arena->bins[2] = DTORS_END-12
    根据布局图可以看出,fake_arena开始的8个字节,都被构造为0x102,剩余部分全部构造为DTORS_END-12,根据malloc_state结构的定义可知,这样构造肯定可以使fake_arena->bins[2] = DTORS_END-12。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    struct malloc_state {
    /* Serialize access.  */
    mutex_t mutex;
    // Should we have padding to move the mutex to its own cache line?
    #if THREAD_STATS
    /* Statistics for locking.  Only used if THREAD_STATS is defined.  */
    long stat_lock_direct, stat_lock_loop, stat_lock_wait;
    #endif
    /* The maximum chunk size to be eligible for fastbin */
    INTERNAL_SIZE_T  max_fast;   /* low 2 bits used as flags */
    /* Fastbins */
    mfastbinptr      fastbins[NFASTBINS];
    /* Base of the topmost chunk -- not otherwise kept in a bin */
    mchunkptr        top;
    /* The remainder from the most recent split of a small request */
    mchunkptr        last_remainder;
    /* Normal bins packed as described above */
    mchunkptr        bins[NBINS * 2];
    /* Bitmap of bins */
    unsigned int     binmap[BINMAPSIZE];
    /* Linked list */
    struct malloc_state *next;
    /* Memory allocated from the system in this arena.  */
    INTERNAL_SIZE_T system_mem;
    INTERNAL_SIZE_T max_system_mem;
    };

    使fake_arena->bins[2] = DTORS_END-12,是为了欺骗glibc修改got[.dtros]:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
      } else
    clear_inuse_bit_at_offset(nextchunk, 0);
     
      /*
    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);  // 返回:&fake_arena->bins[0]
      fwd = bck->fd;  // fd位于malloc_chunk结构体8字节偏移处
                      // 所以fwd = bck->fd = fake_arena->bins[2] = DTORS_END-12
      p->bk = bck;
      p->fd = fwd;
      bck->fd = p;    // fake_arena->bins[2] = p
      fwd->bk = p;    // bk位于malloc_chunk结构体12字节偏移处
                      // 所以这里会将p,写到DTORS_END指向的内存单元,即:got[.dtors] = p
     
      set_head(p, size | PREV_INUSE);
      set_foot(p, size);
     
      check_free_chunk(av, p);
    }

    关于unsorted_chunks()函数返回&fake_arena->bins[0]的设计意图,可以进入这篇帖子:https://bbs.pediy.com/thread-271331.htm,看看其中的bin结构图。

  • (4) ((struct malloc_chunk*)(DTORS_END-12))->bk = p
    在(3)中已经一起解释了,是为了使got[.dtors] = p,代码中的p,对应的是布局图中的chunk2,所以等到.dtors()函数执行时,实际上是执行chunk2位置的"nop; nop; jmp 0x0c"(nop机器码为0x90,jmp 0x0c机器码为0xeb0c,所以共4字节)。
  • (5) jmp 0x0c
    向前跳转0x0c偏移,是因为接紧着的4个字节,用于存放0x40d,再往后的8字节,会被上述代码中的p->bk=bck和p->fd=fwd两条赋值语句覆盖,所以shell code一定要构造在chunk2->bk之后的位置。
  • (6) fake_arena = 0x804a014
    0x804a014也就是在(2)中,为fake_heap->ar_ptr构造的值,作者一开始是将fake_arena构造在0x804a010位置的,还特地在0x804a010位置构造了一个0,作为fake_arena->mutex值,但是他通过调试发现,漏洞程序在执行完free(ptr)后,会将0x804a014位置清0,这样,为fake_arena->max_fast构造的0x102(为了使判断2、判断5不成立),就被覆盖了,从而使后续的攻击逻辑执行失败。作者说被覆盖的原因,可能是由于_int_free()结束之后,执行mutex_unlock()导致的:

    我认为不是这个原因,通过布局图可以看出,构造数据并没有修改chunk->size,所以free(ptr)就是按照正常的glibc逻辑执行的,那么mutex_unlock()修改的一定是main_arena全局全量中的mutex,而不可能是这里,一些其它版本的glibc中,0x804a010、0x804a014位置分别会是fd_nextsize、bk_nextsize,如果释放的chunk不是large chunk,释放函数中就会将这两个值清0,但是碰巧的是,glibc-2.3.6的malloc_chunk结构,没fd_nextsize、bk_nextsize成员,所以如果感兴趣,可以仔细看看代码确定一下。
  • (7) fake_arena->max_fast = 0x102
    在(6)中已经说过了,free(ptr)结束后,会将0x804a014位置清0,所以正好可以作为fake_arena->mutex值,0x804a018位置的0x102,作为fake_arena->max_fast值。
  • (8) 最后调用的malloc()
    漏洞程序退出循环后,又调用了一次malloc(),这也是为了满足"The House of Mind"的利用条件(使判断10不成立)。

攻击可行性证明

  上述已经将攻击流程介绍完毕,但是实际能否成功,还要看到底能不能将glibc欺骗到"fwd->bk = p;"这一行代码上。

  • 判断1
    1
    2
    3
    4
    5
    6
    /*
    * p > -size, 即:p > 0-size,即:p+size > 0,在p、size都大于0的前提下,表示p+size溢出了,但是由于p和size都是无符号数,p+size >= 0恒成立,所以写成p > -size
    * 个人感觉更严谨应该是:p >= -size,因为p+size == 0,也表示溢出了(比如size=0x00000001,则p=-size=0xffffffff(取反+1)时,p+size==0也为溢出)
    */
    if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
        || __builtin_expect ((uintptr_t) p & MALLOC_ALIGN_MASK, 0))
    由于0x40d相比于0x409,只是设置了NON_MAIN_ARENA标志位,并没有将chunk2的大小修改为异常值,也没有修改ptr2的指向,所以这个判断不成立。
  • 判断2
    1
    2
    3
    4
    5
    6
    7
    8
    9
    if ((unsigned long)(size) <= (unsigned long)(av->max_fast)
    #if TRIM_FASTBINS
        /*
      If TRIM_FASTBINS set, don't place chunks
      bordering top into fastbins
        */
        && (chunk_at_offset(p, size) != av->top)
    #endif
        )
    根据构造数据可知,chunk2->size = 0x40d(最低3位为PREV_INUSE、IS_MMAPPED、NON_MAIN_ARENA标志位),av即为图中的fake_arena,而fake_arena->max_fast = 0x102(最低2位为FASTCHUNKS_BIT、NONCONTIGUOUS_BIT标志位),所以这个判断不成立。
  • 判断3
    1
    else if (!chunk_is_mmapped(p))
    由于chunk2->size = 0x40d,IS_MMAPPED标志位为0,表示不是直接从mmap内存区域分配,而是从main arena或者thread arena中分配的,所以这个判断成立。
  • 判断4
    1
    if (__builtin_expect (p == av->top, 0))
    由于攻击者欺骗了glibc,使其认为chunk2属于自己构造的fake_arena,而不再是main_arena,并且通过布局图可知,chunk2 = 0x8100298,fake_arena->top = DTORS_END-12,所以,这个判断不成立。
  • 判断5
    1
    2
    3
    if (__builtin_expect (contiguous (av)
              && (char *) nextchunk
              >= ((char *) av->top + chunksize(av->top)), 0))
    攻击者显然不希望这个判断成立,由于这2个判断条件之间是&&的关系,所以只要满足contiguous(av) == 0,即fake_arena->max_fast的NONCONTIGUOUS_BIT标志位为0,而根据布局图可知,fake_arena->max_fast = 0x102,可以保证这个判断不成立。
  • 判断6
    1
    if (__builtin_expect (!prev_inuse(nextchunk), 0))
    prev_inuse(nextchunk)用于判断chunk2是否已经是free chunk了,如果是,那就是double free,不过由于这是第一次释放chunk2,所以,这个判断不成立。
  • 判断7
    1
    2
    if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0)
    || __builtin_expect (nextsize >= av->system_mem, 0))
    作者使用的shell code很短,不会将chunk2->nextchunk->size覆盖为异常值,即使shell code很长,也可以通过jmp跳过nextchunk->size字段,另外av->system_mem,在攻击者可以构造的范围,所以,可以保证这个判断不成立。
  • 判断8
    1
    if (!prev_inuse(p))
    chunk2->size = 0x40d,PREV_INUSE标志位为1,显然不会通过这个判断,将chunk2与其前一个chunk合并。
  • 判断9
    1
    if (nextchunk != av->top)
    和控制判断4的道理一样,由于av->top的值,是受攻击者控制的,所以相应也很容易控制这个判断,使其成立。
  • 判断10
    1
    if (!nextinuse)
    chunk2->nextchunk,也就是漏洞程序中最后一次执行malloc()分配的chunk,它这时显然还没有释放,所以这个判断不成立,从而最终欺骗glibc执行到else分支中的代码(如果漏洞程序中没有最后一次malloc(),应该也能攻击成功,因为那样的话,chunk2后面就是top chunk,而top chunk一定是inuse状态的,这是通过它顶部的fencepost标记的)。

glibc改造

  随着各种攻击技术的出现,glibc其实一直都在改造,以上看到的这些判断,很多就是为了缓解攻击,但glibc的改造,是受限于两个因素的:

  • 不能影响正常逻辑
    不能添加一个判断后,正常的逻辑也不对了。
  • 不能影响性能
    比如业务层调用free(),glibc会将释放chunk放在相应的缓存链表中,而判断是否double free,只会拿当前释放chunk和链表头中的第一个chunk,进行地址对比,而不会遍历整个链表对比。

  所以,glibc只能缓存攻击,根本避免被攻击,在业务层的源头就要开始防范。


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞3
打赏
分享
打赏 + 100.00雪花
打赏次数 1 雪花 + 100.00
 
赞赏  Editor   +100.00 2022/03/21 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (0)
游客
登录 | 注册 方可回帖
返回