二进制漏洞挖掘之栈溢出-开启RELRO

时间:2024-04-11 09:25:54

二进制漏洞-栈溢出

github地址:https://github.com/ylcangel/exploits/tree/master/stack_overflow

测试平台

系统:CentOS release 6.10 (Final)、32位

内核版本:Linux 2.6.32-754.10.1.el6.i686  i686 i386 GNU/Linux

gcc 版本: 4.4.7 20120313 (Red Hat 4.4.7-23) (GCC)

gdb版本:GNU gdb (GDB) Red Hat Enterprise Linux (7.2-92.el6)

libc版本:libc-2.12.so

漏洞原理

       在对栈缓冲区进行写操作时(如memcpy),未对缓冲区大小进行判断,导致写入数据长度可能大于缓冲区长度。

通用利用方式

       写入数据覆盖返回地址,使返回地址指向恶意代码起始地址。由于我是基于本地测试,也就是libc库的版本已知,而基于远程攻击或不同版本的libc库可能会存在差异。

漏洞测试程序

二进制漏洞挖掘之栈溢出-开启RELRO

 

很明显代码在执行scanf时未对缓冲区大小进行判断,存在栈溢出漏洞。

注意如无特殊说明,本文的exp都是基于该源码编译的二进制实现的。

所有测试均在linux环境下进行

开启RELRO

在前面描述的漏洞攻击中曾多次引入了GOT覆盖方法,GOT覆盖之所以能成功是因为默认编译的应用程序的重定位表段对应数据区域是可写的(如got.plt),这与链接器和加载器的运行机制有关,默认情况下应用程序的导入函数只有在调用时才去执行加载(所谓的懒加载,非内联或显示通过dlxxx指定直接加载),如果让这样的数据区域属性变成只读将大大增加安全性。RELRO(read only relocation)是一种用于加强对 binary 数据段的保护的技术,大概实现由linker指定binary的一块经过dynamic linker处理过 relocation之后的区域为只读,设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。RELRO 分为 partial relro 和 full relro。

Partial RELRO

  1. 现在gcc 默认编译就是 partial relro
  2. some sections(.init_array .fini_array .jcr .dynamic .got) are marked as read-only after they have been initialized by the dynamic loader
  3. non-PLT GOT is read-only (.got)
  4. GOT is still writeable (.got.plt)

Full RELRO

  1. 拥有 Partial RELRO 的所有特性
  2. lazy resolution 是被禁止的,所有导入的符号都在 startup time 被解析
  3. bonus: the entire GOT is also (re)mapped as read-only or the .got.plt section is completely initialized with the final addresses of the target functions (Merge .got and .got.plt to one section .got). Moreover,since lazy resolution is not enabled, the GOT[1] and GOT[2] entries are not initialized. GOT[0] is a the address of the module’s DYNAMIC section. GOT[1] is the virtual load address of the link_map, GOT[2] is the address for the runtime resolver function。

开启RELRO

-z norelro /-z relro -z lazy /-z relro -z now (关闭 / 部分开启 / 完全开启)

不完全开启relro

二进制漏洞挖掘之栈溢出-开启RELRO

 

完全开启relro,此时符号在编译后已经全部被解析

二进制漏洞挖掘之栈溢出-开启RELRO

 

让我们用gdb调试一下看看开启relro和不开启got表项的区别,下图是不开启relro的情况,got表项在函数调用之前未被填充。

注意:

       这里需要说明一下,调试使用的程序已完全开启了relro,这将导致你看到的调试程序的got表项都被填充了正确的外部符号的地址,函数解析方式由懒加载变成了直接加载,并且got表是只读的。而在不完全开启relro选项时情况并非如此,它还是执行懒加载,got表和之前我们测试的程序没什么两样,只有在第一次真正调用符号的时候才被填充,并且它的got表是可写的。小小的区别会导致一些攻击方式失效。

二进制漏洞挖掘之栈溢出-开启RELRO

 

通过读取进程maps我们可以看到未开启relro编译选项,got表地址0x8049xxx落入了可写区域,见下图:

二进制漏洞挖掘之栈溢出-开启RELRO

 

开启relro后got表项变化情况,从下图可以看到got表项在函数未调用前就被填充成正

二进制漏洞挖掘之栈溢出-开启RELRO

 

确的函数地址了,我们通过读取进程maps仍看可以got表落入的段不具备可写属性。

二进制漏洞挖掘之栈溢出-开启RELRO

 

漏洞分析(略)

二进制漏洞挖掘之栈溢出-开启RELRO

 

实现exp

