在多次使用 F8 大佬的 SoFixer 时,碰到导入符号对不上的问题(作者已知的问题),所以就学习了下这个工具的源码并做了简单的修复 github链接。这里简单记录一下修复过程及学习过程中的对elf文件格式新增的体会。
修复的过程主要针对 ElfRebuilder.cpp 中的 relocate 函数。
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 27 28 29 30 31 | switch (type) {
case R_386_RELATIVE:
case R_ARM_RELATIVE:
*prel = *prel - dump_base;
break ;
case 0x401:
case 0x402:{
auto syminfo = si.symtab[sym];
if (syminfo.st_value != 0) {
*prel = syminfo.st_value;
} else {
auto load_size = si.max_load - si.min_load;
if (mImports.size() == 0){
*prel = load_size + external_pointer;
external_pointer += sizeof (*prel);
} else {
const char * symname = si.strtab + syminfo.st_name;
int nIndex = getIndexOfImports(symname);
if (nIndex != -1){
*prel = load_size + nIndex* sizeof (*prel);
}
}
}
break ;
}
default :
break ;
}
|
从打印的属性和符号名来看,0x401也有出现导入符号相关类型,所以这里将 case 0x401 也加上了,不过不加的话,在对应的偏移出好像也能解析出对应符号。IDA中解析导入表时,是按照导入表顺序,逐个创建函数声明(每个占用8字节),SoFixer 中修改导入表时,是按照读到的重定位表中导入符号顺序,逐个加8字节( sizeof(*prel) == 8 )递增赋值。读到的重定位表中导入符号顺序和导入表的顺序大部分相同,但也会有些出路,所以当导入符号数量大的时候,就会对应不上。
这里修复的方法是先将导入符号名按顺序保存到 vector 中,修复时,根据重定位处读到的对应的符号名到 vector 中取数组索引,这样就能保证和导入符号的顺序完全一致。
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 27 28 29 30 31 32 33 34 35 | int ElfRebuilder::GetIndexOfImports(std::string stringSymName){
int nIndex = 0 ;
for (auto& it : mImports){
std::string strImport = it;
if (strImport = = stringSymName){
return nIndex;
}
nIndex + + ;
}
return - 1 ;
}
/ / 将导入表的符号按顺序保存在 std::vector<std::string> mImports; 中,以便后面获得导入符号序号
void ElfRebuilder::SaveImportsymNames(){
Elf_Sym * symtab = si.symtab;
const char * strtab = si.strtab;
int nIndex = 0 ;
bool start = false;
while (true){
Elf_Sym sym = symtab[nIndex];
if (sym.st_name = = 0 && !start){
nIndex + + ;
continue ;
}
start = true;
if (sym.st_name ! = 0 && sym.st_value! = 0 ){
/ / 开始进到非导入表的符号,退出
break ;
}
const char * symname = strtab + sym.st_name;
mImports.push_back(symname);
/ / FLOGD( "NO:%d %s \r\n" , nIndex, symname);
nIndex + + ;
}
}
|
简单修复后,测试修复包含2000多个导入函数的so时,导入函数都能对上号。
elf 文件格式绝对是老生常谈的话题了,这里主要记录一下导入表,重定位表,plt,got表,以及它们在IDA中的解析。
对于文件格式的解析,相信大多数人一开始会时用010Editor和对应文件格式模板查看文件的结构。Elf格式的模块能很好地解析出节表对应的内容,但不能直接解析动态段的内容,所以动态段中的内容学习及认识起来没有节表简单直观。
为方便学习和查看,可以将动态段中内容一次性读出来,在内存中或者直接打印出来查看。SoFixer 中 ReadSoInfo 函数和 linker.cpp 的 prelink_image 函数有将动态段内容读取并存储到 soinfo 变量中,可以参考一下。如果是直接查看,可以直接使用 readelf -d xxx.so 和 objdump -x xxx.so 获得动态段内容,也可以在ida中跳转到动态段的偏移处查看动态段的信息。
so 文件中所有符号都存储在动态符号表,IDA中按照符号属性,将符号划分在导入和导出表。动态符号表自第三项开始,所有 sym_value(代码结构体中是字段st_value)为0的符号是导入符号,IDA会将这些符号按照动态符号表中顺序逐个加到导入表中,如下:

导入符号在so文件中并不存在对应的函数体或数据,所以IDA专门模拟了一段外部段(extern)来记录这些符号的名称及地址,如下:

