The House of Mind
2022-3-21 17:59:0 Author: mp.weixin.qq.com(查看原文) 阅读量:21 收藏


本文为看雪论坛精华文章
看雪论坛作者ID:jmpcall

"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"的利用条件,作者提供了一个用于演示的漏洞程序(现实中这类漏洞当然会隐蔽的多,几乎不会存在这么饥渴难耐的想被宰割的程序)。
/* * 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 */}
按照程序逻辑,归纳程序的执行流程如下:

① ptr = malloc(1024);

heap = (int)ptr & 0xFFF00000; // 将ptr值按1M向下取整

② 循环执行ptr2 = malloc(1024),直到(ptr2 & 0xFFF00000) == (heap + 0x100000)

③ 再次执行一次malloc(1024)

④ fread(ptr, 1024 * 1024, 1, stdin);

使用fead()函数读取用户输入,是为了减小攻击难度,如果换成strcpy()函数,读到'\0'字符就不会再读了,解决这个问题,要使用的就是另外的技术了,作者为了让大家专注于"The House of Mind",就特地避开了更复杂的情况。

⑤ 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是个变局变量,位于数据段,溢出数据到达不了)。
#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。
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]:
  } elseclear_inuse_bit_at_offset(nextchunk, 0);   /*Place the chunk in unsorted chunk list. Chunks arenot placed into regular bins until after they havebeen 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
/** 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
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
else if (!chunk_is_mmapped(p))
由于chunk2->size = 0x40d,IS_MMAPPED标志位为0,表示不是直接从mmap内存区域分配,而是从main arena或者thread arena中分配的,所以这个判断成立。
判断4
if (__builtin_expect (p == av->top, 0))
由于攻击者欺骗了glibc,使其认为chunk2属于自己构造的fake_arena,而不再是main_arena,并且通过布局图可知,chunk2 = 0x8100298,fake_arena->top = DTORS_END-12,所以,这个判断不成立。
判断5
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
if (__builtin_expect (!prev_inuse(nextchunk), 0))
prev_inuse(nextchunk)用于判断chunk2是否已经是free chunk了,如果是,那就是double free,不过由于这是第一次释放chunk2,所以,这个判断不成立。
判断7
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
if (!prev_inuse(p))
chunk2->size = 0x40d,PREV_INUSE标志位为1,显然不会通过这个判断,将chunk2与其前一个chunk合并。
判断9
if (nextchunk != av->top)
和控制判断4的道理一样,由于av->top的值,是受攻击者控制的,所以相应也很容易控制这个判断,使其成立。
判断10
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只能缓存攻击,根本避免被攻击,在业务层的源头就要开始防范。

看雪ID:jmpcall

https://bbs.pediy.com/user-home-815036.htm

*本文由看雪论坛 jmpcall 原创,转载请注明来自看雪社区

# 往期推荐

1.CVE-2012-1889 暴雷漏洞分析与利用小记

2.人工智能竞赛-目标识别指导

3.Android加壳脱壳学习—动态加载和类加载机制详解

4.某APP sig3 48位算法逆向分析

5.CVE-2021-26411漏洞分析笔记

6.Windows本地提权漏洞CVE-2014-1767分析及EXP编写指导

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458433540&idx=1&sn=07c999b8ad246c3cdf32a3feaa015136&chksm=b18f888e86f80198582818d876f6d8ff4025f5d8ef37136991d6f1ae976a6bfb8081d76def91#rd
如有侵权请联系:admin#unsafe.sh