首页
论坛
课程
招聘
《利用之单字节溢出(off-by-one)》笔记
2020-5-8 22:10 4016

《利用之单字节溢出(off-by-one)》笔记

2020-5-8 22:10
4016

原文:https://0x00sec.org/t/null-byte-poisoning-the-magic-byte/3874/4
翻译:https://www.anquanke.com/post/id/88961

 

本文是基于翻译文章的笔记。

 

1.基于一个有off-by-one漏洞的应用程序,程序里有个关键的数据结构:

 

反汇编它:
① 释放chunk

这个函数么嘚任何问题,用c语言写的话,就是这样:

void _free(int chunk_id)
{
    free(chunk_array[chunk_id].data);
    free(chunk_array[chunk_id]);
    chunk_array[chunk_id] = NULL;
}

那么相应的,肯定还会有一个分配函数,并且也么嘚任何问题:

void _alloc(int size, char *data)
{
    int size = strlen(data);
    int chunk_id = 0;

    for (; chunk_id < 100; chunk_id ++) {
        if (chunk_array[chunk_id] == NULL)
            break;
    }

    // 分配chunk结构,以及分配size大小的chunk->data,并赋初值
    chunk_array[chunk_id] = malloc(sizeof(struct chunk));
    // 实际程序检查了chunk_array[chunk_id]是否为空
    chunk_array[chunk_id].data = malloc(size);
    // 实际程序检查了chunk_array[chunk_id].data是否为空
    memcpy(chunk_array[chunk_id].data, data, size);
    chunk_array[chunk_id].size = size;
}

② 编辑chunk

这个函数存在off-by-one漏洞,可以越界写一个'\0'字符,用c语言写的话,类似这样:

void _edit(int chunk_id, char *data)
{
#if 0
    // 根据反汇编
    int _size = chunk_array[chunk_id].size;
    char *_data = chunk_array[chunk_id].data;

    _data[_size] = '\0';
#else
    // 实际代码可能是这样
    // 总之就是可以最多越界写一个'\0'字符
    int size = strlen(data);
    if (size <= chunk_array[chunk_id].size)
        strcpy(chunk_array[chunk_id].data, data);
#endif
}

增删改都有了,肯定还会有一个查看函数:

void _show(int chunk_id)
{
    printf("%s\n", chunk_array[chunk_id].data);
}

2.通过操作应用程序,触发如下执行两次_alloc()函数:
alloc(0x88, 'A'0x88)
alloc(0x108, 'A'
0x108)

① 如果紧挨着chunk0的上一个内存块空闲,chunk0中第一个值表示上个紧挨内存块的大小,否则为上个紧挨内存块的数据部分,比如chunk0不是空闲的,chunk1的pre_size处,就是写入chunk0->data中的"BBBBBBBB";
② chunk0中第二个值,高61位是一个size值,表示当前内存块的大小,最低位inuse,表示上个紧挨内存块是否空闲,比如0x21,表示chunk0大小为0x20,上个紧挨内存块处于分配状态;
③ 从第三个值的位置开始,才是malloc(sizeof(struct chunk))返回给调用者的chunk结构,可以看出,chunk0->size为0x88,chunk0->data指向0x603030,里面的数据全是'A',与alloc(0x88, 'A'*0x88)的吻合;
④ 后面紧接着的分别是:chunk0->data所占内存块、chunk1所占内存块、chunk1->data所占内存块。

 

3.off-by-one最直接的结果:

通过触发执行edit(0, 'A'*0x88),从chunk0->data指向的地址开始,写入0x88个'A',以及一个'\0'字符,将malloc_chunk1->size最低地址处的字节0x21(小端字节序),覆盖为0。
就凭这样,就能控制这个应用程序执行任意代码?作者让我们别急,继续往下看。

 

4.libc基址泄漏(原文没具体介绍)
libc中有一个malloc_state结构,并且用这个结构定义了一个全局变量main_arena,根据ELF格式规范,main_arena会被保存在.bss/.data段,那么加载后它与libc基址的偏移,通过它在libc.so文件中的偏移就能得到,换句话说,如果获取到当前运行的应用程序中main_arena的内存地址,就能计算libc的加载基址。
根据malloc_state结构的定义可以知道,main_arena中包含了一个bins[]数组,bins[0]为unsortbin,bins[1]~bins[62]为smallbin,bins[63]~bins[125]为largebin,如果能获取任何一个bins[x]的地址,就能根据该成员相对于整个结构的偏移,得到main_arena的地址:

struct malloc_state
{
    ...
    mfastbinptr fastbinsY[NFASTBINS];
    mchunkptr bins[NBINS * 2 - 2];
    ...
}

