首页
社区
课程
招聘
[原创] SoFixer导入表问题修复及ELF解析简记
发表于: 2024-6-20 20:01 2667

[原创] SoFixer导入表问题修复及ELF解析简记

2024-6-20 20:01
2667

一. 简介

在多次使用 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期)

最后于 2024-6-20 20:25 被Denny Chen编辑 ,原因:
收藏
免费 5
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//