首页
社区
课程
招聘
[分享]dl_runtime_resolve结合源码分析及常见的几种攻击手法
发表于: 2019-8-13 00:44 11925

[分享]dl_runtime_resolve结合源码分析及常见的几种攻击手法

2019-8-13 00:44
11925

之前粗略的看过32位下的函数重定位的攻击,没过多久就忘得七七八八了,趁着暑假的时间,好好得学了波函数重定向攻击,这篇文章作个归纳总结,希望能够对你有所帮助。

在Linux下,如果ELF想要调用动态函数库中的函数,就必须在程序加载的时候进行动态链接,动态链接会消耗不少的时间解决模块之间的函数引用的符号查找及重定位,但有些函数在程序运行完毕后可能都不会用到,如一些错误处理的函数,为此ELF采取了延迟绑定(Lazy Binding)的做法,其核心思想就是函数用到时才对该函数进行绑定(符号查找及重定位),如果没用到就不进行绑定。

我们接下来将通过调试hello world程序来探究函数重定位的流程


下断点在printf函数后,si进入函数


我们可以发现进入后并没有直接转跳到printf函数,而是到了printf的plt表,这是由于ELF的延迟绑定机制,当函数用到的时候,再借助plt表、got表及其他操作进行绑定,未绑定是printf的got表中存放的是printf@plt+6的地址,绑定过后got存放printf函数的真实地址。
接下来函数取got表中数据进行转跳,转跳回了printf@plt+6的地址,接着执行push 0;push dword ptr [_GLOBAL_OFFSET_TABLE+4]<0x804a004>;这两条指令的含义待会解释。再转跳到dl_runtime_resolve进行函数的绑定

上面我们简单的介绍了函数重定向的流程。但真正对函数进行符号查找和重定位的函数是进入dl_runtime_resolve之后的_dl_fixup这个函数。
因此为实现对函数重定向的攻击,我们必须对_dl_fixup的源码进行分析。可以到https://ftp.gnu.org/gnu/glibc/下载glibc源码,以下的分析都是glibc-2.27版本的源码并且都是32位。64位我将在分析完32位后单独介绍。

忽略掉宏定义_dl_fixup有struct link_map *l和ElfW(Word) reloc_arg两个参数。之前的push dword ptr [_GLOBAL_OFFSET_TABLE+4]<0x804a004>;push 0就分别对应着这两个参数。
push dword ptr [_GLOBAL_OFFSET_TABLE+4]<0x804a004> 就是 将link_map地址压入栈中。
push 0 就是 将reloc_arg压入栈中。

32位、64位都一样,都是将参数压入栈中

.dynamic
.dynamic在IDA中的模样,在dl_runtime_resolve攻击中,我们只需要着重关注DT_SYMTAB,DT_STRTAB,DT_JMPREL,DT_VERSYM(64位下才需要关注)。

.dynamic结构体(在glibc-2.27/elf/elf.h中)

.dynsym
.dynsym在IDA中长这个模样,在上面的.dynamic中的DT_SYMTAB结构体中的d_ptr对应着.dynsym地址。

.dynsym结构体

.dynstr
.dynstr在IDA中长这个样子,同样在上面的.dynamic中的DT_STRTAB结构体中的d_ptr对应着.dynstr地址

.rel.plt
.rel.plt在IDA中是这个样子,在上面的.dynamic中的DT_JMPREL结构体中的d_ptr对应着.rel.plt地址

.rel.plt结构体

以下就是_dl_fixup用于函数重定向的代码,展示了函数重定向的流程。

通过阅读_dl_fixup源码可以总结出一般的函数重定向流程可简略如下:
1.通过struct link_map *l获得.dynsym、.dynstr、.rel.plt地址
2.通过reloc_arg+.rel.plt地址取得函数对应的Elf32_Rel指针,记作reloc
3.通过reloc->r_info和.dynsym地址取得函数对应的Elf32_Sym指针,记作sym
4.检查r_info最低位是否为7
5.检查(sym->st_other)&0x03是否为0
6.通过strtab+sym->st_name获得函数对应的字符串,进行查找,找到后赋值给
rel_addr,最后调用这个函数

从上面的攻击流程中可以发现程序通过一系列操作最终取得函数名称对应的字符串来进行重定位,我们只要任意修改地址、偏移量使得程序最终取得我们伪造的字符串,就可以完成攻击。其实了解了函数函数重定向的流程,就可以先自己尝试着去PWN一些一般的32位ret2dl_resolve的题目。
下面讲述一些ret2dl_resolve常用的几种攻击手段,仅作为提供一种攻击思路的作用,重要的还是自己明白函数重定向的流程并针对其做出攻击。

