-
-
[原创] SoFixer导入表问题修复及ELF解析简记
-
发表于: 2024-6-20 20:01 2624
-
一. 简介
在多次使用 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) { // I don't known other so info, if i want to fix it, I must dump other so file 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); } //printf("type:0x%x offset:0x%x -- symname:%s nIndex:%d\r\n", type, rel->r_offset, symname, nIndex); } } 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 文件解析
elf 文件格式绝对是老生常谈的话题了,这里主要记录一下导入表,重定位表,plt,got表,以及它们在IDA中的解析。
对于文件格式的解析,相信大多数人一开始会时用010Editor和对应文件格式模板查看文件的结构。Elf格式的模块能很好地解析出节表对应的内容,但不能直接解析动态段的内容,所以动态段中的内容学习及认识起来没有节表简单直观。
为方便学习和查看,可以将动态段中内容一次性读出来,在内存中或者直接打印出来查看。SoFixer 中 ReadSoInfo 函数和 linker.cpp 的 prelink_image 函数有将动态段内容读取并存储到 soinfo 变量中,可以参考一下。如果是直接查看,可以直接使用 readelf -d xxx.so 和 objdump -x xxx.so 获得动态段内容,也可以在ida中跳转到动态段的偏移处查看动态段的信息。
1. 导入表
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首地址从这里开始。
2. plt/got 表
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表对应位置还是能正常显示函数符号。
3. 重定位表
通过一个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 # [18] .data.rel.ro PROGBITS 0000000000032d30 00031d30 这是数据节,不属于重定位表 # 动态段 readelf -d libnative-lib2.so | grep REL 0x0000000000000002 (PLTRELSZ) 1992 (bytes) #JMPREL 重定位表的大小 0x0000000000000014 (PLTREL) RELA #指定了 PLTREL 的类型是 RELA 0x0000000000000017 (JMPREL) 0xe5c8 #对应节表中 .rela.plt 节 0x0000000000000007 (RELA) 0x6858 #对应节表中 .rela.dyn 节 0x0000000000000008 (RELASZ) 32112 (bytes) #RELA 重定位表的大小, 1338 个单元 0x0000000000000009 (RELAENT) 24 (bytes) #RELA 中每个元素占的字节大小 0x000000006ffffff9 (RELACOUNT) 886 #Elf64_Rela中r_addend非零的单元的数量 |
从地址上看.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中对应三个字段:
三. 小节
笔者能力有限,如果有写的不对的地方,还请多批评指正!
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)