07-ELF文件格式分析
1.目标文件格式
链接视图和执行视图
链接视图:elf未加载到内存中时完整的文件结构
执行视图:elf加载到内存中时的文件结构
其对应关系如下:
在ELF文件中section是最小的不可分割的元素。
当so加载到文件中,多个section被组合成segment,segment此时是最小的不可分割的元素。
相关源码
在/android/4.2.2/bionic/linker/linker.c:
static soinfo* load_library(const char* name) { //1.打开so文件,获取文件描述符 scoped_fd fd; fd.fd = open_library(name); if (fd.fd == -1) { DL_ERR("library \"%s\" not found", name); return NULL; } // 2.读取ELF header. Elf32_Ehdr header[1]; int ret = TEMP_FAILURE_RETRY(read(fd.fd, (void*)header, sizeof(header))); ...... // 3.验证ELF header. if (verify_elf_header(header) < 0) { DL_ERR("not a valid ELF executable: %s", name); return NULL; } //4.从so文件读取并分配内存,然后加载 program header table到内存. const Elf32_Phdr* phdr_table; phdr_ptr phdr_holder; ret = phdr_table_load(fd.fd, header->e_phoff, header->e_phnum, &phdr_holder.phdr_mmap, &phdr_holder.phdr_size, &phdr_table); if (ret < 0) { DL_ERR("can't load program header table: %s: %s", name, strerror(errno)); return NULL; } size_t phdr_count = header->e_phnum; // 5.遍历program header table,根据LOAD属性的segment,分配足够大的内存空间, //用于保存所有LOAD属性的segment对应的空间 Elf32_Addr ext_sz = phdr_table_get_load_size(phdr_table, phdr_count); …… // 6.分配足够大的内存空间用于加载所有LOAD属性的segment. void* load_start = NULL; Elf32_Addr load_size = 0; Elf32_Addr load_bias = 0; ret = phdr_table_reserve_memory(phdr_table, phdr_count, &load_start, &load_size, &load_bias); ……
//7.加载所有LOAD属性的segment到进程空间中(上一步分配好的空间) ret = phdr_table_load_segments(phdr_table, phdr_count, load_bias, fd.fd); …… }
|
加固提示
elf文件中section header table未加载到内存中,只是在静态分析时用到。即使完全删除也不影响so文件的加载和正常使用。在加固时候,可以篡改section header table让反编译工具解析失败或者完全删除section header table。
2.ELF Header部分
例子
这里,我们使用ndk写一个简单demo,作为后面几个部分文件结构分析使用。
demo1.cpp
#include "com_demo_MainActivity.h" #include <stdio.h>
void init_func() __attribute__((constructor)); void fini_func() __attribute__((destructor));
void init_func(){ puts("---------------- init_func() ---------------------"); }
void fini_func(){ puts("---------------- fini_func() ---------------------"); }
int global_init_var = 99; int global_unint_var;
JNIEXPORT void JNICALL Java_com_demo_MainActivity_test (JNIEnv * env, jobject obj){ global_unint_var = 1;
puts("1st call puts!"); puts("2st call puts!"); } |
然后使用ndk-build编译成demo1.so文件即可。
基本概念
位置:elf.h
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; /* Entry point */ 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; |
几个关键的字段
字段名 |
注释 |
e_phoff |
Program Header Table偏移量 |
e_shoff |
Section Header Table偏移量 |
e_ehsize |
ELF Header大小 |
e_phentsize |
Program Header Table的表项大小 |
e_phnum |
Program Header Table的表项数量 |
e_shentsize |
Section Header Table的表项大小 |
e_shnum |
Section Header Table的表项数量 |
e_shstrndx |
Section Header Table中节区名称字符串表索引 |
借助readelf工具或010 Editor分析上面libdemo1.so中的ELF Header结构,如下:
相关源码
位置: 4.2.2/bionic/linker/linker.c
static int verify_elf_header(const Elf32_Ehdr* hdr) { if (hdr->e_ident[EI_MAG0] != ELFMAG0) return -1; if (hdr->e_ident[EI_MAG1] != ELFMAG1) return -1; if (hdr->e_ident[EI_MAG2] != ELFMAG2) return -1; if (hdr->e_ident[EI_MAG3] != ELFMAG3) return -1; if (hdr->e_type != ET_DYN) return -1; /* TODO: Should we verify anything else in the header? */ #ifdef ANDROID_ARM_LINKER if (hdr->e_machine != EM_ARM) return -1; #elif defined(ANDROID_X86_LINKER) if (hdr->e_machine != EM_386) return -1; #elif defined(ANDROID_MIPS_LINKER) if (hdr->e_machine != EM_MIPS) return -1; #endif return 0; }
|
位置:android7.1.1_r6/bionic/linker/linker_phdr.cpp
bool ElfReader::VerifyElfHeader() { if (memcmp(header_.e_ident, ELFMAG, SELFMAG) != 0) { DL_ERR("\"%s\" has bad ELF magic", name_.c_str()); return false; }
// Try to give a clear diagnostic for ELF class mismatches, since they're // an easy mistake to make during the 32-bit/64-bit transition period. int elf_class = header_.e_ident[EI_CLASS]; #if defined(__LP64__) if (elf_class != ELFCLASS64) { if (elf_class == ELFCLASS32) { DL_ERR("\"%s\" is 32-bit instead of 64-bit", name_.c_str()); } else { DL_ERR("\"%s\" has unknown ELF class: %d", name_.c_str(), elf_class); } return false; } #else if (elf_class != ELFCLASS32) { if (elf_class == ELFCLASS64) { DL_ERR("\"%s\" is 64-bit instead of 32-bit", name_.c_str()); } else { DL_ERR("\"%s\" has unknown ELF class: %d", name_.c_str(), elf_class); } return false; } #endif
if (header_.e_ident[EI_DATA] != ELFDATA2LSB) { DL_ERR("\"%s\" not little-endian: %d", name_.c_str(), header_.e_ident[EI_DATA]); return false; }
if (header_.e_type != ET_DYN) { DL_ERR("\"%s\" has unexpected e_type: %d", name_.c_str(), header_.e_type); return false; }
if (header_.e_version != EV_CURRENT) { DL_ERR("\"%s\" has unexpected e_version: %d", name_.c_str(), header_.e_version); return false; }
if (header_.e_machine != GetTargetElfMachine()) { DL_ERR("\"%s\" has unexpected e_machine: %d", name_.c_str(), header_.e_machine); return false; }
return true; } |
Android4.2.2中verify_elf_object检测的elf header字段较少。这里我们用Android7.1.1中VerifyElfHeader检测的比较多得为准。VerifyElfHeader用于检测加载到内存中的elf header部分字段是否合法。
提示:由上面的源码可知,e_entry、e_flag、e_ident中EI_VERSION和EI_PAD未检查。这4个空闲的字段可在加固的时候用于其他用途,例如用于保存某section或方法的位移地址、大小之类的。
3.Section Header Table部分
基本概念
typedef struct elf32_shdr { Elf32_Word sh_name; //指向.shstrtab字符串表的索引位置 Elf32_Word sh_type; Elf32_Word sh_flags; //定义节区的属性:可写、执行、是否占用内存。 Elf32_Addr sh_addr; //对应section在内存中的虚拟地址,一般跟sh_offset一样 Elf32_Off sh_offset; //对应section在so文件中的偏移量 Elf32_Word sh_size; //对应section的大小 Elf32_Word sh_link; Elf32_Word sh_info; Elf32_Word sh_addralign; Elf32_Word sh_entsize; } Elf32_Shdr; |
section对应在内存的实际地址为:base + sh_addr,其中base为so文件加载到内存的基地址。例如libdemo1.so在内存的基地址:
则base=0x4c142000,即section对应内存的时机地址为:0x4c142000+sh_addr。
借助readelf工具或010 Editor分析上面libdemo1.so中的Section Header Table结构,如下:
几个比较重要的section:
节区名 |
注释 |
.interp |
指定装在动态链接器的路径,如:/system/bin/linker |
.dynsym |
包含dynamic symbol table |
.dynstr |
包含动态符号名称的字符串表,与dynsym对应 |
.hash |
哈希表,用于定位函数符号 |
.rel.dyn |
重定位表,这里是对导入数据进行重定位 |
.rel.plt |
重定位表,这里是对导入函数进行重定位 |
.plt |
包含过程连接表(procedure linkage table),用于延时绑定符号 |
.text |
可执行代码保存在这个section |
.rodata |
保存只读数据 |
.fini_array |
进程终止代码的一部分。当程序正常退出时,系统将安排执行这里的代码。相当于C++的析构函数。 |
.init_array |
进程初始化代码的一部分。当程序开始执行时,系统要在开始调用主程序入口之前执行这些代码。相当于C++的构造函数。 |
.dynamic |
dynamic link table,包含动态链接信息 |
.got |
保存全局变量和导入的函数引用的地址 |
.data |
保存初始化的数据 |
.bss |
未初始化的数据,在elf文件中不分配空间,加载到内存的时候分配空间 |
.shstrtab |
节区名称字符串表 |
加固提示
由链接视图和执行视图,我们知道so加载到内存中时section header table是没有用的,它主要是有助于静态分析elf文件结构。在对so做加固的时候,可以篡改或删除section header table的数据,增加静态分析的难度。
4..Program Header Table部分
基本概念
位置:elf.h
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; |
借助readelf工具或010 Editor分析上面libdemo1.so中的Program Header结构,如下:
结合上面的执行视图和linker源码,我们知道,LOAD类型表示会加载到内存中。
这里我们主要关注p_type=PT_LOAD这个类型。
PHDR[2]表项,在so中范围为offset~offset+FileSiz,即:0x0~1ee0。同理。
PHDR[3]表项,在so中范围为:0x2ea4~0x300c。其对应的so文件范围如下:
相关源码
在/android/4.2.2/bionic/linker/linker.cpp:
int phdr_table_load_segments(const Elf32_Phdr* phdr_table, int phdr_count, Elf32_Addr load_bias, int fd) { int nn; for (nn = 0; nn < phdr_count; nn++) { const Elf32_Phdr* phdr = &phdr_table[nn]; void* seg_addr; if (phdr->p_type != PT_LOAD) continue; /* Segment addresses in memory */ Elf32_Addr seg_start = phdr->p_vaddr + load_bias; Elf32_Addr seg_end = seg_start + phdr->p_memsz; Elf32_Addr seg_page_start = PAGE_START(seg_start); Elf32_Addr seg_page_end = PAGE_END(seg_end); Elf32_Addr seg_file_end = seg_start + phdr->p_filesz; /* File offsets */ Elf32_Addr file_start = phdr->p_offset; Elf32_Addr file_end = file_start + phdr->p_filesz; Elf32_Addr file_page_start = PAGE_START(file_start); Elf32_Addr file_page_end = PAGE_END(file_end); seg_addr = mmap((void*)seg_page_start, file_end - file_page_start, PFLAGS_TO_PROT(phdr->p_flags), MAP_FIXED|MAP_PRIVATE, fd, file_page_start); if (seg_addr == MAP_FAILED) { return -1; } /* if the segment is writable, and does not end on a page boundary, * zero-fill it until the page limit. */ if ((phdr->p_flags & PF_W) != 0 && PAGE_OFFSET(seg_file_end) > 0) { memset((void*)seg_file_end, 0, PAGE_SIZE - PAGE_OFFSET(seg_file_end)); } seg_file_end = PAGE_END(seg_file_end);
if (seg_page_end > seg_file_end) { void* zeromap = mmap((void*)seg_file_end, seg_page_end - seg_file_end, PFLAGS_TO_PROT(phdr->p_flags), MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE, -1, 0); if (zeromap == MAP_FAILED) { return -1; } } } return 0; } |
有上面的源码可知,linker通过解析Program Header Table来分配内存空间并加载LOAD类型的表项内容。
我们通过下述两个命令来查看libdemo1.so加载到内存的位置。
TODO
有两个LOAD加载到内存,为什么实际分成3段?
我们看section header table中flag字段属性。如下:
其实第一个LOAD段是可以分成两部分的。可以看到”.interp”到”.rel.plt”节是只读属性,然后”.plt”和”.text”是可读可执行属性,”.ARM.extab”、”.ARM.exidx”和”.rodata”是可读属性。然后第二个LOAD段则全是可读可写属性。
所以实际加载到内存的时候,是分成了2段来加载。这也为什么实际内存分布有3段。主要是因为根据section的属性来分配的。
对应源码的过程如何???
5.interp节区解析
“.interp”主要是指定linker的加载路径。通过解析Program Header Table,我们知道LOAD属性的segment是包含”.interp”节区的,也就是会加载到内存中让系统读取。
通过010 Editor查看”.interp”节区:
也就是说该so文件指定了android系统中”/system/bin/linker”作为动态链接器。我们可以通过下述命令查看上面demo中so文件的内存状态:
可以看到该so文件使用的动态链接器正是”.interp”节区指定的。
6.dynsym节区解析
基本概念
在这个节区里面存储了导入和导出的函数和变量。
typedef struct elf32_sym{ Elf32_Word st_name; //动态符号名称字符串表的索引,指向dynstr Elf32_Addr st_value; //可能是地址、值等等,根据具体上下文来看 Elf32_Word st_size; unsigned char st_info; //说明符号是数据还是函数类型,以及符号的属性 unsigned char st_other; Elf32_Half st_shndx; //指定符号所在section } Elf32_Sym; |
实例分析
通过readelf –s libdemo1.so查看dynsym节区:
例如上面的symtab[11] Java_com_demo_MainActivity。st_shndx=8,如下,就是”.text”存放代码的section。
而st_shndx为UND表示该符号不是在该so模块中定义,是导入的符号。
通过ELF32_ST_TYPE(st_info)知道该符号类型是函数。同样的。
symtab[10] global_uint_var是在17,即”.bss”存放未初始化变量的section。
symtab[12] global_init_var是在16,即”.data”存放初始化的变量的section。
跟demo1.cpp源码保持一致:
7.dynstr节区解析
保存dynamic symbol table中符号名称字符串的表,上一节中Elf32_Sym结构体中的st_name正是指向这个符号名称字符串表。
我们借助010 editor工具查看libdemo1.so的”.dynstr”节区。
8.shstrtab节区解析
保存section名称的字符串表,Elf32_Shdr结构体的sh_name索引正是指向这个字符串表。
我们借助010 editor工具查看libdemo1.so的”.shstrtab”节区。
9.data和bss解析
基本概念
l 初始化的变量保存在”.data”处。
“.data”节区保存的是初始化变量的值。
l 未初始化的变量保存在”.bss”处,默认初始化为0,在so文件中”.bss”不分配空间
实例分析
(1)“.data”节区分析:
我们在静态分析的时候使用s_addr,即变量在的虚拟地址。看看下面ida的分析:
其映射关系如下:
物理地址s_offset |
虚拟地址s_addr |
值 |
0x3000 |
.data 00004000 |
0 |
0x3004 |
.data 00004004 |
0x63 |
当libdemo1.so加载到内存中时,”.data”节区中变量在内存中的地址为:base + s_addr,其中base是libdemo1.so在内存中的初始地址。如下,我们查看libdemo1.so的内存情况,则其base=0x4c142000。
(2)“.bss”节区分析:
有上面知道Elf32_Shdr结构的s_name属性为SHT_NOBITS,所以此时s_size即使为4非0,但实际是不占用文件空间。
10.rel.dyn和.rel.plt解析
基本概念
(1) “.rel.dyn”和”.rel.plt”两个节区,存储了那些在”.dynsym”和”.plt”中链接时需要重定位的符号。他们的作用是在加载so文件初始化时,告诉linker那些符号需要重定位。具体过程见相关源码部分分析。
(2).rel.dyn记录了加载时需要重定位的变量
(3).rel.plt记录的了需要重定位的函数
(4)“.dynsym”、”.plt”、”.got”、”.rel.dyn”、”.rel.plt”之间关系如下图所示:
.rel.dyn和.rel.plt重定位表项结构如下(”.rel”开头的都是重定位表):
Typedef struct{ Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel |
r_offset表示在虚拟内存中的地址
#define ELF32_R_SYM(i) ((i)>>8),符号索引,指向dynamic symbol table,即”.dynsym”节区
#define ELF32_R_TYPE(i) ((unsigned char)(i)),重定位类型
在ARM结构体中,一共有5种类型:
重定位类型 |
计算方式 |
R_ARM_JUMP_SLOT |
*((unsigned*)reloc) = sym_addr; |
R_ARM_GLOB_DAT |
*((unsigned*)reloc) = sym_addr; |
R_ARM_ABS32 |
*((unsigned*)reloc) += sym_addr; |
R_ARM_REL32 |
*((unsigned*)reloc) += sym_addr - rel->r_offset; |
R_ARM_RELATIVE |
*((unsigned*)reloc) += si->base; |
实例分析
.rel.dyn包含需要重定位的导入变量,使用”readelf –r libdemo1.so”查看:
通过Elf32_Rel的r_info获取对应的符号索引表如下:
需要重定位的变量索引表 |
对应动态符号名称 |
0x17>>8=0 |
空 |
0xc15>>8=0xc=12 |
__gnu_Unwind_Find_exidx |
0x2215>>8=0x22=34 |
__cxa_call_unexpected |
.rel.plt包含需要重定位的导入函数,使用”readelf –r libdemo1.so”查看:
通过Elf32_Rel的r_info获取对应的符号索引表如下:
需要重定位的变量索引表 |
对应动态符号名称 |
0x216>>8=0x2=2 |
__cxa_atexit |
0x116>>8=0x1=1 |
__cxa_finalize |
0x416>>8=0x7=4 |
puts |
0xc16>>8=0xc=12 |
__gnu_Unwind_Find_exid |
0x1216>>8=0x12=18 |
abort |
0x1416>>8=0x14=20 |
memcpy |
0x1f16>>8=0x1f=31 |
__cxa_begin_cleanup |
0x2016>>8=0x20=32 |
__cxa_type_match |
结合上述两个重定位表,参考”.dynsym”节区,如下:
再看看GOT表,我们发现”.rel.plt”和”.rel.dyn”中r_offset其实是跳转到GOT表位置。GOT表中第二列其实就是那些需要重定位的符号地址,在linker加载so重定位时,其实就是修改这里的地址指向符号的内存地址。
具体过程见下面源码。
相关源码
在/android/4.2.2/bionic/linker/linker.c:
static int soinfo_link_image(soinfo* si) { …. //符号重定位 if(si->plt_rel) { if(soinfo_relocate(si, si->plt_rel, si->plt_rel_count, needed)) goto fail; } //符号重定位 if(si->rel) { if(soinfo_relocate(si, si->rel, si->rel_count, needed)) goto fail; }
…. } |
static int soinfo_relocate(soinfo *si, Elf32_Rel *rel, unsigned count, soinfo *needed[]) { Elf32_Sym *symtab = si->symtab; const char *strtab = si->strtab; Elf32_Sym *s; Elf32_Addr offset; Elf32_Rel *start = rel; for (size_t idx = 0; idx < count; ++idx, ++rel) { unsigned type = ELF32_R_TYPE(rel->r_info); //重定位类型 unsigned sym = ELF32_R_SYM(rel->r_info); //重定位的符号表索引 unsigned reloc = (unsigned)(rel->r_offset + si->load_bias); //需要重定位的地址 unsigned sym_addr = 0; char *sym_name = NULL; DEBUG("%5d Processing '%s' relocation at index %d\n", pid, si->name, idx); if (type == 0) { // R_*_NONE continue; } //1.获取sym_addr符号地址 if(sym != 0) { sym_name = (char *)(strtab + symtab[sym].st_name); bool ignore_local = false; ignore_local = (type == R_ARM_COPY); //查找sym_name定义在哪个so s = soinfo_do_lookup(si, sym_name, &offset, needed, ignore_local); if(s == NULL) { s = &symtab[sym]; if (ELF32_ST_BIND(s->st_info) != STB_WEAK) { DL_ERR("cannot locate symbol \"%s\" referenced by \"%s\"...", sym_name, si->name); return -1; } ......
} else { /* We got a definition. */ sym_addr = (unsigned)(s->st_value + offset); } count_relocation(kRelocSymbol); } else { s = NULL; } //2.根据重定位类型进行地址修复 switch(type){ case R_ARM_JUMP_SLOT: count_relocation(kRelocAbsolute); MARK(rel->r_offset); *((unsigned*)reloc) = sym_addr; break; case R_ARM_GLOB_DAT: count_relocation(kRelocAbsolute); MARK(rel->r_offset); *((unsigned*)reloc) = sym_addr; break; case R_ARM_ABS32: count_relocation(kRelocAbsolute); MARK(rel->r_offset); *((unsigned*)reloc) += sym_addr; break; case R_ARM_REL32: count_relocation(kRelocRelative); MARK(rel->r_offset); *((unsigned*)reloc) += sym_addr - rel->r_offset; break; case R_ARM_RELATIVE: count_relocation(kRelocRelative); MARK(rel->r_offset); if (sym) { DL_ERR("odd RELATIVE form...", pid); return -1; } *((unsigned*)reloc) += si->base; break; default: DL_ERR("unknown reloc type %d @ %p (%d)", type, rel, (int) (rel - start)); return -1; } } return 0; } |
由上面的源码分析知,linker在初始化so文件时,通过”.rel.plt”和”.rel.dyn”跳转到GOT表,并修改GOT表对应符号在内存地址,完成重定位过程。过程如下:
11.plt和.got解析
基本概念
.plt和.got的关系如下:
当一个外部符号被调用时,PLT去引用GOT中的其付哈哦对应的绝对地址,然后转入并执行。
“.got”:全局偏移表用于记录在elf文件中所导入的符号的绝对地址。
实例分析
“.plt”节区分析
“.plt”节区的内容如下:
我们用ndk自带的objdump反编译libdemo1.so文件:
arm-linux-androideabi-objdump.exe -D libdemo1.so > out.txt |
如下,PLT表一共有8个表项,查看”rel.plt”节区确实有8个符号需要进行重定位。
Disassembly of section .plt:
|
“.plt”节区开头是一个4x5=20bytes的固定结构,见左边黄色范围。
plt表的每个表项大小为:3x4=12bytes
模型如下:
|
|||||||
00000c38 <__cxa_atexit@plt>: c38: e28fc600 add ip, pc, #0, 12 c3c: e28cca03 add ip, ip, #12288 ; 0x3000 c40: e5bcf3a0 ldr pc, [ip, #928]! ; 0x3a0 |
(1) ip = pc + 0 = 0xc38 + 0x8 = 0xc40 (2) ip = ip + 0x3000 = 0x3c40 (3) pc <= [ip + 0x3a0] = [0x3fe0] (4) 跳转到地址0x3fe0地址,即GOT表,见下图
|
|||||||
00000c44 <__cxa_finalize@plt>: c44: e28fc600 add ip, pc, #0, 12 c48: e28cca03 add ip, ip, #12288 ; 0x3000 c4c: e5bcf398 ldr pc, [ip, #920]! ; 0x398 |
(1) ip = pc + 0 = 0xc44 + 0x8 = 0xc4c (2) ip = ip + 0x3000 = 0x3c4c (3) pc <= [ip + 0x398] = [3fe4] (4) 跳转到地址0x3fe4地址,即GOT表,见下图
|
|||||||
00000c50 <puts@plt>: c50: e28fc600 add ip, pc, #0, 12 c54: e28cca03 add ip, ip, #12288 ; 0x3000 c58: e5bcf390 ldr pc, [ip, #912]! ; 0x390 |
(1) ip = pc + 0 = 0xc50 + 0x8 = 0xc58 (2) ip = ip + 0x3000 = 0x3c58 (3) pc <= [ip + 0x390] = [3fe8] (4) 跳转到地址0x3fe8地址,即GOT表,见下图
|
|||||||
00000c5c <__gnu_Unwind_Find_exidx@plt>: c5c: e28fc600 add ip, pc, #0, 12 c60: e28cca03 add ip, ip, #12288 ; 0x3000 c64: e5bcf388 ldr pc, [ip, #904]! ; 0x388 |
(1) ip = pc + 0 = 0xc5c + 0x8 = 0xc64 (2) ip = ip + 0x3000 = 0x3c64 (3) pc <= [ip + 0x388] = [3fec] (4) 跳转到地址0x3fec地址,即GOT表,见下图
|
|||||||
00000c68 <abort@plt>: c68: e28fc600 add ip, pc, #0, 12 c6c: e28cca03 add ip, ip, #12288 ; 0x3000 c70: e5bcf380 ldr pc, [ip, #896]! ; 0x380 |
(1) ip = pc + 0 = 0xc68 + 0x8 = 0xc70 (2) ip = ip + 0x3000 = 0x3c70 (3) pc <= [ip + 0x380] = [3ff0] (4) 跳转到地址0x3ff0地址,即GOT表,见下图
|
|||||||
以此类推... |
备注:由于ARM采用三级
可见,上表PLT表最终跳转到GOT表对应位置
“.got”节区分析
“.got”节区内容如下:
通过objdump反编译”.got”节区内容如下:
动态调试
在第3章,我们观察libdemo1.so在内存的加载情况,其base地址为: 0x4c142000,则”.got”在内存中的地址为:0x4c145fb4。
下面我们通过ida动态调试libdemo1.so,观察其在内存中状况:
内存中GOT表正好和”.rel.plt”的对应。
linker在动态加载so的过程中,会重定位so中导入的符号地址,其实就是修改”.got”节区中的GOT表,将导入的符号地址指向该符号在内存中的地址,下面我们以libdemo1.so中调用的外部函数puts为例分析下:
puts函数是在libc.so库中,该库在Android系统启动的时候一起加载。
我们先用ida确定puts函数在libc.so文件中的位置:
然后我们查看libc.so在内存中的基地址:
ibc.so在内存的基地址:base = 0x40113000,所以prinf在内存中的地址为:ADDRESS(puts) = 0x40113000+ 0x1e8a8 = 0x40131a8。
我们也可以通过ida动态调试,查看到puts的内存情况,结果是一样的。
通过readelf –r libdemo1.so查看重定位表:
查看上面objdump反汇编后”.got”的内容我们知道:
libdemo1.so未加载到内存的时候,该”.got”GOT表中该puts函数的引用地址为:0xc24。这个地址是临时地址,在libdemo1.so加载到内存后,linker会根据printf在内存中的实际地址来修改GOT表中该符号的地址,并指向正确的地址,这时候printf函数才能被正确调用。
接下来我们通过动态调试跳转到PLT表,PLT表内存地址计算如下:
base = 0x4c142000,addr(PLT) = base + s_addr = 0x4c142000 + 0xc24 = 0x4c142c24。
我们在两个调用puts函数的地方下断点,观察内存状况变化:
这里在 0x4C142D54是第一个调用puts的低值,其指令:BL unk_4C142C50正好是PLT[3]表项,我们跳转进去看到确实是PLT[3]表项位置。
然后再继续单步调试。
LDR PC, [R12, #(off_4C145FE8 – 0x4C145C58)]! |
上面的[R12, #(off_4C145FE8 – 0x4C145C58)] = [0x4C145FE8] = [0x4C142000 + 0x3FE8] = [base + 0x3FE8],正好是GOT表中的地址。也就是说,上面的指令是讲GOT表项中存储的重定位后的符号的地址赋值给PC指针。
继续单步调试,这时候我们就到了puts函数的内存地址。
上述过程可以用下面的流程图表示。
TODO
这里为什么相差1?
分析:thumb模式特征:指令都是2bytes。我们查看用ida查看libc.so的printf的汇编指令:
每个指令相差2,很明显的thumb模式特征。
而thunb模式和arm模式的切换可以通过如下:
所以printf在GOT表和实际的内存地址相差1,就是因为thumb模式。
12.hash解析
基本概念
哈希表结构
nbucket |
nchain |
bucket[0] … bucket[nbucket-1] |
chain[0] … chain[nchain-1] |
chain[i]与symbolTable[i]对应
函数索引过程:
1.获取函数名的哈希值
funHash=elfhash(function_name)
2.获取函数索引
funIndex = bucket[funHash % nbucket]
3.如果funIndex对应的符号不是所需的,则chain[funIndex]给出具有相同哈希值的下一个符号表项,沿着chain链一直搜索,知道找到符合的符号位置
算法如下:
static unsigned elfhash(const char *_name){ const unsigned char *name = (const unsigned char *) _name; unsigned h = 0, g;
while(*name) { h = (h << 4) + *name++; g = h & 0xf0000000; h ^= g; h ^= g >> 24; } return h; } |
实例分析
nbucket=0x25=37
nchain=0x3c=60
….
13.init_array和.fini_array解析
基本概念
带有” __attribute__((constructor))”的函数会放到.init_array初始化,如下:
void init_func() __attribute__((constructor)); |
同样,带有” __attribute__((destructor))”的函数会放到.fini_array初始化,如下:
void fini_func() __attribute__((destructor)); |
实例分析
以上面的libdemo1.so文件为例,”.init_array”节区内容如下:
0x0CDC正好指向init_func函数地址。
以上面的libdemo1.so文件为例,”.fini_array”节区内容如下:
0x0C98和0x0CFC正好分别指向:sub_C98和fini_func两个函数:
参考
[1] ELF文件格式分析. 滕启明. http://staff.ustc.edu.cn/~sycheng/ssat/exp_crack/ELF.pdf
[2] Android 4.2.2、7.1.1中linker和elf.h源码.
[3] Android漫游记(1)---内存映射镜像(memory maps). http://blog.csdn.net/lifeshow/article/details/29174457
[4] http://flint.cs.yale.edu/cs422/doc/ELF_Format.pdf
[5] 通过GDB调试理解GOT/PLT. http://rickgray.me/2015/08/07/use-gdb-to-study-got-and-plt.html
[6] Android ELF文件PLT和GOT http://laokaddk.blog.51cto.com/368606/1160386
[7] Redirecting functions in shared ELF libraries. https://www.codeproject.com/Articles/70302/Redirecting-functions-in-shared-ELF-libraries
[8]《链接器和加载器》3.7 UNIX的ELF格式