Ret2dl_resolve

此原理只能用于不完全开启relro情况下,在不完全开启relro选项时,外部符号还未被解析,此时got表项对应的不是真正的外部函数的地址。只有这样在第一次调用外部符号时才会执行符号解析逻辑,我们才能用此方法攻击。该原理同样适合以上情形。

原理分析

想讲明白实现该exp的原理首先必须讲明白重定位函数的过程原理,在这里详细讨论明显不符合我们的初衷。这里就大致描述一下相关原理。整个过程如下图所示:

二进制漏洞挖掘之栈溢出-开启RELRO

 

单上面这幅图可能让大家有些迷糊,现在我们就重定位scanf函数来演示一下程序执行流程,下图是我们在第一次调用scanf之前的演示图,从图中可以看到此时got表项未被填

二进制漏洞挖掘之栈溢出-开启RELRO

 

充,got表项地址指向[email protected]的第二条指令。接着push $0x18往栈中压入0x18,随后jmp到地址0x8048328处。继续把0x80497a0压入栈,随后调用_dl_runtime_resolve

二进制漏洞挖掘之栈溢出-开启RELRO

(glibc/sysdeps/i386/dl-trampoline.S)。_dl_runtime_resolve前三条指令把eax、ecx、edx压入栈,此时函数栈如下图所示:

二进制漏洞挖掘之栈溢出-开启RELRO

 

从图上可以看出后续继续调用_dl_fixup(在glibc/elf/Dl-runtime.c),_dl_fixup函数是通

二进制漏洞挖掘之栈溢出-开启RELRO

过寄存器传参的,第一个参数eax(esp+0xc)值为0x5ea900对应link_map指针,第二个参数用edx(esp+0x10)传递为0x18对应reloc_arg。

二进制漏洞挖掘之栈溢出-开启RELRO

 

下图是调试跟踪过程:

二进制漏洞挖掘之栈溢出-开启RELRO

 

我们大致分析一下_dl_fixup函数是怎么进行重定位和安装link_map结构体对应库的内存中相应数据结构和函数地址的。reloc_offset宏定义:

二进制漏洞挖掘之栈溢出-开启RELRO

 

DT_PTR宏定义:

二进制漏洞挖掘之栈溢出-开启RELRO

 

首先通过dynamic段标签找到符号表段.dynsym、字符串表段.dynstr、重定位表段.rel.plt

二进制漏洞挖掘之栈溢出-开启RELRO

 

通过输入参数reloc_offset(等价于reloc_arg)找到要重定位的符号表(对应代码中sym)和相应的重定位表(对应代码中reloc)。确定该重定位符号对应got表项地址(对应代码中rel_addr,基址+偏移)。

二进制漏洞挖掘之栈溢出-开启RELRO

 

这段代码我仅摘取了核心部分,如果符号是非内部符号(外部符号,即显性)则调用dl_sym底层函数(_dl_lookup_symbol_x)去查找真正的符号,新link_map(对应代码中result)和原link_map(对应代码l)可能是一个link_map或者新加载的lib库生成的新link_map。最后通过调用宏DL_FIXUP_MAKE_VALUE(该宏实质作用就是返回l->l_addr + sym->st_value)得到查找符号的真实地址。最后代码返回真正的符号地址,回填got表项。

二进制漏洞挖掘之栈溢出-开启RELRO

 

i386分支elf_machine_fixup_plt函数的实现代码如下图所示(实际代码就一行,*rel_addr = value,给rel_addr对应的地址赋值value):

二进制漏洞挖掘之栈溢出-开启RELRO

 

为什么void *const rel_addr = (void *)(l->l_addr + reloc->r_offset) = value就执行了got表项回填呢?让我们看看下面两幅图:

二进制漏洞挖掘之栈溢出-开启RELRO

 

从上面这幅图我们看到rel.plt各表项的offset地址为0x80497a8-0x80497b4之间,我们再通过ida看看这个地址区间对应源程序那个节:

二进制漏洞挖掘之栈溢出-开启RELRO

 

从图上不难看出它们的offset对应的就是.got.plt的各表项的地址值,*rel_addr = value操作恰好是回填了对应的got表项值。让我们看看执行完_dl_fixup函数后的效果(从图上能明显看到got表项已经被填充了):

二进制漏洞挖掘之栈溢出-开启RELRO

 

下面这幅图摘自网络,这幅图也能够表达出解析符号的过程,至此符号解析过程我们分析完

二进制漏洞挖掘之栈溢出-开启RELRO

 

了,知道了解析过程,我们再看看依据这个原理的漏洞利用实现过程。

