AIGC标识 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 什么时候必须上动态调试

把以下几种情况记住,它们就是"该换工具了"的信号:

  1. 字符串被加密:静态看到的全是乱码常量,运行时才解密成明文 URL 或密钥——只能在解密函数返回处下断点。
  2. 加密算法看不清:一个 sign 参数不知道是怎么算出来的,静态跟进去发现层层封装、反射调用、甚至进 Native。
  3. 代码强混淆:类名全是 a.a.a(),静态读不出语义,但运行时可以看到真实参数与返回值,从而反推语义。
  4. 只是想确认一个判断:VIP 判断到底走哪个分支、getIs_vip() 到底返回几。
  5. 加固后无法完整反编译:动态调试可以绕开"读不到全部代码"的问题。

2.2 一个重要的认知:动态调试不改变代码

动态调试只观察,不修改。 你在断点处改变量、改寄存器,影响的是这一次运行的内存状态,关掉 App 就没了。

要让修改持久生效,路径仍然是:

动态调试定位问题 → 记录修改方案(改哪一行、改成什么) → 改 Smali → 回编译 → 重打包签名 → 安装

动态调试的价值在于"精准定位 + 验证假设",而不是"直接改"。 这一点想清楚了,整个工作流就顺了。


三、环境搭建:照着做

3.1 第一步:模拟器开启开发者模式与 USB 调试

操作路径:

  1. 模拟器设置 → 找到「关于平板电脑」→ 快速连续单击 5 次「版本号」 → 提示"您已处于开发者模式";
  2. 返回「系统」→「高级」→「开发者选项」→ 启用 USB 调试;
  3. 回到模拟器设置页,开启 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 中附加

  1. 用 JEB 打开目标 APK(首次会做反编译,稍等);
  2. 切换到 Bytecode / Smali 视图(JEB 的调试断点打在 smali 层面);
  3. 打开 Debugger 面板 → 选择 Start / Attach;
  4. 在设备列表中选中刚转发好的进程(或对应 PID);
  5. 附加成功后,进程会继续运行。

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 这行下断点,然后:

  1. 触发会员判断的界面(进入"我的"页);
  2. 断点命中,查看 Registers 面板中 v1 的值;
  3. 若 v1 = 0x0,说明服务端下发的是普通用户 → 客户端确实按真实状态判断;
  4. 若 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:算法还原的用途(正向价值)

还原登录加密本不是为了伪造登录,它的正当用途是:

  1. 接口自动化测试:构造合法的签名参数,做批量接口测试与回归;
  2. 回归验证:确认客户端加密实现是否符合预期强度;
  3. 合规审计:检查是否存在"客户端硬编码密钥"这一设计缺陷——硬编码密钥意味着任何拿到 APK 的人都能解出通信内容;
  4. 安全报告:把"密钥硬编码在客户端"作为一条明确的风险项提交给厂商。

八、动态调试常见问题

问题 原因 处理
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、检测调试端口、检测常见调试工具进程名。

应对思路有两个方向:

  1. 静态清除:反编译后定位反调试代码(搜 ptrace、TracerPid、isDebuggerConnected、Debug.isDebuggerConnected),删掉或强制返回 false,再重打包;
  2. 运行时绕过:用 LSPosed 模块或 Frida 在运行时 Hook 这些检测方法——这条路更健壮,因为不重打包、不受签名校验影响(详见另一篇 LSPosed 与 HOOK 实战)。

九、安全与合规边界

本文所有技术内容仅适用于获得合法授权的测试场景——你自己的设备、你所在组织书面授权的目标、以及公开的测试靶场。

  • 动态调试会附加到目标进程并读取其内存,属于侵入性操作;对未授权目标实施可能违反《网络安全法》《数据安全法》及目标平台服务条款。
  • 还原出的密钥、算法、令牌属于凭据类敏感信息,必须妥善保管,测试结束后按流程销毁。
  • 代码中出现的手机号、用户 ID、令牌等均为脱敏占位符;实际调试中接触到的真实数据应按最小必要原则处理。
  • 客户端逻辑修改不会带来真实权益,其安全价值在于暴露"服务端信任客户端"这一设计缺陷,应通过厂商漏洞报送渠道反馈。

十、参考链接

工具与教程

资料索引


附:命令与流程速查卡

# —— 连接模拟器 ——
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 → 回编译 → 签名 → 安装
posted @ 2026-09-23 21:01  xsec  阅读(37)  评论(0)    收藏  举报