本章目标是让 App 跑起来观察真实行为:用 adb/logcat 定位流程,用 Burp 或 mitmproxy 验证接口,用 Frida 在授权 Demo 上观察和修改运行时方法。
1. 动态逆向解决什么问题
静态分析回答"代码里可能有什么",动态分析回答"运行时实际发生了什么"。
| 场景 | 静态分析不足 | 动态分析价值 |
|---|---|---|
| 混淆严重 | 类名和方法名不可读 | 通过调用日志和参数观察真实行为 |
| 反射调用 | jadx 不容易看完整调用链 | Hook 反射入口或目标方法 |
| 加密签名 | 只看到算法但不知道明文 | 在加密前打印参数 |
| 网络接口 | 代码里 URL 可能动态拼接 | 抓包确认真实请求 |
| 环境检测 | 检测分散在多处 | Hook 返回值验证影响 |
| 服务端校验 | 客户端代码不能代表服务端逻辑 | 改包、重放、篡改请求验证服务端 |
动态分析必须和授权边界绑定:只抓自建 Demo 或授权测试环境,不采集真实用户隐私、真实 token 或第三方业务数据。
2. adb 与 logcat 动态观察
2.1 进程和页面定位
bash
adb shell pm list packages | grep reversedemo
adb shell dumpsys package com.example.reversedemo | sed -n '/Activity Resolver Table/,$p'
adb shell pidof com.example.reversedemo
adb shell ps -A | grep reversedemo
adb shell dumpsys activity top
理解要点:
- 包名是所有动态工具的基础输入。
- Activity 名可以用于直接启动隐藏页面或测试导出组件。
- PID 用于 Native 调试、
/proc/<pid>观察和 Frida attach。
2.2 日志分析
bash
adb logcat -c
adb logcat | grep -E "ReverseDemo|OkHttp|FATAL|Exception"
日志要关注:
| 日志类型 | 说明 |
|---|---|
| 业务日志 | 可能暴露账号、token、接口参数 |
| 崩溃栈 | 能定位类名、方法、行号或混淆后的调用点 |
| 网络日志 | OkHttp logging interceptor 可能输出请求响应 |
| 安全检测日志 | root、proxy、debug、frida 检测路径 |
正式包原则:不能输出敏感字段,不能依赖日志作为安全控制。
3. 抓包分析
3.1 代理配置
Burp 或 mitmproxy 都可以。以 mitmproxy 为例:
bash
mitmproxy -p 8080
adb shell settings put global http_proxy <电脑IP>:8080
adb shell settings get global http_proxy
清理代理:
bash
adb shell settings put global http_proxy :0
设备需要信任代理证书。Android 7 以后,App 默认不信任用户安装的 CA。自建 Demo 可在 debug 配置中允许用户证书,release 则应更严格。
3.2 Network Security Config 示例
debug 练习环境可配置:
xml
<network-security-config>
<debug-overrides>
<trust-anchors>
<certificates src="user" />
<certificates src="system" />
</trust-anchors>
</debug-overrides>
</network-security-config>
知识点:
- 这只应存在于 debug 或授权测试包。
- release 包不应为了方便抓包而信任用户 CA。
- 如果启用了证书绑定,抓包失败是正常现象,后续要记录"被防护阻断"而不是强行绕过未授权目标。
3.3 Demo:接口风险验证
Demo 请求:
http
POST /api/profile HTTP/1.1
Authorization: Bearer demo-token
X-Timestamp: 1710000000
X-Nonce: abc123
X-Sign: <client sign>
{"userId":"10002","action":"readProfile"}
测试问题:
| 测试 | 操作 | 安全预期 |
|---|---|---|
改 userId |
10002 改成 10001 |
服务端拒绝越权 |
| 重放请求 | 复制旧请求再次发送 | 服务端拒绝过期或重复 nonce |
| 删除签名 | 移除 X-Sign |
服务端拒绝 |
| 修改 body | 修改 action |
签名不匹配,被拒绝 |
| 替换 token | 使用无效 token | 401 或 403 |
结论要写服务端结果,不要只写"抓到了请求"。抓包的重点是验证服务端是否承担最终安全判断。
4. Frida Java Hook 基础
4.1 启动方式
Attach 已运行进程:
bash
frida -U -n ReverseDemo -l hook.js
Spawn 启动进程:
bash
frida -U -f com.example.reversedemo -l hook.js
常见差异:
- attach 适合 App 已经运行的场景。
- spawn 适合 Hook 启动早期逻辑。
- 如果目标方法在启动阶段就执行,attach 可能错过。
4.2 Demo:Hook 会员判断
javascript
Java.perform(function () {
const UserCenter = Java.use("com.example.reversedemo.UserCenter");
UserCenter.isVip.implementation = function (userId) {
const original = this.isVip(userId);
console.log("[isVip] userId=" + userId + ", original=" + original);
return true;
};
});
运行:
bash
frida -U -f com.example.reversedemo -l hook-vip.js
验证:
| 验证项 | 通过标准 |
|---|---|
| Hook 命中 | 控制台打印 [isVip] |
| 参数可见 | 能看到 userId |
| 行为改变 | 非会员进入会员功能 |
| 风险结论 | 客户端判断不能保护核心权益 |
4.3 Demo:打印签名前明文
javascript
Java.perform(function () {
const Signer = Java.use("com.example.reversedemo.Signer");
Signer.sign.implementation = function (path, timestamp, nonce, body) {
console.log("[sign] path=" + path);
console.log("[sign] timestamp=" + timestamp);
console.log("[sign] nonce=" + nonce);
console.log("[sign] body=" + body);
const result = this.sign(path, timestamp, nonce, body);
console.log("[sign] result=" + result);
return result;
};
});
知识点:
- Hook 加密前的入口,比反推加密后的密文更直接。
- 如果客户端持有长期密钥,动态分析可以帮助定位密钥使用点。
- 正确防护不是"把函数名混淆掉"就结束,而是降低客户端掌握核心秘密的程度。
5. Frida 常用 Hook 模式
5.1 Hook 重载方法
javascript
Java.perform(function () {
const Cls = Java.use("com.example.reversedemo.SecurityChecker");
Cls.check.overload("java.lang.String").implementation = function (name) {
console.log("check(String): " + name);
return this.check(name);
};
});
5.2 Hook 构造函数
javascript
Java.perform(function () {
const Request = Java.use("com.example.reversedemo.ApiRequest");
Request.$init.overload("java.lang.String", "java.lang.String")
.implementation = function (path, body) {
console.log("ApiRequest path=" + path + ", body=" + body);
return this.$init(path, body);
};
});
5.3 Hook 系统 API
javascript
Java.perform(function () {
const File = Java.use("java.io.File");
File.exists.implementation = function () {
const path = this.getAbsolutePath();
if (path.indexOf("/su") >= 0) {
console.log("[File.exists] bypass " + path);
return false;
}
return this.exists();
};
});
这个示例只用于自建 Demo 验证 root 检测可被运行时影响。正式设计中,不应把 root 检测作为唯一安全门槛。
6. 动态调试与 Android Studio
如果有源码,优先用 Android Studio 调试自建 Demo:
- 给登录、会员判断、签名函数、网络拦截器打断点。
- 运行 debug 包。
- 输入账号,观察调用链和变量。
- 对比 jadx 反编译结果和真实源码。
- 编译 release 包,再观察混淆后的差异。
源码调试和逆向工具结合能帮助理解:
- Kotlin 代码如何变成 DEX。
- R8 混淆对类名、方法名、控制流有什么影响。
- 哪些信息即使混淆后仍可通过字符串、日志、网络行为暴露。
7. 本章 Demo:动态分析闭环
7.1 实验任务
- 用
adb logcat捕获 Demo 登录和会员页日志。 - 用 Burp/mitmproxy 捕获
/api/profile请求。 - 修改请求体和 Header,验证服务端鉴权、签名、重放保护。
- 用 Frida Hook
UserCenter.isVip(),验证本地判断可变。 - 用 Frida Hook
Signer.sign(),打印签名前明文和签名结果。 - 输出动态分析报告。
7.2 验证矩阵
| 验证项 | 工具 | 预期结果 | 证据 |
|---|---|---|---|
| 日志捕获 | adb logcat |
记录关键流程,无敏感日志更好 | 01-logcat.txt |
| 请求捕获 | Burp/mitmproxy | 看到 Demo 请求和响应 | 02-http-flow.txt |
| 参数篡改 | Repeater | 服务端拒绝越权 | 03-tamper-result.md |
| 重放请求 | Repeater | 服务端拒绝过期或重复 nonce | 04-replay-result.md |
| Hook 会员 | Frida | App 行为被影响 | 05-hook-vip.log |
| Hook 签名 | Frida | 打印签名前参数 | 06-hook-sign.log |
8. 动态分析结论写法
好的结论必须包含"证据、影响、边界、修复":
markdown
## 风险:会员权益依赖客户端判断
- 证据:Frida Hook `UserCenter.isVip()` 返回 true 后,非会员账号可进入 Demo 会员页。
- 影响:攻击者不需要重打包,只要能注入运行时 Hook,就可能影响客户端展示逻辑。
- 边界:本实验只验证自建 Demo;真实业务还要看服务端是否二次校验。
- 修复:会员权益由服务端按订单和账号状态授权,客户端只展示服务端返回结果;关键接口必须校验 token、签名、时间戳和业务归属。
9. 常见问题
| 问题 | 原因 | 处理 |
|---|---|---|
| Frida 找不到类 | 类未加载、混淆、进程不对 | Hook ClassLoader 或先触发页面 |
| Hook 没生效 | 重载选择错误或 attach 太晚 | 使用 overload,尝试 spawn |
| App 闪退 | Hook 返回类型错误或检测 Frida | 先只打印不修改,逐步缩小 |
| 抓不到 HTTPS | 证书不信任或证书绑定 | 记录防护现象,在授权 Demo 中测试配置 |
| 请求篡改无变化 | 服务端没检查该字段或字段不生效 | 找关键业务请求重新验证 |
10. 本章交付物
text
case-reversedemo/
03-dynamic/
01-logcat.txt
02-http-flow.txt
03-tamper-result.md
04-replay-result.md
05-hook-vip.js
06-hook-vip.log
07-hook-sign.js
08-hook-sign.log
09-dynamic-report.md
11. 动态分析策略
11.1 动态分析三条线
动态分析建议同时推进三条线:
| 线索 | 目标 | 工具 | 产出 |
|---|---|---|---|
| 行为线 | 用户操作触发了什么页面、日志、请求 | Android Studio、adb、logcat | 行为时间线 |
| 数据线 | 参数、token、签名、响应如何变化 | Burp/mitmproxy、Frida | 数据流图 |
| 控制线 | 哪些函数决定分支、权限、风控 | Frida、调试器 | Hook 点清单 |
如果只抓包,容易看不到本地判断;如果只 Hook,容易看不到服务端结果;如果只看日志,容易被开发日志误导。
11.2 动态分析时间线模板
markdown
## 行为时间线
| 时间 | 用户动作 | App 日志 | 网络请求 | Hook 命中 | 结论 |
|---|---|---|---|---|---|
| 10:00:01 | 打开 App | MainActivity onCreate | 无 | 无 | 启动 |
| 10:00:10 | 登录 | login clicked | POST /login | Signer.sign | 登录请求签名 |
| 10:00:20 | 进入会员页 | check vip | GET /vip/resource | UserCenter.isVip | 会员判断 |
这种时间线可以把工具输出串起来,避免零散截图无法形成结论。
12. adb 动态诊断
12.1 dumpsys 常用场景
| 命令 | 用途 |
|---|---|
adb shell dumpsys activity top |
当前前台 Activity |
adb shell dumpsys activity activities |
Activity 栈 |
adb shell dumpsys package <pkg> |
包信息、权限、组件 |
adb shell dumpsys meminfo <pkg> |
内存使用 |
adb shell dumpsys netstats |
网络统计 |
adb shell dumpsys connectivity |
网络状态 |
adb shell dumpsys window windows |
窗口和焦点 |
12.2 am 命令扩展
bash
adb shell am start -n com.example.reversedemo/.MainActivity
adb shell am force-stop com.example.reversedemo
adb shell am start -a android.intent.action.VIEW -d "reversedemo://open/vip"
adb shell am broadcast -a com.example.reversedemo.DEBUG_ACTION --es cmd dump
验证点:
- 组件是否能被外部启动。
- Deep Link 参数是否被校验。
- force-stop 后重启是否有持久化状态。
- 广播是否触发敏感行为。
12.3 logcat 过滤技巧
bash
adb logcat -c
adb logcat -v time | grep -E "ReverseDemo|OkHttp|FATAL|AndroidRuntime"
adb logcat -b crash
adb logcat '*:E'
日志分析表:
| 线索 | 说明 | 风险 |
|---|---|---|
Authorization |
token 出现在日志 | 敏感信息泄露 |
password |
密码或验证码输出 | 高风险 |
sign raw |
签名前明文输出 | 签名可复现 |
debug panel |
调试页面日志 | 隐藏入口 |
root detected |
环境检测日志 | 可作为 Hook 入口 |
13. 抓包专题
13.1 HTTP 请求拆解
每个请求至少拆成 8 个部分:
| 部分 | 检查点 |
|---|---|
| Method | GET/POST/PUT/DELETE 是否符合业务 |
| Path | 是否含用户 ID、订单 ID、资源 ID |
| Query | 是否可篡改分页、价格、类型 |
| Header | token、签名、设备 ID、版本 |
| Body | JSON 字段、嵌套对象、金额 |
| Cookie | 会话、CSRF、过期时间 |
| Response | 状态码、错误码、敏感字段 |
| Timing | 时间戳、nonce、重放窗口 |
13.2 Burp Repeater 测试矩阵
| 测试编号 | 改动 | 预期 | 结果 |
|---|---|---|---|
| B-001 | 删除 token | 401/403 | 记录 |
| B-002 | token 改为无效值 | 401/403 | 记录 |
| B-003 | userId 改为他人 |
403 | 记录 |
| B-004 | 删除签名 | 400/401 | 记录 |
| B-005 | 修改 body 不改签名 | 签名失败 | 记录 |
| B-006 | 重放同一 nonce | 重放拒绝 | 记录 |
| B-007 | 时间戳改旧 | 过期拒绝 | 记录 |
| B-008 | 金额改小 | 业务拒绝 | 记录 |
| B-009 | 会员等级改高 | 业务拒绝 | 记录 |
| B-010 | 添加未知字段 | 不应越权 | 记录 |
13.3 mitmproxy 脚本观察
授权 Demo 可用 mitmproxy 记录目标接口:
python
from mitmproxy import http
def request(flow: http.HTTPFlow):
if "reversedemo" in flow.request.pretty_host:
print(flow.request.method, flow.request.path)
print(flow.request.headers)
print(flow.request.get_text())
注意:只在授权环境记录,不保存真实用户隐私数据。
13.4 证书问题排查
| 现象 | 可能原因 | 处理 |
|---|---|---|
| 完全无请求 | 代理未配置或 App 走直连 | 检查系统代理和网络 |
| 只有 CONNECT | TLS 未解密 | 安装并信任 CA |
| 证书错误 | App 不信任用户 CA | debug 配置允许用户证书 |
| 连接失败 | 证书绑定 | 记录防护现象,做授权复测 |
| 部分请求抓不到 | 使用 WebSocket、QUIC、Native 网络库 | 结合日志和 Hook |
14. Frida Java 深入
14.1 枚举已加载类
javascript
Java.perform(function () {
Java.enumerateLoadedClasses({
onMatch: function (name) {
if (name.indexOf("reversedemo") >= 0) {
console.log(name);
}
},
onComplete: function () {
console.log("done");
}
});
});
用途:
- 混淆后找目标类。
- 判断类是否已经加载。
- 发现动态加载的模块。
14.2 Hook ClassLoader
javascript
Java.perform(function () {
const ClassLoader = Java.use("java.lang.ClassLoader");
ClassLoader.loadClass.overload("java.lang.String").implementation = function (name) {
if (name.indexOf("reversedemo") >= 0) {
console.log("[loadClass] " + name);
}
return this.loadClass(name);
};
});
适合处理:
- 插件化。
- 加固壳加载后的真实类。
- 动态 dex。
- 类延迟加载。
14.3 Hook OkHttp
javascript
Java.perform(function () {
const RequestBuilder = Java.use("okhttp3.Request$Builder");
RequestBuilder.url.overload("java.lang.String").implementation = function (url) {
console.log("[OkHttp url] " + url);
return this.url(url);
};
});
更常见的是 Hook 拦截器或请求构造:
| Hook 点 | 目的 |
|---|---|
Request$Builder.url |
URL |
Request$Builder.addHeader |
Header |
RequestBody.create |
Body |
Interceptor.intercept |
完整请求响应 |
CertificatePinner.check |
证书绑定观察 |
14.4 Hook SharedPreferences
javascript
Java.perform(function () {
const Editor = Java.use("android.app.SharedPreferencesImpl$EditorImpl");
Editor.putString.implementation = function (key, value) {
console.log("[SP putString] " + key + "=" + value);
return this.putString(key, value);
};
});
用途:
- 观察 token 是否落盘。
- 观察本地开关是否可被改。
- 验证敏感字段是否明文保存。
15. Frida 实战脚本库
15.1 Hook root 检测 Java API
javascript
Java.perform(function () {
const File = Java.use("java.io.File");
File.exists.implementation = function () {
const path = this.getAbsolutePath();
if (path.indexOf("su") >= 0 || path.indexOf("magisk") >= 0) {
console.log("[bypass file] " + path);
return false;
}
return this.exists();
};
});
15.2 Hook Debug 检测
javascript
Java.perform(function () {
const Debug = Java.use("android.os.Debug");
Debug.isDebuggerConnected.implementation = function () {
console.log("[Debug.isDebuggerConnected]");
return false;
};
});
15.3 Hook Base64
javascript
Java.perform(function () {
const Base64 = Java.use("android.util.Base64");
Base64.encodeToString.overload("[B", "int").implementation = function (input, flags) {
const result = this.encodeToString(input, flags);
console.log("[Base64] " + result);
return result;
};
});
15.4 Hook MessageDigest
javascript
Java.perform(function () {
const MessageDigest = Java.use("java.security.MessageDigest");
MessageDigest.getInstance.overload("java.lang.String").implementation = function (algo) {
console.log("[Digest algo] " + algo);
return this.getInstance(algo);
};
});
这些脚本的目的都是观察授权 Demo 的运行时数据流,而不是绕过第三方业务。
16. 动态风险库
| 风险编号 | 风险 | 动态证据 | 业务判断 |
|---|---|---|---|
| D-001 | token 出现在日志 | logcat | 高风险 |
| D-002 | 本地会员可 Hook | Frida 日志和截图 | 看服务端是否拒绝 |
| D-003 | 签名前明文可打印 | Frida Hook | 客户端秘密不可信 |
| D-004 | 请求可重放 | Burp Repeater | 缺 nonce |
| D-005 | 参数可篡改 | 修改 body | 缺签名或业务校验 |
| D-006 | 证书可被代理 | 抓到 HTTPS 明文 | release 防护不足 |
| D-007 | Root 检测可 Hook | Hook File.exists | 单点检测无效 |
| D-008 | 导出页面可启动 | am start |
缺入口鉴权 |
| D-009 | 本地存储明文 | run-as 查看 |
数据保护不足 |
| D-010 | 崩溃暴露栈 | logcat crash | 信息泄露 |
17. 动态报告模板
markdown
# Dynamic Analysis Report
## 测试范围
- App:
- 版本:
- 包名:
- 授权范围:
## 行为时间线
| 时间 | 操作 | 日志 | 请求 | Hook |
## 抓包结果
| 接口 | 方法 | 鉴权 | 签名 | 重放 | 越权 |
## Hook 结果
| Hook 点 | 参数 | 原返回 | 修改返回 | 行为变化 |
## 风险结论
| 编号 | 风险 | 证据 | 影响 | 修复 |
## 限制
- 未覆盖:
- 未复现:
- 需要后端配合:
18. 进阶练习
| 练习 | 目标 | 通过标准 |
|---|---|---|
| Hook 登录函数 | 打印用户名,不记录真实密码 | 能看到授权 Demo 参数 |
| Hook 签名函数 | 打印签名前 raw string | 能复现签名输入 |
| Hook OkHttp | 输出 URL 和 Header | 能和抓包结果对上 |
| Hook SP 写入 | 观察 token 是否落盘 | 能判断存储风险 |
| 重放请求 | 验证 nonce | 服务端拒绝旧请求 |
| 改 userId | 验证越权 | 服务端拒绝 |
| 关闭代理 | 验证网络恢复 | App 正常请求 |
| 模拟 root | 验证检测路径 | Hook 日志命中 |
19. 动态分析、抓包与 Hook
动态分析关注真实运行时行为:参数、请求、返回值、分支和服务端结果。它用来验证静态结论,而不是替代业务判断。
动态观察
| 知识点 | 核心理解 | Demo/验证 | 常见误区 |
|---|---|---|---|
| 行为时间线 | 把用户操作、日志、请求和 Hook 命中放到同一时间轴。 | 登录、进入会员页、请求资源三步记录。 | 证据散落,无法证明因果。 |
| logcat | 日志能暴露流程、异常和敏感字段。 | 过滤 `ReverseDemo | OkHttp |
| Activity 栈 | 当前页面和跳转路径可通过 dumpsys 观察。 | 用 dumpsys activity top。 |
不知道当前页面就乱 Hook。 |
| 进程状态 | PID、进程名、启动时机影响 Hook 策略。 | 用 pidof 和 ps -A。 |
attach 太晚导致漏 Hook。 |
| 本地数据观察 | SP、SQLite、文件写入能揭示 token 和开关。 | Hook SP 或用 debug run-as。 |
看不到文件就认为没保存。 |
抓包与接口
| 知识点 | 核心理解 | Demo/验证 | 常见误区 |
|---|---|---|---|
| HTTPS 信任链 | 抓包依赖设备信任代理证书,release 应更严格。 | debug 和 release 分别抓包。 | 抓不到就认为服务端安全。 |
| 证书绑定 | pinning 提高中间人攻击成本。 | release 下代理失败并记录错误。 | 用 pinning 替代服务端鉴权。 |
| Header 篡改 | Authorization、签名、设备 ID 都需要服务端校验。 | 删除或替换 Header。 | 只改 body 不测 Header。 |
| Body 篡改 | 金额、等级、userId、订单 ID 是关键字段。 | Burp Repeater 修改 JSON。 | 服务端返回 200 就直接判成功,不看业务码。 |
| 重放请求 | 旧 nonce 或 timestamp 应被拒绝。 | 重复发送同一请求。 | 客户端生成 nonce 但服务端不记录。 |
| 越权验证 | 资源归属必须由服务端按 token 判断。 | 改 userId 或订单 ID。 |
只验证登录,不验证资源归属。 |
Frida Hook
| 知识点 | 核心理解 | Demo/验证 | 常见误区 |
|---|---|---|---|
| spawn | 适合 Hook 启动早期逻辑。 | Hook Application 初始化。 | 只用 attach 导致错过检测。 |
| attach | 适合观察已运行流程。 | 打开会员页后 Hook isVip。 |
目标方法已执行还期待命中。 |
| 重载方法 | Java 方法可能有多个 overload,必须选对签名。 | Hook check(String)。 |
不指定 overload 导致脚本失败。 |
| 构造函数 | Hook $init 可观察对象创建参数。 |
Hook 请求对象构造。 | 只 Hook 业务方法漏掉参数来源。 |
| ClassLoader | 插件化和加固会使用不同 ClassLoader。 | Hook loadClass。 |
类找不到就认为不存在。 |
| 返回值修改 | 返回值修改只证明客户端可变,不等于服务端被绕过。 | Hook isVip() 后访问 /vip/resource。 |
把 UI 变化写成核心越权。 |
| 签名前明文 | Hook 加密前入口可观察真实参数。 | Hook Signer.sign()。 |
只看密文,不分析覆盖范围。 |