论坛首页 移动安全专区 阅读主题

[原创]从0到1构建一个Hook工具之注入器篇(二)

387 浏览 0 回复
#1 楼主 2026-06-01 21:09:13
在上一篇文章中,我们已经了解了注入的基本概念并且实现了attach注入模式,这个时候我们很容易想到attach模式的一个缺陷:当我们想要观察或拦截app启动早期的行为时,使用attach往往已经错过了最有价值的时机。spawn注入模式就可以解决这个问题,并且在逆向场景中这种模式也非常的常见。项目地址:538K9s2c8@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实现注入。我们先理一下详细的核心流程:函数调用链大概如下这部分代码主要参考了c65K9s2c8@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()找模块基址,g

...(已截断)

---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-290387.htm

暂无回复,快来抢沙发吧!

请登录后参与讨论

立即登录 注册账号