【原创】ELF 导入表/导出表 加固原理分析与实现
2021-08-13 18:37:10 Author: mp.weixin.qq.com(查看原文) 阅读量:443 收藏

作者坛账号:fnv1c

ELF格式是被广泛应用的可执行文件、共享库格式。然而ELF文件的加固技术相对于PE文件而言,仍然较为落后。近年来,随着安卓系统的推广,承载native so的ELF格式也日趋流行,因而ELF文件加固技术有了较大提升。本文将讨论ELF动态库的导入/导出表加密的原理与实现(ELF加固的关键环节)。
理解此文可能需要的前置知识:ELF基本概念,ELF动态链接基本概念。
注:本文阐述的导入/导出对象为函数,实验平台为X64 Linux。

众所周知,很多可执行文件都用到了外部库提供的函数。那么函数又是如何被可执行文件找到,如何被调用的呢?这就涉及到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两个表,这样还引入了一次间接跳转,为什么不用性能更高的方法呢  

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



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),和相关符号信息。


随后动态链接器找到symtab,第5条即printf的符号信息,可以看到其记录了"printf"在strtab的位置,动态链接器获得符号名,进行解析。  

我们知道,.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。

上文的加密方法十分巧妙,但也有脆弱性。首先,关键函数调用前,.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的函数的反编译结果也会出错。

之前我一直错误地认为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的对应内容。



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


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

之前CSAPP的动态链接部分看得人一知半解,动手实现才发现其中奥秘。也算是“ 纸上得来终觉浅,绝知此事要躬行。”

如果你想了解更多,不妨看看下面内容
dl-resolve
.dynamic
符号表hash
符号表hash
FULL RELRO

--

www.52pojie.cn

--

pojie_52


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5Mjc3MDM2Mw==&mid=2651136288&idx=1&sn=5e02daffeb28ca83622f75c8f4edd03e&chksm=bd50b1748a273862b66fa5b63f93948c054b963cc7d4d6d7a024bafa28c13a2d9bdf3b853764#rd
如有侵权请联系:admin#unsafe.sh