Stack Overflow - 高级ROP

只有一部分,基础不太好,等基础学完再填坑。

ret2dlreslove

原理

之前学习过ELF使用了延迟绑定,基本思想就是当函数第一次被调用的时候才进行绑定(符号查找、重定位等)。

在Linux中,程序使用_dl_runtime_resolve(link_map_obj, reloc_offset) 来对动态链接的函数进行重定位。如果可以控制相应的参数及其对应地址内容就可以控制解析的函数。要注意的是,32位的reloc_arg和64位的有区别:32位使用reloc_offset, 64位使用reloc_index。

具体一点,动态链接器在解析符号地址时所使用的重定位表项、动态符号表、动态字符串表都是从目标文件中的动态节 .dynamic 索引得到的。如果可以修改其中的某些内容,使得最后动态链接器解析的符号是我们想要解析的符号,那么攻击就达成了。

在linux下,二进制引用的外部符号加载方式有三种,FULL_RELRO、PARTIAL_RELRO、NO_RELRO,在PARTIAL_RELRO和NO_RELRO的情况下,外部符号的地址延迟加载。在 NO_RELRO 设置中,全局偏移表 (GOT) 中的所有条目均为可写;在 PARTIAL_RELRO 设置中,只有部分全局偏移表 (GOT) 条目在动态链接器解析符号后被设置为只读,其他条目仍然可写;使用 FULL_RELRO 设置时,在动态链接器解析所有符号之后,整个全局偏移表 (GOT) 都会被设置为只读。这意味着任何试图覆写 GOT 条目的尝试都会导致段错误,防止攻击者通过函数指针劫持控制流或操纵共享库行为。

_dl_runtime_resolve函数具体运行模式

_dl_runtime_resolve函数如何使程序第一次调用一个函数:

  1. 首先用link_map访问.dynamic,分别取出.dynstr、 .dynsym、 .rel.plt的地址;
  2. .rel.plt + 参数reloc_index,求出当前函数的重定位表项Elf32_Rel的指针,记作rel;
  3. rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym;
  4. .dynstr + sym -> st_name得出符号名字符串指针;
  5. 在动态链接库查找这个函数的地址,并且把地址赋值给*rel -> r_offset,即GOT表;
  6. 最后调用这个函数

通过一个示例来解释:

题目来源:XDCTF2015-pwn200

在0x080485b4处下断点,然后si进入call strlen@plt;

image-20230801142532701

进去后能看到首先会执行下面那一部分

image-20230801155644489

对应之前说的,跳转到自己的plt表项,可以查看jmp的地址存的是什么

image-20230801142743720

存的其实就是下一条指令

image-20230801142845668

接着单步执行,会先push进去一个0x10,然后jmp

image-20230801142918804

jmp的就是它的公共plt表项

image-20230801142955886

push后jmp,jmp跳到了_dl_runtime_resolve这个函数的地址

image-20230801143016416

中间push了两次,第一次是push 0x10,第二次是push DWORD PTR ds:0x804a004

查看0x804a004存放的是什么:

image-20230801143206089

存放的是link_map的地址,通过这个地址能够找到.dynamic的地址,第三个就是.dynamic的地址

image-20230801143302425

通过这个地址能够找到.dynstr和.dynsym和.rel.plt的地址

.dynstr 的地址是 .dynamic + 0x44 -> 0x08048278

.dynsym 的地址是 .dynamic + 0x4c -> 0x080481d8

.rel.plt 的地址是 .dynamic + 0x84 -> 0x08048330

image-20230801143437083

.rel.plt的地址加上参数reloc_arg就是函数重定向表项Elf32_Rel的指针,记作rel

image-20230801143853477

通过这个 rel 可以得到以下信息

r_offset = 0x0804a014 //指向GOT表的指针

r_info = 0x00000407

将r_info >> 8,即0x00000407 >>8 = 4作为.dynsym中的下标,这里的 “>>” 意思是右移

我们来到 0x080481d8(上面找到的那个 .dynsym 的地址)看一下,在标号为 4 的地方,就是函数名称的偏移:name_offset

image-20230801144045059

.dynstr + name_offset 就是这个函数的符号名字符串 st_name

0x08048278 + 0x20 -> 0x8048298‬

image-20230801144307804

最后在动态链接库查找这个函数的地址,并且把地址赋值给 *rel -> r_offset,即 GOT 表就可以了

整理一下

  • dl_runtime_resolve 需要两个参数,一个是 reloc_arg,就是函数自己的 plt 表项 push 的内容,一个是link_map,这个是公共 plt 表项 push 进栈的,通过它可以找到.dynamic的地址
  • 而 .dynamic 可以找到 .dynstr、.dynsym、.rel.plt 的这些节的地址
  • .rel.plt 的地址加上 reloc_arg 可以得到函数重定位表项 Elf32_Rel 的指针,这个指针对应的里面放着 r_offset、r_info
  • 将 r_info>>8 得到的就是 .dynsym 的下标,这个下标的内容就是 name_offset,右移八位是因为r_info高位表示索引,所以要右移八位
  • .dynstr+name_offset 得到的就是 st_name,而 st_name 存放的就是要调用函数的函数名
  • 在动态链接库里面找这个函数的地址,赋值给 *rel->r_offset,也就是 GOT 表就完成了一次函数的动态链接

