2

二进制漏洞

 2 years ago
source link: https://bbs.pediy.com/thread-271544.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
The House of Mind
2022-2-18 22:34 8194

"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 */
}
  • 按照程序逻辑,归纳程序的执行流程如下:
  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是个变局变量,位于数据段,溢出数据到达不了)。
    #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]:

    } 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
    /*
    * 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只能缓存攻击,根本避免被攻击,在业务层的源头就要开始防范。

【公告】看雪团队招聘安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK