ELF 导入表/导出表 加固原理分析与实现
本帖最后由 fnv1c 于 2021-8-6 10:12 编辑# 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 (本ELF的link_map)压栈,并调用GOT (`_dl_runtime_resolve`),对用到的符号进行首次解析。实际参数为`_dl_runtime_resolve(GOT (link_map),123);`
动态链接器会通过link_map和123这个数字找到需要的符号,并解析调用,同时改写abc对应的GOT项目,使其指向abc函数真实地址。
> 想一想:为什么使用PLT和GOT两个表,这样还引入了一次间接跳转,为什么不用性能更高的方法呢
# 0x03 符号动态解析的静态分析
不管你是否看懂0x02的内容,相信你都不明白为什么`_dl_runtime_resolve(GOT (link_map),123);`能成功地找到abc并且调用他。当然,123只是一个序号(reloc_index),需要配合ELF中的其他数据完成对符号的解析。
直接IDA分析比纸上谈兵要容易理解地多。下面以对printf函数的调用为例
link_map是包括ELF基址,dynamic段地址等信息的结构。
```C
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),和相关符号信息。
随后动态链接器找到symtab,第5条即printf的符号信息,可以看到其记录了"printf"在strtab的位置,动态链接器获得符号名,进行解析。
# 0x04 导入表加密之.dynamic部分加密
我们知道,.dynamic在动态库符号解析中,发挥着重要的作用。导入表加密的第一思路一定是在.dynamic段做文章。我们可以将关键的导入函数在.dynamic段的strtab节中对应的符号名加密。并且在函数被调用前,将strtab节中对应字符串解密,得益于延迟绑定特性,我们仍然能查找到正确的符号。但是对导入的静态分析却完全损坏了。我们编写test.so,在constructor中使用printf函数。编译保存,用ida的patch功能将strtab节对应字符串"printf"改为"114514",保存。执行test.so,发现报错,找不到符号"114514"。
然后编写decrypt_import函数,通过dlinfo获取link_map,从而得到.dynamic段地址,寻找strtab节,将114514替换回"printf",之后再调用printf,发现一切正常。
静态分析可以发现,ida已经将114514识别为一个外部函数,imports中找不到printf。
# 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函数地址并调用。
尝试运行,成功。
打开ida,逆向constructor,发现完全看不到使用puts的痕迹,imports也无puts。用到puts的函数的反编译结果也会出错。
# 0x06 导出表加密
之前我一直错误地认为ELF和PE格式一样,无法加密导出表,直到遇到了这个[奇怪壳](https://www.52pojie.cn/forum.php?mod=viewthread&tid=1472169)。
如果你已经对elf的符号查找机制掌握透彻,也能想当然地得出导出表加密的方案。即对.dynamic动手脚。
上文分析的奇怪壳子用的是.dynamic重建,本文讨论.dynamic加密。
实际上,导出表查找也依赖dynamic段的symtab节,先通过hash链表找到可能的symtab_index,再依次查找,如果找到那么完成。我们可以先加密symtab节中的重要符号,然后在动态库被加载后解密symtab节,这样就实现了运行时可加载,但静态分析找不到的导出表加密。
编写test.so,hide_me为隐藏关键函数,编译,用ida修改symtab,将其对应的strtab的"hide_me"改为"114514",编写fix_export函数,解密strtab的对应内容。
编写load.c,导入test.so并且通过dlsym查找hide_me并调用。尝试运行,成功。
静态分析软件显示test.so的exports中没有hide_me,只有114514
# 0x07 结语
之前CSAPP的动态链接部分看得人一知半解,动手实现才发现其中奥秘。也算是“ 纸上得来终觉浅,绝知此事要躬行。”
# 0x08 参考链接
如果你想了解更多,不妨看看下面内容
(https://ypl.coffee/dl-resolve/)
[.dynamic](https://docs.oracle.com/cd/E23824_01/html/819-0690/chapter6-42444.html)
[符号表hash](https://docs.oracle.com/cd/E23824_01/html/819-0690/chapter6-48031.html)
[符号表hash](https://blogs.oracle.com/solaris/post/gnu-hash-elf-sections)
(https://www.redhat.com/en/blog/hardening-elf-binaries-using-relocation-read-only-relro) 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 于 2021-8-18 14:33 编辑
dogydogyfly 发表于 2021-8-17 19:38
有一点不太懂
为什么延迟绑定这块一定需要plt
直接用got缓存地址不可以吗。。。
ELF加载到内存之后执行之前,一般是不会解析导入的符号的,got表默认指向的就是plt表与延迟绑定相关的部分。直到用到时才解析所以才叫延迟绑定。
如果开了FULL RELRO,倒是会提前解析所有符号地址放进got表,但也要走一次plt表的indirect jmp
感谢分享! 感谢分享!!! 感谢分享教程 这段代码怎么有味道啊(恼) 感谢楼主分享! 感谢分享教程 感谢分享教程 二进制加密吗 大佬最好能搞个完整代码演示,必须支持,热心奉上