yuque_diagram

上面的分析可以得出,如果控制相应的参数及其对应地址内容就可以控制解析的函数。动态链接器在解析符号地址时所使用的重定位表项、动态符号表、动态字符串表都是从目标文件中的动态节 .dynamic 索引得到的。所以如果我们能够修改其中的某些内容使得最后动态链接器解析的符号是我们想要解析的符号,那么攻击就达成了。

保护措施和攻击

在学习攻击之前,先复习一下RELRO保护。

前面学习过延迟绑定,简言之就是符号解析只发生在第一次调用时,通过PLT表进行,解析后相应的GOT表条目就会被修改为正确的函数地址,在这种情况下,.got.plt必须是可写的。

RELRO能够将符号重定位表设置为只读,或者在程序启动时就绑定所有的动态符号,从而避免GOT上的地址被篡改。

  • partial PELRO:包括.dynamic和.got等的一些段在初始化后会被标记为只读
  • Full RELRO:除了partial PELRO,延迟绑定会被禁止(有的文章说是惰性解析,问了AI说延迟绑定是一种特定的惰性解析技术,这里就暂时先理解为延迟绑定吧),所有的导入符号将在开始时被解析,.got.plt 段会被完全初始化为目标函数的终地址,并被mprotect标记为只读。GOT[1] 与 GOT[2] 条目即link_map和_dl_runtime_reolve的地址将不会被装入。

思路一——直接控制重定位表项的相关内容

根据上面的分析,其实最终解析符号地址时是依据符号的名字,那如果直接修改.dynstr的内容就可以执行我们想要的函数。比如write改成system。但是,动态字符串表和代码映射在一起,是只读的。此外,类似地,我们可以发现动态符号表、重定位表项都是只读的。如果可以控制程序执行流,那么就可以伪造合适的重定位偏移从而达到调用目标函数的目的。

思路二——间接控制重定位表项的相关内容

可以修改动态节中的内容,能够控制带解析符号对应的字符串,从而达到执行目标函数的目的。

由于动态链接器在解析符号地址时,主要依赖link_map来查询相关表项的地址,因此如果可以成功伪造link_map,也就可以控制程序执行目标函数。

32位

NO RELRO

计算reloc_arg

objdump -s -j .rel.plt ./main

image-20230804203831278

reloc_arg = fake_rel_plt_addr - 0x8048324,得到伪造的 .rel.plt 节段在程序中的实际地址

计算r_info

n = (欲伪造的地址- .dynsym 基地址) / 0x10,其中dynsym_base_addr.dynsym 节段在内存中的起始地址,0x10 是每个符号表项的大小。

r_info = n<<8,将 n 左移 8 位,得到最终的 r_info 值

image-20230804205712057

还需要过#define ELF32_R_TYPE(val) ((val) & 0xff)宏定义,ELF32_R_TYPE(r_info)=7,因此

r_info = r_info + 0x7

计算name_offset

image-20230804211455184

st_name = fake_dynstr_addr - 0x804821c,计算函数名在 .dynstr 节段中的偏移量。

64位

复现完填坑

SROP

原理

SROP(Sigreturn Oriented Programming),sigreturn是一个系统调用,在 unix 系统发生 signal 的时候会被间接调用

当系统进程发起(deliver)一个 signal 的时候,该进程会被短暂的挂起(suspend),进入内核①,然后内核对该进程保留相应的上下文,跳转到之前注册好的 signal handler 中处理 signal②,当 signal 返回后③,内核为进程恢复之前保留的上下文,恢复进程的执行④

内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。

signal handler 返回后,内核为执行 sigreturn 系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn 的调用号为 119(0x77),64 位的系统调用号为 15(0xf)。

攻击方法

内核主要做的工作就是为进程保存上下文,并且恢复上下文。这个主要的变动都在 Signal Frame 中。

  1. Signal Frame 被保存在用户的地址空间中,所以用户是可以读写的
  2. 由于内核与信号处理程序无关 (kernel agnostic about signal handlers),它并不会去记录这个 signal 对应的 Signal Frame,所以当执行 sigreturn 系统调用时,此时的 Signal Frame 并不一定是之前内核为用户进程保存的 Signal Frame

攻击示例

  1. 假设攻击者可以控制用户进程的栈,那么它就可以伪造一个 Signal Frame。当系统执行完 sigreturn 系统调用之后,会执行一系列的 pop 指令以便于恢复相应寄存器的值,当执行到 rip 时,就会将程序执行流指向 syscall 地址,根据相应寄存器的值,此时,便会得到一个 shell。

  2. 如果需要执行函数,那么可以控制栈指针或者把原来的RIP指向的syscall gadget换成syscall; ret gadget

构造 ROP 攻击的时候,需要满足:

  1. 可以通过栈溢出来控制栈的内容
  2. 需要知道相应的地址:”/bin/sh” Signal Frame syscall sigreturn
  3. 需要有够大的空间来塞下整个 sigal frame

示例复现

参考