ELF 导入表/导出表 加固原理分析与实现

查看 145|回复 11
作者:fnv1c   
0x01 前言
ELF格式是被广泛应用的可执行文件、共享库格式。然而ELF文件的加固技术相对于PE文件而言,仍然较为落后。近年来,随着安卓系统的推广,承载native so的ELF格式也日趋流行,因而ELF文件加固技术有了较大提升。本文将讨论ELF动态库的导入/导出表加密的原理与实现(ELF加固的关键环节)。
理解此文可能需要的前置知识:ELF基本概念,ELF动态链接基本概念。
注:本文阐述的导入/导出对象为函数,实验平台为X64 Linux。
0x02 剖析符号动态解析(延迟绑定)
众所周知,很多可执行文件都用到了外部库提供的函数。那么函数又是如何被可执行文件找到,如何被调用的呢?这就涉及到ELF动态链接的知识了。ELF对外部函数的处理默认采用了延迟绑定技术,也就是说当且仅当用到此函数时,才会解析此函数地址(也有例外,例如开启FULL RELRO时,启动时即解析所有符号,本文不讨论此情况)。
这样做的好处是显然的,一方面缩短了应用程序启动时间(不需要在启动时解析所有导入的符号),另一方面不会造成过多的运行时开销。
导入函数的延迟绑定主要又两个表实现,一个是PLT表,一个是GOT表。PLT表负责处理函数解析与调用,GOT表存储解析后函数地址。下面演示延迟绑定的流程。
例如调用函数abc时 主程序:abc() 被编译为 call abc@plt (也就是说,plt段实际上存储的是与延迟绑定相关的指令。)
随后进入 abc@plt : jmp *(abc@got); push 123; jmp resolve_sym;
首先进行了间接跳转,跳到了GOT表中abc对应项目存储的地址。
如果abc已经被解析了,那么就会跳到abc函数的真实地址。
如果abc没有被解析 ,GOT中abc对应地址实际为abc@plt+6 也就是jmp指令后push 123;jmp resolve_sym对应的地址。 这对延迟绑定的实现至关重要。
resolve_sym会将GOT[1] (本ELF的link_map)压栈,并调用GOT[2] (_dl_runtime_resolve),对用到的符号进行首次解析。实际参数为_dl_runtime_resolve(GOT[1] (link_map),123);
动态链接器会通过link_map和123这个数字找到需要的符号,并解析调用,同时改写abc对应的GOT项目,使其指向abc函数真实地址。  

想一想:为什么使用PLT和GOT两个表,这样还引入了一次间接跳转,为什么不用性能更高的方法呢  

0x03 符号动态解析的静态分析
不管你是否看懂0x02的内容,相信你都不明白为什么_dl_runtime_resolve(GOT[1] (link_map),123);能成功地找到abc并且调用他。当然,123只是一个序号(reloc_index),需要配合ELF中的其他数据完成对符号的解析。
直接IDA分析比纸上谈兵要容易理解地多。下面以对printf函数的调用为例


图片.png (33.08 KB, 下载次数: 0)
下载附件
2021-8-6 00:56 上传



图片.png (26.17 KB, 下载次数: 1)
下载附件
2021-8-6 00:56 上传

link_map是包括ELF基址,dynamic段地址等信息的结构。
struct link_map  {    ElfW(Addr) l_addr;                /* ELF基址  */   
char *l_name;                     /* SONAME  */   
ElfW(Dyn) *l_ld;                  /* Dynamic段地址  */   
struct link_map *l_next, *l_prev; /* link_map链表,包含所有加载的动态库的link_map  */  
};
符号动态解析主要与.dynamic段的strtab,symtab,jmprel有关。  reloc_index指导动态链接器寻找本ELF的.dynamic段的jmprel节,找到其中的第reloc_index(6)条,其中记录了printf的got表偏移,printf函数对应的symtab_index(5),和相关符号信息。


图片.png (86.48 KB, 下载次数: 1)
下载附件
2021-8-6 00:57 上传



图片.png (72.72 KB, 下载次数: 1)
下载附件
2021-8-6 00:57 上传



图片.png (54.95 KB, 下载次数: 1)
下载附件
2021-8-6 00:57 上传



图片.png (27.43 KB, 下载次数: 1)
下载附件
2021-8-6 00:57 上传



图片.png (112.63 KB, 下载次数: 0)
下载附件
2021-8-6 00:57 上传

随后动态链接器找到symtab,第5条即printf的符号信息,可以看到其记录了"printf"在strtab的位置,动态链接器获得符号名,进行解析。  
0x04 导入表加密之.dynamic部分加密
我们知道,.dynamic在动态库符号解析中,发挥着重要的作用。导入表加密的第一思路一定是在.dynamic段做文章。我们可以将关键的导入函数在.dynamic段的strtab节中对应的符号名加密。并且在函数被调用前,将strtab节中对应字符串解密,得益于延迟绑定特性,我们仍然能查找到正确的符号。但是对导入的静态分析却完全损坏了。  我们编写test.so,在constructor中使用printf函数。编译保存,用ida的patch功能将strtab节对应字符串"printf"改为"114514",保存。执行test.so,发现报错,找不到符号"114514"。


图片.png (16.22 KB, 下载次数: 1)
下载附件
2021-8-6 00:59 上传



图片.png (18.24 KB, 下载次数: 1)
下载附件
2021-8-6 00:59 上传

然后编写decrypt_import函数,通过dlinfo获取link_map,从而得到.dynamic段地址,寻找strtab节,将114514替换回"printf",之后再调用printf,发现一切正常。


图片.png (167.65 KB, 下载次数: 0)
下载附件
2021-8-6 00:59 上传



图片.png (33.25 KB, 下载次数: 0)
下载附件
2021-8-6 01:00 上传

