这篇 Markdown 文档探讨了如何解析 Android 平台上的
.so
文件格式,以用于脱壳和逆向分析。内容覆盖了 ELF(Executable and Linkable Format)文件的基本结构、各个节的功能,以及如何利用这些信息来定位关键代码和数据。
1. 简介
Android加固中有一步是对so进行加固,加固的方式可以是:删除部分ELF Header、删除Section Header、加密Section等等,导致逆向软件无法正确分析。为了识别加固措施,必须要对so文件有一个深入的理解。
so文件被称为共享目标文件,也称为动态链接文件,其与可执行文件、静态链接文件(.a)都采用ELF文件格式,但与可执行文件不同在于可执行文件在链接的过程中可以合并多个节区(Section),然后变成段(Segment),如图1所示[1]。
ELF文件主要包含了4个部分,如图2所示。
-
ELF Header
-
Program Header Table (程序头表)
-
Section (节区)
-
Section Header Table (节区头表)
ELF Header是文件头,包含固定长度的信息:魔术数字、版本信息、操作系统架构、文件结构偏移量等等;
Section Header Table包含了程序的Section信息:Section名称、Section类型、Section地址等等;
Program Header Table告诉操作系统如何在内存中构建程序的映象。
名称 | 大小 | 对齐 | 作用 |
---|---|---|---|
Elf32_Addr | 4 | 4 | 无符号地址 |
Elf32_Half | 2 | 2 | 无符号short int |
Elf32_Off | 4 | 4 | 无符号文件偏移 |
Elf32_Sword | 4 | 4 | 有符号int |
Elf32_Word | 4 | 4 | 无符号int |
unsigned char | 1 | 1 | 无符号char |
名称 | 大小 | 对齐 | 作用 |
---|---|---|---|
Elf64_Addr | 8 | 8 | 无符号地址 |
Elf64_Half | 2 | 2 | 无符号short int |
Elf32_Off | 8 | 8 | 无符号文件偏移 |
Elf32_Sword | 4 | 4 | 有符号int |
Elf32_Word | 4 | 4 | 无符号int |
Elf64_Xword | 8 | 8 | 无符号long long |
Elf64_Sxword | 8 | 8 | 有符号long long |
unsigned char | 1 | 1 | 无符号char |
2. ELF Header
Android so ELF Header由如下结构体构成(64位和32位一样):
typedef struct elf32_hdr {
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
typedef struct elf64_hdr {
unsigned char e_ident[EI_NIDENT];
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry;
Elf64_Off e_phoff;
Elf64_Off e_shoff;
Elf64_Word e_flags;
Elf64_Half e_ehsize;
Elf64_Half e_phentsize;
Elf64_Half e_phnum;
Elf64_Half e_shentsize;
Elf64_Half e_shnum;
Elf64_Half e_shstrndx;
} Elf64_Ehdr;
# readelf -h输出
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: AArch64
Version: 0x1
Entry point address: 0x15760
Start of program headers: 64 (bytes into file)
Start of section headers: 1244320 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 5
Size of section headers: 64 (bytes)
Number of section headers: 4
Section header string table index: 1
2.1 e_ident
e_ident由16字节组成的数组,给出了ELF的部分标识信息,分别包含了以下部分:
名称 | 位置(字节) | 备注 |
---|---|---|
EI_MAG0-EI_MAG3 | 0-3 | 固定魔术头:7f 45 4c 46<br />0x7f, ‘E’, ‘L’, ‘F’ <br />也常见8进制编码方式:\177ELF |
EI_CLASS | 4 | 有三个值:<br />0 - ELFCLASSNONE: 非法类别<br />1 - ELFCLASS32: ELF 32位<br />2 - ELFCLASS64: ELF 64位 |
EI_DATA | 5 | 数据编码方式:<br />0 - ELFDATANONE:非法数据编码<br />1 - ELFDATA2LSB:低位在前,即小端编码<br />2 - ELFDATA2MSB:高位在前,即大端编码<br />ARM默认是小端模式,所以一般都是1 |
EI_VERSION | 6 | ELF头部版本号:必须为EV_CURRENT = 1 |
EI_OSABI | 7 | 标识操作系统类型: <br /> ELFOSABI_NONE = 0, // UNIX System V ABI |
EI_ABIVERSION | 8 | ABI版本 |
EI_PAD | 9 | 填充 |
EI_NIDENT | 16 | e_ident的size |
2.2 e_type
e_type表示文件的类型,其值可为如下值:
ET_NONE = 0, // No file type
ET_REL = 1, // 可重定位文件
ET_EXEC = 2, // 可执行文件
ET_DYN = 3, // 共享目标文件
ET_CORE = 4, // Core file
ET_LOPROC = 0xff00, // Beginning of processor-specific codes
ET_HIPROC = 0xffff // Processor-specific
ELF文件一般是可重定位文件(.o、.a)、可执行文件和共享文件(.so)。
2.3 e_machine
e_machine表示CPU的架构类型:183表示AARCH64架构。
2.4 剩余参数
名称 | 备注 |
---|---|
e_version | 必须为EV_CURRENT=1 |
e_entry | 程序入口虚拟地址。可重定位文件(.o)一般没有程序入口,可以为 0 |
e_phoff | 程序头部表格(Program Header Table)的偏移量(按字节计算)。如果文件没有程序头部表格,可以为 0 |
e_shoff | 节区头部表格(Section Header Table)的偏移量(按字节计算)。如果文件没有节区头部表格,可以为 0。 |
e_flags | 保存与文件相关的,特定于处理器的标志。标志名称采用 EF_machine_flag的格式。 |
e_ehsize | ELF 头部 的大小(以字节计算)。 |
e_phentsize | 程序 头部 的表项大小(按字节计算)。 |
e_phnum | 程序 头部 的表项数目。可以为 0。 |
e_shentsize | 节区 头部 的表项大小(按字节计算,32位固定为0x28,64位固定为0x40)。 |
e_shnum | 节区 头部表 的表项数目。可以为 0。 |
e_shstrndx | 节区头部表 中与节区名称字符串表相关的表项的索引,是节区中最后一个表。如果文件没有节区名称字符串表,此参数可以为 SHN_UNDEF。 |
如果SO文件处理了e_shnum和e_shstrndx,并且没有处理e_shoff,也可以通过以下公式得到(注:elf32(64)_shdr==e_shentsize):
$e_shnum = (fil_size - e_shoff) \div sizeof(elf32_shdr)$
$e_shstrndx = e_shnum - 1$
所以当ida对so的segment分析出现问题时,首先需要判断e_shnum是否正确,然后判断e_shstrndx是否正确。如果e_shoff与e_shnum都被修改过,则需要更深入的分析。
3. Program Header Table
程序头表说明了如何装载该so,其结构体如下:
typedef struct elf32_phdr {
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;
typedef struct elf64_phdr {
Elf64_Word p_type;
Elf64_Word p_flags;
Elf64_Off p_offset;
Elf64_Addr p_vaddr;
Elf64_Addr p_paddr;
Elf64_Xword p_filesz;
Elf64_Xword p_memsz;
Elf64_Xword p_align;
} Elf64_Phdr;
字段 | 含义 |
---|---|
p_type | 描述了段类型 |
p_offset | 描述了从文件到该段(每个程序头表的data区域)的文件偏移 |
p_vaddr | 描述了段在内存中的偏移 |
p_paddr | 描述了物理地址相关,在应用层无作用。 |
p_filesz | p_offset描述了段在文件中的偏移。那么此成员就描述了在文件中所占的大小,可以为0。 |
p_memsz | 同上,描述了内存中映像所占的字节数。 可以为0 |
p_flags | mmap映射的prot参数。int prot = PFLAGS_TO_PROT(phdr->p_flags); |
p_align | 描述了对齐。 |
p_type包含以下类型:
enum {
PT_NULL = 0, // Unused segment.
PT_LOAD = 1, // Loadable segment.
PT_DYNAMIC = 2, // Dynamic linking information.
PT_INTERP = 3, // Interpreter pathname.
PT_NOTE = 4, // Auxiliary information.
PT_SHLIB = 5, // Reserved.
PT_PHDR = 6, // The program header table itself.
PT_TLS = 7, // The thread-local storage template.
PT_LOOS = 0x60000000, // Lowest operating system-specific pt entry type.
PT_HIOS = 0x6fffffff, // Highest operating system-specific pt entry type.
PT_LOPROC = 0x70000000, // Lowest processor-specific program hdr entry type.
PT_HIPROC = 0x7fffffff, // Highest processor-specific program hdr entry type.
// x86-64 program header types.
// These all contain stack unwind tables.
PT_GNU_EH_FRAME = 0x6474e550,
PT_SUNW_EH_FRAME = 0x6474e550,
PT_SUNW_UNWIND = 0x6464e550,
PT_GNU_STACK = 0x6474e551, // Indicates stack executability.
PT_GNU_RELRO = 0x6474e552, // Read-only after relocation.
// ARM program header types.
PT_ARM_ARCHEXT = 0x70000000, // Platform architecture compatibility info
// These all contain stack unwind tables.
PT_ARM_EXIDX = 0x70000001,
PT_ARM_UNWIND = 0x70000001,
// MIPS program header types.
PT_MIPS_REGINFO = 0x70000000, // Register usage information.
PT_MIPS_RTPROC = 0x70000001, // Runtime procedure table.
PT_MIPS_OPTIONS = 0x70000002, // Options segment.
PT_MIPS_ABIFLAGS = 0x70000003 // Abiflags segment.
};
3.1 PT_PHDR
33PT_PHDR是程序头表的第一部分,表示了程序头表相对于so起始地址的偏移量和程序头表的大小,如图3所示。
$phdr_addr = load_bias + p_vaddr$
此处p_vaddr=0x34h(和e_phoff相等),load_bias指代so在内存中的地址。
图3
还有一种so是没有PT_PHDR条目,该so一般以PT_LOAD开头,如图4所示。
$phdr_addr = load_bias + p_vaddr + e_phoff$
由于p_vaddr=0,所以仍然是so文件在内存的起始地址+程序头的偏移。[2]
图4
3.2 PT_INTERP
PT_INTERP是可选的,一般为/system/bin/linker
,如图5所示。
图5
3.3 PT_LOAD
PT_LOAD是程序头最重要的部分,通过mmap将p_type=PT_LOAD的映射到内存。映射以page为单位,不足以0填充。
3.4 PT_DYNAMIC
PT_DYNAMIC指向.dynamic
节区,.dynamic节区包含了动态链接所需的一切信息,如图6所示。详情见5.4。
图6
4. Section Header Table
节区头表详细的描述了so文件的对应节的各种属性,如果节区头表被删除了,那么像IDA这样的逆向工具也无法正确的显示文件的信息。比如识别不了代码段、数据段等等。
节区头表结构如下:32位占40字节,64位占64字节。
typedef struct elf32_shdr {
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
} Elf32_Shdr;
typedef struct elf64_shdr {
Elf64_Word sh_name;
Elf64_Word sh_type;
Elf64_Xword sh_flags;
Elf64_Addr sh_addr;
Elf64_Off sh_offset;
Elf64_Xword sh_size;
Elf64_Word sh_link;
Elf64_Word sh_info;
Elf64_Xword sh_addralign;
Elf64_Xword sh_entsize;
} Elf64_Shdr;
名称 | 含义 |
---|---|
sh_name | 节名是一个字符串,保存在一个名为.shstrtab的字符串表(可通过Section Header索引到)。sh_name的值实际上是其节名字符串在.shstrtab中的偏移值 |
sh_type | 节类型,见4.1。 |
sh_flags | 节标志位,见4.2。 |
sh_addr | 节地址:节的虚拟地址。如果该节可以被加载,则sh_addr为该节被加载后在进程地址空间中的虚拟地址;否则sh_addr为0 |
sh_offset | 节偏移。如果该节存在于文件中,则表示该节在文件中的偏移;否则无意义,如sh_offset对于BSS节来说是没有意义的,因为BSS节是运行时申请。 |
sh_size | 节大小 |
sh_link、sh_info | 节链接信息 |
sh_addralign | 节地址对齐方式。0和1表示无需对齐,2的正整数幂用来表示对齐。 |
sh_entsize | 节项大小。有些节包含了一些固定大小的项,如符号表,其包含的每个符号所在的大小都一样的,对于这种节,sh_entsize表示每个项的大小。如果为0,则表示该节不包含固定大小的项。 |
4.1 sh_type
名称 | 值 | 含义 |
---|---|---|
SHT_NULL | 0 | 无意义的节 |
SHT_PROGBITS | 1 | 由程序定义的节。代码节、数据节都是这种类型。 |
SHT_SYMTAB<br />SHT_DYNSYM | 2<br />11 | SHT_SYMTAB和SHT_DNYSYM都标识了符号表,区别在于SHT_DNYSYM是SHT_SYMTAB的子集,SHT_SYMTAB包含了完整的符号表。见5.1 |
SHT_STRTAB | 3 | 字符串表,代表符号和节区的名字。见5.2 |
SHT_RELA | 4 | 重定位表,该节包含了重定位信息。 |
SHT_HASH | 5 | 符号的哈希表,一个动态链接对象必须包含符号哈希表。见5.3 |
SHT_DYNAMIC | 6 | 动态链接信息 |
SHT_NOTE | 7 | 提示性信息 |
SHT_NOBITS | 8 | 表示该节在文件中没有内容。如.bss节 |
SHT_REL | 9 | 该节包含了重定位信息 |
SHT_SHLIB | 10 | 保留 |
SHT_DYNSYM | 11 | 动态链接的符号表 |
SHT_INIT_ARRAY | 14 | so加载初始化表 |
SHT_FINI_ARRAY | 15 | so销毁表 |
4.2 sh_flag
常量 | 值 | 含义 |
---|---|---|
SHF_WRITE | 1 | 表示该节在进程空间中可写 |
SHF_ALLOC | 2 | 表示该节在进程空间中需要分配空间。有些包含指示或控制信息的节不需要在进程空间中分配空间,就不会有这个标志。 |
SHF_EXECINSTR | 4 | 表示该节在进程空间中可以被执行 |
5. Section
5.1 符号表(Symbol Table)
5.1.1 符号
符号表的内容是符号,而符号就是函数名、变量名。比如:
int Sym[3]={1, 2, 3};
int main(){}
Sym是全局变量,即全局符号,main函数也是符号,即全局符号。
符号分为3类:
-
全局符号
由A模块定义,并且能在A模块和B模块使用,例如:non-static 全局变量和non-static 函数
- 全局符号里又存在弱符号,弱符号指只定义而没有赋值。具体作用可见[3]
-
外部符号
由B模块定义,在A模块声明和使用。即是一种参照物体系。
-
本地符号
A模块定义的static函数或者static变量。
定义在函数内部的变量不是符号,而是由运行时操作系统分配到栈上。
例如图7:[3:1]
5.1.2 .dynsym和.symtab
两个节区都指向了符号表区域,不同的是.symtab是完整的符号表,包含了许多动态链接中不需要的符号。Linux上可通过strip命令去掉.symtab,去掉后,使用file命令查看文件,就可以发现文件是stripped状态。
Android so一般没有.symtab区域,但一定会有.dynsym区域,用于参与动态链接。
5.1.3 符号表结构
符号表有多个符号表条目构成,每一个符号表条目大小为16字节,通过符号表条目就可以找到符号所在。
struct Elf32_Sym {
Elf32_Word st_name; // Symbol name (index into string table)
Elf32_Addr st_value; // Value or address associated with the symbol
Elf32_Word st_size; // Size of the symbol
unsigned char st_info; // Symbol's type and binding attributes
unsigned char st_other; // Must be zero; reserved
Elf32_Half st_shndx; // Which section (header table index) it's defined in
};
名称 | 含义 |
---|---|
st_name | 符号名,本质上是一个字符串表的偏移量。 |
st_value | 可执行文件和共享文件中,st_value表示符号的虚拟地址。 |
st_size | 符号的大小。 |
st_info | 符号的类型和绑定的属性:<br />1. 绑定的属性<br />STB_LOCAL:本地符号=0<br />STB_GLOBAL:全局符号=1<br />STB_WEAK:弱符号=2<br /><br />2.类型<br />STT_NOTYPE:没有指定的类型=0<br />STT_OBJECT:此符号与数据对象相关联(.data),例如变量、数组等=1<br />STT_FUNC:此符号与函数或可执行代码关联=2<br />STT_COMMON:此符号表示未初始化的公共块(.bss)=5 |
st_other | 符号的可见性描述,一般为STV_DEFAULT=0,表示由绑定的属性决定。 |
st_shndx | 表示该符号所在的节区,值和sh_type有关。 |
图8
5.2 字符串表(String Table)
字符串表保存了符号的名字和节区的名字,一个共享文件可以有多个字符串表,比如:.shstrtab(节区名字符串表)和.dynstr(动态链接所需信息,如5.1符号的名字)。
如图9所示,字符串表以\x00
和\x00
结尾,中间的每一个字符串都是以\x00
结尾。
例如获取符号名字,只需要通过符号条目的st_name获取偏移,然后到.dynstr节区寻找一个完整的字符串即可。
图9
5.3 哈希表(Hash Table)
动态链接过程中用到的符号存在于动态符号表节区(.dynsym),只需通过程序头表找到.dynsym节区,就可以在节区中通过对比st_name,获取对应的函数或者变量,但是由于目标文件存在大量的符号,如果按照顺序找到,那么效率是非常低的。
所以,自然而然的就相到了通过哈希表来进行索引,哈希表存放了符号名和符号地址偏移量的对应关系,如此即可通过符号名,快速的获取符号结构。
so的哈希表结构如图10所示,由n个桶和n个链组成,计算方式如下:
图10
首先通过hash函数将符号名转换为hash value:
uint32_t calculate_elf_hash(const char* name) {
const uint8_t* name_bytes = reinterpret_cast<const uint8_t*>(name);
uint32_t h = 0, g;
while (*name_bytes) {
h = (h << 4) + *name_bytes++;
g = h & 0xf0000000;
h ^= g;
h ^= g >> 24;
}
return h;
}
获取hash value (假设为x) 之后,通过$y=bucket[x\ mod\ bucket.size]$,获取chain[y]的索引,然后通过对比这条链上的每一个元素,直到找到元素。
bool soinfo::elf_lookup(SymbolName& symbol_name,
const version_info* vi,
uint32_t* symbol_index) const {
uint32_t hash = symbol_name.elf_hash();
...
for (uint32_t n = bucket_[hash % nbucket_]; n != 0; n = chain_[n]) {
ElfW(Sym)* s = symtab_ + n;
const ElfW(Versym)* verdef = get_versym(n);
// skip hidden versions when verneed == 0
if (verneed == kVersymNotNeeded && is_versym_hidden(verdef)) {
continue;
}
if (check_symbol_version(verneed, verdef) &&
strcmp(get_string(s->st_name), symbol_name.get_name()) == 0 &&
is_symbol_global_and_defined(this, s)) {
TRACE_TYPE(LOOKUP, "FOUND %s in %s (%p) %zd",
symbol_name.get_name(), get_realpath(),
reinterpret_cast<void*>(s->st_value),
static_cast<size_t>(s->st_size));
*symbol_index = n;
return true;
}
}
...
*symbol_index = 0;
return true;
}
5.4 动态节区(.dynamic)
如果一个目标文件要参与动态链接,那么必定会存在.dynamic节区。目标文件在链接时,会根据程序头表的Dynamic Segment找到动态节区的位置,再从动态节区中找到其他所需节区的信息,如:SHT_STRTAB、SHT_HASH等等。(Android 10 在装载so时,通过 sh头和ph头 找到动态节区做校验,在链接时通过ph头找到动态节区)
动态节区的结构体如下,一共8个字节:
typedef struct dynamic {
Elf32_Sword d_tag;
union {
Elf32_Sword d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
d_tag 控制着结构体的类型;
d_val 不同类型的结构体有着不同的意思;
d_ptr 结构体的偏移地址。
d_tag有如下类型:
名称 | 值 | 含义 |
---|---|---|
DT_NULL | 0 | 代表动态节区的结束 |
DT_NEEDED | 1 | 需要被导入的库,计算:$lib_addr=strtab_addr+d_ptr$ (相对于字符串表的偏移) |
DT_PLTGOT | 3 | PLT和GOT表 |
DT_HASH | 4 | 哈希表 |
DT_STRTAB | 5 | 字符串表 |
DT_SYMTAB | 6 | 符号表 |
DT_INIT | 12 | 初始化节区 |
DT_FINIT | 13 | 终止节区 |
DT_INIT_ARRAY | 25 | 初始化数组节区 |
DT_FINI_ARRAY | 26 | 终止数组节区 |
比如查看字符串表,图11:
图11
比如查看DT_NEEDED,可以得知相对于字符串表的偏移为0x4c0
,所以最终地址:
$0x54c+0x4c0=0xa0c$
图12:
图12
5.5 自定义Section
在声明一个函数或变量时,可以加上attribute((section(“自定义section名”)))前缀的方式,将其添加到自定义段。
Comments NOTHING