reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
166
vendor-new/rev-skills/.claude/skills/re-frida/SKILL.md
Normal file
166
vendor-new/rev-skills/.claude/skills/re-frida/SKILL.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
name: re-frida
|
||||
description: >
|
||||
Frida 动态插桩(桌面+移动统一)。
|
||||
触发词:frida、hook、插桩、绕过、spawn
|
||||
capabilities: [frida-instrumentation]
|
||||
---
|
||||
|
||||
# Frida 动态插桩
|
||||
|
||||
## 何时使用 / 何时不用
|
||||
|
||||
- 用:需要 hook 函数(拦截 / 改参 / 改返回值)、枚举模块与类、绕过证书校验 / root 检测、观察运行时调用链
|
||||
- 用:移动端(Android / iOS)与桌面端(Linux / macOS / Windows)统一的插桩需求
|
||||
- 不用:只需静态分析(Android 走 [[re-apk]],格式走 [[re-format-elf]] / [[re-format-macho]] 系列)
|
||||
- 不用:需要系统调用级跟踪(走 [[re-tracing]])
|
||||
- 不用:需要完整调试器体验(断点 / 单步 / 内存,走 [[re-gdb]] / [[re-lldb]] / [[re-x64dbg]])
|
||||
|
||||
## 工具准备
|
||||
|
||||
动态分析按 [[re-analyze/platform-tips]] 最高原则:移动端在受控设备 / 模拟器快照内执行,桌面端插桩前确认沙箱环境。所有工具先验证再使用。
|
||||
|
||||
### frida-tools —— 主机侧命令行工具
|
||||
|
||||
- 跨平台(Linux / macOS / Windows): `pip install frida-tools`(Python 3.8+;建议 venv: `python3 -m venv venv && venv/bin/pip install frida-tools`)
|
||||
- 验证: `frida --version`(输出 frida 版本号);`frida-ps -U` / `frida-trace -h` 可用
|
||||
|
||||
### frida-server —— 移动端插桩代理
|
||||
|
||||
- 下载:GitHub release `https://github.com/frida/frida/releases`,选 `frida-server-<版本>-android-<架构>`(arm64 选 `-arm64`,32 位选 `-arm`,模拟器 x86_64 选 `-x86_64`)
|
||||
- **客户端与 frida-server 至少须同 major**(对照 `frida --version`);为避免功能/API 差异,推荐使用完全相同且最新的版本。架构与设备匹配,否则连接报协议错误(见坑 1)
|
||||
- Android 推送与启动:
|
||||
```sh
|
||||
adb push frida-server-xxx /data/local/tmp/frida-server
|
||||
adb shell "chmod 755 /data/local/tmp/frida-server"
|
||||
adb shell "su -c /data/local/tmp/frida-server" & # 或 adb root 后直接运行
|
||||
```
|
||||
- iOS 越狱设备:Sileo / Cydia 添加源 `https://build.frida.re` 安装 frida deb,或 `ssh root@<设备IP>` 后安装
|
||||
- 验证: 主机 `frida-ps -U` 能列出设备进程(`-U` = USB 设备,`-R` = 远程 ip:port)
|
||||
|
||||
### objection —— 免写 JS 的快速插桩
|
||||
|
||||
- 要求 Python >=3.10(与 frida 自身 binding 的 `>=3.7` 下限不同,需单独满足): `python -m pip install -U objection`,建议另建 3.10+ 的 venv 或 pipx 隔离
|
||||
- 验证: `objection --version`
|
||||
- 用法: `objection -n <应用或 Bundle ID> start`,内置 `android hooking` / `ios hooking` 子命令(如 `android hooking list activities`、`ios sslpinning disable`)
|
||||
- 1.12.x 起 `-g` / `explore` 已弃用(在 CLI 中隐藏,仅出弃用告警,旧写法仍可跑);新写作用 `-n` + `start`
|
||||
|
||||
## 操作步骤
|
||||
|
||||
按顺序执行,每步记下结果。
|
||||
|
||||
1. **spawn vs attach 选择**:
|
||||
```sh
|
||||
frida -U -f com.target.app # spawn:从零启动应用,能抓启动早期逻辑(解密 / 初始化)
|
||||
frida -U com.target.app # attach:附加到已运行进程(不重启)
|
||||
frida-trace -U -f com.target.app -i "Java!*" # 启动即跟踪 Java 方法调用
|
||||
```
|
||||
规则:抓启动逻辑 / 绕过早期检测用 spawn;只是观察现状用 attach。attach 晚于启动,可能错过已执行完的早期逻辑(见坑 2)。
|
||||
|
||||
2. **JS hook 编写(拦截 / 改参 / 返回值)**:
|
||||
```js
|
||||
// hook.js —— 拦截、改参数、改返回值(Android Java 层)
|
||||
Java.perform(function () {
|
||||
var cls = Java.use("com.example.Target");
|
||||
cls.doLogin.implementation = function (user, pass) {
|
||||
console.log("doLogin(" + user + ", " + pass + ")");
|
||||
return this.doLogin("hacked", "pass123"); // 改参
|
||||
};
|
||||
cls.isLicensed.implementation = function () {
|
||||
return true; // 改返回值
|
||||
};
|
||||
});
|
||||
```
|
||||
```sh
|
||||
frida -U -f com.target.app -l hook.js
|
||||
```
|
||||
原生函数用 `Interceptor.attach(Process.getModuleByName("libfoo.so").findExportByName("func"), { onEnter: ..., onLeave: ... })` 拦截(onEnter 改参数、onLeave 用 `retval.replace()` 改返回值)。桌面端同样本:`frida -p <pid> -l hook.js`。
|
||||
|
||||
3. **枚举与调用(enumerateModules / Java.perform)**:
|
||||
```js
|
||||
Java.perform(function () {
|
||||
Java.enumerateLoadedClasses({
|
||||
onMatch: function (c) { if (c.indexOf("target") >= 0) console.log(c); },
|
||||
onComplete: function () {}
|
||||
});
|
||||
});
|
||||
```
|
||||
```js
|
||||
// 模块与导出枚举(原生层)
|
||||
Process.enumerateModules().forEach(function (m) { console.log(m.name + " " + m.base); });
|
||||
Process.getModuleByName("libfoo.so").enumerateExports().forEach(function (e) { console.log(e.name); });
|
||||
```
|
||||
运行: `frida -U -f com.target.app -l enum.js`。定位到目标后直接主动调用:`Java.use("com.x").method(...)` / 原生导出函数。
|
||||
|
||||
4. **绕过证书校验 / 检测(常见模板)**:
|
||||
```js
|
||||
// Android SSL 绕过模板(配合抓包工具)
|
||||
Java.perform(function () {
|
||||
var X509TrustManager = Java.use("javax.net.ssl.X509TrustManager");
|
||||
var TrustAll = Java.registerClass({
|
||||
name: "com.bypass.TrustAll",
|
||||
implements: [X509TrustManager],
|
||||
methods: { checkClientTrusted: function () {}, checkServerTrusted: function () {},
|
||||
getAcceptedIssuers: function () { return []; } }
|
||||
});
|
||||
var SSLContext = Java.use("javax.net.ssl.SSLContext");
|
||||
SSLContext.init.implementation = function (km, tm, sr) {
|
||||
this.init(km, [TrustAll.$new()], sr);
|
||||
};
|
||||
});
|
||||
```
|
||||
```js
|
||||
// iOS 证书绕过:hook SecTrustEvaluateWithError 返回值
|
||||
var SecTrust = Module.findGlobalExportByName("SecTrustEvaluateWithError"); // Frida 17+:全局符号静态查找
|
||||
Interceptor.attach(SecTrust, { onLeave: function (r) { this.context.x0 = 0; } }); // arm64 返回寄存器 x0;x86_64 用 rdi 场景先验证
|
||||
```
|
||||
现成命令:`objection -n com.target.app start` → `android sslpinning disable` / `ios sslpinning disable`。
|
||||
|
||||
5. **反检测对抗(隐藏 frida-server、改名)**:
|
||||
- 常见检测点:frida-server 默认端口 27042、`/data/local/tmp/frida-server` 路径、`gum-js-loop` / `gmain` 线程名、`/proc/self/maps` 中的 frida 特征、`frida` 字符串
|
||||
- 对策:
|
||||
```sh
|
||||
# 1) 二进制改名后启动(避开路径检测)
|
||||
cp frida-server-xxx /data/local/tmp/fridad
|
||||
adb shell "su -c 'chmod 755 /data/local/tmp/fridad && /data/local/tmp/fridad' &"
|
||||
# 2) 换端口 + adb 端口转发,主机用 -H 连接
|
||||
adb shell "su -c '/data/local/tmp/fridad -l 0.0.0.0:27142' &"
|
||||
adb forward tcp:27142 tcp:27142
|
||||
frida -H 127.0.0.1:27142 -f com.target.app
|
||||
```
|
||||
- root 检测对抗:先 hook 检测函数改返回值再插桩目标:`Java.use("com.target.rootcheck").isRooted.implementation = function () { return false; };`
|
||||
- 仍被检测 → frida-gadget 注入 App 进程(gadget listen(interactive)模式),配合 [[re-apk]] 的 smali 补丁加载 libgadget.so
|
||||
|
||||
7. **脚本模板与对抗方法论**:常用脚本骨架见 [[frida-scripts]](TLS keylog / DEX/SO dump / JNI 注册还原 / 加密拦截 / 检测绕过表);崩溃迭代法与检测面对照表见 [[re-analyze/anti-dynamic-workflow]]——先基线跑看裸崩,再定点 hook,不预置绕过全家桶。
|
||||
|
||||
## 跨域联合
|
||||
- [[re-address-space]]:运行时基址换算(`Process.getModuleByName(...).base` 与链接地址对齐)
|
||||
|
||||
- [[re-mobile]]:工作流第 3 步动态插桩固定调用本技能
|
||||
- [[re-apk]] / [[re-ios]]:静态分析后需要运行时行为(解密 / hook / 绕过 / 脱壳执行)时调用本技能
|
||||
- 系统调用级跟踪互补 → [[re-tracing]];运行时内存提取 → [[re-memdump]]
|
||||
- 本技能被 [[re-analyze]] 的 triage「移动 App 分析」路径调用(re-mobile → re-frida)
|
||||
- 脚本生成:目标特征 → 模板选择 → 改写验证 → [[re-frida-script-author]](模板素材 [[frida-scripts]])
|
||||
|
||||
## 常见坑与陷阱
|
||||
|
||||
- **版本不匹配 → 协议错误**:现象——`frida-ps -U` 报 `unable to communicate with the frida server` / 协议错误;原因——客户端与 frida-server major 不一致,或所用的功能在目标版本上不存在;对策——先查 major 是否匹配(`frida --version` 对照 frida-server 版本,见工具准备),再确认该功能是否被对应版本支持;升级侧用 `pip install -U frida-tools`
|
||||
- **Frida 17 移除静态 Module 查找/枚举 API**:现象——脚本报 `TypeError: Module.findExportByName is not a function`(或 getExportByName / enumerateExports / findBaseAddress 同类);原因——17.0.0 起静态查找与枚举 API 全部移除,静态只余 `Module.load` / `Module.findGlobalExportByName` / `Module.getGlobalExportByName`,其余改为**模块实例方法**;对策——先取实例再查:`Process.getModuleByName("libfoo.so").findExportByName("func")`(find 返 null,get 抛异常)、基址用 `.base`、枚举用 `.enumerateExports()`;全局符号(原 `null` 模块参数)改用 `Module.findGlobalExportByName(...)`;注意 `Process.getModuleByName` 在模块未加载时**抛异常**(旧 `Module.findExportByName` 返 null)——不确定时先用 `Process.findModuleByName` 判空再查;动笔前 `frida --version` 确认版本再选写法(迁移清单见 [[re-frida-script-author]] 版本相关组)
|
||||
- **spawn 时机晚 → 错过早期逻辑**:现象——attach 后 hook 不触发或早期解密已完成;原因——应用启动即解密 / 校验,attach 时已过;对策——用 `-f` spawn 模式起步即插桩;仍错过则 hook `dlopen` / `ClassLoader.loadClass` 这类更早的执行点
|
||||
- **目标检测 frida(端口 / 特征)**:现象——spawn 后应用闪退 / 卡死 / 行为异常;原因——应用扫描 27042 端口、frida-server 路径、`frida` 线程名或 maps 特征;对策——步骤 5 改名 + 换端口;仍检测用 frida-gadget 注入(隐藏于进程内)
|
||||
- **root 检测拦插桩**:现象——frida 可连接但 hook 不生效或直接退出;原因——应用先做 root / 越狱检测,检测到环境直接退出;对策——先 hook 检测函数返回值(步骤 5 模板),过了检测再 hook 目标函数
|
||||
- **JS 脚本静默失败**:现象——脚本加载无报错但 hook 无输出;原因——类名写错、`Java.perform` 外调用 Java API、模块名大小写不符;对策——用步骤 3 的枚举先核对名称,脚本内 `console.log` 打桩定位
|
||||
- **hook 导出 API 被壳绕过(直 syscall / API 名哈希)**:现象——成功 hook 了 `IsDebuggerPresent`/`NtQueryInformationProcess` 等导出,反调试照样触发、进程退出;原因——加壳/加固目标不走导入表:自实现 `GetProcAddress`、API 名存哈希,或直接 syscall(内联 `Nt*` 直调),hook 点根本没经过;对策——hook 更深一层(ntdll 的 syscall 包装点)、跟踪 `GetProcAddress`/哈希解析处反推真实调用点,反调试与反 VM(`RegOpenKeyExA`/`GetSystemFirmwareTable`)API 一并 hook(Arkana 项目实战模板),先用 `Process.enumerateModules()`/`enumerateExports` 确认实际调用目标再插桩
|
||||
- **检测点升级(memfd/JIT 池/内存字符串/管道名)**:现象——改名、换端口启动 frida-server 后仍被检测;原因——新版检测不止端口与路径:扫描 `/proc/self/fd` 的 memfd 名称、JIT 缓存池(pool-frida)、`frida_agent.so`/`frida_rpc` 等内存字符串与导出符号、管道/linjector 名称,甚至非标准端口也会被扫(Promon 式扫描);对策——用 undetected-frida 补丁集(字符串/符号/线程/协议/memfd/JIT 池全量混淆,Magisk/KSU 模块形态),或 frida-gadget + 自编译隐藏版,缩小指纹面
|
||||
- **强加固(Pairipcore 类)整体对抗 Frida**:现象——常见 hook 脚本(证书绕过/反检测/解密)全部失效,spawn 即闪退或行为异常,社区报告 frida-interception 类脚本无法绕过;原因——商业加固做 C++/Java 双层完整性校验 + 伪 VM 指令 + 自定义 dlopen/dlsym/syscall 动态导入混淆 + prctl/clone/waitpid/ptrace 反调试 + `/proc/self/maps`+`/proc/self/status` 进程监控 + 非标准端口扫描,整体防线而非单点;对策——先处理完整性校验与进程监控(hook 校验函数返回、patch 监控点)再过反调试,hook 落到自定义导入解析处而非导出 API,必要时结合 [[re-apk]] 静态改 smali + Native 层插桩配合
|
||||
- **hook 出口函数多进程/多实现排查**:现象——hook `SocketOutputStream` 无结果,抓包却看到流量;原因——应用多进程(发流量的逻辑在子进程)或使用 Netty 的 `SocketChannelImpl`(不走 OutputStream 路径);对策——排查顺序:hook 最外层出口 → 无果 `ps -e` 查子进程分别 hook → 再试 `SocketChannelImpl` 等替代实现;hook 成功后打印堆栈(Netty 的 `MessageToByteEncoder` 链)定位组装/加密代码;从"编解码器链"逐层向上追明文对象与加密产物
|
||||
- **对称加密密钥传输追踪**:现象——抓到 RC4/AES 密文包,但密钥每次会话都变、静态搜不到;原因——对称加密密钥动态生成且在线传输(明文/加密后携带/协商);对策——hook 密钥传入处(加密函数参数 `bArr` 类)拿当前密钥,再向上追踪密钥来源与传输路径,配合抓包对照(首包固定头如 `89 04 01 01` 可作协议锚点)
|
||||
- **多层 TLS 校验栈**:现象——单点 hook 后部分请求仍 SSL 错误;原因——App 同时用 OkHttp/原生 HttpsURLConnection/Conscrypt 多套栈;对策——分层覆盖(OkHttp CertificatePinner + TrustManagerImpl.verifyChain + HostnameVerifier 同时 hook),全栈覆盖才算绕过完成
|
||||
- **ProGuard 混淆后定位目标类**:现象——类名被改成短名;原因——混淆;对策——jadx 里 Find Usages 反查谁实例化关键 Builder,从实例化点反推原类
|
||||
(来源:reverse-skill field-journal,MIT)
|
||||
- **测试环境选错 → 动态假阴性 / 交付假阳性**:现象——同一环境里 hook 不触发、内存 patch 无效果,或模拟器一切正常但换真机就出问题;原因——x86 翻译层模拟器上 live Frida 不可用(`Process.arch==x64` 且找不到目标原生 so),运行时从未执行你的字节;部分目标在模拟器上校验宽松,「模拟器能跑」可能是降级假象;对策——按阶段选环境:静态 patch 与渲染回归可用任意模拟器(含 x86 翻译层),动态插桩必须原生 ARM,交付验证必须与目标匹配的真机;翻译层假象可证伪:把已知必需函数首指令 patch 成裸 `ret`,UI 照常渲染即运行时没执行你的字节,立即停掉该环境的 live 工作(盘上静态 patch 下次启动重新翻译仍可用);真机采集前 `svc power stayon true` 并唤醒,防睡眠屏造成「空白」假截图
|
||||
(来源:reverse-skills(inliver233),MIT)
|
||||
- **redroid 容器搭建与使用坑**:现象——guest 静默挂死 / servicemanager 空转、vold 崩溃、adb 反复掉线、容器无法重启;原因——binder 挂载方式错误(必须 bind-mount `/dev/binder`,不能用 `--device` 或 binderfs)、新内核无 ashmem 时未加 `androidboot.use_memfd=1`、设了 `ro.secure=1 ro.debuggable=0` 杀掉 root adb、停掉的容器 binder 已消失无法 docker restart、adbd 高负载下崩溃;对策——binder 用 bind-mount、新内核强制 use_memfd、保留 root adb(防篡改用保持签名有效而非藏 root)、重建容器后重注入 adb 公钥(`echo $PUBKEY > /data/misc/adb/adb_keys`)、大文件用 docker cp 而非 adb push、动态分析用 spawn 模式(`frida -U -f`,attach 在防篡改下常 0 命中);定位认知——它是数据采集工具而非迭代环境,采完动态数据回稳定环境做静态 patch 回归
|
||||
(来源:reverse-skills(inliver233),MIT)
|
||||
- **AOT 目标上 hook 静默不触发 / 找不到调用者**:现象——hook 无输出、目标函数静态 callers=0、门字段未知无法下针;原因——目标 so 晚于引擎加载(attach 时未加载)、虚方法经 cid 分派墙(不经 BL 调用,静态无捷径)、guard 字段位置未知;对策——等目标 so 加载后再 hook:200ms 轮询 `findModuleByName` 的 waitForLibapp 包装器,起步优先 spawn 模式;破 cid 墙:目标函数 onEnter 记 `lr`(returnAddress)+ `Thread.backtrace(Backtracer.ACCURATE)` 前 20 帧,一次回溯定位分派点;guard 定位三件套:Stalker.follow transform 里对 `ldr` 类指令放 putCallout(首个命中 PC = guard)、MemoryAccessMonitor.enable 等重建触发(arm64 支持不一需回退)、候选 hook 扫射;**运行时翻转验证前置**:live `writePointer` 翻转候选门字段 → 触发重建 → 组件消失即确认门字段,确认后再投静态 patch,省掉反复重打包;frida 17 起 Java bridge 不再内置于底层 GumJS runtime,但 `frida` CLI/REPL 与 `frida-trace` 仍自带三个 bridge,普通 `frida -l` 脚本无需改动;仅自建 agent / 走 bindings 注入脚本时需显式安装 `frida-java-bridge`(必要时用 frida-compile 打包)
|
||||
(来源:reverse-skills(inliver233),MIT)
|
||||
@@ -0,0 +1,224 @@
|
||||
# Frida 脚本模板
|
||||
|
||||
按场景选用模板,替换占位符(`<pkg>` 包名、`<cls>` 类名、`<method>` 方法名)。统一骨架:**先保存 original 引用再带 `this` 调用原方法**;输出双格式(可打印 ASCII + hex);`try/catch` 包裹;全部在沙箱内执行。通用对抗方法论见 [[re-analyze/anti-dynamic-workflow]]。
|
||||
|
||||
## 通用骨架
|
||||
|
||||
```javascript
|
||||
Java.perform(function () {
|
||||
try {
|
||||
var Cls = Java.use('<cls>');
|
||||
var orig = Cls.<method>.overloads[0].implementation;
|
||||
Cls.<method>.overloads[0].implementation = function () {
|
||||
var ret = orig.call(this, ...arguments);
|
||||
console.log(JSON.stringify({ method: '<cls>.<method>', ret: String(ret) }));
|
||||
return ret;
|
||||
};
|
||||
} catch (e) { console.log(JSON.stringify({ error: String(e) })); }
|
||||
});
|
||||
```
|
||||
|
||||
## TLS 密钥日志(SSLKEYLOGFILE 路线)
|
||||
|
||||
hook BoringSSL 的 `SSL_CTX_new`,onLeave 对每个新 SSL_CTX 注入 keylog 回调——输出可直接存为 SSLKEYLOGFILE 喂 Wireshark 解密 HTTPS 明文:
|
||||
|
||||
```javascript
|
||||
var ssl = Process.findModuleByName('libssl.so');
|
||||
if (!ssl) { console.log(JSON.stringify({ error: 'libssl not found' })); }
|
||||
else {
|
||||
var keyLogCallback = new NativeCallback(function (sslPtr, line) {
|
||||
console.log(new NativePointer(line).readCString());
|
||||
}, 'void', ['pointer', 'pointer']);
|
||||
var setKeylog = new NativeFunction(
|
||||
ssl.getExportByName('SSL_CTX_set_keylog_callback'), 'void', ['pointer', 'pointer']);
|
||||
Interceptor.attach(ssl.getExportByName('SSL_CTX_new'), {
|
||||
onLeave: function (retval) { setKeylog(new NativePointer(retval), keyLogCallback); }
|
||||
});
|
||||
}
|
||||
// 局限:仅 Android 系统自带 BoringSSL;App 内置/静态链接的 TLS 栈不覆盖
|
||||
```
|
||||
|
||||
## SSL 固定绕过(TrustManager trust-all + CertificatePinner)
|
||||
|
||||
`SSLContext.init` 时替换为 trust-all 的 `X509TrustManager`(信任任意证书链);OkHttp 的 `CertificatePinner` 是独立校验层,另行置空:
|
||||
|
||||
```javascript
|
||||
Java.perform(function () {
|
||||
try {
|
||||
var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
|
||||
var TrustAll = Java.registerClass({
|
||||
name: 'com.bypass.TrustAll',
|
||||
implements: [X509TrustManager],
|
||||
methods: { checkClientTrusted: function () {}, checkServerTrusted: function () {},
|
||||
getAcceptedIssuers: function () { return []; } }
|
||||
});
|
||||
var SSLContext = Java.use('javax.net.ssl.SSLContext');
|
||||
SSLContext.init.implementation = function (km, tm, sr) {
|
||||
console.log(JSON.stringify({ event: 'ssl_trust_all', keystore: String(km), secureRandom: String(sr) }));
|
||||
this.init(km, [TrustAll.$new()], sr);
|
||||
};
|
||||
var Pinner = Java.use('okhttp3.CertificatePinner');
|
||||
Pinner.check.overload('java.lang.String', 'java.util.List').implementation = function (host, certs) {
|
||||
console.log(JSON.stringify({ event: 'cert_pinner_bypass', host: host }));
|
||||
};
|
||||
} catch (e) { console.log(JSON.stringify({ error: String(e) })); }
|
||||
});
|
||||
// 局限:仅覆盖 Java 层(OkHttp / HttpsURLConnection 等)校验;原生层直调 BoringSSL / libssl 的固定校验不走此路径
|
||||
```
|
||||
|
||||
## 内存 DEX dump(类加载点抓取)
|
||||
|
||||
hook `art::ClassLinker::DefineClass`(libart.so mangled 符号特征匹配),类加载点从参数取 DexFile 读 `begin_`/`size_`,校验 dex magic 后整段落盘:
|
||||
|
||||
```javascript
|
||||
var libart = Process.findModuleByName('libart.so');
|
||||
if (libart) {
|
||||
var sym = libart.enumerateSymbolsSync().find(s =>
|
||||
s.name.includes('ClassLinker') && s.name.includes('DefineClass') && s.name.includes('DexFile'));
|
||||
if (sym) Interceptor.attach(sym.address, {
|
||||
onEnter: function (args) {
|
||||
// 参数位次与字段偏移为版本相关经验值(Android 9:DexFile 引用在第 6 参;begin_=+8、size_=+16)
|
||||
var dexFile = args[5];
|
||||
if (dexFile.isNull()) return;
|
||||
var begin = dexFile.add(Process.pointerSize).readPointer();
|
||||
var size = dexFile.add(Process.pointerSize * 2).readUInt();
|
||||
if (size < 4 || begin.isNull() || begin.readCString(3) !== 'dex') return;
|
||||
// 以 base 去重后 readByteArray(size) 写盘到 files 目录
|
||||
console.log(JSON.stringify({ event: 'dex', base: begin.toString(), size: size }));
|
||||
}
|
||||
});
|
||||
// dlopen / android_dlopen_ext onLeave 再挂一次,兜底 libart 晚加载设备
|
||||
}
|
||||
```
|
||||
|
||||
## 内存 SO dump(运行时解密/加固)
|
||||
|
||||
```javascript
|
||||
function dumpSo(name) {
|
||||
var m = Process.getModuleByName(name);
|
||||
if (!m) return;
|
||||
Memory.protect(m.base, m.size, 'rwx');
|
||||
var buf = m.base.readByteArray(m.size);
|
||||
var out = new File('/data/local/tmp/' + name + '_' + m.base + '.so', 'wb');
|
||||
out.write(buf); out.close();
|
||||
console.log(JSON.stringify({ event: 'so_dump', name: name, base: m.base.toString(), size: m.size }));
|
||||
}
|
||||
```
|
||||
|
||||
## JNI 动态注册还原(RegisterNatives + 汇聚点双 hook)
|
||||
|
||||
libart.so 符号特征匹配(统一排除 CheckJNI);`JNI::RegisterNatives` 建 fnPtr→方法名映射,`ArtMethod::RegisterNative` 是注册汇聚点,直接拿 native 地址与 SO 内偏移:
|
||||
|
||||
```javascript
|
||||
var libart = Process.findModuleByName('libart.so');
|
||||
var map = {};
|
||||
var parseTable = function (methods, count) {
|
||||
var rows = [];
|
||||
for (var i = 0; i < count.toInt32(); i++) {
|
||||
var p = methods.add(i * Process.pointerSize * 3); // 每项 3 指针:name/sig/fnPtr
|
||||
rows.push({ name: p.readPointer().readCString(), sig: p.add(Process.pointerSize).readPointer().readCString(),
|
||||
fn: p.add(Process.pointerSize * 2).readPointer().toString() });
|
||||
}
|
||||
return rows;
|
||||
};
|
||||
if (libart) libart.enumerateSymbolsSync().forEach(function (s) {
|
||||
if (s.name.includes('JNI') && s.name.includes('RegisterNatives') && !s.name.includes('CheckJNI')) {
|
||||
Interceptor.attach(s.address, {
|
||||
onEnter: function (args) {
|
||||
var cls = Java.vm.tryGetEnv().getClassName(args[1]);
|
||||
parseTable(args[2], args[3]).forEach(function (r) { map[r.fn] = cls + '.' + r.name + r.sig; });
|
||||
}
|
||||
});
|
||||
}
|
||||
if (s.name.includes('ArtMethod') && s.name.includes('RegisterNative') && !s.name.includes('Callback')) {
|
||||
Interceptor.attach(s.address, {
|
||||
onEnter: function (args) {
|
||||
var fnPtr = args[1], mod = Process.findModuleByAddress(fnPtr);
|
||||
if (mod) console.log(JSON.stringify({ event: 'registerNative', method: map[fnPtr.toString()] || '(unresolved)',
|
||||
so: mod.name, off: '0x' + fnPtr.sub(mod.base).toString(16) }));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Java 加密算法拦截(overload 全覆盖)
|
||||
|
||||
```javascript
|
||||
Java.perform(function () {
|
||||
var hex = function (b) { var s = ''; for (var i = 0; i < b.length; i++) s += ('0' + (b[i] & 0xff).toString(16)).slice(-2); return s; };
|
||||
var SKS = Java.use('javax.crypto.spec.SecretKeySpec');
|
||||
SKS.$init.overload('[B', 'java.lang.String').implementation = function (key, algo) {
|
||||
console.log(JSON.stringify({ event: 'secret_key', algo: algo, keyHex: hex(key) }));
|
||||
this.$init(key, algo);
|
||||
};
|
||||
var Cipher = Java.use('javax.crypto.Cipher');
|
||||
['(int,java.security.Key)', '(int,java.security.Certificate)',
|
||||
'(int,java.security.Key,java.security.spec.AlgorithmParameterSpec)',
|
||||
'(int,java.security.Key,java.security.SecureRandom)',
|
||||
'(int,java.security.Certificate,java.security.SecureRandom)',
|
||||
'(int,java.security.Key,java.security.AlgorithmParameters)',
|
||||
'(int,java.security.Key,java.security.AlgorithmParameters,java.security.SecureRandom)',
|
||||
'(int,java.security.Key,java.security.spec.AlgorithmParameterSpec,java.security.SecureRandom)'
|
||||
].forEach(function (sig) {
|
||||
try {
|
||||
Cipher.init.overload.apply(Cipher, sig.split(',').map(function (s) { return s.trim(); })).implementation = function (mode, key) {
|
||||
var enc = key.getEncoded ? key.getEncoded() : null;
|
||||
console.log(JSON.stringify({ event: 'cipher_init', mode: mode, keyHex: enc ? hex(enc) : null }));
|
||||
this.init.apply(this, arguments);
|
||||
};
|
||||
} catch (e) {}
|
||||
});
|
||||
// doFinal/update 输入输出双向记录;IvParameterSpec.$init 打 IV;DESKeySpec/RSAPublicKeySpec/KeyPairGenerator 同模式
|
||||
});
|
||||
```
|
||||
|
||||
## 双向 TLS 客户端证书导出(keystore dump)
|
||||
|
||||
hook `KeyStore$PrivateKeyEntry.getPrivateKey` / `getCertificateChain`(双向认证读客户端证书必经点),经 BouncyCastle PKCS12 导出 p12:
|
||||
|
||||
```javascript
|
||||
Java.perform(function () {
|
||||
var PKE = Java.use('java.security.KeyStore$PrivateKeyEntry');
|
||||
PKE.getCertificateChain.implementation = function () {
|
||||
var chain = this.getCertificateChain();
|
||||
try {
|
||||
var ks = Java.use('java.security.KeyStore').getInstance('PKCS12', 'BC');
|
||||
ks.load(null, null);
|
||||
ks.setKeyEntry('client', this.getPrivateKey(), 'hooker'.toCharArray(), chain);
|
||||
var f = Java.use('java.io.FileOutputStream').$new('/data/local/tmp/client.p12');
|
||||
ks.store(f, 'hooker'.toCharArray()); f.close();
|
||||
console.log(JSON.stringify({ event: 'keystore_dump', path: '/data/local/tmp/client.p12' }));
|
||||
} catch (e) {}
|
||||
return chain;
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
## Root / 插桩检测绕过(检测点表)
|
||||
|
||||
| 检测点 | hook 目标 | 手法 |
|
||||
|---|---|---|
|
||||
| 包名 | `ApplicationPackageManager.getPackageInfo` | 命中 root/xposed 包名列表 → 替换为假包名 |
|
||||
| 文件 | `java.io.File.exists` | 命中 su/busybox 等路径 → false |
|
||||
| 命令 | `Runtime.exec` 全部 overload / `ProcessBuilder.start` | "su" → 不存在命令;getprop/mount → "grep" |
|
||||
| 属性 | `SystemProperties.get` | ro.debuggable="0"、ro.secure="1"、ro.kernel.qemu="0" |
|
||||
| test-keys | `String.contains` / `BufferedReader.readLine` | "test-keys" → false / 改写 "release-keys" |
|
||||
| native 文件 | libc `fopen` | basename 命中 → 路径改写 /notexists |
|
||||
| native 命令 | libc `system` | 命令命中 → 改写假命令 |
|
||||
| 裸系统调用 | svc 指令特征码扫描(arm64: `01 00 00 D4`,SYS_OPENAT=56,无 open 系统调用) | 捕获绕过 libc 的文件访问(arm64 openat 路径参数在 x1,x0 是 dirfd) |
|
||||
|
||||
绕过策略不预置全家桶:基线跑原样目标 → 崩溃特征反推保护点(对照表见 [[re-analyze/anti-dynamic-workflow]])→ 定点 hook;迭代 3-5 层是常态。
|
||||
|
||||
## 实现教训(内化)
|
||||
|
||||
- 返回值只能在 **onLeave 用 `retval.replace()`** 改;保存 original 后必须带原 `this` 调用
|
||||
- `Java.use` 同一类有缓存,多个 `.implementation` 赋值**静默覆盖**——相关 hook 合并进一个
|
||||
- 符号定位统一 `enumerateSymbolsSync` + mangled 名特征匹配,排除 CheckJNI / `__va_list`
|
||||
- 参数索引与字段偏移是版本相关经验值(跨版本需对照目标符号核实)
|
||||
- 客户端 hook 无法伪造**服务端** attestation 结论——服务端校验只写「无法绕过」
|
||||
|
||||
## 使用注意
|
||||
|
||||
- 全部在沙箱内执行(见 [[re-analyze/platform-tips]] 最高原则);`--pause` 覆盖早期代码检查
|
||||
- 输出 JSON 供 [[re-analyze/analysis-contract]] 数据契约消费(证据存档)
|
||||
Reference in New Issue
Block a user