概述
前文简易Android ARM&ARM64 GOT Hook (一),基于链接视图,解析section查找GOT表偏移值,无法应对section被处理过的情况(如加固)。
本文基于ELF的执行视图(Execution View
),讨论Android
ARM&ARM64
架构的GOT/PLT Hook
(仍以Hook公共库libc.so
的getpid
函数为例)。
思路
- 通过maps文件获取模块基址,解析内存中的ELF。
- 查找并解析
.dynamic
段,得到.rel.plt
、.rel.dyn
、.dynsym
、.dynstr
和.hash
表。
- 通过
.hash -> .dynsym -> .dynstr
查找导入符号,再遍历.rel.plt
和.rel.dyn
,得到函数偏移。
- 基址加上偏移得到内存地址,修改函数地址即可。
注入方式及编译环境同前文,不再赘述。
具体实现
核心代码
// 基于执行视图解析ELF
uintptr_t hackBySegment(const char *moudle_path, const char *target_lib, const char *target_func,
uintptr_t replace) {
LOGI("hackDynamic start.\n");
// 获取目标函数地址
void *handle = dlopen(target_lib, RTLD_LAZY);
auto ori = (uintptr_t) dlsym(handle, target_func);
LOGI("hackDynamic getpid addr: %lX\n", ori);
// 获取符号地址 (解析Segment)
uintptr_t replaceAddr = getSymAddrDynamic(moudle_path, target_func);
// 替换地址
replaceFunction(replaceAddr, replace, ori);
return ori;
}
完整代码见AndroidGotHook
获取.dynamic段
解析maps文件获取到模块地址后,基于执行视图解析ELF。
从elf header
中得到program header table
的起始偏移(e_phoff
)、program header
大小(e_phentsize
)和总program header
个数(e_phnum
)
遍历program header table
,查找p_type
为PT_DYNAMIC
的program header
,得到.dynamic
段在内存中的偏移值(p_vaddr
)和大小(p_memsz
)
将模块基址与.dynamic
偏移值相加,得到.dynamic
段的起始地址。
解析.dynamic段
遍历.dynamic
段,根据d_tag
解析不同的表。
.rel.plt
:DT_JMPREL
(大小储存在DT_PLTRELSZ
)
.rel.dyn
:DT_REL
(大小储存在DT_RELSZ
)
.dynsym
:DT_SYMTAB
.dynstr
:DT_STRTAB
.hash
:DT_HASH
注:解析.dynamic
的ELF模板来自Android7.0以上命名空间详解(dlopen限制)附上010editor模块
查找符号
通过.hash -> .dynsym -> .dynstr
查找符号。
ELFW(Sym) *target = nullptr;
uint32_t hash = elf_sysv_hash((const uint8_t *) symName);
for (uint32_t i = buckets[hash % buckets_cnt];
0 != i; i = chains[i]) {
ELFW(Sym) *sym = dynsym + i;
unsigned char type = ELF_ST_TYPE(sym->st_info);
if (STT_FUNC != type && STT_GNU_IFUNC != type && STT_NOTYPE != type)
continue; // find function only, allow no-type
if (0 == strcmp(dynstr + sym->st_name, symName)) {
target = sym;
break;
}
}
具体操作为:
-
计算符号名称的哈希值,然后从.hash
获取符号在.dynsym
中的索引。
接受符号名称的散列函数会返回一个值,用于计算 bucket 索引。因此,如果散列函数为某个名称返回值 x,则 bucket [x% nbucket] 将会计算出索引 y。此索引为符号表和链表的索引。如果符号表项不是需要的名称,则 chain[y] 将使用相同的散列值计算出符号表的下一项。
-
通过索引从.dynsym
获取符号,根据符号类型和符号名(通过st_name
从.dynstr
获取字符串),判断与目标函数是否匹配。
PS:也可直接遍历.dynsym
并比对字符串,时间复杂度更高
.hash -> .dynsym -> .dynstr,时间复杂度:O(x) + O(1) + O(1)
.dynsym -> .dynstr,时间复杂度:O(n) + O(1)
计算内存地址
遍历.rel.plt
和.rel.dyn
,比对符号和重定位类型,获取函数偏移值(r_offset
)。与模块基址相加得到内存地址。
for (int i = 0; i < rel_plt_cnt; i++) {
ELFW(Rel) &rel = rel_plt[i];
if (&(dynsym[ELF_R_SYM(rel.r_info)]) == target &&
ELF_R_TYPE(rel.r_info) == ELF_R_JUMP_SLOT) {
// LOGI("target r_offset: %lX", rel.r_offset);
return moduleBase + rel.r_offset;
}
}
for (int i = 0; i < rel_dyn_cnt; i++) {
ELFW(Rel) &rel = rel_dyn[i];
if (&(dynsym[ELF_R_SYM(rel.r_info)]) == target &&
(ELF_R_TYPE(rel.r_info) == ELF_R_ABS
|| ELF_R_TYPE(rel.r_info) == ELF_R_GLOB_DAT)) {
// LOGI("target r_offset: %lX", rel.r_offset);
return moduleBase + rel.r_offset;
}
}
之后替换该内存地址对应的函数即可,同前文。
适配ARM64
#if defined(__LP64__)
#define ELFW(what) Elf64_ ## what
#define ELF_R_TYPE(what) ELF64_R_TYPE(what)
#define ELF_R_SYM(what) ELF64_R_SYM(what)
#else
#define ELFW(what) Elf32_ ## what
#define ELF_R_TYPE(what) ELF32_R_TYPE(what)
#define ELF_R_SYM(what) ELF32_R_SYM(what)
#endif
#if defined(__arm__)
#define ELF_R_JUMP_SLOT R_ARM_JUMP_SLOT //.rel.plt
#define ELF_R_GLOB_DAT R_ARM_GLOB_DAT //.rel.dyn
#define ELF_R_ABS R_ARM_ABS32 //.rel.dyn
#elif defined(__aarch64__)
#define ELF_R_JUMP_SLOT R_AARCH64_JUMP_SLOT
#define ELF_R_GLOB_DAT R_AARCH64_GLOB_DAT
#define ELF_R_ABS R_AARCH64_ABS64
#endif
测试
同前文,替换动态库即可测试。
日志
ARM
ARM64
作为动态链接库
详见项目的vitcim_app
模块。
通过配置CMakeLists.txt
将libinject.so
作为动态库链接,在JNI_OnLoad
调用hackBySegment
替换getpid
函数。
日志如下:
总结
主要学习了.dynamic
段的解析和导入符号的查找方式。虽然实现了基于执行视图解析ELF,但仍存在许多不足。不过这次是真的短期内不会再改动了。(不要重复造轮子)
参考
基于Android的ELF PLT/GOT符号和重定向过程ELF Hook实现
ELF文件格式与got表hook简单实现
重定位节 - 链接程序和库指南
符号表节 - 链接程序和库指南
散列表节 - 链接程序和库指南
动态节 - 链接程序和库指南
ELF文件结构详解
PLT HOOK
bhook
xhook