首页
社区
课程
招聘
[原创]利用hermes agent 详细分析hunter检测器
发表于: 4小时前 246

[原创]利用hermes agent 详细分析hunter检测器

4小时前
246

2026 年 3 月 12 日,Hermes Agent 发布 v2026.3.12。按官方 release 说明,这是 Hermes Agent v0.2.0,也是继 v0.1.0 这个 pre-public foundation 之后的首个正式 tagged release,不是严格意义上的"第一个版本"。截至 2026-05-30 查询官方 GitHub 仓库,NousResearch/hermes-agent 已在 17 万 Star 左右;一个月内破十万这类增长叙事和 OpenClaw 的热潮相互映照,但真正让安全圈记住它的,不是 star 数,而是一个有点"灵异"的故事。

Hermes 的卖点其实并不花哨:一个能长时间自主跑任务的通用 agent,真正的差异化全压在"记得住"和"会自己改进"这两件事上。官方文档把这套能力拆成几块:一层是记忆(memory),核心是 ~/.hermes/memories/ 下的 MEMORY.mdUSER.md,分别保存环境/项目经验与用户偏好,会在会话开始时注入上下文;另一层是技能(skill),反复用到的操作流程被固化成可复用的 SKILL.md,需要时 agent 可用 skill_manage 创建、更新、删除。换句话说,它不是把所有经验塞回模型参数,而是把经验外置成可读、可改、可复用的本地资产 —— 这恰好是下面那个故事的伏笔。

公开安全文章里转述过这样一个案例:有人拿自动化引擎反复攻击 Hermes,反弹 shell 一个变体接一个变体地打。一开始次次得手;可打着打着,Hermes 忽然就不执行了,像是凭空对这种攻击免疫了。这个故事本身不是官方 release note 里的正式结论,更适合当作安全测试转述来看;官方文档能直接佐证的是它确实具备自管 memory、创建/修改 skill、以及后台复盘的机制。若这个案例成立,它的技术解释并不神秘:攻击样本被当成"下次别再犯同一个错"的经验沉淀了下来。

官方文档对这套"复盘"循环的描述更具体:run_agent.py 侧维护 _turns_since_memory_iters_since_skill 两类计数。默认逻辑是每 10 个用户 prompt 触发一次 memory review,每 10 个 tool iteration 触发一次 skill review;达到阈值后调用 _spawn_background_review(messages_snapshot=..., review_memory=..., review_skills=...) fork 出后台 Review Agent。它不接手原任务,而是回看刚刚那段对话,判断"这一段里有什么是下次该记住的?"以及"有什么操作值得固化成 skill?"。复盘完成后,经验被写入 memory 或通过 skill_manage 落盘成 skill,主 agent 继续原任务。

关键在于,这个循环对"什么算经验"并不挑食:用户纠正过的偏好是经验,踩过的坑是经验,复杂任务里反复出现的操作模式也是经验。因此,如果上面那个攻击案例的日志链路完整,它的解释就不必诉诸"灵异":同一种 payload 反复出现、某次被识破后,Review Agent 可能把"这类输入要警惕"当成普通经验存下来。所谓"进化出免疫",更保守的说法是:一套通用复盘机制,可能恰好作用在了攻击样本上。它没学会抽象意义上的"安全",它只是学会了"别再犯同一个错" —— 拒绝执行可疑 payload,刚好是这条通用规律的一个特例。

这个故事迷人,但它还有另一面:让 Hermes 这类 agent 变得有用的那套能力 —— 会读代码、会调工具、能在数百步里不丢线索地复盘 —— 换个方向用,就是今天最锋利的逆向工具。当这样的 agent 经 MCP 接上 IDA、Ghidra、Frida,它能把一个加固 SO 从 JNI 注册表一路扒到风险汇聚点,速度远高于纯手工检索。本文通篇出现的"IDA MCP 实读",就是这么来的。

可问题恰恰也藏在这儿。agent 读伪代码太"顺"了 —— 顺到它会不知不觉把"看起来合理"当成"事实",把"hook 完那个方法、界面里风险项没了"当成"绕过成功"。在一个老实的 app 上,这种幻觉无伤大雅;但这一次我们要拆的是 Hunter —— 一个会用多进程、syscall 跳板、CRC 自检反过来咬你的家伙。在它面前,一条幻觉结论的代价是:你以为已经绕过,实际上 :hunter_server_iso 子进程正通过 Binder 把同一条风险项原样传回 UI。

所以这篇文章不是又一份"我把 Hunter 绕了"的炫技记录。它记录的是:当我们把 agent 放出去拆 Hunter v6.58(versionCode 658 / libhunter.so),怎么让它的每一条结论都不是"它觉得",而是"证据逼它承认"。办法说穿了很朴素 —— 给这只会读代码的 agent 也上一道证据门:每个检测点,都要静态、动态两条独立证据线对上才算数。静态线从 art_register_natives_batch 的 60 项注册表锚定 Java 方法到 native RVA,再用 IDA 伪代码 / 字符串 / xref 还原算法;动态线在汇聚点 build_native_ListItemBean_risk @ 0x278658 落 hook,用 ReportRiskItemLR 反查到精确 callsite,用 MainActivity.n0 的 UI 三元组确认命中来源,最后用 bypass 让目标风险项消失来反向闭环。

Hermes Agent静态 IDA 反编译 + 动态 Frida hook 两条独立证据线,逐条还原 Hunter v6.58 的检测算法。每条结论都满足:

首先从 JNI_OnLoadart_register_natives_batch(env, NativeEngine, off_2EF1F0, 60) 注册表建立 Java 方法到 so 函数的映射。Hunter 不依赖普通 JNI 导出名,而是通过 ArtMethod 直接 patch 的方式注册。运行时枚举 off_2EF1F0 表(每项 {name*, sig*, fn*} = 24 字节,共 60 项),完整 60 项见附录 D。本文 SO 层下沉 bypass 脚本 hook_native_bypass_so.js 明确分类并 Interceptor.replace 了其中 25 个 detection 入口(其余 35 项为 popen / MD5 / 工具方法 / 信息查询 / 还未细化分类的 detection 等,见附录 D)。25 项的 RVA:

getZhenxiInfoH 不在这 60 项表内 —— 它本身不是 native 方法,而是 Java wrapper:getZhenxiInfoH(key) -> getPropInline(key, true)getPropInline 是混淆控制流函数,内部再进入 native getZhenxiInfoZ(key) fallback 或 /dev/__properties__ 属性区解析。这是实测 60 项表缺失 getZhenxiInfoH 后,结合 Java 反编译确认的链路。

随后以风险项字符串和 ReportRiskItem 汇聚点交叉定位:

动态 hook 这个汇聚点可以拿到 native 风险项标题、详情和 LR,例如:

LR = 0x2B4A88 落在 NativeEngine_getZhenxiInfo3 @ 0x2B476C 内,把 UI 文案与 native 检测函数闭环。

动态侧使用三类 hook:

典型动态证据:

前者证明检测命中,后者证明将该 native 返回值置空后 UI 风险项消失。

凡是可能触发 Hunter checksum、maps/RX 扫描或 ELF 段枚举路径的动态分析脚本,都应优先安装 block_bad_precheck hook。原因是 Hunter 的 checksum/mem scan 路径会把 maps 解析出的地址交给自己的可读性预检查 maps_precheck_readable @ 0xC6520,该预检查没有证明 [addr, addr+size) 被可读 mapping 完整覆盖;在当前 Frida 17 注入环境下,某些地址区间会跨越 unmapped hole 或不可读页,后续 NEON / 批量读取可能 SIGSEGV。

实测拦下的越界访问片段(Pixel 6 Pro / Android 15):

off 0xC68A4 位于 sub_C687C(parse_elf_rx_segments 内 ELF 文件读出后的可读性预检),off 0xC7828 位于 scan_rx_segment_neon_sum @ 0xC772C(RX 段扫描路径)。两个 LR 模式覆盖两条独立完整性检查链路的越界点 —— 这条数据反复出现在后面 §5 / §8 / §20 / §21 多条 detection 的实测中。

art_register_natives_batch @ 0xA5C54 静态伪代码(IDA 反编译还原):

libart.so 路径按 SDK 选(build_libart_path / sub_1AFA80 静态确认):

dlsym 目标符号(已 demangle):

libart_dlsym 不走 libdl,而是自己解析 ELF 取符号,目的有二:绕开 libdl hook;取 @hide 未导出符号(ArtMethod::RegisterNative 等并不在 libart 的 .dynsym 公开导出)。

四个 helper 的分工:

ClassLinker* 需要从 Runtime 实例取。Runtime 实例本身也是 @hide 符号,通过两条路径定位:

全局缓存:g_art_class_linker @ qword_2EF7C0,g_art_RegisterNative_fn @ off_2EF7C8,g_fid_Executable_artMethod @ qword_2EF7D0(SDK ≥ 30 反射拿 Executable.artMethod jfieldID 的缓存)。

art_method_set_native_entry 内 SDK > 30 分支调 ClassLinker::RegisterNative 的完整指令序列(IDA MCP 实读 sub_A4C800x0a5224..0x0a5254 区段):

12 条指令直接从 IDA MCP 实读拷贝,X1 错传由 0x0a524c MOV X1, X21 直接证明 —— X21 来源是 0x0a522c BL libart_dlsym 的返回值,中间只经过 MOV X21, X0(0x0a5230)和 null 检查(0x0a523c),从未被调用,直接作为实参传给了 ClassLinker::RegisterNative 的第二个参数槽位。

X2/X3 是从栈上一次性 LDP 拿到的(art_methodfnPtr,在函数早期被存进栈),X8(RegisterNative 函数指针缓存)也是从栈上 LDR 拿到的。X0 = class_linker 直接来自上一条 BL sub_A3CCC 的返回。4 个参数装载位置全部可见

ClassLinker::RegisterNative(this, Thread *self, ArtMethod *method, const void *native) 的第二个参数语义是当前线程的 Thread* 对象,需要通过调用 Thread::CurrentFromGdb() 拿到 —— 这是 ART 内部的 ThreadLocal 访问器,无参 → 返回 Thread*

按 §2.3.1 的反汇编:0x0a5230 MOV X21, X0libart_dlsym 返回的函数指针存到 X21,之后没有 BLR X21 这一步(它本应把函数指针当函数调一次,把返回值即真正的 Thread* 重新放入 X0),直接在 0x0a524c MOV X1, X21 把 X21(仍是函数指针)装入实参槽位。

最终 0x0a5254 BLR X8 调用 ClassLinker::RegisterNative 时,X1 是一个指向 libart.so .text 段的函数指针,而 callee 期望它指向堆上的 art::Thread 对象。Android 12+ 上一旦实际进入这个分支,可以确定的是 ART 内部函数会收到错误的 self 实参;后续影响取决于 ClassLinker::RegisterNative 在对应 Android 版本中的具体实现:

这与 §2.2 art_register_natives_batch 入口处的 if (get_sdk_level() > 33) return false 形成一致解释:作者发现新 Android(14+)走此路径不稳,在批量入口直接挡掉,但 SDK 31-33 区间仍会进入此分支 —— 没补的实现仍带 bug。

实测设备 SDK=35,走的是 batch return false → 回落到 env->RegisterNatives 路径,所以本设备没踩到 bug,但代码层面 BUG 还在。verify_register_natives.js 在 libart 的 RegisterNatives 入口落 hook 间接印证 SDK=35 回落路径生效。

直接读 libhunter.so + 0x2EF1F0 处的 JNINativeMethod 表:

实测输出关键行(完整 25 个 detection):

verify_register_natives.js 在 libart RegisterNatives 入口落 hook 也确认了:SDK=35 环境下 hunter 的注册走标准 RegisterNatives 路径( SDK > 33 时 art_register_natives_batch 在入口处返回 false,上层因此 fallback 到标准 env->RegisterNatives 注册路径)。

在 SO 层直接 Interceptor.replace 25 个 detection 入口:

实测 25 个 detection 全部 replace 成功:

1 missinggetZhenxiInfoH —— 实测验证了它不在 batch 表里的静态结论。Java 层补 NativeEngine.getZhenxiInfoH 拦截兜底。

secure_syscall_trampoline_template{,_q1,_q2}.rodata @ 0x651FC..0x6522C,字节级解读:

这一步的目的不是调用 linker64 的某个完整函数,而是从 linker64 的可执行段里找到一条现成的 SVC #0 指令。ARM64 Linux syscall 的约定是:

Hunter 自己的跳板页已经先把调用者传入的 sysno 放进 X8,并把后续参数顺移到 X0-X5:

因此 FindLinkerSyscallAddr 只需要找一个可信的 SVC #0 地址。它在 /proc/self/maps 中筛 pathname 含 /linker64、权限 r-x 的段,在该 .text 内 4 字节步进扫描,匹配 bionic syscall stub 的标准展开:

