APP攻防5:JEB 动态调试——断点、加密算法还原与会员逻辑绕过
安卓逆向进阶:JEB 动态调试——断点、加密算法还原与会员逻辑绕过
学习自B站小迪安全
静态反编译能看到"代码长什么样",却看不到"运行时到底算了什么"。当代码被混淆、加密逻辑藏在十几层调用之后,或者你只想确认那个判断条件在真实运行中到底取了什么值——静态分析就到此为止了,接下来是动态调试的战场。
一、开始之前:前置知识与工具清单
1.1 前置知识(仅列要点)
- Java 基础语法:类、方法、重载(overload)、继承、
private/final/static修饰符。 - Smali 基础:
.registers/.locals、寄存器v0..vN与p0..pN、const/4、invoke-*、return、if-*、goto。没有 Smali 基础,断点调出来的东西你也改不动。 - APK 结构与
AndroidManifest.xml:classes.dex、AndroidManifest.xml各字段的含义。 - Activity 与生命周期:知道入口 Activity 是什么、在哪里声明。
- ADB 调试桥:
connect、shell、push/pull、am、ps、forward。 - HTTP 抓包基础:会用代理工具抓取并查看请求参数。
- 常见加密算法特征:MD5 / SHA / Base64 / AES / RSA / HMAC 的输出形态特征(第七章有对照表)。
- Java 加密 API 概念:
MessageDigest、Cipher、SecretKeySpec、IvParameterSpec、Base64的用途。 - JDK 与命令行环境变量概念:
JAVA_HOME、PATH的作用。 - 反调试概念:知道 App 可能检测调试器、Root、模拟器、代理。
1.2 工具清单(仅列要点)
| 类别 | 工具 | 用途 |
|---|---|---|
| 动态调试 IDE | JEB(Pro 版) | 反编译 + 反汇编 + 断点动态调试,本文主角 |
| 运行环境 | JDK 11 | JEB 的运行依赖,须配置 JAVA_HOME |
| 调试桥 | ADB | 连接模拟器、启动调试模式、端口转发,须配置环境变量 |
| 设备 | 模拟器或可 root 真机 | MuMu / 夜神 / 雷电 / 逍遥 / 天天 / 海马玩等 |
| 反编译(静态对照) | Jadx-Gui | 图形化阅读 Java 代码,全局搜索 |
| 回编译 | apktool | 反编译出 smali、改完回编译 |
| 签名 | apksigner / zipalign / keytool | 重打包后签名与对齐 |
| 综合管理 | MT 管理器 / NP 管理器 | 手机上直接改包、重签名 |
| 抓包 | Burp Suite 等 | 抓取登录/接口请求,观察加密参数 |
| 抓包/转发 | Postern | App 级代理转发(绕过直连) |
| 动态插桩(备选) | Frida | 无法改包或改包后崩溃时的替代路线 |
| 信息获取 | 开发助手 / 开发者助手 | 查看布局、控件 id、当前 Activity |
二、先建立整体认知:静态分析与动态调试的分工
逆向工程有两个"看代码"的视角,它们不是替代关系,而是互补关系。
| 维度 | 静态分析 | 动态调试 |
|---|---|---|
| 手段 | 反编译后读代码 | 附加调试器,打断点,看运行时状态 |
| 代表工具 | Jadx-Gui、apktool(smali)、JEB 静态视图 | JEB 动态调试、Frida、Android Studio |
| 能回答 | "逻辑写在哪"、"调用了哪些方法" | "运行时这里到底等于几" |
| 看不到 | 运行时的真实值、动态解密出来的字符串、反射调用的目标 | 全局结构(只能看到当前断点附近的执行流) |
| 受限于 | 混淆、加固、字符串加密 | 反调试、反附加、崩溃 |
2.1 什么时候必须上动态调试
把以下几种情况记住,它们就是"该换工具了"的信号:
- 字符串被加密:静态看到的全是乱码常量,运行时才解密成明文 URL 或密钥——只能在解密函数返回处下断点。
- 加密算法看不清:一个
sign参数不知道是怎么算出来的,静态跟进去发现层层封装、反射调用、甚至进 Native。 - 代码强混淆:类名全是
a.a.a(),静态读不出语义,但运行时可以看到真实参数与返回值,从而反推语义。 - 只是想确认一个判断:VIP 判断到底走哪个分支、
getIs_vip()到底返回几。 - 加固后无法完整反编译:动态调试可以绕开"读不到全部代码"的问题。
2.2 一个重要的认知:动态调试不改变代码
动态调试只观察,不修改。 你在断点处改变量、改寄存器,影响的是这一次运行的内存状态,关掉 App 就没了。
要让修改持久生效,路径仍然是:
动态调试定位问题 → 记录修改方案(改哪一行、改成什么) → 改 Smali → 回编译 → 重打包签名 → 安装
动态调试的价值在于"精准定位 + 验证假设",而不是"直接改"。 这一点想清楚了,整个工作流就顺了。
三、环境搭建:照着做
3.1 第一步:模拟器开启开发者模式与 USB 调试
操作路径:
- 模拟器设置 → 找到「关于平板电脑」→ 快速连续单击 5 次「版本号」 → 提示"您已处于开发者模式";
- 返回「系统」→「高级」→「开发者选项」→ 启用 USB 调试;
- 回到模拟器设置页,开启 root 权限,并开启磁盘共享可写入(这一项决定了后面能否用 adb 向系统目录写文件)。
为什么要开"磁盘共享可写入":不开的话,
/system等目录是只读挂载,后续要往系统目录放东西(如证书、模块)会失败。
3.2 第二步:安装 JDK 11 并配置 JAVA_HOME
JEB 对 JDK 版本敏感,推荐使用 JDK 11。配置要点只有一条:
必须保证启动 JEB 时使用的是 JDK 11——通过把
JAVA_HOME指向 JDK 11 的安装目录来实现。
Windows 上的操作:
系统属性 → 高级 → 环境变量
新建:JAVA_HOME = C:\Program Files\Java\jdk-11
编辑 Path,新增:%JAVA_HOME%\bin
验证(重新打开一个终端):
java -version
# 期望输出:openjdk version "11.x.x" 或 java version "11.0.x"
常见坑:机器上装了多个 JDK 时,
Path中的顺序决定了谁生效。把%JAVA_HOME%\bin放到最前面,或者直接删掉其他 JDK 的 Path 条目。
3.3 第三步:安装 ADB 并配置环境变量
要点同样只有一条:
把 ADB 所在目录加入环境变量,保证启动 JEB 时能调用到
adb——JEB 的动态调试功能依赖它来枚举设备与转发 JDWP 端口。
Windows 上的操作:
系统属性 → 高级 → 环境变量
新建:Android = D:\platform-tools
编辑 Path,新增:%Android%
验证:
adb version
# 期望输出:Android Debug Bridge version 1.0.41 等
变量名用
Android还是ANDROID_HOME并不影响结果,关键是adb能在任意目录下被找到——因为 JEB 是从自己的进程里调用它的。
3.4 第四步:JEB 激活
JEB 是商业工具,学习场景下的激活方式可参考公开资料:
3.5 第五步:连接模拟器(端口对照表)
不同模拟器的 ADB 调试端口各不相同,这是新手最容易卡住的地方——直接 adb devices 往往是空的,必须手动 connect。
| 模拟器 | 连接命令 |
|---|---|
| MuMu 模拟器 | adb connect 127.0.0.1:7555 |
| 夜神模拟器 | adb connect 127.0.0.1:62001 |
| 雷电模拟器 | adb connect 127.0.0.1:5555 |
| 逍遥安卓模拟器 | adb connect 127.0.0.1:21503 |
| 天天模拟器 | adb connect 127.0.0.1:6555 |
| 海马玩模拟器 | adb connect 127.0.0.1:53001 |
连接后确认:
adb devices
# 期望看到:127.0.0.1:7555 device
若显示
offline或unauthorized:重启模拟器的 adb 服务,或用adb kill-server && adb start-server重置。注意:模拟器自带一份 adb,与你本机的版本可能冲突——做到"同一时间只有一个 adb 服务在跑"。
四、让 App 支持动态调试:debuggable
默认情况下,正式发布的 APK 都是不可调试的(AndroidManifest.xml 中的 android:debuggable 为 false 或未声明)。直接附加调试器会失败。
4.1 方案 A:反编译改 Manifest 再重打包(推荐)
# 1. 反编译
apktool d target.apk -o target_src
# 2. 编辑 target_src/AndroidManifest.xml
# 在 <application ...> 标签中加入 android:debuggable="true"
修改示意:
<!-- 修改前 -->
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name">
<!-- 修改后 -->
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:debuggable="true">
# 3. 回编译
apktool b target_src -o target_patched.apk
# 4. 对齐 + 签名
zipalign -v -p 4 target_patched.apk target_aligned.apk
apksigner sign --ks my.keystore --ks-pass pass:123456 --key-pass pass:123456 \
--out target_signed.apk target_aligned.apk
# 5. 卸载原版并安装
adb uninstall <目标包名>
adb install -r target_signed.apk
代价:重打包会破坏原签名。若 App 有签名校验,一启动就会闪退或联网失败。这种情况改用方案 B 或 Frida 路线。
4.2 方案 B:其他启用调试的方法
除改 Manifest 外,还有若干不重打包的思路,可参考公开资料:
常见的思路方向包括:修改系统属性 ro.debuggable、用 Xposed/LSPosed 模块在运行时把当前应用的 FLAG_DEBUGGABLE 打开、或用 am set-debug-app 指定调试目标。共同点是都不重新签名,因此不受签名校验影响。
4.3 验证 debuggable 是否生效
adb shell dumpsys package <目标包名> | grep -i "flags\|DEBUGGABLE"
# 若输出中出现 DEBUGGABLE 标志,说明生效
五、启动调试会话:把断点打上去
5.1 以调试模式启动 App
# 通用形式:-n 后面是 "包名/入口Activity类名"
adb shell am start -D -n <目标包名>/<入口Activity>
参数含义(务必分清):
| 参数 | 含义 |
|---|---|
am start |
启动一个 Activity |
-n <包名>/<类名> |
指定要启动的组件(-n 后接 component) |
-D |
将应用设置为可调试模式(Debug),进程会挂起等待调试器附加 |
一个具体形态的例子(包名与类名请替换为你自己的目标):
adb shell am start -D -n com.example.app/.ui.MainActivity
执行后,设备屏幕上通常会出现一个"等待调试器"的提示框——这是正常的,不要点掉,也不要点"强制关闭"。此时进程已经停在启动阶段,等你附加调试器。
为什么要用
-D而不是普通启动:普通启动时 App 会一路跑完初始化,等你附加时关键逻辑早就执行过了。-D让它在最早阶段就挂住,保证你不会漏掉任何一次调用。这是动态调试能否成功的分水岭。
5.2 获取进程 PID 并转发 JDWP 端口
# 1. 查询目标进程 PID
adb shell ps | grep <目标包名>
# 输出形如:u0_a123 12345 678 1234567 ... com.example.app
# 2. 把设备的 JDWP 调试端口转发到本机 8700
adb forward tcp:8700 jdwp:12345 # 把 12345 换成上一步拿到的 PID
JDWP(Java Debug Wire Protocol)是 JVM/ART 的调试协议。
adb forward做的就是把这个调试通道打通到本机,JEB 才能连上。
5.3 在 JEB 中附加
- 用 JEB 打开目标 APK(首次会做反编译,稍等);
- 切换到 Bytecode / Smali 视图(JEB 的调试断点打在 smali 层面);
- 打开 Debugger 面板 → 选择 Start / Attach;
- 在设备列表中选中刚转发好的进程(或对应 PID);
- 附加成功后,进程会继续运行。
5.4 设置断点与查看运行时状态
| 操作 | 位置 | 说明 |
|---|---|---|
| 下断点 | smali 代码行左侧空白处点击 | 命中后执行流会停在该行 |
| 继续执行 | Debugger → Resume | 继续运行到下个断点 |
| 单步 | Step Over / Step Into | 逐行 / 跟进方法内部 |
| 查看寄存器 | 调试面板的 Registers 区 | 看 v0 到底是 0 还是 1,这是静态分析永远给不了的信息 |
| 查看堆栈 | Call Stack 区 | 回答"这个加密函数是被谁调用的" |
| 查看对象 | Locals / this 区 | 展开对象看字段值 |
5.5 附加失败的排查顺序
| 现象 | 原因 | 处理 |
|---|---|---|
| JEB 看不到设备 | adb 未配置或未启动 |
确认 adb devices 能列出设备 |
| 进程列表里没有目标 | debuggable 未生效 |
回到第四章重新确认 |
| 附加后立即断开 | App 有反调试 | 见第八章 |
adb devices 显示 offline |
adb 版本冲突 | adb kill-server && adb start-server |
| 断点永远不命中 | 断点打在了未执行的分支 | 换到 onCreate 或入口方法先打断点,再逐步跟 |
六、实操案例一:会员逻辑绕过(静态定位 + 动态验证)
目标:找到 App 中判断"是否会员"的逻辑,确认其运行时取值,并给出持久化修改方案。
核心思路:静态找到判断点 → 动态断点确认取值 → 记录修改方案 → 改 Smali 重打包。
步骤 1:静态定位判断点
用 Jadx-Gui 打开 APK,全局搜索会员相关关键字:
isVip / is_vip / vipType / vip_level / isMember / getVipLevel
会员 / 开通 / 到期 / 升级
也可以从界面文案入手,反查资源 ID 再找引用(见前一篇的"引用更改"方法)。
命中后你会看到类似这样的逻辑:
public boolean isVip() {
UserInfo user = getUserInfo();
if (user.getVipType() == 0x100) { // 0x100 = 256,某个具体档位
return true;
}
return false;
}
对应的 Smali:
.method public isVip()Z
.registers 3
invoke-virtual {p0}, Lcom/example/app/UserInfo;->getUserInfo()Lcom/example/app/UserInfo;
move-result-object v0
iget v1, v0, Lcom/example/app/UserInfo;->vipType:I
const/16 v2, 0x100
if-ne v1, v2, :cond_not_vip # v1 == 0x100 则继续,否则跳到"非会员"
const/4 v0, 0x1
return v0
:cond_not_vip
const/4 v0, 0x0
return v0
.end method
步骤 2:动态断点验证
在 JEB 中找到这个方法,在 if-ne v1, v2, :cond_not_vip 这行下断点,然后:
- 触发会员判断的界面(进入"我的"页);
- 断点命中,查看 Registers 面板中
v1的值; - 若
v1 = 0x0,说明服务端下发的是普通用户 → 客户端确实按真实状态判断; - 若
v1 = 0x100,说明客户端已经认为你是会员 → 问题在别处(可能是某个更上层的开关)。
这一步的价值:它把"我猜是这里"变成了"我确认是这里"。没有动态验证的修改,等于盲改。
步骤 3:进一步追栈,找到真正的"总开关"
在断点处查看 Call Stack,看这个方法是被谁调用的。如果有上层逻辑(如 checkPermission())先做拦截,那么真正要改的是它。
步骤 4:给出持久化修改方案
方案 A(最简洁)——清空方法体,强制返回 true:
.method public isVip()Z
.registers 3
const/4 v0, 0x1
return v0
.end method
方案 B ——反转条件指令:
# if-ne v1, v2, :cond_not_vip ← 改为下面这行
if-eq v1, v2, :cond_not_vip
方案 C ——无条件跳转到"是会员"分支:
goto :cond_is_vip
步骤 5:回编译、签名、安装
apktool b target_src -o target_patched.apk
zipalign -v -p 4 target_patched.apk target_aligned.apk
apksigner sign --ks my.keystore --ks-pass pass:123456 --key-pass pass:123456 \
--out target_signed.apk target_aligned.apk
adb uninstall <目标包名>
adb install -r target_signed.apk
步骤 6:为什么改了还是没效果——分清"三处判断"
这是会员类逻辑最容易踩的坑。同一个"是否会员",在代码里通常存在三处独立的判断:
| 位置 | 作用 | 改了会怎样 |
|---|---|---|
| 显示层(界面渲染) | 决定界面显示"会员"还是"开通会员" | 只改这里 → 界面变了,功能还是不能用 |
| 本地校验层(客户端权限判断) | 决定本地功能是否放行 | 改这里 → 本地功能可用 |
| 服务端校验层 | 服务端依据数据库真实状态决定是否下发内容 | 改不动 → 云端内容依然拿不到 |
结论:客户端修改只能影响前两层。任何涉及服务端下发权益的功能(云端内容、真实交易、需联网鉴权的接口),客户端改不出真实权益。 这正是这类问题的安全价值所在——它说明服务端必须独立校验,不能信任客户端传来的任何状态。
七、实操案例二:登录加密算法还原
目标:搞清登录请求中那个"算不出来"的参数(如 sign、password、token)是怎么生成的。
核心思路:抓包看密文形态 → 静态找加密入口 → 动态跟栈拿到算法与密钥 → 还原算法。
步骤 1:抓包分析,先看密文长什么样
用代理工具抓取登录请求,观察参数:
POST /api/user/login HTTP/1.1
Host: <接口域名>
username=alice&password=0f3a9c7e2b1d8f4a6c5e0b3d7a9f2c8e&sign=8d3c...
这一步能提供关键线索:
password是 32 位十六进制 → 极可能是 MD5(或 MD5 加了盐/拼接);- 长度是 4 的倍数且含
+/=→ Base64; - 长度固定且很长(128+ 字符)→ RSA;
- 明文完全不可读且长度是 16 的倍数 → AES/DES 的密文或 Base64 后的结果;
- 请求里同时有
timestamp(10 位或 13 位数字)和nonce→ 大概率是"拼接后做摘要"的签名机制。
步骤 2:常见加密算法输出特征对照表
| 算法 | 输出长度特征 | 字符集 | 快速判据 |
|---|---|---|---|
| MD5 | 32 位 | 0-9a-f |
长度恰好 32 的小写/大写 hex |
| SHA-1 | 40 位 | 0-9a-f |
长度恰好 40 的 hex |
| SHA-256 | 64 位 | 0-9a-f |
长度恰好 64 的 hex |
| Base64 | 4 的倍数 | A-Za-z0-9+/ |
末尾常有 = 或 == |
| AES | 密文为 16 字节的倍数 | 二进制 / Base64 | 十六进制长度是 32 的倍数 |
| DES / 3DES | 密文为 8 字节的倍数 | 二进制 / Base64 | 十六进制长度是 16 的倍数 |
| RSA | 等于密钥长度 | 二进制 / Base64 | 1024 位 → 十六进制 256 字符 / Base64 约 172 字符 |
| HMAC | 与底层摘要一致 | hex / Base64 | 通常出现在 sign / signature 字段 |
| 时间戳 | 10 位(秒)/ 13 位(毫秒) | 纯数字 | 纯数字且接近当前时间 |
| UUID / 随机数 | 32/36 位 | hex + - |
每次请求都变 |
步骤 3:静态定位加密入口
在 Jadx-Gui 中搜索以下特征:
搜索加密 API:
MessageDigest / digest / "MD5" / "SHA-1" / "SHA-256"
Cipher / getInstance / "AES" / "DES" / "RSA"
Base64 / encodeToString / decode
SecretKeySpec / IvParameterSpec / Mac.getInstance
Signature / sign
搜索业务关键字:
sign / signature / password / encrypt / encode
timestamp / nonce / appSecret / key / salt
重点:如果搜索 SecretKeySpec 命中,附近往往就有硬编码的密钥——这是最容易直接"拿到答案"的地方。
步骤 4:动态调试跟栈(关键环节)
静态看到的是"用了 AES",但密钥从哪来、明文拼了什么,往往只有运行时才知道。
方法一:用 JEB 在加密调用处打断点
在 Cipher.doFinal(...) 或 MessageDigest.digest(...) 的调用行下断点,命中后:
- 看 Registers 面板 →
v0等寄存器里就是加密前的明文(或其引用); - 看 Call Stack → 逐层向上翻,找到"拼装明文的那个方法"。
方法二:用 Frida Hook 加密 API(更直观,推荐)
Frida 脚本运行在目标进程内,可以直接打印所有加密调用的输入输出。
// 十六进制转字符串工具函数
function bytesToHex(bytes) {
var hex = [];
for (var i = 0; i < bytes.length; i++) {
var b = (bytes[i] & 0xff).toString(16);
if (b.length === 1) b = "0" + b;
hex.push(b);
}
return hex.join("");
}
Java.perform(function () {
// ① Hook 摘要算法:MD5 / SHA-1 / SHA-256
var MessageDigest = Java.use("java.security.MessageDigest");
MessageDigest.digest.overload().implementation = function () {
var algo = this.getAlgorithm();
var result = this.digest();
console.log("[DIGEST] algo=" + algo + " result=" + bytesToHex(result));
return result;
};
// ② Hook 密钥构造:直接拿到 AES/DES 的密钥
var SecretKeySpec = Java.use("javax.crypto.spec.SecretKeySpec");
SecretKeySpec.$init.overload("[B", "java.lang.String").implementation = function (key, algo) {
console.log("[KEY] algo=" + algo + " key(hex)=" + bytesToHex(key));
return this.$init(key, algo);
};
// ③ Hook 初始化向量 IV
var IvParameterSpec = Java.use("javax.crypto.spec.IvParameterSpec");
IvParameterSpec.$init.overload("[B").implementation = function (iv) {
console.log("[IV] iv(hex)=" + bytesToHex(iv));
return this.$init(iv);
};
// ④ Hook 对称加解密入口,同时打印算法名与输入输出
var Cipher = Java.use("javax.crypto.Cipher");
Cipher.doFinal.overload("[B").implementation = function (input) {
console.log("[CIPHER] algo=" + this.getAlgorithm() + " IN=" + bytesToHex(input));
var out = this.doFinal(input);
console.log("[CIPHER] OUT=" + bytesToHex(out));
return out;
};
// ⑤ Hook Base64
var Base64 = Java.use("android.util.Base64");
Base64.encodeToString.overload("[B", "int").implementation = function (data, flags) {
var r = this.encodeToString(data, flags);
console.log("[BASE64] IN=" + bytesToHex(data) + " OUT=" + r);
return r;
};
});
运行:
frida -U -f <目标包名> -l crypto_trace.js
然后触发一次登录,终端上就会把算法名、密钥、IV、明文、密文全部打印出来。
这一步的实战价值极高:它把"逆算法"变成了"读日志"。只要 App 用的是 Java 层的标准加密 API,几乎必然被上面的脚本捕获。
步骤 5:判断"标准算法"还是"魔改算法"
| 观察结果 | 结论 |
|---|---|
| 密钥长度是 16/24/32 字节,输出是 16 字节的倍数 | 标准 AES(CBC/ECB) |
| 密钥是 8 字节 | 标准 DES |
| 输出 32 位 hex,且与标准 MD5 对得上 | 标准 MD5 |
| 用工具算不出来,但 Hook 到了密钥和明文 | 魔改算法(如自定义 S 盒、异常轮数、密钥异或) |
| Hook 不到任何 Java 层加密 API | 加密在 Native 层(.so),需 IDA/Frida Native Hook |
验证是否标准算法:用 Hook 到的密钥与 IV,拿同一段明文在本地用标准库跑一遍,比对结果是否一致。
# 示例:用 openssl 验证 AES-128-CBC
echo -n "明文内容" | openssl enc -aes-128-cbc \
-K <十六进制密钥> -iv <十六进制IV> -base64
步骤 6:算法还原的用途(正向价值)
还原登录加密本不是为了伪造登录,它的正当用途是:
- 接口自动化测试:构造合法的签名参数,做批量接口测试与回归;
- 回归验证:确认客户端加密实现是否符合预期强度;
- 合规审计:检查是否存在"客户端硬编码密钥"这一设计缺陷——硬编码密钥意味着任何拿到 APK 的人都能解出通信内容;
- 安全报告:把"密钥硬编码在客户端"作为一条明确的风险项提交给厂商。
八、动态调试常见问题
| 问题 | 原因 | 处理 |
|---|---|---|
am start -D 后 App 停在"等待调试器" |
正常现象,设计如此 | 不要点掉,去 JEB 附加 |
| JEB 列表里找不到进程 | debuggable 未生效 / JDWP 未转发 |
校验 dumpsys package;补做 adb forward |
| 附加瞬间断开 | 反调试 | 见下方说明 |
| 断点不命中 | 代码走了另一分支 / 类被多次加载 | 先在 onCreate 打断点,再单步跟进 |
| 改了 Manifest 重打包后闪退 | 签名校验 | 改用运行时不改包的方案(第四章方案 B / LSPosed / Frida) |
| 调试时 App 卡死 | 断点命中的方法被高频调用 | 只在高价值方法上下断点,或用条件断点 |
adb devices 为空 |
模拟器端口未 connect | 查第三章端口对照表 |
| 加固 App 看不到 smali | 已加固 | 先脱壳,或改用 Frida 直接 Hook |
关于反调试:App 常见的反调试手段包括 ptrace 自附加、检测 android:debuggable、检测 TracerPid、检测调试端口、检测常见调试工具进程名。
应对思路有两个方向:
- 静态清除:反编译后定位反调试代码(搜
ptrace、TracerPid、isDebuggerConnected、Debug.isDebuggerConnected),删掉或强制返回 false,再重打包; - 运行时绕过:用 LSPosed 模块或 Frida 在运行时 Hook 这些检测方法——这条路更健壮,因为不重打包、不受签名校验影响(详见另一篇 LSPosed 与 HOOK 实战)。
九、安全与合规边界
本文所有技术内容仅适用于获得合法授权的测试场景——你自己的设备、你所在组织书面授权的目标、以及公开的测试靶场。
- 动态调试会附加到目标进程并读取其内存,属于侵入性操作;对未授权目标实施可能违反《网络安全法》《数据安全法》及目标平台服务条款。
- 还原出的密钥、算法、令牌属于凭据类敏感信息,必须妥善保管,测试结束后按流程销毁。
- 代码中出现的手机号、用户 ID、令牌等均为脱敏占位符;实际调试中接触到的真实数据应按最小必要原则处理。
- 客户端逻辑修改不会带来真实权益,其安全价值在于暴露"服务端信任客户端"这一设计缺陷,应通过厂商漏洞报送渠道反馈。
十、参考链接
工具与教程
- JEB 激活参考:https://www.52pojie.cn/forum.php?mod=viewthread&tid=1598242
- 启用动态调试的其他方法:https://www.52pojie.cn/thread-1714727-1-1.html
资料索引
- 配套资料(录像、素材、工具包等)下载地址:https://docs.qq.com/doc/DQ3Z6RkNpaUtMcEFr
附:命令与流程速查卡
# —— 连接模拟器 ——
adb connect 127.0.0.1:7555 # MuMu
adb connect 127.0.0.1:62001 # 夜神
adb connect 127.0.0.1:5555 # 雷电
adb connect 127.0.0.1:21503 # 逍遥安卓
adb connect 127.0.0.1:6555 # 天天
adb connect 127.0.0.1:53001 # 海马玩
adb devices
# —— 环境自检 ——
java -version # 期望 11.x
adb version
# —— 开启 debuggable ——
apktool d target.apk -o target_src
# 编辑 target_src/AndroidManifest.xml,在 <application> 加 android:debuggable="true"
apktool b target_src -o target_patched.apk
zipalign -v -p 4 target_patched.apk target_aligned.apk
apksigner sign --ks my.keystore --ks-pass pass:123456 --key-pass pass:123456 \
--out target_signed.apk target_aligned.apk
adb uninstall <目标包名> && adb install -r target_signed.apk
# —— 验证 debuggable ——
adb shell dumpsys package <目标包名> | grep -i DEBUGGABLE
# —— 调试模式启动 + 端口转发 ——
adb shell am start -D -n <目标包名>/<入口Activity>
adb shell ps | grep <目标包名> # 取 PID
adb forward tcp:8700 jdwp:<PID> # 转发 JDWP
# 随后在 JEB:Debugger → Start/Attach 选择进程
# —— Frida 抓加密(无需重打包)——
frida -U -f <目标包名> -l crypto_trace.js
# —— 排查 ——
adb kill-server && adb start-server
adb logcat | grep "AndroidRuntime"
动态调试五步法
① 开 debuggable → am start -D 让 App 挂起
② 转发 JDWP → 附加调试器
③ 下断点 → 定位到目标方法
④ 看寄存器/堆栈 → 拿到运行时真实值与调用链
⑤ 记录方案 → 改 Smali → 回编译 → 签名 → 安装

浙公网安备 33010602011771号