这个场景中可以利用的是unsortbin,通过操作触发_free(0)释放chunk0,根据libc的逻辑,chunk0结构本身的大小在fastbin的范围,会被加到某个fastbinsY[x]中,chunk0->data内存块大小不属于fastbin的范围,会被加到unsortbin,即bins[0],bins[0]是一个双向链表头,由于此时bins[0]是空链表,所以chunk0->data内存块的fd和bk都指向链表头&bins[0]:

此时再触发_alloc(0x8, 'A'*0x8),刚放入fastbin中的chunk0又会重新分配出来,作为新的chunk0,新chunk0->data会从刚放入bins[0]的内存块切割,并且被写入0x8个'A',这会将fd覆盖掉,但bk还在,这样再触发_show(0),就可以泄漏0x7ffff7dd37b8这个地址了,然后根据上述的说明反推,就能最终获得libc的基址了。

 

5.构造交互过程和交换数据,控制程序执行system()函数
基于步骤4,继续依次触发:
① _alloc(0x208, 'D'*0x1f0 + p64(0x200)) // chunk_id=2

fastbins空了,chunk x结构本身也要从unsortbin中切割,这样unsortbin就会剩余0x50大小,chunk x->data需要0x210大小的内存块,unsortbin中不够切割了,malloc()内部会通过brk()扩展堆的大小,即将top值增加0x210。
这里在chunk x->data的0x1f8偏移处写入了0x200,为步骤⑤作准备,暂时忽略不用理解。

 

② _alloc(0x108, 'E'0x108),_alloc(0x108, 'F'0x108)
unsortbin还剩余0x50大小,chunk y、chunk y'结构本身,仍然可以从unsortbin中切割,chunk y->data、chunk y'->data,仍然要通过扩展top分配得到:

 

③ _free(2),释放chunk x

到目前为止,都属于正常操作,内存都还是正确的,chunk x释放后,chunk y的pre_size被设置为chunk x大小0x201,并且inuse被清0,这些是由libc的free()函数完成,chunk x的size和inuse也都是正确的,不过可以通过触发_edit(1, 'B'*0x108),即编辑chunk1->data,将其最低地址的0x11,覆盖为0。

 

④ chunk1未溢出时,alloc(0x108, 'G'0x108)

这里只是为了描述一下正常的情况,使步骤⑤更容易理解:chunk y上个紧挨内存块,被切割走了一部分,空闲部分还剩0x100,所以chunk y的pre_size被更新了。为了能成功利用漏洞,是不会直接触发alloc(0x108, 'G'
0x108)的。

 

⑤ chunk1溢出后,再alloc(0x108, 'G'*0x108)
步骤③已经说明过了,编辑chunk1->data溢出一个'\0'字符,会将空闲的chunk x->size从0x210修改为0x200,相当于使chunk x的大小缩减了0x10,相应的,malloc()也会认为chunk x的下一个内存块在0x6030e0处,而不再是是0x6030f0了,这也是步骤①在0x6030e0处写入0x200的原因,因为这时它就是pre_size,free()函数会检查它是否与chunk1->size相等,保证了程序不会异常退出,同时malloc()会将剩余空闲块的大小,也记录在0x6030e0处,真正chunk y的pre_size保持不变,从而让它仍然以为前面0x210的内存块是空闲的:

 

⑥ alloc(0x80, 'H'*0x80)

这时仍然有0x10大小的错位,仍然不会修改真正chunk y的pre_size,这时释放chunk y,根据pre_size=0x210和inuse=0,会将前面0x210大小的内存块连带着一起释放,从而让从0x6031e0开始的0x320大小的内存块,变成空闲状态,其实就相当于chunk w已经释放了,但仍然可以通过触发_edit(3,"xx")对这块内存进行写操作,就将漏洞类型转变为UAF了。

 

⑦ alloc(0x140, 'Z'*0x110 + p64(8) + p64(atoi_got))
这步操作,又会从原chunk x开始处进行切割,并且chunk z->data的0x120偏移处,正好是chunk w->data指针所在的位置,通过这种参数,结果就是将chunk->data指针值,修改成了atoi_got的地址。
另外说明一下,为什么要选atoi()函数的GOT,因为_edit()这些函数的chunk_id参数,往往是通过交互数据中的字符串转换来的,这样就可以主动触发。

 

⑧ _edit(3,p64(system()函数地址))
这时,实际上已经是在往atoi_got中写数据了,很显然,我们肯定希望写入system()函数的地址,步骤3泄漏的libc的基址正是用于计算atoi_got和system在运行程序中的地址,这样程序再次执行atoi()函数时,实际上执行的就是system()函数了,不过system()还需要"hs/nib/"参数,需要另外的技巧,作者就没有继续介绍了。


[注意] 招人!base上海,课程运营、市场多个坑位等你投递!

收藏
点赞0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回