第一种攻击手法:伪造函数名称对应字符串所在地址

获取函数名称对应的字符串:strtab+sym->st_name
这里就产生了两个可能的攻击对象,一个是strtab,另一个就是sym->st_name
我们可以先想程序bss段写入'system',再修改strtab或者sym->st_name,使strtab+sym->st_name指向'system'所在地址。
不过修改时需要注意DT_STRTAB和sym->st_name所在的地址有可写权限。
可以通过gdb中的vmmap来查看是否有可写权限
这个攻击手法不管是32位还是64位都能用。

第二种攻击手法:32位下的伪造reloc_arg,伪造结构体
宏观的从函数重定向流程来看,程序根据reloc_arg和各个section的地址来取得偏移量,最终定位到函数名称所对应的字符串地址。
既然reloc_arg是存放在栈中的,我们可以伪造reloc_arg和Elf32_Sym等结构体,通过虚假的reloc_arg引导程序指向我们伪造的结构体,进而取得我们伪造的偏移量,最终取得伪造的函数字符串。
除此之外,在伪造结构体的过程中,我们还要注意程序对reloc->r_info最低位、sym->st_other的检测

这个攻击手法有一定的限制,32位能够随便用,64位有大概率导致失败,具体原因下面将会讲

example:XMAN 2016-LEVEL 3
程序流程非常简单,直接让你输入来个栈溢出。
这道题虽然有write函数可以直接泄露,但我们这边可以通过ret2dl_resolve来不泄露直接劫持程序流程到system函数。

我们在bss段上构造.dynsym、.dynstr、.rel.plt,将三个伪造的结构体放在一起后,在开头放个数据作为伪造的reloc_arg和push link_map指令的地址,利用stack pivot便可以让程序将我们伪造的数据作为reloc_arg,最终让程序重定向到我们需要的函数。

在构造结构体的时候,一定要注意输入结构体的位置。由于我们最后使用stack pivot使得栈降到了bss段,这个程序从0x804a000-0x804b000有可写权限,如果输入结构体的位置离0x804a000太近的话,在重定向过程中可能esp小于0x804a000不可写而导致攻击失败。输入结构体的位置最好是在0x804a000+0x800的位置。

下面是我写的辣鸡脚本,适当的做了点注释,如果不太会的话可以参考以下

在64位中,_dl_fixup的逻辑没有改变,但一些相关的变量和结构体发生了变化。

在glibc-2.27/sysdeps/x86_64/dl-runtime.c中定义了

我们可以发现reloc_arg不再像32位中作为偏移量来使用,而是作为.rel.plt的数组下标存在。

另外,Elf32_Sym升级为Elf64_Sym,Elf32_Rel升级为Elf64_Rela(注意结构体大小的改变),Elf32_R_SYM、Elf32_R_TYPE定义升级为Elf64_R_SYM、Elf64_R_TYPE

之前的32位下的伪造reloc_arg,伪造结构体方法在64位下并不适用,原因就在下面这段代码中

程序会先取.dynamic中的DT_VERSYM所在的地址判断是否为0。
接着取DT_VERSYM结构体的d_ptr赋值给指针变量vernum。
将(reloc->r_info)>>32作为vernum下标取值。
问题就在这边出现了!这边容易出现非法内存地址访问的问题。

const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
在取得函数所对应的Elf64_Sym过程中,也是将(reloc->r_info)>>32作为下标。
64位程序bss段被映射到0x600000,我们伪造的.dynsym也在0x600000+,而DT_SYM结构体中的d_ptr还是在0x400000,这就意味着我们的r_info必然很大,再加上数据类型大小的不同,导致(reloc->r_info)>>32作为vernum下标取值时十分容易访问到0x400000和0x600000之间的不可读区域

下面通过gdb的调试来更好的说明这点

64位下0x401000-0x600000为不可读区域


symtab+(r_info>>32)*24得到函数所对应的Elf64_Sym地址


r10存放link_map地址
l->l_info[VERSYMIDX (DT_VERSYM)]对应着link_map+0x1c8
rdx存放(r->info>>32)的值
*(vernum+(r_info>>32)*2)

我随手找了个64位的程序算了下
const ElfW(Sym) const symtab = 0x400280
const ElfW(Half)
vernum = 0x4003d8
(r_info>>32)作为symtab的下标时,其值大于0x600000小于0x601000,也就是说0x1553a<(r_info>>32)≤0x15fe5
(r_info>>32)作为vernum下标时,要使得不产生非法内存访问,需要0≤(r_info>>32)≤0x614或者0xffe14≤(r_info>>32)≤0x100614
无法找到同时满足二者的(r_info>>32)的值
因此,对待64位程序如果像之前的32位程序那样构造的话,很容易攻击失败。

