【茶余饭后-Frida hook 系统点位原理分析 --Linker】此文章归类为:茶余饭后。
什么是linker/linker64
- 加载并链接可执行文件(ELF)及其依赖的共享库(.so)
- 解析符号(Symbol Resolution)
- 重定位 (Relocation)
- 初始化程序环境(TLS, 构造函数)
在Android 中,linker/linker64是32/64位架构下的动态链接器(后面统一用linker 代替动态链接器),其作用是:
位置
默认
也可以通过 find 查找
APEX (Android Pony Express)
Android 版本 | linker64 路径 | 说明 |
Android 5-8 | /system/bin/linker64 | 传统路径 |
Android 9+ | /system/bin/linker64_arm64<br>/system/bin/linker64_x86_64 | 按架构细分路径 |
Android 10+ | /apex/com.android.runtime/bin/linker64 | 引入 APEX 模块后,路径可能变化 |
- find /system -name linker64
- find /apex -name linker64
文件格式:APEX 本质是一个ZIP 压缩包, 包含以下
挂载路径:系统启动时,APEX模块会挂载到 /apex/<module_name>路径下,例如:
- Android 10 开始,linker64 会优先从APEX 模块加载依赖库。
文件/目录 | 说明 |
apex_manifest.json | 模块元数据(名称、版本、依赖等) |
AndroidManifest.xml | 模块声明(兼容性要求) |
payload.bin | 实际内容(文件系统镜像) |
payload_properties.txt | 镜像属性(如哈希值、大小) |
apex_pubkey | 签名公钥(用于验证模块) |
apex_signature | 模块签名(防篡改) |
/apex/com.android.runtime/bin/linker64 # linker64 动态链接器
/apex/com.android.art/bin/dalvikvm # ART 虚拟机
模块名称 | 作用 |
com.android.art | Android 运行时(ART) |
com.android.runtime | Bionic C 库、linker64 动态链接器 |
com.android.llvm | Clang 编译器工具链 |
com.android.conscrypt | Java 加密库(Conscrypt) |
com.android.media | 多媒体框架(如 OMX、MediaCodec) |
com.android.os.statsd | 系统统计服务(StatsD) |
- Android 10 引入的一种系统模块容器格式, 用于将底层系统组件以模块化的方式打包和管理。 核心目标是解耦系统组件与系统镜像(system.img), 以实现更灵活更新,隔离和安全控制
APEX 应用场景
APEX 结构和工作原理
- 源码位置 : 位于AOSP 项目的 system/core/linker
Android 位置 (有些厂商会做自定义修改)
核心流程分析
linker 调用顺序图示
ElfReader::Load()
│
├── soinfo::soinfo() 创建 soinfo
│
├── soinfo::parse_dynamic() 解析动态段
│
├── soinfo::load_library() 加载依赖库
│ └── soinfo::find_library()
│ └── soinfo::load_library_from_path()
│ └── 再次调用 ElfReader::Load()
│
├── soinfo::resolve_symbol() 解析符号
│
├── soinfo::relocate() 重定位
│
├── soinfo::allocate_tls() TLS 初始化
│
└── soinfo::call_constructors() 调用构造函数
│ └── .init() 初始化函数
│ └── .init_array() 初始化函数组
加载ELF 文件
- 读取ELF 文件头
解析程序头,确定加载段(PT_LOAD)
- 分配内存并映射可执行段 (.text, .rodata等)
- 返回加载后的及地址 (base)
字段名 | 含义 |
p_type | 段类型(值为 PT_LOAD 表示可加载段)。 |
p_offset | 文件中的偏移量(即该段在文件中的起始位置)。 |
p_vaddr | 虚拟地址(VMA):段在进程地址空间中的目标加载地址。 |
p_paddr | 物理地址(LMA):通常用于嵌入式系统,普通系统中可忽略。 |
p_filesz | 文件中该段的大小(字节)。 |
p_memsz | 内存中该段的大小(可能比 p_filesz 大,未初始化部分填充零)。 |
p_flags | 内存权限标志:PF_R(读)、PF_W(写)、PF_X(执行)。 |
p_align | 对齐方式(通常按页对齐,如 0x1000)。 |
- 代码段(.text): 可执行命令
- 数据段(.data):已初始化的全局变量
- BSS段(.bss):未初始化的全局变量(运行时分配)
- 堆栈空间:某些编译器会通过PT_LOAD 预留堆栈内存
PT_LOAD 段定义了进程执行时需要从文件加载到内存的连续区域。 这些段通常包含:
- 操作系统会根据PT_LOAD 的描述将文件中的特定部分映射到进程的虚拟地址空间,并设置内存权限(可读,可写,可执行)
在ELF 文件中, PT_LOAD 段描述由ELF_Phdr 结构体定义,不同架构略有差异
ElfReader::Load()
- AOSP 路径:system/core/linker/ElfReader.cpp
创建soinfo 结构
struct soinfo {
const char* name; // 模块名
void* base; // 加载基地址
size_t size; // 模块大小
Elf_Phdr* phdr; // 程序头
size_t phnum; // 程序头数量
Elf_Dyn* dynamic; // 动态段指针
soinfo* next; // 链表指针
void (*entry)(void); // 入口点
void call_constructors(); // 构造函数调用
};
- soinfo 是linker 内部用于表示加载的ELF 模块的数据结构
- AOSP 路径:system/core/linker/soinfo.cpp 实现soinfo 结构及加载,解析逻辑
关键字段如下
解析动态段
遍历 .dynamic 段,提取关键信息
- .dynamic 段是 动态链接器工作的基础,当程序启动时,内核会加载linker, 并由linker执行 .dynamin 段中的信息,递归加载所有依赖的.so , 并完成符号绑定和重定位
- DT_SYMTAB:符号表地址
- DT_STRTAB: 字符串表地址
- DT_JMPREL:PLT 重定位表
- DT_RELA/DT_REL: 动态重定位表
- DT_INIT/DT_INIT_ARRAY: 构造函数地址
- DT_FINI/DT_FINI_ARRAY:析构函数地址
- DT_NEEDED:依赖的共享库名称
soinfo::parse_dynamic()
- AOSP 路径:system/core/linker/soinfo.cpp 实现soinfo 结构及加载,解析逻辑
加载依赖库
- 遍历DT_NEEDED表中的依赖库名称
- 对每个依赖库调用 soinfo::find_library() 或 soinfo::load_library_from_path()
- 递归加载依赖库形成依赖链
soinfo::load_library(const std::vector<std::string>& needed)
- AOSP 路径:system/core/linker/soinfo.cpp 实现soinfo 结构及加载,解析逻辑
符号解析
遍历.dynsym 符号表,查找未定义符号(UND)
- 使用dlsym或全局符号表查找符号定义
- 更新符号地址用于后续重定位
typedef struct {
uint32_t st_name; // 符号名称在 .dynstr 中的偏移
unsigned char st_info; // 符号类型和绑定信息
unsigned char st_other; // 符号可见性
uint16_t st_shndx; // 所在段索引(如 SHN_UNDEF 表示未定义)
uint64_t st_value; // 符号地址(虚拟地址)
uint64_t st_size; // 符号大小
} Elf64_Sym;
- 在.rela.dyn 或 .rela.plt 段中, 重定位条目会引用 .dynsym 表中的符号索引,以确定要修正的地址
- .dynsym 表中记录了模块导出的全局符号(如函数,全局变量), 供其他模块调用。
- 例如:
libnative.so
导出的 Java_com_example_MyClass_myMethod
函数会出现在 .dynsym
中。
- 在动态链接过程中,linker 会遍历 .dynsym 表,查找未定义符号的定义位置
- Eg: 在.so 文件中引用了printf 函数, 动态链接器会在libc.so 的 .dynsym中查找printf的地址
.dynsym 是ELF 文件的一个关键段,用于存储动态链接所需的符号信息。
符号解析
动态链接
重定位
.dynsym 结构
soinfo::resolve_symbol()
- AOSP 路径:system/core/linker/symbol.cpp 实现符号解析逻辑
重定位
- 遍历.rela.dyn 和 .rela.plt 重定位段
根据重定位类型(如 R_AARCH64_RELATIVE, R_AARCH64_JUMP_SLOT)修改代码段中的地址引用
修正全局偏移表(GOT)和过程链接表(PLT)和 动态重定位段(DYN)
Android 中的动态链接流程
特性 | R_AARCH64_RELATIVE | R_AARCH64_JUMP_SLOT |
用途 | 修正基于基地址的绝对地址 | 修正 PLT 表项(外部函数地址) |
是否需要符号表 | 否 | 是 |
应用场景 | 全局变量、静态变量、函数指针 | 外部函数调用(如 PLT) |
性能影响 | 低(无需符号解析) | 较高(需符号解析) |
与 ASLR 的关系 | 直接支持 ASLR | 间接支持 ASLR(通过 PLT 修正) |
ASLR (Address Space Layout Randomization) 是Android 系统的内存保护机制,通过随机化进程的地址空间布局(如堆,栈,共享库,可执行文件基址等)来增加攻击者预测内存地址的难度,从而防御缓冲区溢出等攻击
可执行文件基址 | 若启用 PIE(Position Independent Executable),程序入口地址随机化。 |
动态链接库(.so) | 加载地址随机化(通过 /system/bin/linker 实现)。 |
堆(Heap) | 堆起始地址随机化(由内核的 mmap 分配策略决定)。 |
栈(Stack) | 栈基址随机化(由内核的 execve 调用决定)。 |
mmap 区域 | 动态分配的内存(如 mmap)地址随机化。 |
- 缓冲区溢出,是由程序未正确检查输入数据边界,导致写入缓冲区的数据超出其分配内存范围,从而覆盖相邻内存区域的安全漏洞。
随机化范围
- 用于.PLT表项的重定位, 即动态链接器在运行时解析外部函数地址后,将其填充到.plt 表中
- PLT 表项的地址 = 符号(外部函数)的实际地址(运行时解析)+家数(通常为0)
- 用于相对地址重定位。即在程序加载到内存时,根据模块的基地址动态调试某些地址值
- 需要修正的目标地址 (.got 表项地址)=模块加载的基址(运行时linker确定)+符号的偏移或常量
R_AARCH64_RELATIVE
R_AARCH64_JUMO_SLOT
二者对比
- 结构: 每个外部函数在.PLT对应一个跳转桩(Stub)初始跳转桩会触发动态链接器解析函数地址,后续直接跳转到实际地址
__GOT_printf
是 .GOT
中 printf
的条目,初始指向 linker
的解析函数。; PLT 示例代码(ARM64)
printf@PLT:
adrp x16, __GOT_printf@PAGE
ldr x16, [x16, __GOT_printf@PAGEOFF]
br x16
- 结构:每个外部函数或变量在.GOT中对应一个条目, 初始时,.GOT 的地址可能会指向动态链接器的解析逻辑,后续通过 linker 填充实际地址。
- 示例: 对printf 的调用:程序首次调用时,.GOT 中的printf 地址指向linker 的解析函数,解析后地址更新为libc.so 中的printf 的真实地址
GOT (Global Offset Table) 全局偏移表,用于存储外部符号(如库函数,全局变量)的实际地址
PLT (Procedure Linkage Table)过程链接表,用于间接调用外部函数,延迟绑定时,通过PLT 调用动态解析GOT中的地址
- DYN (Dynamic Relocation) 动态重定位段。记录了需要动态链接器处理的全局变量,外部函数地址等符号的重定位信息。主要用于修正数据引用(全局变量,静态变量)或非PLT 函数调用地址。
- 若设置环境变量LD_BIND_NOW=1。所有外部符号在程序启动时解析,而非延迟绑定。
- LD_BIND_NOW 是linker 的一个环境变量,强制linker启动时立即解析所有外部符号,可以减少.GOT被修改的窗口期,但是会增加启动时间,部分支持,受Android版本影响
- 原理:仅在首次调用某个外部符号时解析其地址
当程序首次调用外部函数(如 printf)时:
- 执行.PLT 中的跳转桩,跳转到.GOT 条目
- .GOT 条目初始指向linker的解析函数
- linker 解析符号的实际地址。更新.GOT 条目
- 后续调用直接跳转到.GOT 中已解析的地址
- Linker 加载主程序和依赖的.so 库
- 解析.dynamin 段,确定依赖关系和符号表
程序启动
延迟绑定(Lazy Binding):
立即绑定:
soinfo:relocate()
- AOSP 路径:system/core/linker/relocation.cpp 实现重定位逻辑
TLS 初始化
- 为模块分配TLS(Thread-Local Storage) 内存
- 初始化TLS模板,用于线程创建时复制
soinfo::allocate_tls()
- TLS(Thread Local Storage 线程局部存储) 一种线程私有数据存储基质,允许每个线程拥有独立的变量副本,避免多线程环境下的数据竞争问题。
- AOSP 路径:system/core/linker/tls.cpp 实现TLS初始化逻辑
调用构造函数
- Android10 call_constructors 偏移量:0x523FC (不同设备偏移有差异)
调用.init段中的构造函数
调用.init_array段中的构造函数数组
- 顺序:依赖库的构造函数先执行,当前模块的后执行
- .init 段时一个单入口函数指针,通常指向一个初始化函数,用于执行全局初始化逻辑
- .init_array 段存储的是函数指针列表。每个指针指向一个初始化函数。 Jni_Onload 就是其中的一个函数
soinfo:call_constructors()
- 核心流程
void soinfo::call_constructors() {
// 1. 查找 DT_INIT 段(旧式构造函数)
for (Elf_Dyn* d = dynamic; d->d_tag != DT_NULL; ++d) {
if (d->d_tag == DT_INIT) {
void (*init_func)(void) = reinterpret_cast<void (*)(void)>(base + d->d_un.d_val);
init_func(); // 调用构造函数
}
}
// 2. 查找 DT_INIT_ARRAY 段(新式构造函数数组)
for (Elf_Dyn* d = dynamic; d->d_tag != DT_NULL; ++d) {
if (d->d_tag == DT_INIT_ARRAY) {
void (**init_array)(void) = reinterpret_cast<void (**)(void)>(base + d->d_un.d_ptr);
size_t count = d->d_un.d_val / sizeof(void*);
for (size_t i = 0; i < count; ++i) {
init_array[i](); // 依次调用每个构造函数
}
}
}
}
获取Android 设备中的 偏移地址, 将linker 拖入ida 可得
- AOSP bionic/linker/linker_soinfo.cpp 调用构造函数
- AOSP system/core/linker/linker_main.cpp 主流程控制
liker 使用场景
- Android 应用中通过 JNI 调用的Native代码 也是由linker加载的
- 所有使用C/C++编写的远程程序在启动时都会由linker加载
- 这些依赖的.so 文件 也会通过linker 进行动态链接
- 在Android 启动过程中,zygote进程是通过/system/bin/app_process 启动的
- app_process 是一个ELF 可执行文件,动态链接器位linker
系统启动阶段
原生程序 (Native Apps)
JNI 调用
更多【茶余饭后-Frida hook 系统点位原理分析 --Linker】相关视频教程:www.yxfzedu.com