匹配后返回 start + (n12-8),即 SVC #0 那一条指令的地址,而非 MOV W8 的 stub 入口。

为什么返回 SVC 而非 stub 入口:跳板自己已经把 X8 设为调用者指定的 syscall number。例如 safe_syscall_via_trampoline(56, ...) 进入跳板后,X8=56,表示要执行 openat。如果跳到 stub 入口的 MOVZ W8, #93,刚设置好的 X8=56 会被覆盖成 93,调用就会变成 exit。直接跳到下一条 SVC #0,才能保留 Hunter 自己设置的 X8,借用 linker 这条 SVC 指令发起任意 syscall。

返回链也依赖这个 stub 后半段。跳板页用 BLR X17 跳到 SVC #0,所以 LR/X30 被设置为跳板页中 BLR X17 的下一条指令。内核完成 syscall 后,用户态继续从 SVC #0 后面的 CMN/CSINV/B.cond/RET 执行;最后 linker stub 的 RET 使用这个 LR/X30 返回到 Hunter 跳板页,跳板页再恢复栈帧并 RET 回调用者。

为什么用 sysno=93:93 = SYS_exit(ARM64 Linux)。本环境 linker64 中存在匹配该 sysno 的标准 syscall stub,Hunter 选择它作为 SVC 指令定位锚;跨 Android / bionic 版本是否始终稳定,应以目标系统 linker64 实测扫描为准。

字面量在 0x5dd44,全局构造器 sub_1B3088 @ 0x1b3088 通过:

