修改so导出函数名称
最近有一个需求是将某so(无源码)的某些函数名称改掉。搜遍网络没有直接的解决方案,最后是综合多个地方的资料和建议及类似代码,自己弄出了个不通用的解决方案。
首先,so没有源码,所以不可能直接修改导出函数,一开始的思路就是下面这两种解决方案:
- 通过ida将so反编译成源码,然后再手动整理成可编译的代码,再去改。
- so是ELF格式的,直接以二进制方式修改这个ELF文件应该可以达到目的。
以上两种方法中,1最彻底,实行完之后代码就是可维护可更新的了,以后加什么新功能或其它改动都可以。但我使用ida及Hex-ray插件生成c伪代码后看了看代码就放弃了。一个264KB的so,反编译出来了795KB的代码,这你敢信。。。 反编译出来的代码中大量的sub_xxx函数,还结合各种汇编代码,各种跳转,除了调用其它so以及自身的导出函数的名称是可见的,其它一概不可见。预计恢复难度较高,至少在一周的工作时间,所以暂时搁置了。
然后就研究第2种修改ELF格式的方法,首先查找了一些资料如下:
- ELF文件格式分析
- String Table Section格式
- http://www.cppblog.com/tqsheng/archive/2012/12/07/196107.aspx
- http://docs.oracle.com/cd/E26926_01/html/E25910/chapter6-48031.html#scrolltoc
以及elf分析的相关工具:
也找到同样尝试修改导出函数名称的人,还有半成品代码:
- how-to-rename-dynamic-symbols-in-arm-elf-so-file
- (http://binutils.sourceware.narkive.com/fZHiSvNm/objcopy-redefine-dynamic-symbols)
然后花了点时间熟悉了makefile及linux编译(没有使用Android Studio+NDK来实现,因为毕竟是工具类),再在github上一个elf工具集的基础上增加了个redefine工具。
看到这里,我假设你已经读了上面一些ELF相关资料,所以下面的解释对ELF相关术语不做详述。
一开始很顺利,找到类型为DYNSYM
的section,然后找到它对应的类型为STRTAB
的section,然后直接取出整个section,遍历里面的所有字符串(都是以\0结尾,和c字符串一样),然后将字符串修改成新的字符串,目前只支持将长字符串替换成短的,然后把\0前未替换的部分前移(如果替换前后一样长就不用前移了),在后面多余的地方补0。
替换完了后,使用dlsym加载发现找不到符号,然后就发现了* ELF格式可执行文件,更改符号名称要注意的地方,所以又改进了代码,加上了rehash的步骤。
然后测试修改用ndk生成的一个hellojni的示例后成功。但是在实际使用中发现了一个奇葩的问题,即比如原导出函数Java_com_example _test_TestJni_mprotect,被我替换成Java_com_abcdef_test_TestJni_mprotect,在加载时提示dlopen failed: cannot locate symbol "protect" referenced by "/data/app/com.xxxx.xxxx-2/lib/arm/libmxxxx_xxx.so"
。
dlopen failed: cannot locate symbol "protect" referenced by "/data/app/com.xxxx.xxxx-2/lib/arm/libmxxxx_xxx.so"
使用ida反编译修改后的so发现,有一个import function名为mprotect,在修改后变成了protect,再使用readelf查看section信息:
1 | Section Headers: |
其中 [ 3] .dynsym
和[17] .dynamic
都引用的是索引为4的[ 4] .dynstr STRTAB
。那么问题很明显了,应该是导入表和导出表共用了字符表,并且共用了同一个字符串,只不过索引不同。在修改函数名称时,因为是将example替换成了abcdef后,将后面的_test_TestJni_mprotect
前移了1位,导致mprotect这个符号引用的name指向了protect。
那么如果想解决这个问题,还得在修改字符表后,枚举所有引用了同一个字符表的section,并查看它里面的每一个符号引用的字符串是某是被修改并移位过的,也要修改它的st_name,然后保存。
已经在这个问题上折腾了几天的我不想再继续倒腾了,所以就调整了下替换字符串的长度,把abcdef换成了abcdefg,这样的话所有符号的st_name索引都不会变化了。
如果以后有机会,会把未完成的功能做完:
- 支持短字符替换成长字符。
- 替换字符串并移位后检查所有引用同一字符串的符号,修改其st_name。
- 测试对于不同平台上的的so的兼容性。
- 支持GCC编译出来的GNU_HASH(没有.hash段)的Section。
最后附上代码:https://github.com/k1988/ELFkickers/tree/master/redefine