-
-
[原创]从0到1构建一个Hook工具之注入器篇(二)
-
发表于: 2026-3-18 23:03 2247
-
在上一篇文章中,我们已经了解了注入的基本概念并且实现了attach注入模式,这个时候我们很容易想到attach模式的一个缺陷:当我们想要观察或拦截app启动早期的行为时,使用attach往往已经错过了最有价值的时机。
spawn注入模式就可以解决这个问题,并且在逆向场景中这种模式也非常的常见。
项目地址:8b9K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6^5x3h3q4G2L8U0q4F1k6#2)9J5c8V1&6A6L8X3A6W2j5%4c8G2M7R3`.`.
这里有两种实现spawn的方案,一种是传统的直接hook zygote实现,另一种则是参考了Frida的Zymbiote方案,这里我们分别对两种方案做一个介绍和实现。后续有什么讲的不对的或者可以补充改进的欢迎在评论区提出!
使用attach注入时,目标进程已经是启动的状态,我们很容易就可以知道他的pid,而spawn注入时,目标进程还没有被创建,pid也就无从所知了,我们必须就先在某个上游位置埋点,并且要保证能在目标子进程出现时被命中
spawn注入的核心思路其实就是:先用attach把一个so(后文记作helper)注入zygote,再由zygote内部常驻的这个so安装fork和子进程初始化hook,在命中目标包名后,最终由目标子进程自己执行dlopen(target.so),详细讲就是:
这里其实可以分为三层结构:主程序、helper、目标app子进程。
第一步首先是复用attach,主程序先把helper注入到zygote中,这个helper的定位并不是一个playload,而是spawn模式中常驻在zygote内部的一个中间控制模块。
此时我们已经成功将helper注入到了zygote中并且返回了一个handle,接下来要做的就是远程调用一个ainject方法,这里就需要用到dlsym函数,结合前一篇文章我们很容易就能完成
上文我们简单提到ainject的作用是保存目标信息和安装hook,这里的hook我们先借助dobby框架实现,暂时不去理会他的具体实现。
到了这一步,在zygote内部的helper已经知道了目标包名、知道了target.so的路径并且进入了等待目标子进程的状态,但是由于此时子进程还没有完全的初始化,因此还不能判断出是否是我们的目标子进程,所以需要在子进程中安装hook,用于获取进程名,这里hook点选择了android_os_process_setArgV0和selinux_android_setcontext
然后当子进程中命中了Hook之后就会进入判断包名是否匹配,从而决定是否加载target.so,并且此时之前的hook已经不需要了可以执行unhook了
相较于之前的方案,Zymbiote不是直接给目标app打补丁,而是先“污染”zygote(很符合Symbiote这个名字),核心不再是将一个helper常驻到zygote中,而是直接patch zygote启动链中某个关键运行时入口的槽位,让目标app正常启动过程中自然进入一段预埋的stub,并在命中目标进程后执行dlopen实现注入。
我们先理一下详细的核心流程:
函数调用链大概如下
这部分代码主要参考了1fbK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6y4M7X3q4U0K9#2)9J5c8W2c8u0L8X3A6W2j5%4c8G2M7W2)9#2k6W2y4&6L8h3u0A6
链路开始是collect_spawn_source_pids获取zygote的pid然后inject_spawn_symbi_by_pids()执行注入流程,实现Zymbiote的核心逻辑就在这个方法中。接下来按顺序来介绍核心逻辑的流程:
collect_symbi_context这是整个Zymbiote最核心的准备函数,他负责回答了5个问题:
先解析目标包的uid,具体实现就是读取/data/system/packages.list逐行匹配包名获取uid。
然后解析zygote的/proc/<pid>/maps,方便后续查找libandroid_runtime.so、libstagefright.so、boot heap等的位置。
接着是选择shellcode的落点,这里选择的是libstagefright.so的可执行段中选最后一页,因为这页本身就已经在进程中映射好了,而且是可执行页,避免需要额外通过mprotect修改内存页权限:
通过libandroid_runtime.so基址 + 导出偏移求得真实地址,这是后面搜索ArtMethod的“目标指针值”的基础。
接下来是解析setArgV0Native的真实运行地址,在前面我们已经找到了libandroid_runtime.so,下一步就可以通过get_module_base()获取模块基址,再调用get_symbol_offset_from_elf()获取符号偏移从而计算出实际的地址。
boot heap:如果某段映射路径中包括boot.art、boot-framework.art、dalvik-LinearAlloc并且是可读写区域,就把他加入boot heap候选区heap_candidates,这一步实际就是在近似的定位ART的boot heap区域。
为什么需要他呢?因为比如android.os.Process.setArgV0Native()对应的Java/native方法元信息最终会体现在ART的对象布局里,这些元信息一般会驻留在boot image相关heap中,如果我们知道原始native函数地址,就可以在这个heap_candidates中反向搜索“谁指向了他”,这正是后面定位ArtMethod slot的基础。
如何在boot heap中搜索到ArtMethod slot呢?在上面,我们已经得到了setArgV0Native的实际地址,因此我们直接在每一块heap内存中搜索其对应的指针值,如果找到了就把该地址记为art_method_slot即可,原理就如上文所述,android.os.Process.setArgV0Native() 对应的 ArtMethod 结构里,就会有一个 entry point 字段指向它真正的 native 实现。既然我们已经知道真实的 native 地址,就可以在 ART 的方法元信息区中,反向搜索“谁保存了这个地址”。
然后是stub相关的逻辑,stub是一段“裸写入进程内存的二进制blob”,他不会像正常的so那样经过动态链接器的完整重定位流程,所以他内部要调用的函数地址都必须由注入器外部算好然后回填,比如在这里我们需要解析libc.so:getuid, libdl.so : dlopen,liblog.so:__android_log_print,这部分通过get_remote_symbol()函数完成,具体实现其实就是get_module_base()找模块基址,get_process_maps()找真实文件路径,get_symbol_offset_from_elf()算偏移,然后base + offset即可。
接下来就到了write_stub_and_patch_slot的部分了,我们需要先暂停zygote进程,然后直接修改/proc/<pid>/mem即可,那写的内容是什么呢?当然是我们的stub,此时我们不仅已经有了编译好了二进制blob,并且需要的上下文信息都已经搜集完毕,只需要找到配置区并回填即可,这里就需要通过配置块的定位标记。
回填好后,函数将stub写到shellcode_base位置,然后将art_method_slot改成shellcode_base,至此我们就完成了在zygote中放入一段可执行stub,并将setArgV0Native的ArtMethod entry point改指向这段stub。
当目标app启动之后,他会从已经打过patch的zygote中fork出来,随着app启动流程推进,android.os.Process.setArgV0Native()会被调用,然后进入stub的流程,命中uid后便会指向dlopen完成target.so的注入。
get_module_base
get_symbol_offset_from_elf
get_remote_symbol
handle = inject_so_handle_by_pid(zygote_pid, ncore_path);
if (handle == nullptr) {
LOGE("prepare_spawn_in_zygote: inject ncore failed");
return false;
}
remote_sym_name = remote_alloc_string(zygote_pid, "ainject");
remote_pkg = remote_alloc_string(zygote_pid, package_name);
remote_so = remote_alloc_string(zygote_pid, so_path);
remote_ainject = call_remote_function<void*, void*, const char*>(
zygote_pid,
reinterpret_cast<void*>(dlsym),
handle,
reinterpret_cast<const char*>(remote_sym_name)
);
params[0] = reinterpret_cast<long>(remote_pkg);
params[1] = reinterpret_cast<long>(remote_so);
call_remote_call<void>(zygote_pid, reinterpret_cast<long>(remote_ainject), 2, params);
g_target_package = strdup(package_name);
g_target_so = strdup(so_path);
g_payload_loaded = false;
INSTALL_HOOK(fork, fork);
INSTALL_HOOK(vfork, vfork);
static void install_child_hooks() {
LOGI("ncore: installing child hooks pid=%d", getpid());
g_android_os_Process_setArg = DobbySymbolResolver(
"libandroid_runtime.so",
"_Z27android_os_Process_setArgV0P7_JNIEnvP8_jobjectP8_jstring");
if (g_android_os_Process_setArg != nullptr) {
INSTALL_HOOK(android_os_Process_setArgV0, g_android_os_Process_setArg);
} else {
LOGE("ncore: android_os_Process_setArgV0 not found");
}
g_selinux_android_setcontext = DobbySymbolResolver("libselinux.so", "selinux_android_setcontext");
if (g_selinux_android_setcontext != nullptr) {
INSTALL_HOOK(selinux_android_setcontext, g_selinux_android_setcontext);
} else {
LOGE("ncore: selinux_android_setcontext not found");
}
}
static bool load_payload_if_needed(const char* package_name) {
if (!matches_target(package_name)) {
return false;
}
if (g_payload_loaded) {
LOGI("ncore: payload already loaded for %s", package_name);
return true;
}
LOGI("ncore: target matched, loading payload package=%s so=%s",
package_name != nullptr ? package_name : "(null)",
g_target_so != nullptr ? g_target_so : "(null)");
unhook_all();
void* handle = dlopen(g_target_so, RTLD_NOW | RTLD_NODELETE | RTLD_GLOBAL);
if (handle == nullptr) {
LOGE("ncore: dlopen failed for %s: %s", g_target_so, dlerror());
return false;
}
g_payload_loaded = true;
LOGI("ncore: payload loaded for %s => %s", package_name, g_target_so);
send_status_to_injector(package_name, g_target_so);
return true;
}
main
-> collect_spawn_source_pids
-> inject_spawn_symbi_by_pids
-> collect_symbi_context
-> prepare_target_so_for_app
-> stop_process
-> open_remote_mem
-> write_stub_and_patch_slot
-> resume_process
-> start_target_app_symbi
-> 等待 Ctrl+C
-> restore_original_slot
目标app启动后在子进程中走:
android.os.Process.setArgV0Native
-> stub_replacement_set_argv0
-> original_set_argv0
-> getuid
-> dlopen(target_so)
struct SymbiContext {
pid_t zygote_pid = -1;
uid_t target_uid = static_cast<uid_t>(-1);
uintptr_t shellcode_base = 0;
uintptr_t set_argv0_address = 0;
uintptr_t art_method_slot = 0;
uintptr_t original_ptr = 0;
uintptr_t remote_getuid = 0;
uintptr_t remote_dlopen = 0;
uintptr_t remote_log_print = 0;
std::string libandroid_runtime_path;
std::string shellcode_map_path;
uintptr_t shellcode_map_start = 0;
size_t shellcode_map_offset = 0;
std::string remote_so_path;
std::vector<uint8_t> original_shellcode_area;
};
if (ctx->shellcode_base == 0 &&
map.pathname.find("libstagefright.so") != std::string::npos &&
map.perms[2] == 'x') {
ctx->shellcode_base = map.end - static_cast<uintptr_t>(getpagesize());
ctx->shellcode_map_path = map.pathname;
ctx->shellcode_map_start = map.start;
ctx->shellcode_map_offset = map.offset;
}
#define SYMBI_TARGET_SYMBOL "_Z27android_os_Process_setArgV0P7_JNIEnvP8_jobjectP8_jstring"
uintptr_t libandroid_runtime_base = get_module_base(zygote_pid, ctx->libandroid_runtime_path);
uintptr_t symbol_offset = get_symbol_offset_from_elf(ctx->libandroid_runtime_path, SYMBI_TARGET_SYMBOL);
if (libandroid_runtime_base == 0 || symbol_offset == 0) {
LOGE("symbi: failed to resolve setArgV0");
return false;
}
ctx->set_argv0_address = libandroid_runtime_base + symbol_offset;
if ((map.pathname.find("boot.art") != std::string::npos ||
map.pathname.find("boot-framework.art") != std::string::npos ||
map.pathname.find("dalvik-LinearAlloc") != std::string::npos) &&
map.perms[0] == 'r' && map.perms[1] == 'w') {
heap_candidates.push_back(map);
}
for (const auto& heap : heap_candidates) {
size_t region_size = static_cast<size_t>(heap.end - heap.start);
if (region_size < sizeof(uintptr_t)) {
continue;
}
std::vector<uint8_t> buffer(region_size);
ssize_t read_size = pread(mem_fd, buffer.data(), region_size, static_cast<off_t>(heap.start));
if (read_size != static_cast<ssize_t>(region_size)) {
continue;
}
auto* found = reinterpret_cast<uint8_t*>(
memmem(buffer.data(), region_size, &ctx->set_argv0_address, sizeof(ctx->set_argv0_address)));
if (found != nullptr) {
ctx->art_method_slot = heap.start + static_cast<uintptr_t>(found - buffer.data());
break;
}
}
ctx->remote_getuid = get_remote_symbol(zygote_pid, "libc.so", "getuid");
ctx->remote_dlopen = get_remote_symbol(zygote_pid, "libdl.so", "dlopen");
ctx->remote_log_print = get_remote_symbol(zygote_pid, "liblog.so", "__android_log_print");
if (ctx->remote_getuid == 0 || ctx->remote_dlopen == 0 || ctx->remote_log_print == 0) {
LOGE("symbi: failed to resolve remote helper symbols");
return false;
}
char remote_pattern[] = "/ningningning123123";
uintptr_t marker = reinterpret_cast<uintptr_t>(
memmem(stub_binary, stub_binary_size, remote_pattern, sizeof(remote_pattern)));
if (marker == 0) {
LOGE("symbi: failed to locate stub marker");
return false;
}
std::vector<uint8_t> stub_copy(stub_binary, stub_binary + stub_binary_size);
uintptr_t offset = marker - reinterpret_cast<uintptr_t>(stub_binary);
auto* stub_cfg = reinterpret_cast<TStub*>(stub_copy.data() + offset);
stub_cfg->uid = static_cast<int>(ctx.target_uid);
memset(stub_cfg->so_path, 0, sizeof(stub_cfg->so_path));
strncpy(stub_cfg->so_path, ctx.remote_so_path.c_str(), sizeof(stub_cfg->so_path) - 1);
stub_cfg->original_set_argv0 =
reinterpret_cast<int (*)(JNIEnv*, jobject, jstring)>(ctx.set_argv0_address);
stub_cfg->slot_addr = ctx.art_method_slot;
stub_cfg->getuid = reinterpret_cast<uid_t (*)()>(ctx.remote_getuid);
stub_cfg->dlopen = reinterpret_cast<void* (*)(const char*, int)>(ctx.remote_dlopen);
stub_cfg->log_print =
reinterpret_cast<int (*)(int, const char*, const char*, ...)>(ctx.remote_log_print);
ssize_t written_code = pwrite(mem_fd,
stub_copy.data(),
stub_copy.size(),
static_cast<off_t>(ctx.shellcode_base));
if (written_code != static_cast<ssize_t>(stub_copy.size())) {
LOGE("symbi: failed to write stub to shellcode_base=0x%lx", ctx.shellcode_base);
return false;
}
uintptr_t new_ptr = ctx.shellcode_base;
ssize_t written_ptr = pwrite(mem_fd,
&new_ptr,
sizeof(new_ptr),
static_cast<off_t>(ctx.art_method_slot));
if (written_ptr != static_cast<ssize_t>(sizeof(new_ptr))) {
LOGE("symbi: failed to patch art_method_slot=0x%lx", ctx.art_method_slot);
return false;
}
LOGI("symbi: wrote stub to 0x%lx and patched slot=0x%lx -> 0x%lx",
ctx.shellcode_base, ctx.art_method_slot, new_ptr);