写入全局 dest_(即 xmmword_2F09E8 / src_0 一对地址,对应 get_trampoline_id_tag 读源)。首字符 ( (0x28) 不是真字符,而是 libc++ SSO 控制字节:低位 0 标记 SSO 模式,高 7 位 0x14 = 20 即字符串长度。作者把 22 字节(header + 20 字符 + \0)预编译成一个 C 字符串,strcpy 一次落地一个合法 std::string。

verify_4_5_passive.js 只挂 InitSecureTrampolinePage onLeave,单次取证后即刻 detach。运行后 ctx 字段实测值:

用缓存地址被动重建 §3.2.2 的指纹序列:

三处一致 ✓。

为不触发 hunter 自检,v2 脚本只在 /apex/.../linker64 内的 SVC #0(由 ctx.page[+0x30] 缓存的地址)落观察点,LR 严格等于 ctx.page + 0x28 才计入。共 5300+ 次 跳板调用样本:

静态 callsite 数与运行时调用数测量的是两件不同的事:

本轮 kill 0 次说明当前观察窗口内没有进入 SIGKILL 分支。结合 Frida 线程扫描返回 0、ReportRiskItem 未出现 Frida 命中,可作为"本轮未被 Frida 自杀分支识别"的辅助证据;但不能单独证明所有 SIGKILL 路径均不可达。

未观察到的 4 个 sysno 各有原因:

0x1AFE4C..0x1AFEF8 一组 7 个小函数(0x18~0x24 字节),每个把固定 sysno 转发到 safe_syscall_via_trampoline,等价于"安全版 libc 子集":

后续 detection 中大量敏感文件读取会走这 7 个 ssyscall 包装器,尤其是 openat/read/lseek/close/readlinkat 组合;但并非所有 /proc、maps、进程枚举和命令输出路径都走跳板。完整 I/O 监控需要同时覆盖跳板、libc/stdio、popenopendir/readdir 等路径。

明显不在跳板列表里的:writemmapmprotectgetdents64newfstatat(79)。这些路径要么走 libc(可被 hook 但不致命),要么内联在调用方。做完整 syscall 监控不能只 hook 跳板入口

该函数内部引用:

Hex-Rays 把 OLLVM 平坦化后的 build_native_ListItemBean_risk 错认为 1 参函数,真实签名 6 个参数(对每个 callsite 反汇编核对一致):

sub_2447A8 的作用是 std::string::__init_from_cstr(out, lit)。用 SSO/long 双形式自适应读法:

83 个 callsite 全部静态扫一遍:

W5 子类别归纳(从 caller × W5 推):

InfoBase 19 callsite 跨多个 W5,因为它是"全家桶":Linker Hook / Magisk / Mount / Seccomp / Signal / Uname 等多类检测打包在一个 JNI 入口里。

每条 callsite 的 title/detail 字符串候选源自该 callsite 前 ≤80 条指令内的 ADRL/ADR → C 字符串。粗体为最可能的 title;<dyn> 表示纯动态拼接:

js/hook_report_risk_precheck.js0x278658 落 hook:

实测输出片段(/proc/mounts APatch 命中):

对回方法:this.context.lr - 4 = 静态 callsite 的精确 RVA(LR 是 BL build_native_ListItemBean_risk 之后下一条指令的地址,所以 lr - 4 即 callsite)。

闭环对回成功:每条 [ReportRiskItem] 输出都能通过 lr - 4 反查到 §4.4 字典里精确的 caller × callsite,且 arg1 命中静态候选字面量。arg2 是动态上下文(具体路径/线程名/接口名),静态字典里没有,这部分实测最有取证价值。

兜底拦截在 0x278658 入口按 title 名单丢弃:

动态验证:开启 BLOCK 后 UI 端 risk 列表中对应 title 不再出现,印证"汇聚点拦截 = 22 caller × 83 callsite 全部消音"。

Hunter 对 libart.so / libc.so / linker64 / libhunter.so 做运行时 r-x 段累加和自检,两条独立链路:

两条链共享底层算法栈:

anti_inject_collect_results @ 0xC8670 拼 4 个目标路径(build_libart_path / get_libc_path / get_linker64_path / std::string from xmmword_2F3C08),每个路径调 scan_modules_for_path(path) @ 0xC8500,把结果 std::to_string 后存入 path → sum_str 红黑树。

scan_rx_segment_neon_sumparse_elf_rx_segments 共享同一段 NEON 字节累加和算法(也与 InitSecureTrampolinePage 内对跳板 48B 算 baseline 的算法完全一致):

这是没有取模、没有移位、没有 lookup table 的纯 sum:sum = Σ(byte)

输入:模块路径。输出:struct s:

file_checksum 是 cache miss 时当场 calloc + ssyscall_read ELF 文件 + 算和得到的。该 file_checksum 路径上的文件读取走 §3.6 的 ssyscall 包装器,即使 hook libc 的 fopen/fread 也拦不到。

cache 容器 qword_2EF8B0 是 path → struct map,首次访问后被永久缓存(sub_CACC0 写、sub_CA670 是 sentinel/init 检查)。

注意:scan_rx_segment_neon_sum 在原地比较 v14 == s->segs[i].file_checksum;它只是把"运行时所有段 sum 之和"累加进 v5 返回。

也就是说,Java 侧拿到的 HashMap value = "整个模块所有 PF_X 段 runtime NEON sum 的总和",真正的比对在 Java 侧的 ig.apply

scan_modules_for_path 把 baseline s 套到内存上时,主备两条:

主路径未命中 → 退到备路径。这意味着仅 hook libc 让 /proc/self/maps 撒谎是不够的。

三条件全成立才报警 → 任一为 0 即跳过。

只有 r-xp 段返回非零 checksum,其他 r--p / rw-p 段返回 0。最终模块 checksum 等于唯一 r-xp 段的 sum。

libhunter.so 验证最终模块 checksum 是各段非零段 sum 的累加:

注意 rwxp 段不代表 checksum 必然 0(参看 libhunter.so 的 0xb69f4 与 linker64 的 0)。

UI 原文:

local = 856386775remote = 846246994 → 触发。

LR 0x2ada40 落在 getZhenxiInfoF @ 0x2AD9A8 内,与 §4.4 字典中 getZhenxiInfoF × 8 callsite (W3=0, W4=3/4, W5=6) 一致。

off 0xC68A4(在 sub_C687C,parse_elf_rx_segments 内 ELF 文件读出后)和 off 0xC7828(在 scan_rx_segment_neon_sum,RX 段扫描路径)各反复触发,印证两条独立链路的越界点位置

不同运行中 libhunter.so 的本地 CRC 值可能变化:某次 258223051,另一次 252914579,本节实测 254442033

这是 hunter "对运行时 RX 段/映射状态做采样"的特征体现:libhunter.so 的多个段(包括 rwxp 段)随运行时状态变化(GOT 修改、绑定地址写入等)产生不同 NEON sum。不应把这个值当作 APK 文件静态 CRC。

两层处理(js/bypass_crc_detection.js):

动态验证:

CRC 风险项消失。

NativeEngine_getZhenxiInfo3 @ 0x2B476C 引用的风险项字符串:

完整短路顺序:

它不解析 mount 字段,不检查 mount type、source、target 的结构合法性 —— 纯 substring 命中。

命中行 APatch /debug_ramdisk tmpfs rw,seclabel,relatime 0 0 同时命中 APatch/debug_ramdisk 两个 marker。

getZhenxiInfo3 是短路链,默认运行时先被 sub_D5E34 命中,后续分支不会跑。强制 sub_D5E34 返回 0 后:

链路完整跑通:真实命中点有两个 —— sub_D5E34(mounts APatch 命中)和 sub_DDB9C(netlink permission 命中)。

同一进程中能看到不同 marker 向量复用该 helper:

对应 Java 静态代码 src/YouAreLoser/y00.java:

因此须区分:

只有当调用链是 NativeEngine_getZhenxiInfo3 -> detect_root_mark_in_proc_mounts_maps -> file_contains_any_marker、并且 marker 向量是 /riru|...|APatch|kernelsu|... 时,才归类为本节的 APatch/root mount 检测。

输入源 6 个、marker 向量 18 项 —— 任一源任一行包含任一 marker 即触发。对抗需要:

本设备命中 APatch /debug_ramdisk tmpfs rw,... 一行命中两个 marker,只替换其中一个不够

动态验证:

主进程风险项消失。但 :hunter_server_iso / :hunter_server_twin 子进程通过 Binder 仍可回传同类 bean(详 §22),需配合 UI 层兜底或多进程 spawn。

NativeEngine_getZhenxiMapCheck @ 0x291858 引用字符串:

Java 触发片段:

伪代码:

关键地址:

两个独立判定:

不校验 libart 文件内容,只看进程内 libart.so 的 maps 形态。Frida / LSPosed / SandHook 等会让 ART 代码页被重映射 / 拆分 / 产生异常小 mapping,触发本条。

汇聚点原始日志:

Java UI:

对抗时必须保证向目标进程暴露的 maps 中 libart.so 不出现异常 4KB 分片,也不要把 libart.so 拆成过多段。单纯隐藏 Frida 字样、LSPosed 字样不能解决此分支,它匹配的是 libart.so 本身

动态验证:

风险项消失。

§5 已完整还原 CRC 双链算法栈。本节只补 native 直接上报(链路 2,getZhenxiInfoF)的 detection 细节。

NativeEngine_getZhenxiInfoF @ 0x2AD9A8 内有 8 个 callsite(W3=0,W4=3 或 4,W5=6,详 §4.4 字典),覆盖:

三者共享 scan_modules_for_path @ 0xC8500 同源算法栈(详 §5.2)。

LR 0x2ada40 落在 getZhenxiInfoF @ 0x2AD9A8 内,size=0x2ada40 - 0x2AD9A8 = 0x98 处的 callsite。

动态验证:

native 直接上报路径(链路 2)的风险项消失。但 task_MAIN_CRC_check 后台任务仍可能触发同类 detection,需配合 SO 层 Interceptor.replace 在 JNI 入口直接 return 0 拦截,或在 0x278658 汇聚点按 title 名单兜底。

Java 触发逻辑(src/YouAreLoser/GC.java):

NativeEngine_getZhenxiInfoInjection @ 0x28DF24 是 6 子检测的汇总:

伪代码:

完整返回字符串(后续运行的 native 端 retval):

读取每个子函数隐藏返回参数 x8 指向的 std::string,在记录真实输出后临时清空前序非空结果让 wrapper 继续:

当前环境命中两类:

Frida agent 在主进程映射匿名 RWX 区是触发本条的主要原因。对抗思路是隐藏这类 mapping(可在 maps 解析侧/物理映射侧欺骗)。

动态验证:

Java 侧条件 !s.isEmpty() 不满足,bean 不构造。返回空字符串是源头 bypass。

第一阶段:/proc pid 枚举

进程名与关键字比较(精确/模糊):

命中后:

第二阶段:ps -ef 采集(sub_D0DA4 @ 0xD0DA4 动态 dlopen("libc.so")/dlsym("popen")):

不同会话样本:

第二阶段会把 hunter 自己的 twin 进程 + ps -ef 命令本身一并写入 detail。

risk=NoRisk —— 这是信息项而非阻断项。但会暴露当前分析环境(常驻 sh / logcat / grep)。

对抗简单粗暴:

动态验证:

Java 触发(src/com/zhenxi/hunter/MainActivity.java):

另一轮采证 ratio 略有差异但仍归同一档:

这条 detection 基于 timing,无法用文件系统层欺骗绕过。对抗思路:

返回空字符串时 Java 仍可能包装空 data 风险项,正确做法返回 null:

动态验证:

"当前手机已经被解锁" 文案有两条独立来源,容易混淆。

字段语义:

NativeEngine.getZhenxiInfoH(String) 是 Java wrapper,不在 art_register_natives_batch 的 60 项表里。它调用混淆函数 getPropInline(key, true),后者内部再走 native getZhenxiInfoZ(key) fallback 或 jK 解析 /dev/__properties__ 属性区。因此这条属性链最终包含 native 层,但 native 注册项不是 getZhenxiInfoH 本身。

UI 实例展开:

两条独立 → 需要分别绕:

TEE 链:

getprop 链:

动态验证:

注意:两条都拦才能彻底消除。即使 TEE 链 bypass 成功,getprop 链仍会单独产生 orange 风险项。

NativeEngine_getZhenxiInfoEnv @ 0x28CDB0 引用字符串 "Get Env Info"。Java 触发片段:

伪代码:

实际 detail 涵盖:

信息采集项,不直接判定 root/hook,但暴露运行环境和进程上下文。属于"画像"类 detection,绕过优先级低于阻断类。

动态验证:

getZhenxiInfoK 是 §6.2.5 file_contains_any_marker 的 Java 暴露包装:

Java 使用示例:

完整 markers 12 项:

getZhenxiInfoK 本身是通用工具,不应作"是否 detection"的边界。检测属性取决于调用方:

对抗时若全量 bypass getZhenxiInfoK 可能破坏其他正常调用 —— 应按 path × markers 组合细化。

不强行返回 null,而是 hook 对应 Java caller(例如 y00 类的检测方法)更安全。

NativeEngine_checkRiskFile @ 0x2AC514 先构造固定风险路径表,再按顺序调 sub_1A6228(path) 检查存在性。sub_1A6228 @ 0x1A6228 非常短:

不读文件内容、不 stat、不看 SELinux label;只要 access(path, F_OK) 返回 0 即命中

完整 25 条路径表(IDA 全部字符串引用提取):

本设备测试环境无任何上述路径存在,实测:

ret=0x0 即返回 NULL jobject,Java 拿到 null 不构造 bean。

静态闭环 / 当前环境未命中。要实测命中分支,需主动构造任一路径文件后再触发检测。

不依赖 /proc/mounts,不依赖 maps,直接检查文件系统路径。对抗面:

KPM 可在内核侧对 hunter 进程的 access/相关文件查询路径按名单返回 -ENOENT,从数据源上绕过本节这条文件存在性检测;但它不覆盖 logcat/AVC、maps 或其它独立检测链。

Java 层最简单:

或 SO 层 Interceptor.replace @ 0x2AC514 入口直接返回 NULL jobject。

Java 触发(src/YouAreLoser/GC.java):

核心步骤(由静态字符串和 libc callee 反推):

关键 libc callee:closeaccess__open_2getxattrreadlinkusleep

本测试环境未触发命中,函数 entered 后直接返回 null。

仅隐藏文件不够 —— 即使 access/open 返回 ENOENT,如果 kernel 历史 AVC denial 仍保留敏感路径,Hunter 能从 logcat 反推它们存在。真正对抗:

Java 层最简单:

或 SO 层 Interceptor.replace @ 0x2AAE54 入口返回 NULL。

两阶段。

进程标签异常时不返回风险项,而是自杀。这条调试串还泄露了开发环境路径 F:/HunterNew/app/src/main/cpp/main.cpp

本测试环境 Java 端传入 "com.zhenxi.hunter",本设备无额外的同包名 ELF 注入,return null。

正确结论:自保护标签通过,maps 包名 ELF 白名单扫描未发现额外可疑映射,不是"没有该检测"。

防的是"以 app 同包名注入额外 native 库"的 hook 攻击。绕过思路:

"MYAAKzWEBR" (10 字符 base64) 是 hunter APK 真实签名的开头部分。任何重打包的 hunter 都会因为重签后这 10 字符改变而被检测出。

本设备用的是官方 APK,签名匹配,return NULL,未生成 UI 风险项。

检测目标是防止 hunter 自身被重打包(改变签名)。对真实用户来说装的是官方 APK,该检测不会命中。

或彻底:确保 hunter APK 不被重打包(对真实用户来说,装的就是官方,该检测不会命中)。

sub_CDFC4 / sub_CDAD4 / sub_CE000 都没字符串,纯 OLLVM 隐藏。从上下文推断:它们从某个 binder / service 拿一个值(可能是 IBinder 接口的 hash、system_server 进程的状态、ServiceManager 拿到的 native binder pointer 等),解密后与某个 hardcoded 预期值比对,不匹配 → "system_server 链有人 hook 中转"。

sub_251C18 是跳板模块同款解密器,与 §3.2.4 runtime_syscall_code 的解码路径同源 —— 这是把"敏感字符串"以 obfuscated 形式存储的通用机制。

本设备未命中 system_server hook,return NULL。

检测目标:Binder IPC 中间没有"人坐在中间"。对应攻击:VirtualXposed / SandHook 等用 Java 层 IBinder proxy 拦截 system_server 调用的场景。

进一步还原 sub_CDFC4 / sub_CDAD4 / sub_CE000 需要对 callee 链回溯,或用 Frida hook 它们入口/出口看实参与返回值 —— 本节到"检测什么"为止,精确算法待补。

或 SO 层 Interceptor.replace @ 0x28C9BC

hook 策略收紧到 3 个 hunter 内 hook + 1 个 libc:

测试条件:Pixel 6 Pro / Android 15,无 VPN App,WiFi 已连。

sub_A8CC8 hook 50 个接口被 hunter 全部扫到:

大量 rmnet* 是 Pixel 6 Pro 的硬件特征,说明 hunter 看见的是真实 kernel 接口表,没有被 /proc/net/dev 那一层欺骗(这是 NETLINK 路径的核心目的)。

tunl0 前 3 字符是 "tun",但 CheckVpn 没命中。等价于 strcmp("tunl0", "tun") != 0 在 hunter 视角下成立。

本次共 6 次 socket(AF_NETLINK) 创建,但 CheckVpn 入口只触发 1 次完整流程。其余 NETLINK socket 的调用方尚未逐一用 callstack 或 LR 归因;候选包括 getZhenxiInfoBase / Info4 / checkZygisk / Info3 内 sub_DDB9C,需要后续按 caller 复核。

11 条静态结论里 8 条直接 ✓、1 条间接 ✓、2 条诚实标"未验证"(S4/S5)。结构性结论(S1-S3, S6-S11)全部得到证据,§20.2.1 静态算法可作为可靠事实。

my_strcmp精确比较,静态名单只有 tun/ppp/pptp 三个裸字符串。Linux 真实 VPN 接口名几乎都不是这三个:

Hunter 当前实现只精确匹配接口名 tun/ppp/pptp。常见接口名如 tun0/ppp0/wg0/xfrm0 不会被该条件命中;因此在使用这些默认命名的 VPN 环境中,该检测存在明显漏检风险。是否覆盖所有主流客户端需要单独样本验证。

本会话中也观察到:

与 CheckVpn 完全无关,只是同窗口并发的 self-check 自检漏判,反过来加强 §5.3.5 关于"两条独立完整性检查链路"的论断

对绝大多数真实用户,方案 D 已经够用 —— 这是 §20.5.1 的直接推论。

这三个全局跨 tid 迭代从不重置 —— 这是 §21.5 buffered reader bug 的源头。

getZhenxiInfo5 @ 0x2A4760 拿到 sub_C6A84 返回值后分发(从 0x2a47ac 处的 CMP W0, #2 / B.EQ; CMP W0, #1 / B.NE 看出):

block_bad_precheck + NativeEngine_getZhenxiInfo5(entry/leave)+ sub_C6A84(leave)+ libc opendir/readdir/closedir。结果:

F8 失败只能说明本轮未命中 gum-js-loop / gmain,不能单独证明 buffered reader bug。其它候选解释还包括 Frida 线程名变化、线程创建时序错位或读取路径未覆盖目标 tid。

第二轮移除 libc 目录遍历 hook,改挂 ssyscall_read:onEnter 记 (fd, buf),onLeave 在 sub_C6A84 嵌套内才记录 (fd, len, head)

设计动机:区分 3 个候选解释(buffered reader 复用 / 线程名变更 / 命名时序错位)。

决定性结果:sub_C6A84 一次调用内 ssyscall_read 仅触发 2 次(77 个 tid 预期至少应 read 数十次):

两次 fd 都是 171,不是同一个 tid 的 fd —— Linux 在 close 后开 open 复用最低空闲 fd,每个 tid 用完 171 关掉,下一个 tid 又开成 171。

第一轮证据(必要但不充分):

第二轮决定性证据:sub_C6A84 一次调用内 ssyscall_read 仅触发 2 次。

第三层证据(由前两层推得):两次 read 的 fd 都是 171,但不是同一个 tid 的 fd —— Linux fd 复用机制。

实际运行展开(2 次 read × 77 tid × status 文件 ~1.5KB):

净效果:本轮 77 个 tid 中实际只有 ~2 个的 Name 字段被真扫(er_main_processmali-cmar-backe)。剩下大量 tid 的 Name 字段从未触达 strstr_,会显著降低 gum-js-loop / gmain 的命中概率。

由于本轮 buffered reader bug 已显著降低命中概率,当前脚本没有专门绕过 Info5 也未触发该风险项。若后续版本修复 buffer 重置问题,仍需要单独处理 sub_C6A84 / strstr_ / /proc/self/task/*/status 读取面。

本节补充多进程 Binder 上报链路:即使主进程检测入口已被 bypass,子进程仍可能通过 Binder 把 ListItemBean 传回主进程 UI。

主进程已经做了三层 bypass:

理论上所有 detection 都应该被切断。但实测仍有一条 Check Find Root In Mounts 漏网,UI 兜底拦下时打出 Java 栈:

回看 Manifest:

YouAreLoser.TC 是 AIDL stub 实现,onTransact 解包后通过 TC.v(bean) 转发给 MainActivity.n0(bean) 展示。Binder.execTransact 是 Binder server 端响应方法,来自另一个进程的 IPC 调用

§6.3.1 的实测中也观察到同一条 detection 出现两次,data 字段不同:

两条记录的 data 前缀分别是主进程和隔离进程。

[ReportRisk] 行未出现:这条 risk 未经过主进程的 0x278658 汇聚点

子进程也加载了 libhunter.so,独立跑一遍 detection,结果 binder 传回主进程。Frida 默认只 attach 主进程,子进程的 SO 层 hook 完全没装,那边的 getZhenxiInfo3 正常跑完整 mount 扫描,生成 bean,binder 一路过来,主进程的 MainActivity.n0 接收并展示。

这对绕过工作给出两个启示:

实测当前文章脚本组合用方案 A,验证可用。

hunter 不是"无懈可击的检测"。有 4 个已经在静态 + 动态双向证据下确认的实现缺陷,KPM 设计不应依赖这些缺陷做长效绕过(作者修起来都很容易)。

KPM 设计原则:不依赖缺陷做长效绕过。这些缺陷在 hunter 下个版本就可能被修;KPM 应优先做基于数据源的内核侧对抗(隐藏文件 / 隐藏 procfs、maps、netlink 返回 / 过滤 syscall 读写结果 / 阻断 SELinux audit 暴露)而不是利用 hunter 自己的 bug。若选择拦截 hunter detect 函数入口,应归类为用户态 hook/patch 或内核辅助修改用户态代码,仍需面对 §5 CRC 链。

每个关键点都至少满足 5 类证据:

Check Find Root In Mounts 为例:

这种交叉印证比"hook 某方法后界面没了"更强,因为它证明了检测算法、命中原因、绕过点之间的因果关系

拆到最后才看清,Hunter 从来不是"一个 root 检测函数",而是一座会还手的工事:art_register_natives_batch 直接 patch ArtMethod,绕过对 RegisterNatives 的监控(§2);syscall 借 linker64 段里一条裸 SVC #0 发起,把整个 libc hook 面甩在身后(§3);大量检测的风险项收口到 build_native_ListItemBean_risk @ 0x278658(22 caller × 83 callsite,§4);libart / libc / linker / libhunter 还各有一条 CRC + r-x 段 NEON 自检(§5 / §8)。你以为拦住了主进程,后台的 task_MAIN_CRC_check:hunter_server_iso 子进程又通过 Binder 把风险项原样递回 UI(§22)。任何"单点 bypass"都会被另一条链路悄悄兜回 —— 这也是为什么本文从头到尾都在"汇聚点拦截 + 源头 bypass + 多进程 spawn"三层一起上。

这座工事最阴险的地方,不在于层数多,而在于它专挑"读代码读得太顺"的分析者下套。你 hook 掉 getZhenxiInfo3,主进程界面那条 root 风险项当场消失 —— 一个只盯着界面的 agent 到这步就会写下"已绕过"。可它没看见:同一条 bean 正由 :hunter_server_iso 通过 TC.onTransact 原样传回,UI 下一帧又把它画了回来;它也没看见后台 task_MAIN_CRC_check 压根不经 Java,直接在 native 侧把"libart 被改"报上去。界面干净从不等于检测失效。这就是前言那个幻觉陷阱的真身 —— Hunter 把"看起来绕过了"和"真的绕过了"之间那道缝,撑得足够宽,宽到一个轻信的 agent 必然掉进去。

真正让这件事有意思的,是一组不对称。Hunter 是冻结的:编译进 libhunter.so 的那一刻,这一版的检测集合、阈值、汇聚点就钉死了,v6.58 不会在运行时学会新招;但它跨版本进化 —— 今天 v6.58,早晚 v6.59,RVA 重排、新 marker 加入、汇聚点挪窝。攻击侧的 agent 恰恰相反:它在单次会话里就是自适应的,能边读边改、边复盘边沉淀(前言提到的 Hermes 官方 memory / skill / background review 机制,证明这类外置经验循环已经产品化)。一边是会换代的死工事,一边是会自学的活工具 —— 标题那半句"工具会进化",说的正是这层:无论守方攻方,工具都在往前跑。剩下的问题只有一个:跑得越快,什么东西反而越要钉死不动?

但真要从这篇里带走点什么,不该是某条会随版本漂移的 RVA,而该是那套贯穿 25 个检测点、一次都没松过的工作流:

这五步不是流水账,每一步都在堵一类幻觉,缺一步就漏一类。少了第 1 步的 JNI 锚定,你连"这段 native 对应哪个 Java 方法"都说不准,后面全是空中楼阁;少了第 2 步的静态还原,你只知道"它报了 root",却不知道它怎么判的、该从哪改;少了第 3 步的 LR 对回,build_native_ListItemBean_risk 那 83 个 callsite 你分不清是哪一个在响,"命中原因"只能靠猜;少了第 4 步的 UI 三元组,你证明不了 native 这条上报真的走到了用户看见的那一格;少了第 5 步的 bypass 复测,你那套"算法理解"永远停在纸面,没人替你担保它真拦得住。五步互为对方的证人 —— 静态说"它该这么判",动态回"它确实这么判了",bypass 再补一刀"拦掉它界面就真少一项";三方口供对上,才敢盖"已确认"的章。

把这套流程写规整,其实就是一条 skill —— 而这,正好接回开头那个 Hermes 的故事。官方文档确认的部分是:run_agent.py 会在阈值触发后 fork 后台 Review Agent 回看对话,再把值得保留的内容写入 memory 或用 skill_manage 沉淀成 skill。至于"挨打中长出防御"这个案例,应作为公开安全测试转述理解,不把它当成本文证据链的一部分。同一套机制,完全可以把"逆向一个 native 壳"这件事也固化下来,大致长这样:

但请留意这条 skill 里最反常的一行:gate 被刻意冻结,不参与自进化。这恰恰是 Hermes 这类 self-improving agent 带来的反面提醒。它的魅力在于 skill 会自己越长越强;可一个能改自己的 agent,同样有能力把自己的判断标准越改越松 —— 今天为了"跑通"放宽一格,明天那道证据门就名存实亡。所以这套流程里,skill 体尽可天天进化,唯独那把尺子 —— 五类证据齐不齐 —— 必须只读、必须版本化。agent 负责规模,框架负责可信;能力越强,这道门越不能省。

能自我进化的系统,最危险的退化方向从来不是能力退化,而是标准退化:它依然很能干,只是悄悄不再诚实。有意思的是,怎么防住这种退化,Hunter 自己早把答案写在了代码里。它整套反篡改的命门,就是"冻结一个可信基准,再拿运行时实测去比":syscall 跳板把 48 字节模板的累加和算成 baseline_sum 焊死在 ctx 里,之后每次调用都重算一遍比对,差一个字节就重建、再不对就 exit(§3);CRC 自检把 libart / libc / linker 的 r-x 段基准 sum 存好,运行时谁动了一行代码,sum 一对就露馅(§5)。Hunter 信不过运行时的自己,于是给自己留了一把改不动的尺子。我们对 agent 该用的,是同一招:把"五类证据齐不齐"这把尺子冻结、版本化、设成只读,让它独立于那个会自我进化的 skill 体之外。agent 尽可天天给自己加新本事,但不能动这把尺子 —— 就像 libart 可以照常运行,却不能在 Hunter 眼皮底下改自己的 r-x 段。一个能顺手改掉自己证据门的 agent,等于一个能顺手改掉自己 baseline_sum 的 Hunter:那道防线名存实亡。

所以这套流程里的分工是刻意的:skill 体尽可进化 —— 新 marker 向量、新汇聚点、新的壳,都欢迎回写成可复用片段;唯独那把尺子,只读、版本化、不参与自进化。agent 负责规模,框架负责可信 —— 这两件事必须交给不同的东西去守,因为它们会互相腐蚀:让追求"产出"的同一个回路去管"可信",它迟早会为产出牺牲可信。能力越强,这道门越不能省 —— 不是因为我们不信任 agent,恰恰是因为它已经强到:一旦它想松,没人拦得住,除了一把它自己改不动的尺子。

也得老实承认,agent 今天还接不住全部。§24 里就明摆着:getZhenxiInfoBase 的 19 个 callsite、getZhenxiInfoO 的 5 处模拟器检测仍停在"字典级";还有 5 项(checkRiskFilecheckRootFromAVCLoggetZhenxiInfoMPNIgetZhenxiInfo2getZhenxiInfoCBinder)是"静态闭环但本环境未触发",其中 getZhenxiInfoCBinder 的纯 OLLVM callee 还没完全还原。这些恰好卡在 agent 最吃力的地方 —— OLLVM 反平坦化、跨进程时序复现、timing 侧信道(§11)这类得主动构造实验、而非"读出事实"的活。批量枚举和取证,agent 已经很能打;但设计一个能证伪自己的实验,目前还得靠人。这也正是 skill 末尾 escalate_to_human 两行的意思 —— 它不是免责声明,是 agent 自己划下的能力边界。

把这条线的形状看清,比记住它今天画在哪更有用。agent 真正擅长的,是"读出已经在那儿的事实"——枚举 60 项注册表、跨 83 个 callsite 对回 LR、把字符串 / xref / 伪代码织成一条静态链,这类活它又快又稳,几十倍于人肉。它真正吃力的,是"造一个事实出来":OLLVM 平坦化的 callee 得主动反推控制流(§24 的 CBinder),跨进程上报得搭一套多进程 spawn 才复现得了(§22),timing 侧信道得自己设计采样、控噪、定阈(§11)。前者是"看",后者是"做实验"—— 后者得先构造一个能证伪自己的实验,再从结果里把事实逼出来。这道坎短期内还得靠人迈;而 agent 肯在够不着时老实标回"字典级 / 未触发"、把活交还人手,本身就是那把尺子在起作用 —— 一个肯说"我不知道"的 agent,远比一个事事都敢盖"已确认"的 agent 可信。

Hermes 那个故事的结尾说:结束是另一个开始。这篇也一样。agent 已经把"产出一份逆向分析"的成本踩到了地板 —— 过去一个人啃半个月的加固壳,如今一个接好 IDA、Frida 的 agent 一两天就能扒出 25 个检测点的链路。可成本塌方的同时,一个旧问题被放大了:当"看起来很完整的分析"能批量生产,怎么分清哪一份是真的?于是留给我们的功课,不再是"能不能分析出来",而是与产能配套的另一道门槛 —— ✓ 闭环 / △ 静态 / 字典级 这套证据分级,和"宁可标未触发,绝不标已确认"的那点克制。

工具会换代,RVA 会漂移,今天逐字核对的 0x278658,到 v6.59 多半要挪窝 —— 本文里每一个具体地址都有保质期。会过期的东西,不配当结论的核心去记;真正该从这篇带走的,是那套把"已知"和"看起来像"死死分开的纪律:它不依赖任何一个 RVA,换个壳、换个 agent、换到三年后都成立。标题那半句"门不能松",落到实处就是这个意思 —— 工具越强、产出越快,那把分辨真伪的尺子就越要钉死。Hunter 用一把改不动的 baseline_sum 守住自己的完整性;轮到我们用 agent 去拆它,也得给自己留一把同样改不动的尺子,守住结论的完整性。这,才是 agent 时代的逆向,最不该弄丢的东西。

通过 IDA MCP get_bytes(0x2EF1F0, 1440) + 逐项解析 {name*, sig*, fn*},再 get_string 取 name/sig 字符串。完整 60 项实测结果:

SDK 路径
≤ 28(Android 9-) /system/lib64/libart.so
29(Android 10) /apex/com.android.runtime/lib64/libart.so
≥ 30(Android 11+) /apex/com.android.art/lib64/libart.so
SDK dlsym 目标符号 调用形式
> 30 art::ClassLinker::RegisterNative(art::Thread*, art::ArtMethod*, void const*) RegisterNative(this, Thread*, ArtMethod*, fnPtr)
== 30 art::ArtMethod::RegisterNative(void const*) RegisterNative(this, fnPtr)
< 30 art::ArtMethod::RegisterNative(void const*, bool) RegisterNative(this, fnPtr, true)
函数 RVA 作用
custom_dlopen 0x1AFF14 mmap libart.so + 解析 ELF header / Program Header,建立内部 handle
custom_dlsym 0x1B00B8 先查 .dynsym(导出符号表)
custom_dlsym_fallback 0x1B0720 .dynsym 找不到时查 .symtab(完整符号表,含 @hide)
custom_dlclose 0x1B0060 munmap + 释放 handle
静态结论 动态印证 状态
off_2EF1F0 起 60 项 JNINativeMethod 运行时枚举 60 项全部解析成功
每项 24 字节 {name*, sig*, fn*} 60 项 name/sig 都是合法字符串,fn 都在 libhunter 范围内
getZhenxiInfo5 是 idx=2 实测 #02 = getZhenxiInfo5
getZhenxiLoctionCrc 是 idx=25,RVA=0x2AE064 实测 #25 = getZhenxiLoctionCrc rva=0x2ae064
CheckVpn 是 idx=53 实测 #53 = CheckVpn
getZhenxiInfoH 不通过 batch 注册 实测 60 项表无 getZhenxiInfoH 条目 ✓(反常发现)
SDK > 33 路径回落 RegisterNatives hook libart RegisterNatives 可看到 hunter 注册(SDK=35 环境) 间接 ✓
偏移(相对 n12) 字节模式 / 等价反汇编
n12-12 MOVZ/MOVN W8, #imm,要求 imm == 93(mask (v11 & 0x1F80001F) == 0x12800008 锁定 Rd=W8)
n12-8 0xD4000001 = SVC #0
n12-4 0xB140041F = CMN X0, #0x1000
n12+0 0xDA809400 = CSINV X0, X0, X0, LS
n12+4 0xFF000010 与后 == 0x54000000 —— B.cond
n12+8 0xD65F03C0 = RET
偏移 字段 写入处 读取处
+0x00 void *page InitSecureTrampolinePage mmap 返回 safe_syscall_via_trampoline *v3
+0x08 size_t length = 48 *(_QWORD *)(v46 + 8) = 48 v3[1]
+0x10 std::string id_tag std::string::operator=(v47, v52) v3 + 2
+0x28 uint64_t baseline_sum *(_QWORD *)(g_secure_trampoline_ctx + 40) = v39 v3[5]
+0x30 uint8_t initialized = 1 *(_BYTE *)(g_secure_trampoline_ctx + 48) = 1
字段 实测值 静态推断
ctx 指针 0xb40000752359c1b0(arena 分配,带 MTE tag)
ctx[+0x00] page 0x773e804000 .rodata 模板起始地址匹配位置 ✓
ctx[+0x08] length 48
ctx[+0x10] id_tag "runtime_syscall_code"(20 字节,libc++ SSO) 字面值首次确认 ✓
ctx[+0x28] baseline_sum 0x14E0 与 JS 端朴素累加结果一致 ✓
ctx[+0x30] initialized 1(byte)
page 权限 r-x
page 首 48B 与 .rodata 模板逐字节对比 100% 一致
page[+0x30] 缓存 SVC 地址 0x7741a00694
偏移 实测指令 期望
@stub-4 0xd2800ba8 = MOVZ W8, #93, LSL #0 MOVZ/MOVN W8, #93
@stub 0xd4000001 = SVC #0 0xD4000001
@stub+4 0xb140041f = CMN X0, #0x1000 0xB140041F
所在段 0x77419ee000-0x7741b0e000 r-x /apex/com.android.runtime/bin/linker64 r-x ✓
sysno 名称 静态 callsite 数 静态 % 运行时次数 运行时 %
78 readlinkat 6 4.9% 4251 80.2%
56 openat 2 1.6% 461 8.7%
57 close 1 456 8.6%
63 read 1 76 1.4%
207 recvfrom 1 28 0.5%
62 lseek 1 12 0.2%
101 nanosleep 1 12 0.2%
43 statfs 2 1.6% 2 0.04%
80 fstat 1 1 0.02%
160 uname 1 1 0.02%
129 kill 102 83.6% 0 0%
167 prctl 1 0
9 lgetxattr 1 0
462 mseal 1 0
编号 静态结论 动态印证 状态
S1 跳板页 48 字节 ctx[+0x08]=48,且 page 首 48B 与 .rodata 模板一致
S2 id_tag 字符串 "runtime_syscall_code" ctx[+0x10] SSO 解码 = 字符串字面值
S3 id_tag SSO 控制字节首字 0x28(长度 20) 静态 strcpy(...,"(runtime_syscall_code") + ctx 实读
S4 baseline = NEON byte sum(48B) 静态推算 / JS 朴素累加 / ctx 字段三向一致 = 0x14E0
S5 page[+0x30] 保存 linker64 SVC #0 地址 实测 0x7741a00694,落在 linker64 r-x 段
S6 该地址前一条 = MOVZ W8, #93 实测 @stub-4 = 0xd2800ba8 = MOVZ W8,#93
S7 sysno 静态分布 kill 占 83.6% 运行时 kill 0%(检测路径未失败,自杀分支未触发) 结构性错位 ✓
S8 sysno 运行时主稳态:readlinkat + openat + close 三者占运行时 97.5%
S9 recvfrom(sockfd, buf, 0x2000, 0, NULL, NULL) sockfd=197 / len=8192 / flags=0,28 次
S10 anti_ptrace_poll_loopnanosleep 节拍 12 次,间隔规整
S11 statfs("/system") / statfs("/vendor") 实测 statfs 路径完全对得上
S12 readlinkat 用于 fd → 路径解析 80.2%,样本均为 /proc/.../fd/<N>
sysno 未触发的原因
kill (129) 本轮检测路径未进入自杀分支;只能说明当前触发条件/观察窗口下未命中 kill 路径
prctl (167) option=31 极大概率在 .init_array 阶段就执行完了,运行时观察窗口起点已晚于它
mseal (462) 同上,一次性硬化保护,运行时不重复
lgetxattr (9) 路径 "security.selinux" 的 SELinux 标签读取,与具体被检测文件触发条件挂钩
包装器 地址 sysno 等价
ssyscall_openat 0x1afe4c 56 openat(dirfd, path, flags, mode)
ssyscall_openat_atfdcwd 0x1afe70 56 openat(AT_FDCWD=-100, path, flags, mode)
ssyscall_read 0x1afe94 63 read(fd, buf, count)
ssyscall_lseek 0x1afeac 62 lseek(fd, offset, whence)
ssyscall_close 0x1afec4 57 close(fd)
ssyscall_nanosleep 0x1afedc 101 nanosleep(req, rem)
ssyscall_readlinkat 0x1afef8 78 readlinkat(dirfd, path, buf, sz)
对抗目标 跳板路径如何破坏
Interceptor.attach(Module.findExportByName('libc.so', 'syscall')) 完全不调用 libc::syscall,Hook 永不触发
对具名 syscall 包装(openat/read/getdents64 …)做 PLT/inline-hook 同上,直接走 linker 段内的裸 SVC
safe_syscall_via_trampoline / 跳板页头 16~20 字节做 inline-hook 每次调用前 calc_additive_checksum 重算 48B 累加和与 baseline_sum 比对,失配即重建,再失配 exit(9)
重建跳板时假冒身份 get_trampoline_id_tagctx[+0x10] 串比对,身份不符即销毁重建
在初始化阶段 hook fopen/fgets 干扰 maps 解析 时序上 hunter JNI_OnLoad 极早(Zygote 预加载阶段),通常先于第三方 hook 运行
字段 实测分布 推断含义
W3(risk) 0 × 66(80%) / 1 × 17(20%) isDeadly boolean。1 几乎只出现在严重路径:APK 签名失败 / Find Others Process / file lib error / Linker hooked+kill。但 W3=0 ≠ 非高危 —— InfoFcheckLibCheckSum W3=0 但 UI 是 Deadly,说明 W3 不是 UI 的 risk 等级,而是 native 侧是否标记 deadly 上报
W4(Type) 3 × 44 / 1 × 17 / 2 × 15 / 4 × 7 对应 Java 端 FB.a/b/c/d 类常量。1/4 偏 Info,2/3 偏 Risk
W5(ShowPriority) 0/1/3/4/6/7/8/9/10/11 都有 detection 子类别 ID,与 caller 强相关
W5 类别 主要 caller
0 misc / 杂项 CheckVpn, MPNI, checkFromZygote, Env, MapCheck, Base 部分
1 Root mount Info3, checkRootFromAVCLog
3 APK 签名 / Maps Hide / Base 检测 Info2, sub_2A1C38, Base 部分
4 Process / File lib / Binder Info4, Info0, CBinder, VV
6 Library checksum InfoF
7 Info7 Info7
8 Frida 检测 Info5(gum-js-loop / gmain)
9 Info6 Info6
10 Linker hook / Risk file InfoL, checkRiskFile
11 Simulator 检测 InfoO
Caller callsite (W3, W4, W5) 关键字面量
CheckVpn 0x2ab014 (0, 1, 0) "tun" "ppp" "pptp" "Find Vpn Native"
checkFromZygote 0x2ac0f8 (0, 1, 0) "Zygote Check Find Risk Mark"
checkRiskFile 0x2ace44 (1, 2, 10) "Find Risk File" + 25 条 /data/local/tmp/* 路径
checkRootFromAVCLog 0x2aaec4 (0, 1, 1) "Find Root File In Sniff"
checkZygisk 0x2a9690 (0, 2, 0) <dyn>
getZhenxiInfoCBinder 0x28cd08 (0, 3, 4) "find system server hook"
getZhenxiInfoEnv 0x28d3a4 (0, 4, 0) "Get Env Info"
getZhenxiInfoMPNI 0x2994f4 (0, 1, 0) "/oat/arm64/" "Find Mark ELF"
getZhenxiInfoVV 0x2c2854 (0, 3, 4) <dyn>
getZhenxiInfo3 × 7 0x2b490c..0x2b4ed4 (0, 1, 1) "Check Find Root In Linker" / "Check Find Root Permission" / "Find Root Mark In Mountinfo"
getZhenxiInfo4 × 4 0x2a3f30..0x2a43f8 (0 或 1, 3 或 4, 4) "Find Others Process" + "find other process" + "ps error"
getZhenxiInfo5 × 3 0x2a47f4 / 0x2a5204 / 0x2a5470 (0, 2, 8) "Find Frida Mark" + "gum-js-loop" / "gmain" / <dyn>
getZhenxiInfo6 × 2 0x2a6084 / 0x2a6294 (0 或 1, 2 或 3, 9) <dyn>
getZhenxiInfo7 × 2 0x2a65ec / 0x2a662c (0 或 1, 3, 7) <dyn>
getZhenxiInfoF × 8 0x2ada3c..0x2adf74 (0, 3 或 4, 6) "checkLibCheckSum" / "checkLinkerCheckSum"
getZhenxiInfoL × 2 0x2a87b8 / 0x2a91c8 (0 或 1, 3, 10) "Find Linker Is Hook" / "Check Linker Crc Error"
getZhenxiInfoO × 5 0x2bec24..0x2bf71c (0, 2, 11) "Find Simulator OpCode Mark!" / "Find Simulator Env Mark!" / "Find Simulator Mount Mark!"
getZhenxiInfo0 × 6 0x2c00d4..0x2c15d8 (0 或 1, 3, 3 或 4) "Insufficient permissions" / "Check App Sign Error From Path Lib" / "Mem Find Mark"
getZhenxiInfo2 × 11 0x29d6c0..0x2a0304 (0 或 1, 3, 3) "HunterCheckApkSignError" + 11 种子原因(详 §18)
getZhenxiInfoBase × 19 0x292e18..0x2952a4 (0, 1/2/3, 多种) "Linker Find Hook Mark" + "lsposed" / "zygisk" / "Find Prop Modify Mark" / "Find Hook Mark" / "Base Check Inline Flag" / "Find Mnt Mark" / "Find Magisk Modules Mark" / "Seccomp Check Arch Find Mark" / "Check Signal Not Match" / "Check Uname Spoofing"
getZhenxiMapCheck × 3 0x2923e8 / 0x292434 / 0x2924b4 (0, 2 或 3, 0) "libart.so piecewise ,memory is not legal" / "find libart.so hooked"
sub_2A1C38 × 2 0x2a2c20 / 0x2a2c68 (0, 3, 3) "Check Maps Find Hide"
编号 静态结论 动态印证 状态
C1 函数 size 0x8EC,被多 caller 调用 30s 实测打印 N 条 [ReportRiskItem] 来自不同 lr_offset
C2 函数签名 6 参,X1/X2 是 std::string* readStdString(args[1]) 拿到合法字符串("Check Find Root In Mounts" 等)
C3 callsite 0x2b4a88-4 = 0x2b4a84 位于 getZhenxiInfo3 @ 0x2B476C 内部 实测 lr=0x2b4a88,该 callsite 静态字面量含 "Check Find Root In Mounts"
C4 callsite 0x2924b8 位于 getZhenxiMapCheck @ 0x291858 内 size==0x1000 分支 实测 lr=0x2924b8,arg1="find libart.so hooked 0"
C5 callsite 0x28d3a4 位于 getZhenxiInfoEnv @ 0x28CDB0 实测 lr=0x28d3a8,arg1="Get Env Info"
C6 callsite 0x2a3f6c 位于 getZhenxiInfo4 @ 0x2A2D98 实测 lr=0x2a3f6c,arg1 含 "Find Others Process"
C7 callsite 0x2ada40 位于 getZhenxiInfoF @ 0x2AD9A8 内 checkLibCheckSum 分支 实测 lr=0x2ada40,arg1="检测到libart.so被修改"
编号 静态结论 动态印证 状态
H1 4 个目标路径:libart / libc / linker64 / libhunter 实测 [crc-module] 出现这 4 个 path
H2 NEON 字节累加和算法,纯 sum 不取模 libhunter.so 各段 sum 累加 = module sum,数学验证 ✓
H3 只对 r-x 段累加,其他段返回 0 实测各 r--p / rwxp / rw-p 段大多 segmentChecksum=0(libhunter 的 rwxp 段 0xb69f4 是个例外)
H4 文件读取走 ssyscall 包装器 §3.3.4 实测 openat/read/close/lseek 出现在跳板调用面
H5 cache 容器 qword_2EF8B0 首次后永久缓存 多轮调用中只首次出现 parse_elf_rx_segments entered 日志 间接 ✓
H6 Java 侧 ig.apply 三条件比较 实测 [evidence:crc-java] ig.apply 命中 4 个目标路径
H7 失配 UI 文案 = "ISO CRC NOT MATCH LOCALITY CRC" 实测 UI title 完全匹配
H8 链路 2 不经 Java,native 直接报 "检测到libart.so被修改" 实测 [evidence:report] title=检测到libart.so被修改 lr=0x2ada40
H9 链路 2 LR 落在 getZhenxiInfoF @ 0x2AD9A8 LR 0x2ada40 - 0x2AD9A8 = 0x98(在 InfoF 函数体内)
H10 maps_precheck_readable unmapped hole bug 影响 scan_rx_segment_neon_sum 实测 block_bad_precheck 反复在 off 0xc7828(scan_rx_segment_neon_sum 内)触发
H11 同时影响 parse_elf_rx_segments 的可读性预检 实测 block_bad_precheck 同样在 off 0xc68a4(sub_C687C 内)触发
顺序 子函数 RVA UI title 主要监测点
1 0xD2CDC Check Find Root File 固定 root/hook 文件路径、/proc/<pid>/attr/*、OverlayFS、/data/adb/modules、mount namespace、/proc/modules
2 0xD5E34 Check Find Root In Mounts /proc/mountsmountinfo、线程 maps、df -h 中的 root/APatch/Magisk marker
3 0xD703C Check Find Root Mark /data/adb*、Magisk/LSPosed/Riru marker、/proc/fs/jbd2 与 ext4 loop 痕迹
4 0xD9604 Check Find Root Magisk Mode 当前版本直接 return 0,保留分支
5 0xD216C Check Find Root In Linker dl_iterate_phdr 收集 linker 已加载 ELF,匹配可疑 Zygisk engine 路径
6 0xDDB9C Check Find Root Permission netlink 权限侧信道,命中详情前缀 netlink Find Permission
7 0xDF51C Find Root Mark In Mountinfo /proc/self/exe 类型、/proc/self/mountinfo 的 overlay/system/bin 痕迹
编号 静态结论 动态印证 状态
M1 NativeEngine_getZhenxiInfo3 是 7 路短路链 trace_info3_branch_experiment.js 强制 miss 第二分支后,后续 5 个分支均依次执行
M2 sub_D5E34 是其中 mounts 分支 trace_info3_branch_experiment.js 看到 sub_D5E34 title="Check Find Root In Mounts"
M3 18 项 marker 向量 [evidence:mounts-file] 打印 markers 完整列出 18 项
M4 读取源 6 个:/proc/mounts/proc/self/mountsmountstatsmountinfo → 线程 maps → df -h 第一项 /proc/mounts 即命中,后续未执行(短路);但其它实验路径中可看到 /proc/self/mountstats 等被读 ✓ 部分
M5 substring 匹配(line.find(marker))而非结构化解析 命中行 APatch /debug_ramdisk tmpfs rw,... 同时命中 APatch/debug_ramdisk
M6 ReportRiskItem callsite 在 getZhenxiInfo3 LR 0x2b4a88,(LR-4)=0x2b4a84 落在 0x2B476C 函数体内
M7 file_contains_any_marker 通用 helper 同一会话中同时观察到 root markers / docker markers / 模拟器路径 markers 三组复用
M8 sub_DDB9C 是 netlink 权限侧信道 trace_info3_branch_experiment.js 看到 sub_DDB9C title="Check Find Root Permission" ret=0x1(本设备命中)
编号 静态结论 动态印证 状态
A1 JNI 函数 getZhenxiMapCheck @ 0x291858 实测 60 项表 #45 = getZhenxiMapCheck rva=0x291858
A2 解析 /proc/self/maps 找 libart.so 形态 [evidence:art-map] NativeEngine_getZhenxiMapCheck entered 后输出 map item 详情
A3 size==0x1000 立即上报"find libart.so hooked" 实测 arg1="find libart.so hooked 0",lr=0x2924b8
A4 0x2924B8 是 size==0x1000 分支的上报点 LR 完全等于 0x2924B8
A5 libart_count >= 6 上报"piecewise" 本设备命中 single 4KB 分支(优先返回),未触发 piecewise 间接 ✓
A6 UI risk=Deadly 但 W3=0 W3 从 0x278658 onEnter 看为 0,但 UI 显示 Deadly,W4=2/3 决定 UI 等级
编号 静态结论 动态印证 状态
F1 JNI 函数 getZhenxiInfoF @ 0x2AD9A8 60 项表 #(查到)= getZhenxiInfoF rva=0x2ad9a8
F2 内部调用 check_libart_elf_checksum_status @ 0xC9130 [evidence:checksum] check_libart_elf_checksum_status entered
F3 检测命中返回 1 [evidence:checksum] check_libart_elf_checksum_status ret=0x1
F4 callsite 在 getZhenxiInfoF LR 0x2ada40 在 [0x2AD9A8, 0x2AD9A8+size) 范围
F5 UI 文案"检测到libart.so被修改" [evidence:ui] title=检测到libart.so被修改
F6 W5=6(checksum 类别) UI type=6
F7 scan_rx_segment_neon_sum 共享算法 block_bad_precheck off 0xc7828(scan_rx_segment_neon_sum 内)反复在此 detection 触发
子函数 检测对象 标志字符串
0x100AAC 匿名 / 未知路径的可执行内存(rwxp + inode=0 + path 空或 ?) "mem, suspicious execution procedure ->" / "find rwx item 111" / "libc_malloc" / "scudo"
0x102814 memfd JIT cache mapping "memfd:jit-cache" / "memfd:jit-zygote-cache"
0x103610 maps 行格式异常 <dyn>
0x104D54 missing page(可执行但 mapping 缺失页) "missing page, suspicious execution procedure ->" / "pid ->"
0x105ACC NativeBridge / AndroidRuntime hook dlopen("/system/lib64/libandroid_runtime.so") + _ZN7android14AndroidRuntime10getRuntimeEv / "Found Hook NativeBridge"
0x10610C 匿名内存 / jit-cache 一致性 "Found suspicious anonymous memory" / "find inconsistent dalvik-zygote-jit-code-cache :" / "[anon:dalvik-zygote-jit-code-cache]" / "/memfd:jit-cache"
编号 静态结论 动态印证 状态
I1 JNI 函数 getZhenxiInfoInjection @ 0x28DF24 60 项表实测 rva=0x28df24
I2 6 个子检测短路链 拆分实验中,sub_100AAC 默认命中;清空后 sub_10610C 也命中;sub_102814 / 103610 / 104D54 / 105ACC 当前环境未命中 ✓ 部分
I3 sub_100AAC 检测匿名 rwxp + inode=0 实测 detail 含 7727002000-7727012000 rwxp 00000000 00:00 0 ?
I4 静态字符串 "find rwx item 111" 实测 detail 末尾出现 find rwx item 111
I5 sub_10610C 检测 "Found suspicious anonymous memory" 拆分实验中 sub_10610C outLen=3106 含此字面量
I6 Java 端判 s != null && !s.isEmpty() 才构造 bean bypass 返回空字符串后 UI 不再出现该 bean
I7 UI 文案 "Hunter Find Unknown Exec Item From Mian"(注意 "Mian" 拼写) UI 实测原样显示 "Mian"
编号 静态结论 动态印证 状态
P1 JNI 函数 getZhenxiInfo4 @ 0x2A2D98 60 项表 rva=0x2a2d98
P2 两阶段:/proc/<pid>/comm|cmdline + ps -ef 不同会话分别命中第一阶段和第二阶段
P3 第一阶段 callsite LR 0x2a3f6c 实测 lr=0x2a3f6c,(LR-4)=0x2a3f68 在 InfoInfo4 函数体内
P4 第二阶段 callsite LR 0x2a43b0 实测样本中第二阶段 lr=0x2a43b0
P5 UI 文案前缀 "Find Others Process" 实测 UI title 完全匹配
P6 risk=NoRisk type=4 UI 显示 risk=NoRisk type=4
P7 关键字含 sh / logcat / grep / ls / getprop 实测命中样本含 logcat/ls/sh/grep
编号 静态结论 动态印证 状态
S1 JNI 函数 SideChanne @ 0x2B3BD4 60 项表 #59 = SideChanne rva=0x2b3bd4
S2 内部调用 detect_root_side_channel_apatch_kernelsu @ 0xE43BC [evidence:side-channel] detect_root_side_channel_apatch_kernelsu entered
S3 阈值 1.4 < ratio ≤ 2.2 归 "APatch/KP Mid Latency" 实测 ratio0=1.604278 / ratio1=1.657754,两者均 > 1.4 且 < 2.2
S4 阈值 ratio > 2.2 归 "KernelSU High Latency" 本环境未达到此阈值 间接 ✓
S5 UI 文案 "Check Root For SideChanne" 实测 UI title 完全匹配
S6 返回字符串完整含 ratio 数值 实测 data 串包含 ratio0:1.604278 ratio1:1.657754
编号 静态结论 动态印证 状态
V1 链路 A 来自 TEE attestation rootOfTrust UI data 含 "tee check device unlock"
V2 链路 B 来自 ro.boot.verifiedbootstate=orange [evidence:bootstate-java] 实测看到 prop 返回 "orange"
V3 两条独立,各自命中均产生风险项 UI 同时出现两条 "当前手机已经被解锁",一条 data="tee check device unlock" 一条 data="orange"
V4 TEE 链路 e.b == 1/2/3 → "Find Verified Boot Mark" 实测 verifiedBootState=Unverified(=2)→ UI 出现 "Find Verified Boot Mark"
V5 getZhenxiInfoH 不在 batch 表 60 项表枚举无此 name,SO 层 Interceptor.replace 报 miss
编号 静态结论 动态印证 状态
E1 JNI 函数 getZhenxiInfoEnv @ 0x28CDB0 60 项表 rva=0x28cdb0
E2 callsite LR 0x28D3A8 实测 [evidence:report] lr=0x28d3a8
E3 遍历 environ 全部 key=value 实测 detail 含 PATH/ANDROID_*/BOOTCLASSPATH/TMPDIR 等
E4 risk=NoRisk(W3=0, W4=4, W5=0) UI risk=NoRisk type=0
编号 静态结论 动态印证 状态
K1 JNI 函数 getZhenxiInfoK @ 0x2B93BC [evidence:file-marker-java-native] NativeEngine_getZhenxiInfoK entered
K2 底层调用 file_contains_any_marker @ 0x1A6300 [evidence:file-marker] hook 在 0x1A6300 落点
K3 容器 markers docker, lxc_volumns 实测 markers 字符串完全匹配
K4 模拟器 markers 12 项 实测 markers 字符串包含全 12 项
K5 file_contains_any_marker 是通用 helper 同一会话观察到 3 套不同 markers(root / docker / 模拟器)复用同 helper
类别 路径
投屏 /data/local/tmp/vysor.pwd, /data/local/tmp/oat/arm64/scrcpy-server.odex, /data/local/tmp/mqc-scrcpy.jar
触屏/操控 /data/local/tmp/minicap.so, /data/local/tmp/minicap, /data/local/tmp/mini, /data/local/tmp/mini/minicap, /data/local/tmp/minitouch
触动/按键精灵 /data/local/tmp/tc/mobileagent, /data/local/tmp/tc/input3.sh, /data/local/tmp/tc/mainputjar7, /data/local/tmp/com.cyjh.mobileanjian.id, /data/local/tmp/com.cyjh.mobileanjianen.id
UI 自动化 /data/local/tmp/uiautomator-stub.jar
云手机/云控 /data/local/tmp/cloudtestig/cloudscreen, /data/local/tmp/cloudtesting/touchserver, /data/local/tmp/juejinAzykb/, /data/local/tmp/juejinAzykb/TouchService.jar, /data/local/tmp/maxpresent.jar
截屏 SO /data/local/tmp/screen-shread10x64.so, /data/local/tmp/screen-shread5x32.so
Shizuku /data/local/tmp/shizuku, /data/local/tmp/shizuku_starter
设备信息 /data/local/tmp/mobile_info.properties
一键玩 /data/local/tmp/yijianwanservice.apk
编号 静态结论 动态印证 状态
RF1 JNI 函数 checkRiskFile @ 0x2AC514 60 项表 #50 = checkRiskFile rva=0x2ac514
RF2 25 条 /data/local/tmp/* access 实测函数 entered 且 ret=0x0(当前环境 25 路径均不存在) ✓ 间接
RF3 callsite 0x2ace44(W3=1, W4=2, W5=10) 本设备未触发该 callsite 静态闭环 / 未触发
编号 静态结论 动态印证 状态
AV1 JNI 函数 checkRootFromAVCLog @ 0x2AAE54 60 项表 #56 = checkRootFromAVCLog rva=0x2aae54
AV2 内部调用 sub_E0AA8 hook 内部函数(本测试未细做) 间接 ✓
AV3 通过 logcat 反读 AVC denial 行 当前环境 SELinux audit 未暴露被探测路径,或 logcat 已被清,导致未命中 静态闭环 / 未触发
AV4 UI 文案 "Find Root File In Sniff" 当前环境未生成此 UI title 静态闭环 / 未触发
编号 静态结论 动态印证 状态
MP1 JNI 函数 getZhenxiInfoMPNI @ 0x298374 60 项表 rva=0x298374
MP2 阶段 1 进程标签自保护,失败时自杀 本环境标签通过,未触发自杀路径 间接 ✓
MP3 阶段 2 maps 包名 ELF 白名单扫描 函数 entered + return null,符合"标签通过 + 无额外 ELF"判断
MP4 白名单 8 项 静态字符串引用完整列出
编号 静态结论 动态印证 状态
AS1 JNI 函数 getZhenxiInfo2 @ 0x29AEFC 60 项表 rva=0x29aefc
AS2 11 个 callsite 覆盖 11 种失败子原因 静态字符串引用完整列出 11 类失败文案
AS3 预期签名前 10 base64 = "MYAAKzWEBR" 字符串静态确认,本设备签名匹配未触发 ✓ 静态
AS4 检查链覆盖 path / cert / base64 / hex / fd readlink / fstat 7 步 字符串 + xref 完整可见 ✓ 静态
AS5 官方 APK 不触发 实测 return 0
编号 静态结论 动态印证 状态
CB1 JNI 函数 getZhenxiInfoCBinder @ 0x28C9BC 60 项表 rva=0x28c9bc
CB2 阶段 1 进程标签自保护(同 MPNI) 本环境标签通过,未自杀 间接 ✓
CB3 内部用 sub_251C18 解密器比对预期值 hook 内部函数(本测试未细做) 间接 ✓
CB4 UI 文案 "find system server hook" 本环境未生成此 UI title 静态闭环 / 未触发
编号 静态结论 印证手段
S1 sub_AA914 分配 0x2000 buffer §3.3.4 实测 recvfrom(... len=0x2000 ...)
S2 socket(AF_NETLINK=16, SOCK_RAW|SOCK_CLOEXEC=0x80003, NETLINK_ROUTE=0) hook libc socket 看实参
S3 顺序发两条 dump:type=18 → 22 回复方观察到 type=16 后再 type=20
S4 nlmsghdr flags=0x301 hook sub_AD0EC 才能直接看
S5 sub_AD0EC 是发送辅助 (未挂发送 hook)
S6 接收走 safe_syscall_via_trampoline(207=recvfrom) §3.3.4 实测
S7 RTM_NEWLINK(16) → 解 IFLA_IFNAME → 节点 name hook sub_A8CC8,解出字符串与 ip link 一致
S8 RTM_NEWADDR(20) → 按 ifa_index 匹配已建节点 hook sub_A8CC8
S9 主循环用 my_strcmp 精确匹配 tun/ppp/pptp 设备若有 tun* 但 ≠ "tun" 且返回 NULL 可反向证明
S10 命中即 build_native_ListItemBean_risk("Find Vpn Native", iface_name, 0, 1, 0) hook 0x278658
S11 无 VPN 设备 → return NULL hook 入口 onLeave 看 retval
编号 状态 实测证据
S1 间接 ✓ §3.3.4 已观察 recvfrom len=0x2000 = 8192
S2 [S2] socket(AF_NETLINK=16, type=0x80003 ✓SOCK_RAW|SOCK_CLOEXEC, proto=0 ✓NETLINK_ROUTE)
S3 一次 CheckVpn 调用内 50 帧 RTM_NEWLINK(=16) 后 7 帧 RTM_NEWADDR(=20)
S4 未验证 本轮未 hook 发送方向
S5 未验证 同上
S6 §3.3.4 已验证(本轮 RTM_NEW* 帧能进 sub_A8CC8 hook 间接印证)
S7 50 个接口的 IFLA_IFNAME 全部正确解出,与 ip link 一致
S8 7 次 RTM_NEWADDR 全部正确关联(lo×2, dummy0×1, wlan0×4)
S9 ✓(关键) 设备存在 tunl0(ipip tunnel,前 3 字符 = "tun")但 CheckVpn return 0x0
S10 未触发 设备无 VPN
S11 return = 0x0 ✓ NULL
方案 位点 效果 风险
A 用户态 hook / inline patch NativeEngine_CheckVpn @ 0x2AAF44 入口直接 return 0 覆盖该入口所有调用 修改用户态 .text,可能触发 §5 CRC 链
B 用户态 hook / inline patch sub_A8CC8,看到 IFLA_IFNAME == tun/ppp/pptp 时改写进入链表的 name 隐蔽,链表里就没"危险"名字 改 hunter 用户态执行流或数据,需保证内存一致性,同样有 CRC/自校验风险
C KPM 在 NETLINK_ROUTE/rtnetlink 内核侧 filter,改写或截走对 hunter 进程的 RTM_GETLINK 回复 不改 hunter 用户态代码,隐蔽性更高 实现复杂,需适配内核版本和 netlink 路径
D 不动:用户场景下 OpenVPN/WireGuard 接口名本就不裸叫 tun/ppp/pptp 0 工作量 仅适用于不命中场景
sub_C6A84 返回 走的分支 callsite title / detail 自杀?
2 gmain 命中 0x2a5204 "Find Frida Mark" / "gmain" ✅ 三连 safe_syscall(129=kill, pid, 9)
1 gum-js-loop 命中 0x2a47f4 "Find Frida Mark" / "gum-js-loop" ❌ 仅上报,不杀
其它 (0/-1) "无命中"路径 0x2a5470 OLLVM 状态机解码出的 <dyn>,detail 为空
编号 静态结论 印证位
F1 opendir("/proc/self/task") libc opendir hook,arg = /proc/self/task
F2 readdir 遍历自身所有 tid libc readdir hook,d_name 是数字串
F3 对每个 tid ssyscall_openat("/proc/self/task/<tid>/status", O_RDONLY) hook ssyscall_read 看 fd
F4 strstr_ 子串匹配(非精确) 通过 F8 命中行为反推
F5 优先级 gum-js-loop > gmain(if/else if 顺序) sub_C6A84 返回 1 优先于 2
F6 返回 0/1/2/-1 各对应不同语义 hook sub_C6A84 onLeave
F7 getZhenxiInfo5 分发 1→仅上报,2→上报+SIGKILL,else→空 hook getZhenxiInfo5 onLeave + 进程存活观察
F8 当前 Frida 17.x 测试环境中,若 gum-js-loop 线程名被真实读到,sub_C6A84 应返回 1 hook 直接看 W0
F9 返回 1 分支触发 SIGKILL 进程存活
F10 命中后 getZhenxiInfo5 返回 non-NULL ListItemBean onLeave retval ≠ 0
F 第一轮
F1 opendir("/proc/self/task") -> 0x... 非 NULL
F2 readdir 遍历到 77 个 tid
F6 ✓ 返回值 = 0(类型对)
F8 Frida 在跑但 sub_C6A84 返 0,不是预期的 1
F9 ✓ 进程存活
F10 ✗ retval = 0(因 F8 ✗)
F 第一轮 第二轮 最终状态
F1
F2
F3 未直接验 ✓ 2 次 ssyscall_read 都来自 /proc/self/task/<tid>/status
F4/F5 静态正确,F8 ✗ 导致没法用命中数据印证,但 ✗ 原因与此无关
F6 ✓ 返 0 ✓ 再次 = 0 ✓ 技术上对,值意外
F7 间接 ✓:retval=NULL → 走了"else 空 detail"或退出循环
F8 第二轮找出原因 ✗ (已查明原因,详 21.5)
F9
阶段 行为 cursor 变化
tid #1 open(fd=171)read 1024B(read#1) → 解出首行 Name:\ter_main_process\nclose(171) cursor: 0 → ~22
tid #2 open(fd=171, reused) → cursor(22) < end(1024) → 不 read → 消费 buffer 22..32 = Umask:\t0077\nclose(171)不是该 tid 的真实数据! cursor: 22 → ~32
tid #3..~#30 同上,持续消费第一次 read 残留的 ~1000 字节(每 tid 一行 = 一次 break) cursor: 32 → ~1024
~tid #30 cursor 到 1024,end=1024 → read 触发(read#2),fd=171 此时属于这个 tid(碰巧是 mali-cmar-backe) cursor: 1024 → 0; end → 1024
tid #30..#60 同上消费 mali 的 status cursor: 0 → ~1024
后续 一直循环到 readdir 没新条目
视角 结论
对绕过 Frida 检测的实战意义 在本轮 Pixel 6 Pro / Android 15 / Frida 17.x 环境下,该实现只真实读取少量 tid,显著降低 Frida 线程名命中概率;其它 Frida 版本、注入方式和线程命名策略需单独复测
对未来 hunter 升级的预警 若后续版本在每次 fd open 后重置 qword_2EFCB8/CC0,该缺陷即可修复。KPM 设计不能依赖此缺陷做长效绕过
用户态备选(在 bug 修复后) A. hook sub_C6A84 直接 return 0;B. hook strstr_ —— 进入 sub_C6A84 上下文时,看到 needle == "gum-js-loop"/"gmain" 强制返 NULL。二者都属于用户态 hook/patch,可能触发 §5 CRC 链
KPM 备选(在 bug 修复后) read syscall 或 procfs 读取路径内核侧过滤 hunter 读取到的 /proc/self/task/*/status 内容,隐藏线程名字段中的 Frida 特征
编号 静态结论 动态印证 状态
B1 Manifest 声明 :hunter_server_iso 隔离进程 UI data 前缀 com.zhenxi.hunter:hunter_server_iso:...
B2 Manifest 声明 :hunter_server_twin 孪生进程 getZhenxiInfo4 第二阶段实测捕获到 com.zhenxi.hunter:hunter_server_twin 进程
B3 子进程也加载 libhunter.so,独立跑 detection 主进程 SO 层 25 个 NativeEngine.* 全 replace 后,仍收到 bean
B4 结果通过 Binder 回传主进程 Java stack 显示 Binder.execTransact -> TC.onTransact -> TC.v -> MainActivity.n0
B5 主进程 0x278658 汇聚点拦不到子进程的 bean 未出现 [ReportRisk] 行,只出现 MainActivity.n0
方案 位置 效果
A UI 兜底:MainActivity.n0 按 title 名单丢弃 一处统一,实测可用
B TC dispatcher 层:hook YouAreLoser.TC.vTC.onTransact,Binder 一收到 ListItemBean 直接丢弃 比 UI 层更上游,不依赖 title 名单
C 多进程 spawn(--enable-spawn-gating):让子进程也装同一套 SO 层 hook 最彻底,但 spawn-gating 调试复杂
# 缺陷 位置 静态证据 动态证据 修复成本
1 maps_precheck_readable 不验证目标范围全被可读 mapping 覆盖,unmapped hole 被误判为可读 0xC6520 函数体只检查"重叠且 perms[0]!='r'",IDA 反编译可见 block_bad_precheck 多次拦下 addr=0x7741a35000 size=0x1266d8 off 0xc7828 等越界访问(详 §5.3.5) ~5 行 C 代码,补"全范围被可读 mapping 覆盖"判定
2 sub_C6A84 buffered reader(ptr_/qword_2EFCB8/CC0)跨 fd 不重置 0xC6A84 三个全局 .bss 在 fd 迭代中从不重置(IDA xref 可见) 77 tid 实际只触发 2 次 ssyscall_read,Frida 的 gum-js-loop 在该 buffered reader 缺陷下存在漏检(详 §21.3.2 / §21.5) 2 行代码,每次 open 后重置 cursor/end
3 art_register_natives_batch SDK 31-33 路径 ClassLinker::RegisterNative 调用,Thread* 实参错传成函数指针(没调用 CurrentFromGdb()) 0xA5254 汇编 MOV X1, X21(X21 = dlsym 拿到的函数指针,未调) → BLR X8;且 SDK > 33 被显式禁用与本 BUG 一致 SDK=35 环境下 hunter 走 RegisterNatives 回落路径(SDK > 33 入口 return false),verify_register_natives.js 间接印证 1 行代码,补一次 ((Thread*(*)())CurrentFromGdb)() 间接调用
4 CheckVpnmy_strcmp 精确比较 + 仅 3 个裸字符串,使用 tun0/ppp0/wg0/xfrm0 等常见命名时存在明显漏检风险 0x2AAF44 my_strcmpstrcmp 等价函数(IDA 反编译);静态名单只有 tun/ppp/pptp 本设备的 tunl0 存在但不命中,return 0(详 §20.5.1) 2-3 行代码,改 strncmp 或扩名单
UI/日志标题 Java 入口 Native RVA 内部核心 RVA 动态证据 闭环度
Check Find Root In Mounts getZhenxiInfo3 0x2B476C 0xD5E34/proc/mounts 等 6 源 + 18 marker LR 0x2B4A88,data 含 APatch /debug_ramdisk ✓ 闭环
Check Find Root Permission getZhenxiInfo3 0x2B476C 0xDDB9C netlink 权限侧信道 trace_info3_branch_experiment 中 sub_DDB9C ret=0x1 ✓ 闭环
find libart.so hooked getZhenxiMapCheck 0x291858 size==0x1000 / count>=6 双判 LR 0x2924B8 arg1="find libart.so hooked 0" ✓ 闭环
ISO CRC NOT MATCH LOCALITY CRC getZhenxiLoctionCrc + Java ig.apply 0x2AE064 0xC8670 收集 RX 段 NEON sum + Java 比较 remote CRC Java data 显示 local/remote CRC ✓ 闭环
检测到libart.so被修改 getZhenxiInfoF 0x2AD9A8 0xC9130 / 0xC90A0 / 0xC89BC checksum status LR 0x2ADA40(libart 分支) ✓ 闭环
Hunter Find Unknown Exec Item From Mian getZhenxiInfoInjection 0x28DF24 0x100AAC + 5 个辅助子检测 data 含 rwxp 00:00 0 ? ✓ 闭环
Find Others Process getZhenxiInfo4 0x2A2D98 /proc/<pid>/comm|cmdline + popen("ps -ef") data 含 logcat / ls / sh / grepps -ef 输出 ✓ 闭环
Check Root For SideChanne SideChanne 0x2B3BD4 0xE43BC timing ratio ratio0/ratio1 ≈ 1.604/1.658 ✓ 闭环
当前手机已经被解锁 z5.b / j40.a getprop Java 主导 TEE rootOfTrust + ro.boot.verifiedbootstate data="tee check device unlock" 或 "orange" ✓ 闭环
Find Verified Boot Mark z5.b Java 主导 e.b == 1/2/3 data="the boot.img maybe have been patched" ✓ 闭环
Get Env Info getZhenxiInfoEnv 0x28CDB0 遍历 environ LR 0x28D3A8 data=PATH=... ✓ 闭环
当前手机可能是模拟器&云手机 getZhenxiInfoK 0x2B93BC 指定 path × markers 数组,逐行匹配 docker / lxc / 12 路径 markers 动态出现 ✓ 闭环
Find Vpn Native CheckVpn 0x2AAF44 0xA8BE4 NETLINK_ROUTE 枚举 + my_strcmp 精确比较 50 接口枚举,return NULL(无 VPN 环境) ✓ 闭环
Find Frida Mark(gum-js-loop / gmain) getZhenxiInfo5 0x2A4760 0xC6A84 buffered reader 扫线程名 sub_C6A84 ret=0(buffered reader bug 漏检) ✓ 闭环(含 bug)
Find Risk File checkRiskFile 0x2AC514 25 条 /data/local/tmp/* access 函数 entered, ret=0x0(当前环境 25 路径均不存在) △ 静态闭环 / 未触发
Find Root File In Sniff checkRootFromAVCLog 0x2AAE54 0xE0AA8 logcat avc:denied 路径侧信道 函数 entered, ret=0x0 △ 静态闭环 / 未触发
Find Mark ELF getZhenxiInfoMPNI 0x298374 进程标签自保护 + maps 包名 ELF 白名单 return null(标签通过 + 无额外 ELF) △ 静态闭环 / 未触发
HunterCheckApkSignError 多种 getZhenxiInfo2 0x29AEFC APK path / cert / base64 / hex / fd readlink / fstat 7 步 return 0(官方 APK 不触发) △ 静态闭环 / 未触发
find system server hook getZhenxiInfoCBinder 0x28C9BC sub_CDFC4 / CDAD4 / CE000 / 251C18(纯 OLLVM) return null △ 静态闭环 / 内部 callee 未完全还原
ROM 属性多种 getZhenxiInfoH(key) Java wrapper,不在 batch 表 getPropInline 混淆链:getZhenxiInfoZ fallback / /dev/__properties__ ro.boot.verifiedbootstate -> orange 实测 ✓ 链路已收敛
Find Simulator OpCode Mark! getZhenxiInfoO 0x2BE8EC 0x2bec24..0x2bf71c 5 callsite,4 子类 (未细做,W5=11) 字典级 / 未细拆
Linker Find Hook Mark 等 19 类 getZhenxiInfoBase 0x29288C 19 callsite, Linker Hook / Magisk / Mount / Seccomp / Signal / Uname 等 (字典级) 字典级 / 待拆
Mem Find Mark getZhenxiInfo0 0x2BFDD0 6 callsite, APK lib mem 类 (字典级) 字典级 / 未细拆
Find Linker Is Hook getZhenxiInfoL 0x2A8724 2 callsite (字典级) 字典级 / 未细拆
Zygote Check Find Risk Mark checkFromZygote 0x2ABCD0 1 callsite (字典级) 字典级 / 未细拆
Zygisk 相关 checkZygisk 0x2A93F8 1 callsite, <dyn> title (字典级) 字典级 / 未细拆
Check Maps Find Hide sub_2A1C38 0x2A1C38 2 callsite (字典级) 字典级 / 未细拆
闭环度 数量 列表
✓ 静态+动态双闭环 14+ §6 / §7 / §8 / §9 / §10 / §11 / §12 / §13 / §14 / §20 / §21(buffered reader bug) + 多进程 binder §22
△ 静态闭环 / 未触发 5 §15 checkRiskFile / §16 checkRootFromAVCLog / §17 getZhenxiInfoMPNI / §18 getZhenxiInfo2 / §19 getZhenxiInfoCBinder
字典级 多条 InfoBase 19 callsite、InfoO 5 子类、Info0 / InfoL / checkFromZygote / checkZygisk / sub_2A1C38 等
idx name sig fn (RVA)
#00 getZhenxiInfo2 (Landroid/content/Context;)Lcom/zhenxi/hunter/bean/ListItemBean; 0x29aefc
#01 getZhenxiInfo4 (Landroid/content/Context;)Lcom/zhenxi/hunter/bean/ListItemBean; 0x2a2d98
#02 getZhenxiInfo5 (Landroid/content/Context;)Lcom/zhenxi/hunter/bean/ListItemBean; 0x2a4760
#03 getZhenxiInfo6 (Landroid/content/Context;Ljava/lang/String;)Lcom/zhenxi/hunter/bean/ListItemBean; 0x2a5544
#04 getZhenxiInfo7 (Landroid/content/Context;)Lcom/zhenxi/hunter/bean/ListItemBean; 0x2a636c
#05 getZhenxiInfo8 ()V 0x2a66ac
#06 getZhenxiInfo9 ()V 0x299640
#07 getZhenxiInfoE ()V 0x2ae010
#08 getZhenxiInfoF ()Lcom/zhenxi/hunter/bean/ListItemBean; 0x2ad9a8
#09 getZhenxiInfoL ()Lcom/zhenxi/hunter/bean/ListItemBean; 0x2a8724
#10 getZhenxiInfoM ([Ljava/lang/Class;Z)[[Ljava/lang/Object; 0x2b7f9c
#11 getZhenxiInfoO (Landroid/content/Context;)Lcom/zhenxi/hunter/bean/ListItemBean; 0x2be8ec
#12 getZhenxiInfo1 ()[Ljava/lang/String; 0x2b5224
#13 getZhenxiInfo3 ()Lcom/zhenxi/hunter/bean/ListItemBean; 0x2b476c
#14 getZhenxiInfoC (I)V 0x2a77a8
#15 getZhenxiInfoZ (Ljava/lang/String;)Ljava/lang/String; 0x2b34a8
#16 getZhenxiInfoP ()Z 0x2b1af8
#17 getZhenxiInfoQ ()Z 0x2b244c
#18 getZhenxiInfoI (Ljava/lang/String;)I 0x2bdc70
#19 getZhenxiInfoT ()V 0x2b8a14
#20 getZhenxiInfoK (Ljava/lang/String;[Ljava/lang/String;)Ljava/lang/String; 0x2b93bc
#21 getZhenxiInfoXX ()Ljava/util/ArrayList; 0x2bbb98
#22 getZhenxiInfoLL (Ljava/lang/String;)Ljava/util/ArrayList; 0x2ba438
#23 popen (Ljava/lang/String;)Ljava/lang/String; 0x2ae400
#24 popen_list (Ljava/lang/String;Ljava/lang/String;)Ljava/util/ArrayList; 0x2b01d4
#25 getZhenxiLoctionCrc ()Ljava/util/HashMap; 0x2ae064
#26 getZhenxiInfoVV ()Lcom/zhenxi/hunter/bean/ListItemBean; 0x2c1850
#27 getZhenxiInfo0 (Ljava/lang/String;)Lcom/zhenxi/hunter/bean/ListItemBean; 0x2bfdd0
#28 getZhenxiInfoMark ()V 0x2c29f4
#29 getZhenxiInfoMM ()Z 0x2b1f88
#30 getZhenxiInfoAtt ()V 0x2c30a8
#31 getZhenxiInfoNHIL ()Ljava/util/ArrayList; 0x289e94
#32 getZhenxiInfoHMP (Ljava/util/ArrayList;Ljava/lang/String;)V 0x289db8
#33 getZhenxiInfoMPNI (Ljava/lang/String;)Lcom/zhenxi/hunter/bean/ListItemBean; 0x298374
#34 getZhenxiInfoLLLI ()Ljava/util/ArrayList; 0x28d44c
#35 getZhenxiInfoOFLI ()Ljava/util/ArrayList; 0x297790
#36 getZhenxiInfoSH ()Lcom/zhenxi/hunter/bean/ListItemBean; 0x28d578
#37 getZhenxiInfoEnv ()Lcom/zhenxi/hunter/bean/ListItemBean; 0x28cdb0
#38 getZhenxiInfoCBinder ()Lcom/zhenxi/hunter/bean/ListItemBean; 0x28c9bc
#39 getZhenxiInfoMapELF ()Ljava/util/ArrayList; 0x290958
#40 getZhenxiInfoInjection ()Ljava/lang/String; 0x28df24
#41 getZhenxiInfoApkEnv ()Lcom/zhenxi/hunter/bean/ListItemBean; 0x2a1dac
#42 getZhenxiInfoOFRL ()Ljava/util/ArrayList; 0x295878
#43 getZhenxiInfoBase ()Ljava/util/ArrayList; 0x29288c
#44 runInZygisk ()V 0x291690
#45 getZhenxiMapCheck ()Lcom/zhenxi/hunter/bean/ListItemBean; 0x291858
#46 check_jvm_method (Ljava/lang/reflect/Method;)Ljava/lang/String; 0x2ad350
#47 getZhenxiInfoTwin (I)V 0x2a6e70
#48 getZhenxiInfoTwinStart ()Z 0x2b28dc
#49 DetectHardwareBreakpoints ()V 0x2ad0b4
#50 checkRiskFile ()Lcom/zhenxi/hunter/bean/ListItemBean; 0x2ac514
#51 checkFromZygote ()Ljava/util/ArrayList; 0x2abcd0
#52 initHunterBase (Landroid/content/Context;)V 0x2ab0a4
#53 CheckVpn ()Lcom/zhenxi/hunter/bean/ListItemBean; 0x2aaf44
#54 checkZygisk ()Lcom/zhenxi/hunter/bean/ListItemBean; 0x2a93f8
#55 getCPUFingerprinting ()Ljava/lang/String; 0x2a92a0
#56 checkRootFromAVCLog ()Lcom/zhenxi/hunter/bean/ListItemBean; 0x2aae54
#57 getLibBaseInfo ()Ljava/lang/String; 0x2a9764
#58 MD5 (Ljava/lang/String;)Ljava/lang/String; 0x2b4fbc
#59 SideChanne ()Ljava/lang/String; 0x2b3bd4
返回类型 数量 含义 / 用途
Lcom/zhenxi/hunter/bean/ListItemBean; 17 单条风险项(本文 §6-§19 主要 detection 都在这里)
Ljava/util/ArrayList; 10 风险项 / 信息列表(InfoXX / LL / NHIL / LLLI / OFLI / MapELF / OFRL / Base / checkFromZygote 等;每项可能含多条 bean)
Ljava/lang/String; 8 字符串(prop wrapper InfoZ / 文件 marker InfoK / 注入 InfoInjection / 指纹 getCPUFingerprinting / getLibBaseInfo / 哈希 MD5 / SideChanne / popen)
Ljava/util/HashMap; 1 getZhenxiLoctionCrc 模块 CRC 汇总
V (void) 9 初始化 / 内部状态修改类(InfoMark / Att / T / Twin / C / 8 / 9 / E / runInZygisk / HMP 等)
Z (boolean) 4 状态查询类(InfoP / Q / MM / TwinStart)
I (int) 1 getZhenxiInfoI(String) 整数查询
[Ljava/lang/String; 1 getZhenxiInfo1 字符串数组(uname 等)
[[Ljava/lang/Object; 1 getZhenxiInfoM 反射工具
(Landroid/content/Context;)V 1 initHunterBase

[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

上传的附件:
收藏
免费 2
打赏
分享
最新回复 (3)
雪    币: 2209
活跃值: (1253)
能力值: ( LV6,RANK:95 )
在线值:
发帖
回帖
粉丝
2
本文在r0ysue老师指导下完成
4小时前
0
雪    币: 5007
活跃值: (2429)
能力值: (RANK:140 )
在线值:
发帖
回帖
粉丝
3
我前两周也分析了一下这个版本的hunter,但是,远远不及你这个详细。强
2小时前
1
雪    币: 2209
活跃值: (1253)
能力值: ( LV6,RANK:95 )
在线值:
发帖
回帖
粉丝
4
感谢版主的认可,
2小时前
0
游客
登录 | 注册 方可回帖
返回