静态分析可以发现,ida已经将114514识别为一个外部函数,imports中找不到printf。


图片.png (12.64 KB, 下载次数: 1)
下载附件
2021-8-6 01:00 上传

  
0x05 导入表加密之GOT劫持
上文的加密方法十分巧妙,但也有脆弱性。首先,关键函数调用前,.dynamic中内容已经被解密,且不会再恢复加密状态。其次,GOT表中会存储真实函数地址,动态调试可以恢复真实导入表。那么,如何规避这两点问题?  我们知道,GOT表存储的是函数真实地址,若没有被解析,则指向函数解析的相关代码。如果我们劫持GOT表地址,指向我们定义的函数,会如何?
程序会忽略掉延迟绑定的全套流程,直接跳到我们的自定义函数。我们可以根据这一特性实现导入表加密。
编写test.so,在constructor中使用puts函数。编译保存。通过ida的patch功能将puts函数对应的symtab项修改,改成__stk_chk_failed的sym项,保存。
再编写fix_got函数,修改puts@got为puts_proxy。在puts_proxy中解析puts函数地址并调用。  


图片.png (114.44 KB, 下载次数: 0)
下载附件
2021-8-6 01:00 上传



图片.png (28.92 KB, 下载次数: 1)
下载附件
2021-8-6 01:01 上传

尝试运行,成功。


图片.png (24.2 KB, 下载次数: 1)
下载附件
2021-8-6 01:01 上传

打开ida,逆向constructor,发现完全看不到使用puts的痕迹,imports也无puts。用到puts的函数的反编译结果也会出错。


图片.png (12.43 KB, 下载次数: 0)
下载附件
2021-8-6 01:02 上传



图片.png (9.02 KB, 下载次数: 2)
下载附件
2021-8-6 01:02 上传



图片.png (18.53 KB, 下载次数: 0)
下载附件
2021-8-6 01:02 上传



图片.png (10.32 KB, 下载次数: 1)
下载附件
2021-8-6 01:02 上传


0x06 导出表加密
之前我一直错误地认为ELF和PE格式一样,无法加密导出表,直到遇到了这个奇怪壳。
如果你已经对elf的符号查找机制掌握透彻,也能想当然地得出导出表加密的方案。即对.dynamic动手脚。
上文分析的奇怪壳子用的是.dynamic重建,本文讨论.dynamic加密。
实际上,导出表查找也依赖dynamic段的symtab节,先通过hash链表找到可能的symtab_index,再依次查找,如果找到那么完成。我们可以先加密symtab节中的重要符号,然后在动态库被加载后解密symtab节,这样就实现了运行时可加载,但静态分析找不到的导出表加密。
编写test.so,hide_me为隐藏关键函数,编译,用ida修改symtab,将其对应的strtab的"hide_me"改为"114514",编写fix_export函数,解密strtab的对应内容。


图片.png (150.72 KB, 下载次数: 0)
下载附件
2021-8-6 01:02 上传



图片.png (3.95 KB, 下载次数: 1)
下载附件
2021-8-6 01:03 上传

编写load.c,导入test.so并且通过dlsym查找hide_me并调用。尝试运行,成功。


图片.png (14.67 KB, 下载次数: 0)
下载附件
2021-8-6 01:03 上传



图片.png (10.89 KB, 下载次数: 1)
下载附件
2021-8-6 01:04 上传

静态分析软件显示test.so的exports中没有hide_me,只有114514


图片.png (3.22 KB, 下载次数: 0)
下载附件
2021-8-6 01:03 上传



图片.png (61.44 KB, 下载次数: 1)
下载附件
2021-8-6 01:03 上传

0x07 结语
之前CSAPP的动态链接部分看得人一知半解,动手实现才发现其中奥秘。也算是“ 纸上得来终觉浅,绝知此事要躬行。”
0x08 参考链接
如果你想了解更多,不妨看看下面内容
dl-resolve
.dynamic
符号表hash
符号表hash
FULL RELRO

下载次数, 函数

fnv1c
OP
  


dogydogyfly 发表于 2021-8-20 16:50
嗯 感谢解答
我想知道 为什么一定需要indirect jmp 这一步 这相当于还是通过代码段到plt再到got 只是时 ...

我网上查了下资料,没找到答案。我个人觉得这个和linker的实现相关。
可重定位目标文件调用外部函数的时候是call blabla(e8 00 00 00 00),再声明一个elf重定位项,链接的时候把00改成适当的offset。如果你链接的目标文件里有blabla的定义,链接器就直接把00改成到那个函数的偏移,如果没有,再改成到plt表的偏移。假设直接代码里indirect jmp,就没办法处理前者的情况了。
也就是说当你把c文件编译成可重定位目标文件时,编译器把没定义的函数都一视同仁了,他也不知道是外部函数,还是链接阶段你会给出定义了这个函数的目标文件,所以就这么处理了
fnv1c
OP
  


dogydogyfly 发表于 2021-8-17 19:38
有一点不太懂
为什么延迟绑定这块一定需要plt
直接用got缓存地址不可以吗。。。

ELF加载到内存之后执行之前,一般是不会解析导入的符号的,got表默认指向的就是plt表与延迟绑定相关的部分。直到用到时才解析所以才叫延迟绑定。
如果开了FULL RELRO,倒是会提前解析所有符号地址放进got表,但也要走一次plt表的indirect jmp
菠萝蜜   

感谢分享!
xiahhhr   

感谢分享!!!
kuagnkuangkuang   

感谢分享教程
艾莉希雅   

这段代码怎么有味道啊(恼)
CNEggplant   

感谢楼主分享!
xiaoniba2016   

感谢分享教程
kimay5211   

感谢分享教程
您需要登录后才可以回帖 登录 | 立即注册

返回顶部