本系列将完全是参考 https://space.bilibili.com/37877654/channel/seriesdetail?sid=1467282 这些视频而来,可以看作是一个文字版记录,建议先看视频。
工具
010 Editor
去官网下载正版,会有30天的试用期,windows不好处理,其他平台删个配置文件就可以无限续期了。
镜像
使用 boot 过的手机或者 arm64 模拟器。
脚本
由于将会对 /system/bin/ls
这个程序做修改,修改完后再推回手机查看变化,所以提供一个脚本方便使用:
adb push ls /data/local/tmp/ls
adb shell "chmod 777 /data/local/tmp/ls"
adb shell "/data/local/tmp/ls"
需要首先将/system/bin/ls
pull 出来。
elf_header
elf_header 下有一个子结构体,e_ident_t,这里面其实就第一个 file_identification 有用,它是固定的,为 .ELF。
struct e_ident_t e_ident
其他的,比如 ei_class_2_e,虽然它是用来描述该 elf 文件为 64 位的,但是实际上linker加载该 elf 文件的时候,根本不会在意这个值。
我们可以使用 010 将这个值改成 EE,然后 push 到手机上运行看看是否可以正常运行。
这里将 e_intent_t 里面除了file_identification 之外的全部改成了 EE,测试是否可以正常运行。
执行脚本,可以看到输出如下:
说明,linker确实不会关心这些字段,所以我们研究 elf 结构的时候,可以先关注重点字段,也就是elf文件加载的时候,会使用到的,这样可以集中精力。
总结,struct e_ident_t e_ident
里面只需要关心.ELF
这个值即可。
e_type
这个的值虽然是一个枚举,但是是实际上无论是 exe 还是 so,它们的值都必须是 ET_DYN (3)
,如果是一个 exe 文件,将这个值改成 ET_EXEC (2)
,它反而不能运行。
e_machine
这个字段说明CPU平台,比如 x86,arm32,arm64 等。
e_version
这个没啥用。
e_entry_START_ADDRESS
这个是非常重要的一个值。是当 elf 加载到内存中,其执行的起始地址,是一个虚拟地址。
e_phoff_PROGRAM_HEADER_OFFSET_IN_FILE
这个就是 program_header_table 段的偏移。因为 elf_header 后面就是 program_header_table,其实就是 elf_header 的大小。除非有人故意在这两者之间插入一些无用的数据。
e_shoff_SECTION_HEADER_OFFSET_IN_FILE
没啥用,实际上跟 section 有关的都没啥用,虽然很多书籍都对 section 部分大书特书,但是由于 elf 加载的时候根本就不使用 section 相关的东西,所以就没必要太在意这部分。
e_flags
没用
e_ehsize_ELF_HEADER_SIZE
没用,加载的时候根本不检查。所以改动 elf_header 的大小会有不可预料的后果。
e_phentsize_PROGRAM_HEADER_ENTRY_SIZE_IN_FILE
由于 program_header_table 是一个数组,这个表示数组元素的大小。
e_phnum_NUMBER_OF_PROGRAM_HEADER_ENTRIES
由于 program_header_table 是一个数组,这个表示数组的大小。
e_shentsize_SECTION_HEADER_ENTRY_SIZE
section 相关,无用。
e_shnum_NUMBER_OF_SECTION_HEADER_ENTRIES
section 相关,无用。
e_shtrndx_STRING_TABLE_INDEX
section 相关,无用。
总结:section 相关,无用,program 相关,有用。
我们可以做一个实验,将 section header 部分全部使用 EE 覆盖,push 到手机上,是依然可以运行的。
覆盖完之后,将该程序使用 ida 打开,发现 ida 根本解析不了,这是因为 IDA 是依靠 section 来解析的。
实际上加载一个 so 文件的时候,ida 的 segment view 里面就是解析的 section header。如果我们破坏了甚至是弄一个假的 .text/.data section,那么 ida 就没有办法正常解析了:
program_header_table
它是一个数组,里面的元素结构体为是 program_table_element。
p_type
段类型
p_flags
段属性,可读,可写,可执行。
p_offset_FROM_FILE_BEGIN
段在文件中的偏移。
p_vaddr_VIRTUAL_ADDRESS
段的虚拟地址。这个非常有用,这里表示的是一个相对偏移,因为段被加载到的虚拟地址是不确定的,所以真实的虚拟地址 = 加载的虚拟地址的基址 + 这个虚拟地址
p_paddr_PHYSICAL_ADDRESS
段的物理地址,这个没啥用,段只有虚拟地址才有意义。
p_filesz_SEGMENT_FILE_LENGTH
段在文件中的长度。
p_memsz_SEGMENT_RAM_LENGTH
段在内存中的长度。
p_align
段的对齐方式,没啥用,只能说是一个建议,os 一般不管这个值。
上面这几个字段,描述的其实是将段加载到内存中时,elf文件中的段映射到了内存中。
p_offset_FROM_FILE_BEGIN 与 p_filesz_SEGMENT_FILE_LENGTH 表示了文件中的段。
p_vaddr_VIRTUAL_ADDRESS 与 p_memsz_SEGMENT_RAM_LENGTH 表示了内存中的段。
这两者构成一个映射关系,linker 在加载 elf 的时候也是采用的 mmap。
p_filesz_SEGMENT_FILE_LENGTH 与 p_memsz_SEGMENT_RAM_LENGTH 的大小不一定一样,因为为了节省 elf 文件大小,有些值为0的段,比如 .bss 就不占文件空间。但是加载到内存后,还是要分配空间的。
整体看下段信息:
发现,可加载段实际上只有2个,这2个就是我们需要重点关注的段。
看其权限,可知,R_X 这个段必定是代码段。
根据,elf_header 的 e_entry_START_ADDRESS 值,我们知道其代码执行的入口在 0x000000000000CCC4 这个地址。
这个地址,加上段的虚拟地址的偏移 0,所以,我们知道其入口代码的虚拟地址是 CCC4。我们再看其文件映射关系:
是1对1映射,起始地址都为0,也就是说入口代码的文件地址也是 CCC4:
那么我们将这个入口指令改成一个死循环,就可以将 ls 卡住。
我们查看其 maps 文件:
发现,这里有3段,对比一下权限与大小,可以看出中间一段是用于填充的(有误,看下一篇),没啥用。
我们也可以更改elf文件段的 p_flags,它会影响到 maps 里面段的属性,因为它们会保持一致。但是对于非可加载段,这个 p_flags 是没有用的。