https://bbs.kanxue.com/thread-191649.htm
ELF 文件结构
typedef struct { unsigned char e_ident[EI_NIDENT]; /* File identification. */ Elf32_Half e_type; /* File type. */ Elf32_Half e_machine; /* Machine architecture. */ Elf32_Word e_version; /* ELF format version. */ Elf32_Addr e_entry; /* Entry point. */ Elf32_Off e_phoff; /* Program header file offset. */ Elf32_Off e_shoff; /* Section header file offset. */ Elf32_Word e_flags; /* Architecture-specific flags. */ Elf32_Half e_ehsize; /* Size of ELF header in bytes. */ Elf32_Half e_phentsize; /* Size of program header entry. */ Elf32_Half e_phnum; /* Number of program header entries. */ Elf32_Half e_shentsize; /* Size of section header entry. */ Elf32_Half e_shnum; /* Number of section header entries. */ Elf32_Half e_shstrndx; /* Section name strings section. */ } Elf32_Ehdr;
010 打开之后是这样的:
ELF header ELF 文件头 里面是基础信息,装载时看的是 Program header,涉及到 ELF header 里的变量
e_phoff
偏移e_phentsize
每个的大小和e_phnum
数量。同样,链接时看的是 Section header ,涉及到 ELF header 里的变量e_shoff
偏移,e_shentsize
每个的大小和e_shnum
数量,e_shstrndx
section 的字符串表Program header table 一般紧贴着 ELF header,里面长这样
类型、权限、文件里的位置、要加载到虚拟内存中的位置、(现在没用的物理位置)、在文件中的大小、在内存中的大小、对齐大小
Section header 里面长这样 段表
名字、类型、权限、虚拟内存位置(addr)、对应的文件偏移(offset)、大小、关联的 section、其他信息、对齐、某些特别的段的表的每一项的大小(如重定位表、符号表、哈希表、动态链接表这些)
s_name64_t s_name; /* Section name */ s_type64_e s_type; /* Section type */ s_flags64_e s_flags; /* Section attributes */ Elf64_Addr s_addr; /* Virtual address in memory */ Elf64_Off s_offset <format=hex>; /* Offset in file */ Elf64_Xword s_size; /* Size of section */ Elf64_Word s_link; /* Link to other section */ Elf64_Word s_info; /* Miscellaneous information */ Elf64_Xword s_addralign; /* Address alignment boundary */ Elf64_Xword s_entsize; /* Entry size, if section has table */
dynamic 段里长这样:
linker 需要获取里面的比较关键的信息是:(
d_tag
)指向原始笔记的链接
DT_NEEDED
:依赖的共享库列表。DT_SYMTAB
:动态符号表( .dynsym )地址。DT_RELA
/DT_REL
:重定位表( .rela.plt )地址。DT_JMPREL
:PLT 重定位表(通常与 .rela.plt 相同)。DT_HASH
/DT_GNU_HASH
:符号哈希表地址。
正向视角
Important
linker 是基于装载视图解析 so 文件的
- e_type(文件类型)、e_machine、e_version 和 e_flags (架构)之类的完全可以做手脚
- 动态链接库不用
e_entry
,设定的跳转是动态链接器的位置 - 在内存装载这一步,与链接视图无关,所以 e_shoff、e_shentsize、e_shnum 和 e_shstrndx 也是可以随便动手脚的。但是可惜的是 IDA 分析需要的是链接视图(毕竟要分析符号、代码、不同的表这些)
section table 到最后跳到链接器的时候就要用到了吧?
并不会。section table 的作用只影响到静态链接目标文件(合并 section),之后的动态链接的事情,完全可以由 segment 处理了,因为 segment 里面有专门的 dynamic 标志,只要动态链接器能够知道这个 segment 在哪就够了
根据上面的知识,我们能够设计一套比较隐蔽的加密特定 section 的方案
加密流程
- 加密用取反来作为实例:预先的操作就是在文件中找到需要加密的段的数据,全部取反
- 找的方法是找 section header 里面的数据
- 把在内存中找到这个地方的所需要的数据——虚拟内存中的偏移地址、大小,一起塞到
e_shoff
这种无关紧要的地方(顺便直接给 IDA 干爆了)(高位地址、低位大小)(或者多用几个都行啦),获取的方式是找 section table 对应的数据
解密流程
- 程序运行解密:在 init_array 里面留一个解密函数
- 函数里面这么找到需要解密的地方:调用
getLibAddr
(或者其他找到基址的函数) 得到 so 文件中的起始地址 - 通过读取一个偏移直接在 ELF header 里面找到
e_shoff
,获取大小和位置 - 修改这个地方的内存权限,解密,然后改回来原来的权限
这里不能一股脑全部把 .text
全加密了,因为重定位会用到这里的代码。initArray的加载问题 里的原理中也可以看出,如果解密不是第一个的时候,可能会出大问题
// 解密函数(需保留在未加密的.text中)
void __attribute__((constructor)) decrypt_mytext() {
// 1. 获取基地址
void* base = get_module_base();
// 2. 从ELF头读取元数据
Elf64_Ehdr* ehdr = (Elf64_Ehdr*)base;
Elf64_Off offset = (ehdr->e_entry >> 32) & 0xFFFFFFFF;
size_t size = ehdr->e_entry & 0xFFFFFFFF;
// 3. 计算虚拟地址
void* mytext_vaddr = base + offset;
// 4. 修改内存权限并解密
mprotect(mytext_vaddr, size, PROT_READ | PROT_WRITE);
for (size_t i = 0; i < size; i++) {
((uint8_t*)mytext_vaddr)[i] = ~((uint8_t*)mytext_vaddr)[i];
}
mprotect(mytext_vaddr, size, PROT_READ | PROT_EXEC);
}
上面的方式还有挺大的改进空间,如只要把 e_shoff
修好了,那么 section 的情况就暴露了,会直接看到多一个 section,还有什么 init_array 之类的全出来了(如果没删干净的话)
我想,在 dynamic segment 里面乱找也可以看出来吧,只要看懂了话还是挺好弄的。可能得弄个什么 1000 多个假函数在 dynamic 里上工作量难度对抗
一种更精细的方案是加密某个函数:这里设加密的函数是导出函数,所以能够在
加密方案
- 找到 dynamic 段的位置,然后在里面遍历 tag 找到 dynsym(符号表)dynstr(符号表里面指向的对应的字符串表)hash(字符串哈希后映射到这个 so 里的所有导出符号的地址的哈希表)
- 根据函数名称,计算哈希,然后在哈希表里找到 Sym 里面就有函数地址和大小之类的东西了
- 然后就对这块地方加密吧
这里利用了外面的 so 调用这里面的符号,根据字符串找函数地址这么个路径才搞定的。挺偷鸡的吧
解密方案
- 找基地址、然后找 Program header,找到 dynamic segment 里面的
p_vaddr
(虚拟地址位置)和p_filesz
(段大小) - 然后和上面一个样啦
逆向修复
于是乎,修复 so 的思路就是把 section 信息找回来,segment 修复好、重定位那些地址修复好的过程。为了内存对齐什么的,内存中的东西和文件上会有微妙的偏移,把文件相应的扩大就行了。 因为是装载视图,section header 什么的就可能已经被丢光了,所以也要根据.dynamic 里的东西修复出一个全新的 section header。
下面用 https://github.com/Chenyangming9/SoFixer 这个版本介绍
修复 segment(program header) 信息
bool ElfRebuilder::RebuildPhdr() {
auto phdr = (Elf_Phdr*)elf_reader_->loaded_phdr();
for(auto i = 0; i < elf_reader_->phdr_count(); i++) {
phdr->p_filesz = phdr->p_memsz; // 扩展文件大小到内存大小
phdr->p_paddr = phdr->p_vaddr; // 修复物理地址
phdr->p_offset = phdr->p_vaddr; // 修复文件偏移
phdr++;
}
}
最重要的修复 section 信息(IDA 看这个)
bool ElfRebuilder::RebuildShdr() {
// 根据segment信息重建各个section信息
// 包括.dynsym, .dynstr, .hash, .rel.dyn, .rel.plt等关键section
// 计算section地址、大小等
if(sDYNSYM != 0) {
shdrs[sDYNSYM].sh_link = sDYNSTR;
auto sNext = sDYNSYM + 1;
shdrs[sDYNSYM].sh_size = shdrs[sNext].sh_addr - shdrs[sDYNSYM].sh_addr;
}
}
修复重定位信息
bool ElfRebuilder::RebuildRelocs() {
SaveImportsymNames(); // 保存导入符号名称
if(elf_reader_->dump_so_base_ == 0) return true;
// 处理两种类型的重定位表
if (si.plt_type == DT_REL) {
// .rel.dyn 段的重定位
auto rel = si.rel;
for (auto i = 0; i < si.rel_count; i++, rel++){
relocate<false>(si.load_bias, rel, elf_reader_->dump_so_base_);
}
// .rel.plt 段的重定位
rel = si.plt_rel;
for (auto i = 0; i < si.plt_rel_count; i++, rel++){
relocate<false>(si.load_bias, rel, elf_reader_->dump_so_base_);
}
}
}
符号重定位
- 处理
.rel.dyn
和.rel.plt
两个重定位表 - 对每个重定位项:
- 如果是 R_ARM_RELATIVE 类型,进行基址修正,因为是偏移,所以知道 base 的情况下直接相减就行了
* prel = * prel - dump_base
- 如果是外部符号引用,需要重建导入表
- 如果是 R_ARM_RELATIVE 类型,进行基址修正,因为是偏移,所以知道 base 的情况下直接相减就行了
case R_386_RELATIVE:
case R_ARM_RELATIVE:
*prel = *prel - dump_base;
break;
导入表重建
- 保存所有导入符号名到
mImports
数组 - 若符号地址( st_value )已知,直接写入目标地址。
- 若符号未解析(如动态库的导入符号),通过符号名查找索引,并计算新地址。
- 地址计算 : 新地址 = 内存加载大小 + 符号索引 × 指针大小(模拟导入表布局,后面文件中会重建一个)。
auto sym = ELF64_R_SYM(rel->r_info);
...
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;
符号表定义如下:
typedef struct {
Elf64_Word st_name; // 符号名在字符串表中的偏移
unsigned char st_info; // 符号类型(低4位)和绑定属性(高4位)
unsigned char st_other;
Elf64_Half st_shndx; // 符号所在段的索引
Elf64_Addr st_value; // 符号的地址或偏移(核心字段)
Elf64_Xword st_size; // 符号大小
} Elf64_Sym;
这个过程本质上是:
- 将内存中的绝对地址转换回相对地址
- 重建符号链接信息
- 确保 SO 文件可以在新的加载地址正确运行
最后通过
bool ElfRebuilder::Rebuild() {
return RebuildPhdr() && // 修复segment
ReadSoInfo() && // 读取SO信息
RebuildShdr() && // 重建section
RebuildRelocs() && // 修复重定位
RebuildFin(); // 生成最终文件
}
串联起来,然后生成完整文件
bool ElfRebuilder::RebuildFin() {
auto load_size = si.max_load - si.min_load;
rebuild_size = load_size + shstrtab.length() + shdrs.size() * sizeof(Elf_Shdr);
rebuild_data = new uint8_t[rebuild_size];
// 复制段数据
memcpy(rebuild_data, (void*)si.load_bias, load_size);
// 添加section字符串表
memcpy(rebuild_data + load_size, shstrtab.c_str(), shstrtab.length());
// 添加section headers
memcpy(rebuild_data + shdr_off, (void*)&shdrs[0], shdrs.size() * sizeof(Elf_Shdr));
}