修改so导出函数名称

最近有一个需求是将某so(无源码)的某些函数名称改掉。搜遍网络没有直接的解决方案,最后是综合多个地方的资料和建议及类似代码,自己弄出了个不通用的解决方案。

首先,so没有源码,所以不可能直接修改导出函数,一开始的思路就是下面这两种解决方案:

  1. 通过ida将so反编译成源码,然后再手动整理成可编译的代码,再去改。
  2. so是ELF格式的,直接以二进制方式修改这个ELF文件应该可以达到目的。

以上两种方法中,1最彻底,实行完之后代码就是可维护可更新的了,以后加什么新功能或其它改动都可以。但我使用ida及Hex-ray插件生成c伪代码后看了看代码就放弃了。一个264KB的so,反编译出来了795KB的代码,这你敢信。。。 反编译出来的代码中大量的sub_xxx函数,还结合各种汇编代码,各种跳转,除了调用其它so以及自身的导出函数的名称是可见的,其它一概不可见。预计恢复难度较高,至少在一周的工作时间,所以暂时搁置了。

然后就研究第2种修改ELF格式的方法,首先查找了一些资料如下:

以及elf分析的相关工具:

也找到同样尝试修改导出函数名称的人,还有半成品代码:

然后花了点时间熟悉了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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .note.gnu.build-i NOTE 00000114 000114 000024 00 A 0 0 4
[ 2] .hash HASH 00000138 000138 002210 04 A 3 0 4
[ 3] .dynsym DYNSYM 00002348 002348 0047b0 10 A 4 3 4
[ 4] .dynstr STRTAB 00006af8 006af8 012751 00 A 0 0 1
[ 5] .gnu.version VERSYM 0001924a 01924a 0008f6 02 A 3 0 2
[ 6] .gnu.version_r VERNEED 00019b40 019b40 000040 00 A 4 2 4
[ 7] .rel.dyn REL 00019b80 019b80 000250 08 A 3 0 4
[ 8] .rel.plt REL 00019dd0 019dd0 000260 08 AI 3 9 4
[ 9] .plt PROGBITS 0001a030 01a030 0003a4 04 AX 0 0 4
[10] .text PROGBITS 0001a3d4 01a3d4 0180e0 00 AX 0 0 4
[11] .rodata PROGBITS 000324b4 0324b4 0021ac 00 A 0 0 4
[12] .ARM.extab PROGBITS 00034660 034660 003d24 00 A 0 0 4
[13] .ARM.exidx ARM_EXIDX 00038384 038384 0025a0 00 AL 10 0 4
[14] .init_array INIT_ARRAY 00040c74 040c74 00000c 00 WA 0 0 4
[15] .fini_array FINI_ARRAY 00040c80 040c80 000008 00 WA 0 0 4
[16] .data.rel.ro PROGBITS 00040c88 040c88 0000b8 00 WA 0 0 8
[17] .dynamic DYNAMIC 00040d40 040d40 000128 08 WA 4 0 4
[18] .got PROGBITS 00040e68 040e68 000194 04 WA 0 0 4
[19] .data PROGBITS 00041000 041000 000070 00 WA 0 0 4
[20] .bss NOBITS 00041070 041070 0029a4 00 WA 0 0 4
[21] .comment PROGBITS 00000000 041070 000027 01 MS 0 0 1
[22] .ARM.attributes ARM_ATTRIBUTES 00000000 041097 000031 00 0 0 1
[23] .shstrtab STRTAB 00000000 0410c8 0000dd 00 0 0 1

其中 [ 3] .dynsym[17] .dynamic都引用的是索引为4的[ 4] .dynstr STRTAB。那么问题很明显了,应该是导入表和导出表共用了字符表,并且共用了同一个字符串,只不过索引不同。在修改函数名称时,因为是将example替换成了abcdef后,将后面的_test_TestJni_mprotect前移了1位,导致mprotect这个符号引用的name指向了protect。

那么如果想解决这个问题,还得在修改字符表后,枚举所有引用了同一个字符表的section,并查看它里面的每一个符号引用的字符串是某是被修改并移位过的,也要修改它的st_name,然后保存。

已经在这个问题上折腾了几天的我不想再继续倒腾了,所以就调整了下替换字符串的长度,把abcdef换成了abcdefg,这样的话所有符号的st_name索引都不会变化了。

如果以后有机会,会把未完成的功能做完:

  1. 支持短字符替换成长字符。
  2. 替换字符串并移位后检查所有引用同一字符串的符号,修改其st_name。
  3. 测试对于不同平台上的的so的兼容性。
  4. 支持GCC编译出来的GNU_HASH(没有.hash段)的Section。

最后附上代码:https://github.com/k1988/ELFkickers/tree/master/redefine