本文为看雪论坛优秀文章
看雪论坛作者ID:jmpcall
接着啃http://phrack.org/issues/66/10.html,前段时间在论坛发的The House of Mind(https://bbs.pediy.com/thread-271544.htm),已经对其中的4.1节--THE HOUSE OF MIND做了总结。本文接着原文的后续内容,对4.1.1节--FASTBIN METHOD、4.1.2节--av->top NIGHTMARE、4.2节--THE HOUSE OF PRIME,进行总结。
每种THE HOUSE OF XX,各自代表一种利用技术,适用于不同的漏洞形式,每种利用技术,可能又包含多种具体的利用方法,FASTBIN METHOD 和 av->top NIGHTMARE,正是THE HOUSE OF MIND的另外两个子方法,其中av->top NIGHTMARE是一种不成立的方法,作者将它写出来,可能是想表达:不要只想着学习已有的方法,自己也要尝试想想别的路子。漏洞代码已经在4.1节列出,为了方便阅读,这里再来一遍:/*
*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、chunk2->size = 0x0d,fake_heap->ar_ptr = DTORS_END-4这个漏洞程序分配内存的布局,已经在The House of Mind(https://bbs.pediy.com/thread-271544.htm)这篇帖子中做了说明(由于FASTBIN METHOD不需要使用ptr指向的空间。所以相比于之前的内存布局图,下图将蓝色区域合并到灰色区域一起,这样只是为了让图片简洁,布局并没变化):仍然欺骗NON_MAIN_ARENA=1,从而诱使glibc认为0x8100000处为chunk2所属heap的管理结构,与之前不同的是,0x0d表示chunk2的大小为8字节。(2) fake_heap->ar_ptr = DTORS_END-4FASTBIN METHOD的目标是欺骗glibc按上图中的流程执行,先假设可以按期望欺骗各个判断条件,最终欺骗glibc执行以下2行代码:fb = &(av->fastbins[fastbin_index(size)]);
*fb = p; // av->max_fast = p
av和p分别对应布局图中的fake_arena和chunk2,而max_fast相对fake_arena起始位置偏移4字节,这正是欺骗glibc认为fake_arena位于DTORS_END-4的原因,因为这样就可以将位于DTORS_END处的指针值,修改为chunk2的地址,而不再指向.dtors()函数。另外,这个方法欺骗glibc执行的代码,是直接修改fake_arena这块内存,而之前的方法是欺骗glibc修改fake_arena->bins[2]指向的内存,所以,从布局图上看,这个方法比之前的方法,要少绕个弯。由于可以修改.dtors函数指针指向chunk2,所以很容易就能想到,chunk2处应该构造shell code,只不过为了满足(1)、(2)步骤中的欺骗条件,chunk2->size必须构造为0x0d。为了使"chunk_at_offset(p, size)->size <= 2*SIZE_SZ"判断条件不成立,对ptr处的值也有一定要求,除此之外,glibc执行"p->fd = *fb"这条语句时,还会修改ptr处的值,所以shell code必须往后放,chunk2位置放一条jmp指令跳转过去即可。在步骤(2)中,只是假设可以按期望欺骗各个判断条件,但是按照思路1的构造数据,这个假设是不成立的,主要因为两个原因:.dtors节的内容,是有规范的,由编译器决定(0xFFFFFFFF(DTORS_LIST开始标记) - dtors1()函数地址 - .. - dtorsN()函数地址 - 0x00000000(DTORS_END))。比如漏洞程序没有设置.dtors()函数,.dtors节中就只有0xFFFFFFFF、0x00000000两个值,这样就导致fake_arena->mutex = 0xFFFFFFFF,进一步导致漏洞程序执行free()函数,调用内部的_int_free()之前,会被lock在外面,另外fake_arena->max_fast = 0,也无法欺骗判断2成立。system_mem偏移malloc_state结构1848字节,一般位于数据段和堆区之间的一块空白区域(程序加载时各个区段之间有对齐空间,不是紧挨着的),这块区域都被加载器填充为0,也就是说fake_arena->system_mem = 0,从而导致"chunksize(chunk_at_offset(p, size)) >= av->system_mem"判断成立。所以,即使能执行到_int_free()函数,也会在判断3处与期望相反。(不过,.got节一般在.dtors节之后,如果漏洞程序比较大,.got节相应也比较大的时候,fake_arena->system_mem就会落于.got节,就大概率不存在这个问题,因为.got节存放的经常是些地址值,当然也就比较大)2、chunk2->size = 0x0d,fake_heap->ar_ptr = &.got[f2]-4即fake_heap->ar_ptr = &.got[f1],仍然存在与思路1同样的问题,首先,.got[f1]存储的是f1()函数的地址,也会使fake_arena->mutex != 0,另外,.got节和DTORS_LIST位置离的很近,再加上小程序的.got节很小,所以不管f1选择什么函数,&.got[f1]离DTORS_LIST位置一定也不远,这样,两个思路中fake_arena->system_mem的位置其实差不多,一般都为0。3、chunk2->size = 0x0d,fake_heap->ar_ptr = EBP由于main()栈帧上没有EIP(main()函数没有上一层函数可以返回,main()函数中的return会被编译器替换成exit()),所以为了说明后续思路,作者调整了一下漏洞程序:int fvuln()
{
// 原漏洞程序main()函数中的代码
}
int main( int argc, char *argv[] )
{
return fvuln();
}
思路3的目的是,欺骗glibc认为chunk2所属的arena在EBP处(EBP为执行fvuln()函数时的EBP值,即fvuln()的栈帧起始位置,这个地方存的是main()的栈帧起始位置,往上4个字节存的是main()函数地址)。这样,利用"fake_arene->max_fast = chunk2",就可以将fvuln()的返回地址修改为chunk2,而不再是main()函数,从而在fvuln()函数返回时,执行shell code。但是,这样仍然存在fake_arena->mutex != 0的问题,因为main()的栈帧起始位置不可能为0。4、chunk2->size = 0x15,fake_heap->ar_ptr = EBP-40x15表示chunk2的大小为16字节,"*fb = p"就会等效于"fake_arena->fast_bins[0] = chunk2",也就是会修改相对fake_arena偏移8字节处的内容,那么fake_arene也应该位于EIP下方8字节处,即EBP-4,这样fake_arena->mutex的位置就可以与EBP错开了,它的值就可以等于EBP-4处的0。另外,将EBP-4作为fake_arena的起始位置,fake_arena->system_mem往往都是一个比较大的随机值(这个可以认为是经验,可以在gdb中用x/8x $ebp+1848验证),所以,将ptr处构造为0x09,是很容易让判断3不成立的,最终满足所有欺骗条件。但是我对EBP-4处为0这一点,比较疑惑,为此做了一个实验:结论就是EBP-4处为0的条件其实还挺苛刻的,可以理解为对漏洞程序、运行环境,又增加了要求,原文是这样说的:不管怎么说,调整chunk2的大小,至少为FASTBIN METHOD打开了一条新路,如果 EBP-4处仍然不为0,大不了继续调整chunk2的伪造大小,直到fake_arena->system_mem落在一个0值的位置。av->top NIGHTMARE的目标是欺骗glibc按如下流程执行:
但是,为了使判断9不成立,就得将chunk2->size构造为超级大的值(这样nextchunk才可以与被要求指向栈区的av->top相等),然后横跨几乎整个进程空间去构造nextchunk->size,以欺骗之前的其它判断,但是这样肯定会出现段错误,因为当中的大片空间,并没有跟系统分配,所以这个方法不成立。THE HOUSE OF PRIME适用于,可以利用bug控制malloc()返回值,并且漏洞程序中存在往该"分配内存"写用户输入的逻辑(比如先欺骗malloc()返回漏洞函数的EBP,后续通过用户输入,就可以改写漏洞函数的返回地址了),原文中的示范漏洞程序如下:#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void fvuln(char *str1, char *str2, int age)
{
int local_age;
char buffer[64];
char *ptr = malloc(1024);
char *ptr1 = malloc(1024);
char *ptr2 = malloc(1024);
char *ptr3;
local_age = age;
strncpy(buffer, str1, sizeof(buffer)-1);
printf("\nptr found at [ %p ]", ptr);
printf("\nptr1ovf found at [ %p ]", ptr1);
printf("\nptr2ovf found at [ %p ]\n", ptr2);
printf("Enter a description: ");
fread(ptr, 1024 * 5, 1, stdin);
free(ptr1);
printf("\nEND free(1)\n");
free(ptr2);
printf("\nEND free(2)\n");
ptr3 = malloc(1024);
printf("\nEND malloc()\n");
strncpy(ptr3, str2, 1024-1);
printf("Your name is %s and you are %d", buffer, local_age);
}
int main(int argc, char *argv[])
{
if(argc < 4) {
printf("Usage: ./hop name last-name age");
exit(0);
}
fvuln(argv[1], argv[2], atoi(argv[3]));
return 0;
}
根据The House of Mind的经验,这里就不再过于详细说明了,直接将内存布局、构造数据,以及漏洞程序的执行流程,汇总如下:
伪造chunk1->size,欺骗glibc将main_arena->max_fast修改为chunk1的地址,正常逻辑中,max_fast最大可以等于512字节,表示小于512字节的都是fast chunk,被改成chunk1地址(0x80XXXXX),就表示大小不超过0x80XXXXX的chunk,glibc都会将其当作fast chunk处理。
有了步骤(1)的铺垫,chunk2会被glibc当作fast chunk释放,并且归属于main_arena->fastbins[289],这显然已经在main_arena结构之外了,实际上正好与arena_key全局变量重叠(这是将chunk2->size构造为0x919的原因。可以事先通过objdump,查看arena_key与main_arena之间的偏移,并计算得到),从而又欺骗glibc将arena_key修改为chunk2,这样,通过构造chunk2的内容,就可以控制下一次malloc()返回任意想要的地址。
经过步骤(2)后,glibc就会从arena_key分配内存,而arena_key->max_fast与chunk2->size是重叠的,即arena_key->max_fast=0x919,所以1032字节大小的chunk,会被当作fast chunk,从arena_key->fastbins[127]链表中摘取,根据构造数据可知,这个值为EBP。
(4) strncpy(ptr3, argv[2], 1024-1)
此时ptr3=EBP,执行strncpy(),就会向上覆盖返回地址的值,并且拷贝内容来自用户输入,显然可以覆盖成shell code的地址,根据布局图可以看见,shell code的位置可以有很多选择,具体构造就不再重复叙述了。
用于欺骗判断条件的构造数据,布局图中已经使用虚线标出,都不难理解,这里主要说明一下_int_malloc()中的判断2,要求chunk3->size必须与分配大小匹配(示范漏洞程序的第三个启动参数,是写入栈中的,所以正好可以被利用,满足这一点)。
实际中漏洞程序,逻辑是不受攻击者控制的,比如示范漏洞程序中最后一次malloc()的参数值,使判断1不成立,_ini_free()就会走后续逻辑,从unsorted bin中分配内存:
unsorted bin相当于一种缓存,每次尝试从unsorted bin分配chunk时,glibc总是从unsorted bin链表头依次取出其中的chunk,如果恰好满足分配内存的大小,就返回给调用者,否则转移到相应的fast bin(av->fastbins[x])或者bin(av->bins[x])。上图代码就是从unsorted bin链表头摘除chunk的过程,当中的if用于判断是否为last remainder chunk,构造数据可以很容易使其不成立,因此可以简化成以下4条语句:victim = unsorted_chunks(av)->bk; // victim位置确定,内容x
bck = victim->bk; // bck位置x->bk,内容EIP-8
unsorted_chunks(av)->bk = bck; ↑
bck->fd = unsorted_chunks(av); // bck内容EIP-8
通过最后一条语句可知,如果欺骗bck=EIP-8,就可以将漏洞函数的返回地址,改写为unsorted_chunks(av),即布局图中&arena_key->bins[0],再通过第一条和第二条语句可知,如果往unsorted_chunks(av)->bk即&av->bins[0]+12处放入x,并往x->bk处放入EIP-8,就可以使bck=EIP-8。所以,x值其实有很多选择,只要保证通过溢出数据可以覆盖到,并且不与其它关键的构造位置冲突即可,原文中选的是&av->bins[0]+4,那么相应地,往x->bk即&av->bins[0]+16处放入EIP-8,并在&arena_key->bins[0]位置构造shell code,即可实现利用。
本次学习的利用技术,都依赖于可以将chunk大小伪造成8字节,而在较高的glibc版本中,已经增加了对释放chunk大小的检查,要求释放chunk必须满足最基本的大小(32位系统中为16字节,至少具有存放prev_size、size、fd、bk的空间),从而使这些攻击技术失效。所以攻防技术之间是持续博弈的,学习和创新,也需要不断持续,最近总结的两篇文章,只是二进制漏洞利用技术中的冰山一角,但也足以领教到真正计算机大佬的"扭曲"思维,有时候真觉得这些大佬就是"神经病",自己只能平凡的做个正常人。
看雪ID:jmpcall
https://bbs.pediy.com/user-home-815036.htm
*本文由看雪论坛 jmpcall 原创,转载请注明来自看雪社区