漏洞利用

  • 控制EIP为PLT[0]的地址,只需传递一个reloc_arg参数二进制漏洞挖掘之栈溢出-开启RELRO

 

下图可以清晰的看到,其他过程调用再把reloc_offset压入栈之后都会jmp到plt0指令处,而plt0是不执行压栈操作的。

二进制漏洞挖掘之栈溢出-开启RELRO

 

  • 控制reloc_arg的大小,使reloc的位置落在可控地址内
  • 伪造reloc的内容,使sym落在可控地址内二进制漏洞挖掘之栈溢出-开启RELRO

 

  • 伪造sym的内容,使name落在可控地址内二进制漏洞挖掘之栈溢出-开启RELRO

 

  • 伪造name为任意库函数,如system

为了实现exp我们需要使用stack pivot技术劫持栈,让bss+一个偏移成为我们能够控制的栈,之所以这么做是因为需要构造的结构比较大,相比而言没有比bss更适合的地方用来存储这些结构了(基本上bss都至少占一个页大小,但是bss空间里面有很多未使用的区域)。为方便测试程序仅开启relro和nx选项。读写内存我们需要借助read、write函数。

二进制漏洞挖掘之栈溢出-开启RELRO

1. 覆盖eip为read函数地址,劫持栈到bss段,让esp落入bss我们控制的范围

stack pivot的核心是mov %ebp,%esp,但在执行这个指令前必须让ebp保持一个正确的值,也就是说你需要先pop %ebp。我们可以在pop %ebp时让栈上对应的值为bss+某个偏移,这样我们就把栈劫持到了bss区域,这样我们可控制栈的范围就变大了。

栈劫持只需要使用leave; ret指令即可完成。这里有一个坑,劫持栈的起始地址需要仔细设计(可能需要多次测试调试获得,不是简单的加上一个偏移就可以),否则程序在调用

二进制漏洞挖掘之栈溢出-开启RELRO

_dl_fixup过程中失败(通过调试知道可能和.gnu.version有关,最好使得 ndx = VERSYM[(reloc->r_info) >> 8] 的值为 0,以便于防止找不到的情况)。

二进制漏洞挖掘之栈溢出-开启RELRO

2. 伪造重定位结构,伪造重定位符号st_name指向system

二进制漏洞挖掘之栈溢出-开启RELRO

3. 向劫持栈写入剩余rop链和伪造的重定位结构,rop链主动调用plt0,完成攻击

二进制漏洞挖掘之栈溢出-开启RELRO

这里我就不把所有代码都粘贴出来了,我会在把源码都上传,请在源码中查看。执行结果:

二进制漏洞挖掘之栈溢出-开启RELRO

 

fake linkmap(略)

我们在分析源码时有提到过,如果符号是外部符号则调用_dl_fixup函数进行解析,如果是

二进制漏洞挖掘之栈溢出-开启RELRO

内部符号(即非显性符号)它会走如下图的else分支调用DL_FIXUP_MAKE_VALUE宏,该宏最终执行结果为value = l->l_addr + sym->st_value(l_addr为对应函数库加载的基址)。我们只要能伪造l_addr 和link map->l_info结构体就可以实现攻击。另外必须注意我们必须让ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0成立。

二进制漏洞挖掘之栈溢出-开启RELRO

其攻击原理大致和dl_resolve相同,但是复杂度要高很多,虽然说核心是伪造l_addr和st_value,但它们都依赖link map结构体。如果存在任意内存写漏洞根据上述原理实现攻击相对简单一些。如果不存在这样的漏洞你可能需要构造一个完整的link_map结构体(确切的说需要构造l->l_addr、l->l_info指向的Elf_Dyn结构、l->l_info[DT_JMPREL]指向的Elf_Rel结构、l->l_info[DT_SYMTAB]指向的Elf_Sym结构、link map其他地方全部填充0即可)。我们可以让l_addr的值为已经解析出的libc的某函数地址,让st_value为system函数相对这个函数的偏移,这样它们加起来就正好是system函数的地址了。 这种情况在完全full relro开启的情况下变得可行。我们知道full relro开启 情况下符号在编译时就全部被解析,它不会主动调用_dl_runtime_reslove函数(link_map数据结构不会被初始化),但是我们可以通过构造假的link map结构,然后通过rop主动调用_dl_runtime_reslove函数来完成攻击。

利用fake link map方式实现栈漏洞利用攻击实现起来相对复杂,我这里就不在做exp演示了,原理很简单,大家谁感兴趣可以自行编写测试。