第三种攻击手法:64位下的修改reloc_arg,伪造结构体
我们之前分析了64位下无法像32位下那样修改reloc_arg,伪造结构体的原因

既然进入这个if语句会出错,那我们就想办法让程序不进入这个if语句
从之前的gdb调试的图片中我们也可以发现l->l_info[VERSYMIDX (DT_VERSYM)]对应着link_map+0x1c8(对应的,32下是link_map+0xe4)
我们需要先泄露link_map地址,再将link_map+0x1c8设置成不为0
之后就是和32位下的思路一样了,根据64位下的结构体伪造结构体,伪造reloc_arg来进行攻击。

不过有一点很尴尬,这么做的前提是必须要能供泄露地址,但既然都能泄露地址了,我为什么还要费那么大力气来攻击函数重定向呢,就我个人而言认为这个思路有点鸡肋。但也确实是一种思路,说不定什么时候能用到。
第三种攻击手法仍需要泄露信息,接下来第四种攻击手法不需要泄露信息就可以完成64位下的函数重定向攻击(不过需要知道libc的版本)。

第四种攻击手法:伪造link_map(需要知道libc版本)

在第三种攻击手法中也说了,当(sym->st_other)&0x03 == 0时,我们还需要将link_map+0x1c8设置为非0。
在这里来看看我们之前忽略掉了else语句,DL_FIXUP_MAKE_VALUE用来计算出函数的真实地址,我们只要将(sym->st_other)&0x03设置为非0,进入else语句,l->l_addr + sym->st_value指向system语句即可进入system函数。
那么问题就来了,我们并不知道system函数的真实地址。我们可以这样做,让sym->st_value落在某个已经解析了的函数got表上,l->l_addr设置为system函数和这个已经解析的函数的偏移值。另外,sym->st_value落在某个已经解析了的函数got表上,说明这个函数对应的sym = 这个got表地址-8,通常而言sym对应着另外一个函数的got表地址,这种情况你需要确保另外一个函数也是已经解析过的,此时sym->st_other一般为0x7f,才能保证(sym->st_other)&0x03 != 0。如果sym不是对应着另一个函数的got表,需要确保(*(sym+5))&0x03 != 0。

我们需要将l->l_addr设置成我们想要的值,又不用泄露link_map地址,这就要求我们来伪造link_map结构体。我们还需要控制symtab和reloc->r_info,因此我们还要伪造位于link_map+0x70的DT_SYMTAB指针、link_map+0xf8的DT_JMPREL指针,另外strtab必须是个可读的地址,因此我们还需要伪造位于link_map+0x68的DT_STRTAB指针。之后就是伪造.dynamic中的DT_SYMTAB结构体和DT_JMPREL结构体以及函数所对应的Elf64_Rela结构体。为了方便,我在构造的过程中一般将reloc_arg作为0来进行构造。
总的来说要满足以下几个条件:
1.link_map中的DT_STRTAB、DT_SYMTAB、DT_JMPREL可读
2.DT_SYMTAB结构体中的d_ptr即sym,(*(sym+5))&0x03 != 0
3.(reloc->r_info)&0xff == 7
4.rel_addr = l->addr + reloc->r_offset即原先需要修改的got表地址有可写权限
5.l->l_addr + sym->st_value 为system的地址

example:XMAN 2016-LEVEL3_64
这个程序和上一个例子差不多,就是变成了64位,尝试在不使用write函数的前提下对函数重定向完成攻击。
可以尝试使用伪造link_map的方法来完成攻击。
libc就直接用本地的libc好了

下面是本菜鸡写的脚本:

这个方法需要知道libc版本后才能计算出system的地址。

这些东西的话在理解了原理之后就是慢慢得撸偏移,反正我当初第一次搞这东西调了好久才调对。


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2019-8-19 00:45 被g3n3rous编辑 ,原因:
上传的附件:
收藏
免费 6
支持
分享
最新回复 (2)
雪    币: 0
活跃值: (89)
能力值: ( LV6,RANK:96 )
在线值:
发帖
回帖
粉丝
2
tql
2019-8-17 13:20
0
雪    币: 0
活跃值: (115)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
感谢作者分享。CTF-Wiki 伪造Link map部分是对ctf-wiki很好的补充。ret2dlresolve这个攻击看下来,感觉还是很鸡肋啊。有输出函数可以利用时,根本不需要用它;没输出的时候,还不能有full RELRO。 最后撸偏移还简直就是苦力活。
2021-4-3 22:07
0
游客
登录 | 注册 方可回帖
返回
//