pwn真是技巧多。。。这是一个比较麻烦的攻击技巧,特地开一个空文章来写
2018年修改
发现博客居然被好多人看了。。。回过头来看发现这个介绍的return-to-dl-resolve
并不是标准攻击方法QvQ,所以在文章后半段会介绍常用的ret2dl-resolve应该如何攻击。
return-to-dl-resolve
我们知道,程序分为静态链接和动态链接,在处理动态链接的时候,elf文件会采取一种叫做延迟绑定的技术,也就是当我们位于动态链接库的函数被调用的时候,编译器才会真正确定这个函数在进程中的位置,从而在第二次调用的时候减少绑定的时间。
延迟绑定(lazy load)
这里我们以HBCTF中的pwn200的程序为例子讲解一下漏洞的利用。首先我们以read函数为例子:
在这里,read函数的地址是0x08048380。这里的read函数位于.plt段内,内部并没有什么实际内容,而是跳转到ds:08049804指定的位置,然后我们跟踪过去看:
可以看到,这个地方是一个叫做.got.plt的段的位置,但是这里具体写了什么呢,IDA并没有告诉我们,所以这里我们要尝试直接使用gdb跟踪看看:
可以看到,这个位置就是read函数中,jmp指令的下一个指令的地址。由于jmp指令是取出当前地址中存储的值作为对应地址,也就是说接下来会跳转到地址0x080498386中。由于IDA中并没有将后续的指令显示出来,我们在gdb中截图为:
这里关注jmp之后的两个指令。这两个指令首先push了一个0x0(这个值叫做reloc_offset
,下文会提到),然后跳转到了这个0x08048370地址中,这个地址中的代码为:
1 | 0x8048370: push DWORD PTR ds:0x80497fc |
这里把地址0x080497fc压栈并且调到上个地址下一个位置中存放的内容中去,这个0x080497fc
地址中的函数叫做**_dl_runtime_resolve(动态链接运行解析?)**
这里我们来分析一下这些过程在做什么:
首先,程序为了解析read函数的位置,跳转到了.plt(Procedure Linkage Table, 程序连接表)中查询read函数的地址。然而我们此时是第一次调用,还暂时未知read函数所在的位置,于是程序在.got.plt中存放的不是read函数真正的地址,而是read函数在.plt段
中地址+6的一个push + jmp
的地址。为了实现解析的过程,.plt中会存放当前函数在.rel.plt节中的偏移。ELF文件中的文件信息是以节section的形式存在的,这个read函数的节也不例外,所以当我们去查找read函数的基本信息的时候,自然是要在对应的节中查找。这个节的查看方式我们可以用
1 | readelf -d |
来查看dynamic(重定位节):
这里我们关注这四个节:
- JMPREL : .rel.plt的开始位置,是一个记录了动态重定位表的segment
- REL: 重定位函数段开始的位置,也就是之前提到的,函数再跳转至**.plt**段之后返回,之后第二次跳转的位置。
- RELSZ: 重定位表单总共的大小,32bytes
- RELENT: 每一个表单的大小,8 bytes
通过分析,我们能够找到程序定义的重定位表的开始位置,同时我们可以从上述得知,我们此时共有四项需要被重定位的内容。我们用
readelf -a
整体查看一下:
这个.rel.dyn中存放的是需要重定位的变量,而**.rel.plt**则是存放了我们需要重定位的函数。这个段中的函数的数据结构为:
1 | typedef struct { |
也就是说,前面一段存放的是当前函数的偏移量,后面存放的则是当前函数的信息。我们看还未运行时候的read中存放的是:
正好就是我们开始讨论的那个read跳转到的.got.plt
的地址,在结构体中就是r_offset。
然而光知道上述信息,我们怎么能够在动态链接中找到我们指定的函数呢?这里就要谈到另一个节**.dynsym**
这个节存放的是所有动态符号表中所包含的符号,也就是.rel的集合。这个表中的内容的结构体为:
1 | typedef struct |
然后可以看到我们例子中的read为
我们回到上述。rel.plt图中所示,我们的read->r_info == 0x107,此时的ELF32_R_SYM(info)(字面理解就是在.dynsym中的偏移位置)就是1,正好就是第一条数据。此时read中存放的st_name = 0x27,然后我们此时会去相应的.dynstr节中查找对应的下表指向的字符串:
通过上述方法,我们就能够找到"read"这个符号。我们这里可以回想一下PA实验(或者所gdb中)是如何完成变量表达式求值这个过程的:我们首先将字符串读入一个函数中,然后我们便利所有的符号表,从而找到一个符号名字与我们提供的字符串的名字相同的符号,然后再在相应的符号表中查询对应的值。类比一下的话,这个动态链接函数的过程也就是类似的:
1 | // param link_map:链接标识符 |
此处的link_map
为一个很长长长长的结构体,里面的结构大致如下:
1 | struct link_map |
是一个用来记录当前进程中的每一个.so
文件的基本信息,里面包含了一些与函数导入相关的内容。
调用这个_dl_runtime_resolve
的时候,主要操作为:
- 首先找到plt中的相对位置,也即是.rel.plt中的对象ELF_rel
reloc = (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
- 利用这个reloc计算得到.dynsym中的ELF_rel的位置
&symtab[ELF32_R_SYM(reloc->r_info)]
- 然后利用偏移量找到当前动态链接函数对应的字符串
strtab + sym->st_name
- 利用字符串和
link_map
结合,在进程中将函数给找出来。
以上过程便是ret2dl-resolve
攻击方式需要知道的工作流程。接下来我们介绍针对这个流程的几个弱点进行的攻击思路。
攻击方法
在讨论攻击方法之前,我们首先要明白动态链接过程中的关键参数:
- 当前函数传入的,自身在
.rel.plt
节中的偏移量 - 通过偏移量找到的当前函数的对应的动态链接的函数名字符串
首先通过reloc_offset
能够找到当前函数在.rel.plt
中的偏移量,然后能够找到函数在动态链接库中存放的字符串,从而能够定位到函数本身的地址。因此这个过程中可以针对这两个地方进行攻击
同时,根据上述提到的方法,我们能够总结出当前攻击的特征:
- 需要存在栈溢出点
- 不需要泄露libc的地址,即可完成攻击
- 可以使用到未在导入表中出现过的函数
- 需要一点点ROP
非典型的攻击手段(直接伪造整个.dynstr动态链接的函数名称字符串)
此方法相对简单,但是其需要的条件比较苛刻
- 此时允许读入的字符串数量较大
- 在完成构造之后,需要存在至少一个未进行延迟绑定的函数。
这次的题目提供的解法关键就是修改这个**.dynstr**节。我们把这个节里面指定的字符串改成我们指定的字符串,那么这个_dl_fixup在查找的时候就会去查找我们修改过的函数的地址,从而调用我们想要调用的函数:
也就是说,攻击成功的关键就在于找到.dynstr的位置。我们首先使用
readelf -a infoless
找到.dynstr的地址:
也就是说,我们需要把这个地址改成我们伪造的字符串地址。这个.dynstr的规则是:
- 开头是空字符
- 字符串之间用\x00相间隔
然后我们需要找到这个地址存放的地方,从而将这个地址修改成指定的字符串地址。使用readelf -R .dynamic infoless
指令能够看到当前程序中的.dynamic
段中所有的entries的地址,在这里能够找到这个.dynstr所在的位置。
最终通过将这个红框中的地址换成我们伪造了的动态链接的函数名称字符串的地址,从而让解析过程总,将指定的函数(这里以fflush为例子)解析成我们指定的函数。
典型攻击手段(修改偏移量)
之前提到,整个的搜索流程最依赖的就是传入的reloc_offset
,也就是整个流程中最开始的确认的位置。如果我们修改了reloc_offset
,我们刚刚提到的四个流程中,除了得到动态链接库中对应函数的字符串外,其他的结构体都需要进行伪造。不过相对的,这里只需要能够伪造我们选择的一个函数即可。
攻击的流程需要做到如下的事情:
- 伪造某个函数的
_dl_runtime_resolve(struct link_map *l, ElfW(Word) reloc_offset)
过程,具体来说就是通过修改传入参数offset,让整个识别过程偏移到一个我们可控的区域内,然后完成对变量ElfW(Sym)
,ELF_rel
以及Symtab
三个值的伪造,从而伪造整个动态绑定的过程 - 使用ROP手段,控制函数的栈,让函数栈能够迁移到整个伪造的攻击栈上
首先这里的ROP链构造,我们构造如下栈
1 | +---------------------+ |
通过将栈构造成如上的形式,首先利用pop,将ebp的地址修改程我们指定的一个栈的起始地址,然后通过leave指令,将当前的ebp的值传递给esp,从而实现将整个栈迁移到fake_stack_addr的功能。
tips:leave这个指令执行完esp <- ebp之后,还会执行一次pop,所以在整个fake_stack_addr_的最开始要放入一个padding用于传递给ebp
之后esp就相当于指向了一个受控制的栈。在这个栈中,我们尝试去调用一个_dl_runtime_resolve
,进行一次伪造的动态函数地址绑定。接着上面的栈,此时我们的栈内容为:
1 | +---------------------+ |
这里的ret_addr和arg1可以通过动态调试得知。ret_addr为当前整个绑定完成之后会回到的返回地址,而arg1则是发生绑定过程中所调用的函数所在的位置。在攻击的最后一步,需要在arg1的为之中填写我们需要调用的参数(例如我们最后将函数绑定到了system函数上,此处我们就应该填写/bin/sh
的地址)
这里我们要伪造成任意一个函数调用_dl_runtime_resolve
的样子。设此时我们伪造的Elf32_Rel结构体起始地址为fake_reloc,这个fake_rel_addr最好设置程.bss
段上的一个位置。由于rel_offset在函数中_dl_runtime_resolve的用法如下:
1 | Elf32_Rel reloc = addr(.rel.plt) + reloc_offset |
则此时我们需要伪造的rel_offset为
1 | fake_rel_offset = fake_reloc - addr(.ret.plt) |
之后我们来讨论关于这个fake_reloc的伪造过程。
首先这个fake_reloc的结构体为:
1 | typedef struct { |
第一个变量中填写了需要重定位的函数的.got
表地址,此处可以随意选择一个函数作为触发,这里以fflush函数为例。之后的r_info则是会跳转到Elf32_Sym
结构体上:
1 | Elf32_Sym *sym = &symtab[ELF32_R_SYM(reloc->r_info)]; |
这里我们发现,程序会检查reloc->r_info,观察其低八位是否为ELF_MACHINE_JMP_SLOT(也就是0x07)。因此我们可以让我们的fake_Elf32_Sym的地址按照0x100对齐。同时,因为是以取地址的形式进行的,所以这个地址需要按照0x10(也就是一个ELF32_Sym结构体的大小)进行安排。此处我们这个Elf_Rel
结构体应该为:
1 | +---------------------+ |
其中fake_sym_addr
和fake_r_info
满足数学关系
1 | fake_r_info&&0xff == 0x07 |
然后是结构体Elf32_Sym结构体,结构体如下
1 | typedef struct |
现在已经到了最后的阶段,此时我们只需要修改一个st_name
,让这个st_name指向一个我们可控的字符串地址,之后的内容可以完全照搬内存中的值:
此时的结构为
1 | +---------------------+ |
此时fake_st_name要满足
1 | fake_st_name = func_name_addr - addr(.dynstr) |
至此,整个动态绑定就完成了。完成后,当前的fflush函数就会绑定到另一个函数身上,完成控制流的截获。
整体攻击流程
首先,我们利用当前栈溢出,控制函数调用read函数,从而进行输入的控制:
这里注意,由于我们需要切栈,所以我们还需要一次ROP的机会,要让函数回到这个可以被exploit的函数上。
之后我们伪造当前的栈,以及在此处伪造动态绑定,结构体如图所示
在最后的位置,我们可以往其中塞入一些字符串,例如说调用的函数"system"的字符串,以及要调用的"/bin/sh"字符串等。于是之后,我们可以将当前构造改造为:
这里比较麻烦的是确定fake_r_info
,因为我们需要满足的条件为
- fake_r_info&&0xff == 0x07
- fake_sym_addr = fake_r_info / 0x10 + symtab_addr
则此时我们可以基于之前的地址来求当前地址的位置:
- 首先我们在
fake_rel_offset
的基础上+8,跳过当前结构体 - 然后我们在这个地址的基础上强行计算当前地址,即
fake_sym_addr = fake_rel_offset+0x8 - symtab_addr
- 这个基础上,我们检查其对其的偏差值,即
align = 0x10 - (fake_sym_addr & 0xf)
- 最后我们重新计算fake_sym_addr的值,即
fake_sym_addr = align + fake_sym_addr
- 最后用得到的
fake_sym_info
计算,就能得到我们此时填写的内容为fake_r_info = ((fake_sym_addr - symtab_addr) / 0x10) | 0x7
之后我们的fake_st_name就可以填写我们之前确定的"system"的字符串的地址减去strtab_addr的偏移量,即可完成构造
第二次来到受攻击的函数上后,我们布置好ROP链,就能够将esp转移到.bss
段上,完成攻击。
附上这种方法的exp:
1 | # -*- coding:utf-8 -*- |
总结
首先既然是写的博客,肯定是要被大家看的。。。所以每次写的时候还是要注意注意再注意。。尤其是我这种粗心的人QvQ
然后是每种攻击方法要深刻理解。最初看到这个方法的时候,以为是通过符号进行查找的一种攻击方法,所以想到说要利用很大的控制地址,而且需要关掉很多保护等等。后来学习过程中才明白整个攻击的逻辑,发现其实限制条件没有我想象的那么多。
最后是exp写的很不熟练,最近有阵子没玩ctf,也没怎么研究漏洞啥的,结果写个exp又调了一整天。。。尤其这一次还是有了很多类似的exp的帮助的情况下。。。真的很佩服各位比赛的时候居然能这么快的调试exp,菜鸡果然还是菜啊。。。