翻译声明:本文由英文原文经 AI 翻译整理,可能存在表述或技术细节上的偏差。如有歧义,请以 英文原文及源码为准。

一句话:gozinject 趁应用原生反篡改尚未武装时,把 libjnilog.so 送进全新的应用进程;随后 jnilog 记录 JNI 边界上的每一次调用,并附带足以直接用于逆向的类型信息。


本次抓取目标是 myphotocom.allfasttranslate.transationtranslator(“All Photo Translator”,版本 10.278.00),一款免费含广告的 OCR/翻译应用,是个相当真实的分析目标:
一次冷启动在前 35 秒内产生了 5,599 个 JNI 事件。
冷启动早期,直接来自 logcat:
不打开反汇编器,从上往下读就已经有价值:SDK_INT 暴露加固壳正在走的设备/ROM 判定路径,needX86Bridge 是 Jiagu 的 x86 桥接决策,FLAG_DEBUGGABLE 是一次反调试检查,GET_SIGNATURES 开始一次签名/篡改校验。
关于地址列:上面每个调用方都是裸地址 0x72ef…——本阶段 Jiagu 引导逻辑运行在已解密的匿名内存里,没有文件映射的模块可作基准,裸地址就是最诚实的结果。一旦执行落入已映射的 libjiagu_64.so,同一列就符号化为 libjiagu_64.so!<offset>,给出稳定、与重定位无关的位置:
每行 Call*Method 都可能有独立的返回行,两者用单调递增的 #id 关联,因此多线程交错写入 logcat 仍可读,调用即便在返回前崩溃也依然可见。下面是应用读取 Android ID 并把它带入原生代码:
重点不只是 getString() 被调用了——返回值被配对、被渲染,紧接着就能看到它通过 GetStringUTFChars 跨入原生内存。输出是带类型的:jstring 渲染为带引号的字符串,jclass 渲染为点分类名,数组按上限截断显示元素,对象返回值被渲染出来而不是留下一个不透明句柄。
libjnilog.so = Go 运行时 + C hook 层,由 go build -buildmode=c-shared 构建。加载时其构造函数解析 ART/JNI 入口、找到存活的 JavaVM,复制一份 JNINativeInterface 表、替换表项、临时改写页保护完成替换,此后每个线程的 JNIEnv 都路由经这张被 hook 的表。当前覆盖全表 228 项(方法调用 93、字段 36、查找 5、注册 2、引用 9、字符串 12、数组 44、异常 7、类/对象/缓冲区/其它 20);难以手写维护的几族用 X-macro 生成。
热路径不回调 Go。 早期版本用 C→Go 的 cgo 回调,但在受保护应用上,每事件的 cgo 跨越所引发的 Go 调度活动本身就是一个完整性信号。当前改用一条二进制事件管道:
数据报上限 8192 字节,字符串带长度前缀、分帧字节转义、截断回退到安全的 UTF-8 边界。消费端落后时,hook 丢弃事件而非阻塞应用线程,并周期性汇总丢弃计数。对象渲染也被安全地延迟:只有消费线程就绪才创建全局引用,所有权仅在数据报确实送达时才转交 Go,否则 hook 自行删除引用——否则泄漏的全局引用最终会耗尽 VM 的引用表。
自给自足的 C 核心。 C hook 层在自身热路径上刻意避开可被重定向(reroutable)的 libc 调用:str*/mem*、snprintf、malloc、mutex/futex、send/open/read/mprotect(内联 svc #0 系统调用)、以及 dladdr/符号化(私有 /proc/self/maps + .dynsym)全部仓库内自实现。一道 readelf 导入门禁强制这点——任何已迁移的 libc 符号一旦重新出现在动态导入里,构建即失败。这并不是说整个进程零 libc 痕迹(Go 运行时仍有冷启动导入),而是更窄也更有用的主张:执行在 JNI hook 入口上的那段 C 桥接,避开了加固壳与同进程日志器最常 hook 的那些 libc 符号。
下面是真实抓取里的电话服务 hook 字符串,以及随后解密出的原生库加载级联:
gozinject 不 attach 运行中的应用,而是在 fork 时刻捕获目标:
vma_hide 是唯一需要内核协助的层。没有该模块时,注入与 soinfo 摘除仍然有效,但载荷映射在 /proc/self/maps 中仍可见。设想的威胁模型是运行在应用 UID 内的反篡改扫描器;root 权限的读取者按设计可绕过该过滤。
配置默认读 /data/local/tmp/jnilog.json(或 JNILOG_CONFIG);无配置即“记录一切”。include 列表为空表示全开,一旦填入 functions/categories 就切换到白名单模式,exclude 始终优先;类别有 methods / fields / lookups / strings / arrays / refs / exceptions。log_sinks 当前默认在 logcat 之外再加一个异步文件 sink(带缓冲、周期 flush、受 log_queue_size 限流,溢出丢行而不阻塞 hook 线程)。tools/jnilogcfg 是独立的 Go 模块,提供 TUI/CLI 编辑并经 adb 推送配置。

当前 logcat tag 是 JNILogPayload;若你按旧 tag(如 JniLog)过滤,可能载荷在正常工作,而你的过滤器把每一行追踪都藏了起来。
[内核课程]《Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。