这些导入函数的地址是按照导入表顺序逐个排列的,每个占用8字节。那这个extern段的首地址是怎么来的呢?IDA会按照节中的 s_addr 为需要空间的段分配一段空间(有的不用空间来展示的,就不用,如:shstrtab节),分配完最后一个后,紧接着便是extern的空间。SoFixer中修复so后,data节之后正好是 si.max_load - si.min_load,所以导入表extern首地址从这里开始。
plt节全名是 Procedure Linkage Table,是程序代码和got表符号调用的过渡程序,可用于延迟绑定got表符号,但其实延迟绑定的机制在Android中应该是很少用,看到很多应用的so的 FLAGS 都是 BIND_NOW,也就是加载so时就绑定符号,不会延迟。
plt节中函数会通过got表上函数符号地址所在分页首地址(adrp指令的作用),加上对应偏移获取对应的函数地址。从本地文件和内存中代码对比来看,plt 表不存在重定位的情况,代码上可以理解为adrp获取分页首地址是根据符号自动获取,然后取相对偏移处内容,类似取相对偏移处内容。如下,机器码除了大小尾不同外,其它都一样:

从截图中也可以看出,plt表中连续的两个函数之间也只是加的偏移不一样,分别取got表中不同偏移处的函数地址进行调用。
got表中存储着符号地址,毫无疑问,这里的地址肯定是会被重写或者重定位的。直接010打开elf文件,可以发现,got表的前0x18字节都是0,后面存储的都是plt节对应的首地址,如下:

但是IDA中,跳到对应got表偏移处,发现对应的机器码并不是这个,如下:

如果将got表改掉,让ida识别不到真实的got表,那么ida中对应的偏移处的内容和elf文件中的内容又是一样的,如下:

这说明,IDA在识别到got表之后,有对其中的内容做修改,改成用户方便查看的方式,例如导入函数,本地函数等的地址加到got表上,就类似so动态加载时,linker为got上内容重定位或重写。解析重定位表就能获取got表对应偏移,符号名,重定位地址等的信息,IDA应该也是解析了重定位表,然后实现对got表处的内容重写。
如果got表没有或者找不到,但got对应地址处的内容有修改成对应符号的地址,则IDA会按照正常的代码去解析它的符号。F8大佬的SoFixer中就是这么做的,将所有重定位表对应地址的内容改写了(其中自然也包括got表符号的重定位),所以即使got表没修复,got表对应位置还是能正常显示函数符号。
通过一个demo,分别通过节和动态段查看如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | readelf -S libnative-lib2.so | grep rel
[ 8] .rela.dyn RELA 0000000000006858 00006858
[ 9] .rela.plt RELA 000000000000e5c8 0000e5c8
readelf -d libnative-lib2.so | grep REL
0x0000000000000002 (PLTRELSZ) 1992 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0xe5c8
0x0000000000000007 (RELA) 0x6858
0x0000000000000008 (RELASZ) 32112 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffff9 (RELACOUNT) 886
|
从地址上看.rela.dyn 节对应 RELA,.rela.plt 节对应 JMPREL,但其实两段重定位表也是首尾连接的,内存上是连成一块的(0x6858 + 32112 == 0xe5c8)。
重定位表的数据结构类型有Elf_Rel和Elf_Rela,对应REL和RELA两中类型的重定位表,后者相对前者多了个字段 r_addend,以64位为例,如下:
1 2 3 4 5 6 | typedef struct
{
Elf64_Addr r_offset; / * Address * /
Elf64_Xword r_info; / * Relocation type and symbol index * /
Elf64_Sxword r_addend; / * Addend * /
} Elf64_Rela;
|
r_offset 字段是在elf文件偏移,重定位修改的就是这个偏移处的内容,r_info 字段可以转换成重定位类型和符号索引,符号索引配合动态符号表可以找到对应符号,进而找到符号名和符号在文件中偏移位置(如果是导入符号,该值为0,如果是非导入的函数,偏移位置就是函数地址),有了r_offset和r_info之后,就能定位和修改符号信息了,IDA中修改got表部分的内容应该是根据这个改的。r_addend 字段记录了需要额外的的加数,一些重定位类型需要一个额外的加数来完成重定位,如TLS(Thread-Local Storage)模型中的一些情况(搜自chatgpt)。
可以在IDA中定位到重定位表的偏移,查看重定位表的情况,如下,每一单元(行)中分别是Elf_Rela中对应三个字段:

笔者能力有限,如果有写的不对的地方,还请多批评指正!
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2024-6-20 20:25
被Denny Chen编辑
,原因: