Android通过修改ELF实现注入
在实现外挂的过程中,是通过将外挂的模块进行注入到对应的游戏进程中去实现的,我们可以通过相同的原理去注入so,来实现so注入进程实现frida-gadget的注入
首先是通过ELF文件的修改注入so
+------------------------+
| ELF Header | 文件头,描述整体结构和入口点
+------------------------+
| Program Header Table | 程序头表,描述加载的段
+------------------------+
| Segment/Sections | 实际段内容,如代码、数据等
+------------------------+
| Section Header Table | 段表,描述每个段的位置和功能
+------------------------+
这里是ELF文件的文件结构类型
ELF Header (ELF 文件头)
作用:描述文件类型、目标架构和入口点等元数据。
字段 | 描述 |
---|---|
e_ident |
魔数、文件类型标识、架构信息 (32/64 位,大小端) |
e_type |
文件类型(可执行文件、共享库、目标文件等) |
e_machine |
目标架构(如 x86、ARM 等) |
e_version |
ELF 版本 |
e_entry |
程序入口点虚拟地址 |
e_phoff |
程序头表在文件中的偏移 |
e_shoff |
段表在文件中的偏移 |
e_flags |
与架构相关的标志 |
e_ehsize |
ELF Header 的大小 |
e_phentsize |
程序头表中每个条目的大小 |
e_phnum |
程序头表中的条目数 |
e_shentsize |
段表中每个条目的大小 |
e_shnum |
段表中的条目数 |
e_shstrndx |
段表中字符串表的索引 |
** Program Header Table (程序头表)**
作用:描述进程运行时需要加载到内存的段信息,仅对可执行文件和共享库有用。
字段 | 描述 |
---|---|
p_type |
段类型(如 PT_LOAD 表示加载段) |
p_offset |
段在文件中的偏移 |
p_vaddr |
段在内存中的虚拟地址 |
p_paddr |
段在内存中的物理地址(通常忽略) |
p_filesz |
段在文件中的大小 |
p_memsz |
段在内存中的大小 |
p_flags |
段的访问权限(读、写、执行) |
p_align |
段在内存中的对齐粒度 |
常见段类型:
PT_LOAD
:加载到内存的段,通常对应.text
、.data
等。PT_DYNAMIC
:动态链接相关信息。PT_INTERP
:解释器路径(动态加载器)。PT_NOTE
:注释段,通常用于核心转储文件。PT_NULL
:无效条目,表示结束。
在Program Header Table中是包含了对于程序运行时的进程需要加载到内存的数据,而其中就保留对于依赖库,函数库..的相关函数信息,so文件的信息也包含在内。
所有我们要注入so到进程中去,主要的过程就是去实现so的注册以及写入
修改 DT_STRTAB
指向的新字符串表
通过定位 .dynstr
段找到对应的当前字符串表的位置,里面包含了各种字符串数据,通过也包含了现有 .so
名称列表
创建新的字符串表,由于修改原字符串表会使得其下的所有文件偏移都被修改了,为了使得文件偏移都不变,我们就继续保留原本的Program Header Table,同时将原字符串表复制到文件尾部,添加新的 .so
名称(如 frida-gadget.so
)。
更新 PT_LOAD
表项,由于新的字符串表要被映射到新的内存,需要通过PT_LOAD来实现
比如这样:
原始 Program Header Table:
Entry Index | p_type |
p_offset |
p_vaddr |
p_filesz |
p_memsz |
p_flags |
---|---|---|---|---|---|---|
0 | PT_LOAD |
0x00000040 | 0x08000000 | 0x00100000 | 0x00100000 | R+E |
1 | PT_LOAD |
0x00100040 | 0x08001000 | 0x00001000 | 0x00001000 | RW |
... | ... | ... | ... | ... | ... | ... |
新 Program Header Table(文件末尾):
Entry Index | p_type |
p_offset |
p_vaddr |
p_filesz |
p_memsz |
p_flags |
---|---|---|---|---|---|---|
0 | PT_LOAD |
0x00000040 | 0x08000000 | 0x00100000 | 0x00100000 | R+E |
1 | PT_LOAD |
0x00100040 | 0x08001000 | 0x00001000 | 0x00001000 | RW |
... | ... | ... | ... | ... | ... | ... |
5 | PT_LOAD |
文件尾偏移 | 映射地址 | 大小 | 大小 | R |
使得我们添加在文件末尾的数据被真实的加载映射了
修改动态段 (.dynamic
):因为添加了新的字符串数目,对应的字符串表的虚拟地址以及字符串总数目也会改变
更新 DT_STRTAB
和 DT_STRSZ
:
- 修改
DT_STRTAB
,使其指向新字符串表的虚拟地址。 - 修改
DT_STRSZ
,更新字符串表的总大小。
新增 DT_NEEDED
条目:
- 在 .dynamic 的尾部新增一个条目:
d_tag
:DT_NEEDED
。d_val
:新字符串表中.so
名称的偏移。
同时更新ELF header 的e_phoff,这里指向的是Program Header Table的位置,所有设置为新的Program Header Table
原始的ELF文件结构:
+------------------------+
| ELF Header | 包含文件元信息(如入口点、PHT/SHT 偏移等)
+------------------------+
| Program Header Table | PHT,描述 Segments 的加载信息
+------------------------+
| Segment 1 (.text) | 包含代码段
+------------------------+
| Segment 2 (.data) | 包含已初始化的全局和静态变量
+------------------------+
| Segment 3 (.bss) | 包含未初始化的全局和静态变量(运行时分配)
+------------------------+
| Section Header Table | SHT,描述 Sections 的信息
+------------------------+
注入 .so 后的 ELF 文件结构:
+------------------------+
| ELF Header | 更新了 e_phoff 指向新的 PHT 位置
+------------------------+
| (原) Program Header | 仍在文件起始,但未被使用
+------------------------+
| Segment 1 (.text) | 包含代码段
+------------------------+
| Segment 2 (.data) | 包含已初始化的全局和静态变量
+------------------------+
| Segment 3 (.bss) | 包含未初始化的全局和静态变量
+------------------------+
| Section Header Table | SHT,描述 Sections 的信息
+------------------------+
| 新字符串表 (.dynstr) | 新增,包含注入的 .so 名字
+------------------------+
| 新 Program Header Table| 移动到文件末尾,新增 PT_LOAD 表项
+------------------------+