第4章:静态分析工具实战(apktool + jadx)
目录
- 4.1 apktool 工作原理
- 4.2 apktool 使用详解
- 4.3 jadx 反编译原理
- 4.4 jadx 使用详解
- 4.5 反编译代码阅读技巧
- 4.6 代码对照:原始代码 vs 反编译代码
- 4.7 实战案例:分析登录验证 App
- 4.8 常见问题与解决方案
4.1 apktool 工作原理
4.1.1 apktool 简介
apktool 是一个用于反编译和重新编译 Android APK 文件的工具,由 Ryszard Wiśniewski 开发。它能够:
- 将 APK 解包为可读的资源文件和 Smali 代码
- 处理资源混淆和编码问题
- 重新编译修改后的文件为 APK
4.1.2 APK 解包流程
apktool 的反编译(解包)流程如下:
APK 文件
↓
1. 解析 ZIP 结构
↓
2. 提取文件到临时目录
↓
3. 解码 AndroidManifest.xml (AXML → XML)
↓
4. 解码 resources.arsc (二进制 → 可读格式)
↓
5. 反编译 DEX → Smali
↓
6. 处理资源文件(9-patch、XML 等)
↓
输出目录(包含所有可编辑文件)
详细步骤:
-
ZIP 解压
- 使用 Java 的 ZIP API 解压 APK
- 提取所有文件到临时目录
-
AndroidManifest.xml 解码
- 读取二进制 AXML 格式
- 解析字符串池和资源 ID
- 转换为可读的 XML 格式
-
resources.arsc 解析
- 解析资源索引表
- 提取资源类型、名称、值
- 生成可编辑的资源文件
-
DEX 反编译
- 使用 baksmali 将 DEX 反编译为 Smali
- 处理类、方法、字段定义
- 生成 .smali 文件
-
资源文件处理
- 解码 9-patch 图片
- 处理 XML 布局文件
- 提取 drawable、values 等资源
4.1.3 资源混淆处理
资源混淆问题:
某些加固工具会对资源进行混淆:
- 资源名称混淆(如
res/layout/a.xml) - 资源 ID 重新映射
- resources.arsc 结构修改
apktool 的处理方法:
-
资源 ID 修复
- 分析 resources.arsc 结构
- 重建资源 ID 映射表
- 修复引用关系
-
资源名称还原
- 从 resources.arsc 提取原始名称
- 恢复文件目录结构
- 修复 XML 中的资源引用
-
9-patch 图片处理
- 识别 9-patch 图片格式
- 提取 padding 信息
- 正确解码和编码
4.1.4 回编译流程
apktool 的重新编译(回编译)流程:
反编译目录
↓
1. 编译 Smali → DEX
↓
2. 编码 XML → AXML
↓
3. 编码 resources.arsc
↓
4. 处理资源文件
↓
5. 打包为 ZIP (APK)
↓
输出 APK 文件
关键步骤:
-
Smali 编译
- 使用 smali 工具编译 .smali 文件
- 合并为 classes.dex
- 验证 DEX 格式
-
资源编码
- 将 XML 编码为 AXML
- 重建 resources.arsc
- 更新资源 ID
-
APK 打包
- 创建 ZIP 结构
- 添加所有文件
- 生成 APK
4.2 apktool 使用详解
4.2.1 apktool 安装
Windows 安装:
-
下载 apktool:
- apktool.jar: ibotpeaches.github.io/Apktool/
- apktool.bat: raw.githubusercontent.com/iBotPeaches…
-
将两个文件放到同一目录(如
C:\tools\apktool\) -
添加到系统 PATH
macOS/Linux 安装:
# 使用 Homebrew (macOS)
brew install apktool
# 或手动安装
wget https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.9.0.jar
sudo mv apktool_2.9.0.jar /usr/local/bin/apktool.jar
sudo chmod +x /usr/local/bin/apktool.jar
# 创建脚本
echo '#!/bin/bash
java -jar /usr/local/bin/apktool.jar "$@"' | sudo tee /usr/local/bin/apktool
sudo chmod +x /usr/local/bin/apktool
验证安装:
apktool -version
# 应该显示:Apktool v2.9.0
4.2.2 apktool 反编译命令详解
基本反编译命令
# 基本用法
apktool d app.apk
# 指定输出目录
apktool d app.apk -o output_dir
# 强制覆盖已存在的目录
apktool d -f app.apk -o output_dir
# 不反编译资源(只反编译代码)
apktool d -s app.apk -o output_dir
# 不反编译代码(只反编译资源)
apktool d -r app.apk -o output_dir
命令参数说明
| 参数 | 说明 | 示例 |
|---|---|---|
d | 反编译(decode) | apktool d app.apk |
-f, --force | 强制覆盖输出目录 | apktool d -f app.apk |
-o, --output | 指定输出目录 | apktool d app.apk -o output |
-s, --no-src | 不反编译代码(保留 classes.dex) | apktool d -s app.apk |
-r, --no-res | 不反编译资源 | apktool d -r app.apk |
-b, --no-baksmali | 不反编译 DEX | apktool d -b app.apk |
-p, --frame-path | 指定框架文件路径 | apktool d -p framework app.apk |
--api-level | 指定 API 级别 | apktool d --api-level 28 app.apk |
反编译输出结构
output_dir/
├── AndroidManifest.xml # 解码后的清单文件
├── apktool.yml # apktool 配置文件
├── original/ # 原始文件备份
│ ├── AndroidManifest.xml
│ └── META-INF/
├── res/ # 资源文件目录
│ ├── layout/
│ ├── drawable/
│ ├── values/
│ └── ...
└── smali/ # Smali 代码目录
└── com/
└── example/
└── MainActivity.smali
处理资源混淆
安装框架文件:
# 从设备提取框架文件
adb pull /system/framework/framework-res.apk
# 安装框架文件到 apktool
apktool if framework-res.apk
# 使用框架文件反编译
apktool d -p framework app.apk
处理混淆的资源:
# 如果遇到资源混淆,尝试:
# 1. 安装正确的框架文件
apktool if framework-res.apk
# 2. 使用 -r 参数先不反编译资源
apktool d -r app.apk
# 3. 手动处理资源文件
4.2.3 apktool 回编译命令详解
基本回编译命令
# 基本用法
apktool b app_dir
# 指定输出文件
apktool b app_dir -o new_app.apk
# 使用 aapt2(推荐,更快)
apktool b --use-aapt2 app_dir -o new_app.apk
# 不检查更新
apktool b --no-crunch app_dir -o new_app.apk
命令参数说明
| 参数 | 说明 | 示例 |
|---|---|---|
b | 回编译(build) | apktool b app_dir |
-o, --output | 指定输出文件 | apktool b app_dir -o app.apk |
-f, --force-all | 强制覆盖 | apktool b -f app_dir |
--use-aapt2 | 使用 aapt2(更快) | apktool b --use-aapt2 app_dir |
--no-crunch | 不压缩图片资源 | apktool b --no-crunch app_dir |
-a, --aapt | 指定 aapt 路径 | apktool b -a /path/to/aapt app_dir |
回编译常见问题
问题 1:资源文件格式错误
症状:
W: .../res/values/strings.xml:XX: error: ...
解决方案:
- 检查 XML 文件格式是否正确
- 检查资源 ID 是否冲突
- 验证资源引用是否正确
问题 2:Smali 语法错误
症状:
Exception in thread "main" brut.androlib.AndrolibException: ...
解决方案:
- 检查 Smali 语法是否正确
- 检查寄存器使用是否超出范围
- 验证方法签名是否正确
4.2.4 apktool.yml 配置文件
apktool.yml 是 apktool 的配置文件,包含反编译和回编译的元数据。
配置文件内容:
!!brut.androlib.meta.MetaInfo
apkFileName: app.apk
compressionType: false
doNotCompress:
- resources.arsc
- png
- jpg
- jpeg
- gif
isFrameworkApk: false
packageInfo:
forcedPackageId: '127'
renameManifestPackage: null
sdkInfo:
minSdkVersion: '21'
targetSdkVersion: '33'
sharedLibrary: false
sparseResources: false
unknownFiles: {}
usesFramework:
ids:
- 1
version: 2.9.0
versionInfo:
versionCode: '1'
versionName: 1.0
重要字段说明:
apkFileName: 原始 APK 文件名sdkInfo: SDK 版本信息packageInfo: 包信息usesFramework: 使用的框架文件
4.3 jadx 反编译原理
4.3.1 jadx 简介
jadx 是一个强大的 Android 反编译工具,由 skylot 开发。它能够:
- 将 DEX 文件反编译为 Java 代码
- 提供 GUI 和命令行两种使用方式
- 支持代码搜索、跳转、导出等功能
4.3.2 DEX → Java 转换算法
jadx 的反编译流程:
DEX 文件
↓
1. 解析 DEX 结构
↓
2. 提取类、方法、字段信息
↓
3. 解析字节码指令
↓
4. 构建控制流图(CFG)
↓
5. 类型推断和变量恢复
↓
6. 生成 Java 代码
↓
Java 源码
关键步骤:
-
DEX 解析
- 读取 DEX Header
- 解析字符串表、类型表、方法表
- 提取字节码指令
-
控制流分析
- 构建基本块(Basic Block)
- 分析跳转关系
- 构建控制流图
-
类型推断
- 分析变量类型
- 推断方法返回类型
- 恢复泛型信息(如果可能)
-
代码生成
- 将指令序列转换为 Java 语句
- 恢复变量名(如果可能)
- 生成可读的 Java 代码
4.3.3 反编译准确性分析
可以准确还原的内容
✅ 结构信息
- 类、方法、字段定义
- 继承关系
- 接口实现
✅ 控制流
- if-else 语句
- 循环结构
- switch-case 语句
✅ 基本操作
- 算术运算
- 方法调用
- 字段访问
可能损失的内容
❌ 变量名
- 原始变量名丢失
- 使用
var1,var2等占位符 - 局部变量名难以恢复
❌ 注释
- 所有注释丢失
- 无法恢复原始注释
❌ 代码风格
- 格式化可能不同
- 代码结构可能改变
❌ 混淆后的代码
- 混淆的类名、方法名
- 控制流混淆
- 字符串加密
反编译质量对比
原始 Java 代码:
public class LoginActivity {
private static final String PASSWORD = "admin123";
public boolean checkPassword(String input) {
return input.equals(PASSWORD);
}
}
jadx 反编译结果:
public class LoginActivity {
private static final String PASSWORD = "admin123";
public boolean checkPassword(String str) {
return str.equals(PASSWORD);
}
}
对比分析:
- ✅ 类结构完全还原
- ✅ 方法逻辑完全还原
- ✅ 常量值正确还原
- ⚠️ 参数名从
input变为str(变量名丢失)
混淆后的代码反编译:
原始代码(混淆后):
public class a {
public boolean b(String str) {
return str.equals("admin123");
}
}
jadx 反编译结果:
public class a {
public boolean b(String str) {
return str.equals("admin123");
}
}
对比分析:
- ✅ 逻辑正确还原
- ❌ 类名和方法名仍然是混淆后的名称
- ❌ 需要结合上下文理解功能
4.4 jadx 使用详解
4.4.1 jadx 安装
下载 jadx:
访问 GitHub 发布页面:github.com/skylot/jadx…
Windows 安装:
- 下载
jadx-1.4.7.zip - 解压到任意目录(如
C:\tools\jadx\) - 运行
bin\jadx-gui.bat(GUI)或bin\jadx.bat(命令行)
macOS/Linux 安装:
# 下载并解压
wget https://github.com/skylot/jadx/releases/download/v1.4.7/jadx-1.4.7.zip
unzip jadx-1.4.7.zip -d ~/tools/
cd ~/tools/jadx/bin/
# 使用 GUI
./jadx-gui
# 使用命令行
./jadx -d output app.apk
验证安装:
jadx --version
# 应该显示:jadx version: 1.4.7
4.4.2 jadx 命令行使用
基本命令
# 基本反编译
jadx app.apk
# 指定输出目录
jadx -d output app.apk
# 指定线程数(加快速度)
jadx -d output -j 4 app.apk
# 只反编译代码,不反编译资源
jadx -d output --no-res app.apk
# 反编译为 Gradle 项目
jadx -d output -e app.apk
# 显示反编译进度
jadx -d output --show-bad-code app.apk
命令参数说明
| 参数 | 说明 | 示例 |
|---|---|---|
-d, --output-dir | 指定输出目录 | jadx -d output app.apk |
-j, --threads-count | 线程数 | jadx -j 4 app.apk |
-e, --export-gradle | 导出为 Gradle 项目 | jadx -e app.apk |
--no-res | 不反编译资源 | jadx --no-res app.apk |
--no-src | 不反编译代码 | jadx --no-src app.apk |
--show-bad-code | 显示无法反编译的代码 | jadx --show-bad-code app.apk |
--deobf | 启用反混淆 | jadx --deobf app.apk |
--deobf-usage | 反混淆使用情况 | jadx --deobf-usage app.apk |
-f, --fallback | 反编译失败时使用 fallback | jadx -f app.apk |
输出结构
output/
├── resources/ # 资源文件
│ ├── AndroidManifest.xml
│ └── res/
├── sources/ # Java 源码
│ └── com/
│ └── example/
│ └── MainActivity.java
└── .jadx/ # jadx 元数据
4.4.3 jadx GUI 界面使用
界面布局
[图示:jadx GUI 界面布局]
┌─────────────────────────────────────────┐
│ File Edit Navigate Window Help │ 菜单栏
├──────────┬──────────────────────────────┤
│ │ │
│ 包结构树 │ 代码显示区域 │
│ │ │
│ com/ │ public class MainActivity { │
│ example/│ ... │
│ Main │ } │
│ Activity│ │
│ │ │
│ │ │
├──────────┴──────────────────────────────┤
│ 搜索框: [________________] [搜索] │ 搜索栏
└─────────────────────────────────────────┘
主要功能
1. 文件浏览
- 左侧显示包结构树
- 双击类名打开源码
- 右键菜单:复制、查找引用等
2. 代码搜索
- 快捷键:
Ctrl+F(当前文件搜索) - 全局搜索:
Ctrl+Shift+F - 搜索类型:
- 类名搜索
- 方法名搜索
- 字符串搜索
- 正则表达式搜索
3. 代码跳转
- 跳转到定义:
Ctrl+B或F3 - 查找引用:
Alt+F7 - 返回:
Alt+Left - 前进:
Alt+Right
4. 代码导出
- 导出单个类: 右键 → "Save as"
- 导出整个项目: File → "Save as" → 选择 Gradle 项目
使用技巧
技巧 1:快速定位关键代码
1. 使用全局搜索(Ctrl+Shift+F)
2. 搜索关键字符串(如 "password"、"login")
3. 查看搜索结果,定位相关代码
4. 双击跳转到代码位置
技巧 2:追踪方法调用链
1. 找到目标方法
2. 右键 → "Find Usage" 或 Alt+F7
3. 查看所有调用该方法的地方
4. 分析调用链,理解代码流程
技巧 3:对比反编译代码
1. 使用 jadx 反编译得到 Java 代码
2. 使用 apktool 反编译得到 Smali 代码
3. 对比两者,理解反编译的准确性
4. 对于不准确的部分,查看 Smali 代码
4.5 反编译代码阅读技巧
4.5.1 识别关键逻辑
方法 1:搜索字符串
步骤:
-
在 jadx 中使用全局搜索(
Ctrl+Shift+F) -
搜索关键字符串:
- 错误消息:"Login Failed"、"Invalid Password"
- API 端点:"/api/login"、"api.example.com"
- 常量值:"admin"、"secret"
-
查看搜索结果,定位相关代码
示例:
// 搜索 "Login Failed"
// 找到以下代码:
if (!checkPassword(password)) {
showMessage("Login Failed");
return;
}
// 可以定位到验证逻辑
方法 2:追踪方法调用链
步骤:
-
找到入口方法(如
onCreate、onClick) -
追踪方法调用:
- 查看调用了哪些方法
- 分析参数传递
- 理解返回值处理
-
构建调用链图
示例调用链:
onCreate()
↓
checkPassword()
↓
encrypt()
↓
sendToServer()
方法 3:分析异常处理
步骤:
- 查找 try-catch 块
- 分析捕获的异常类型
- 理解错误处理逻辑
示例:
try {
String result = api.login(username, password);
// 成功处理
} catch (NetworkException e) {
// 网络错误处理
} catch (AuthException e) {
// 认证错误处理
}
4.5.2 追踪数据流
变量赋值追踪
步骤:
- 找到关键变量(如
password、token) - 追踪变量的赋值:
- 从哪里获取(用户输入、网络请求、本地存储)
- 经过哪些处理(加密、编码、转换)
- 最终如何使用(验证、发送、存储)
示例:
// 1. 获取用户输入
String input = editText.getText().toString();
// 2. 加密处理
String encrypted = encrypt(input);
// 3. 发送到服务器
api.login(username, encrypted);
// 追踪流程:input → encrypt() → encrypted → api.login()
参数传递追踪
步骤:
- 找到方法调用
- 追踪参数来源:
- 参数从哪里来
- 经过哪些处理
- 如何传递
示例:
// 方法调用
boolean result = checkPassword(encryptedPassword);
// 追踪 encryptedPassword 的来源
String password = getPasswordFromInput(); // 来源1
String encrypted = encrypt(password); // 处理1
String encryptedPassword = base64Encode(encrypted); // 处理2
// 最终传递给 checkPassword
4.5.3 理解混淆代码
识别混淆模式
类名混淆:
// 混淆后
public class a {
public void b() { }
}
// 识别方法:查看方法功能,推断类名
// 如果方法处理登录,可能是 LoginManager
方法名混淆:
// 混淆后
public void a(String str) { }
// 识别方法:
// 1. 查看方法参数和返回值
// 2. 分析方法功能
// 3. 查看调用上下文
反混淆技巧
技巧 1:字符串分析
- 查找字符串常量
- 分析字符串用途
- 推断方法功能
技巧 2:调用关系分析
- 分析方法的调用者
- 分析方法的调用目标
- 构建调用关系图
技巧 3:结合 Smali 分析
- jadx 反编译不准确时
- 查看 Smali 代码
- 理解真实逻辑
4.6 代码对照:原始代码 vs 反编译代码
4.6.1 示例 1:简单方法
原始 Java 代码:
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
apktool 反编译的 Smali 代码:
.class public LCalculator;
.super Ljava/lang/Object;
.method public add(II)I
.registers 3
.param p1, "a" # I
.param p2, "b" # I
.prologue
add-int v0, p1, p2
return v0
.end method
jadx 反编译的 Java 代码:
public class Calculator {
public int add(int i, int i2) {
return i + i2;
}
}
对比分析:
- ✅ 逻辑完全一致
- ✅ 方法签名正确
- ⚠️ 参数名从
a, b变为i, i2(变量名丢失)
4.6.2 示例 2:条件判断
原始 Java 代码:
public class Validator {
public boolean isValid(String input) {
if (input == null || input.isEmpty()) {
return false;
}
return input.length() > 5;
}
}
apktool 反编译的 Smali 代码:
.method public isValid(Ljava/lang/String;)Z
.registers 3
.param p1, "input" # Ljava/lang/String;
.prologue
if-eqz p1, :cond_0
invoke-virtual {p1}, Ljava/lang/String;->isEmpty()Z
move-result v0
if-eqz v0, :cond_0
invoke-virtual {p1}, Ljava/lang/String;->length()I
move-result v0
const/4 v1, 0x5
if-le v0, v1, :cond_1
:cond_0
const/4 v0, 0x0
return v0
:cond_1
const/4 v0, 0x1
return v0
.end method
jadx 反编译的 Java 代码:
public class Validator {
public boolean isValid(String str) {
if (str == null || str.isEmpty()) {
return false;
}
return str.length() > 5;
}
}
对比分析:
- ✅ 逻辑完全一致
- ✅ 条件判断正确还原
- ✅ 控制流正确还原
- ⚠️ 参数名从
input变为str
4.6.3 示例 3:循环结构
原始 Java 代码:
public class ArrayUtils {
public int sum(int[] array) {
int result = 0;
for (int i = 0; i < array.length; i++) {
result += array[i];
}
return result;
}
}
jadx 反编译的 Java 代码:
public class ArrayUtils {
public int sum(int[] iArr) {
int i = 0;
for (int i2 = 0; i2 < iArr.length; i2++) {
i += iArr[i2];
}
return i;
}
}
对比分析:
- ✅ 循环逻辑完全一致
- ✅ 变量作用域正确
- ⚠️ 变量名全部改变(
result→i,array→iArr,i→i2)
4.6.4 示例 4:混淆后的代码
原始 Java 代码(混淆后):
public class a {
private static final String b = "secret";
public boolean c(String str) {
return str.equals(b);
}
}
jadx 反编译结果:
public class a {
private static final String b = "secret";
public boolean c(String str) {
return str.equals(b);
}
}
对比分析:
- ✅ 逻辑正确还原
- ❌ 类名和方法名仍然是混淆后的名称
- ✅ 字符串常量正确还原(可以用于分析)
改进方法:
-
结合上下文分析
- 查看类的使用场景
- 分析方法功能
- 推断原始名称
-
字符串分析
- 查找字符串常量
- 分析字符串用途
- 推断类和方法功能
-
重命名(在 jadx GUI 中)
- 右键类名 → "Rename"
- 重命名为有意义的名称
- 帮助理解代码
4.7 实战案例:分析登录验证 App
4.7.1 创建测试应用
目标: 分析一个带登录验证的 Demo App,定位验证逻辑,理解校验流程,并修改 Smali 代码绕过验证。
测试应用代码:
package com.example.login;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class LoginActivity extends Activity {
private EditText usernameEdit;
private EditText passwordEdit;
private Button loginButton;
private static final String CORRECT_USERNAME = "admin";
private static final String CORRECT_PASSWORD = "admin123";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
usernameEdit = findViewById(R.id.username);
passwordEdit = findViewById(R.id.password);
loginButton = findViewById(R.id.login);
loginButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String username = usernameEdit.getText().toString();
String password = passwordEdit.getText().toString();
if (validateLogin(username, password)) {
showMessage("Login Success!");
} else {
showMessage("Login Failed! Invalid username or password.");
}
}
});
}
private boolean validateLogin(String username, String password) {
if (username == null || password == null) {
return false;
}
return username.equals(CORRECT_USERNAME) &&
password.equals(CORRECT_PASSWORD);
}
private void showMessage(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
}
编译为 APK:
使用 Android Studio 编译,生成 login_app.apk
4.7.2 步骤 1:使用 apktool 反编译 APK
反编译命令:
# 反编译 APK
apktool d login_app.apk -o login_decompiled
# 查看输出目录结构
tree login_decompiled/
输出结构:
login_decompiled/
├── AndroidManifest.xml
├── apktool.yml
├── res/
│ ├── layout/
│ │ └── activity_login.xml
│ └── values/
│ └── strings.xml
└── smali/
└── com/
└── example/
└── login/
└── LoginActivity.smali
查看 Smali 代码:
cat login_decompiled/smali/com/example/login/LoginActivity.smali
4.7.3 步骤 2:使用 jadx 打开 APK
启动 jadx GUI:
jadx-gui login_app.apk
界面操作:
[图示:jadx GUI 界面]
1. 左侧包结构树,展开 com.example.login
2. 双击 LoginActivity 打开源码
3. 代码显示在右侧窗口
查看反编译的 Java 代码:
在 jadx 中可以看到反编译后的代码(可能变量名不同,但逻辑一致)。
4.7.4 步骤 3:定位验证逻辑
方法 1:搜索字符串
- 在 jadx 中按
Ctrl+Shift+F打开全局搜索 - 搜索 "Login Failed"
- 找到相关代码位置
方法 2:分析代码结构
- 打开
LoginActivity.java - 找到
onCreate方法 - 查看
onClick监听器 - 找到
validateLogin方法调用
定位到的关键代码:
// jadx 反编译结果
private boolean validateLogin(String str, String str2) {
if (str == null || str2 == null) {
return false;
}
return str.equals("admin") && str2.equals("admin123");
}
4.7.5 步骤 4:分析校验流程
验证逻辑分析:
- 输入检查: 检查用户名和密码是否为 null
- 用户名验证:
username.equals("admin") - 密码验证:
password.equals("admin123") - 结果返回: 两个条件都满足才返回 true
流程图:
用户输入
↓
validateLogin()
↓
检查 null?
├─ 是 → return false
└─ 否 ↓
检查 username == "admin"?
├─ 否 → return false
└─ 是 ↓
检查 password == "admin123"?
├─ 否 → return false
└─ 是 → return true
4.7.6 步骤 5:修改 Smali 代码绕过验证
查看原始 Smali 代码:
.method private validateLogin(Ljava/lang/String;Ljava/lang/String;)Z
.registers 4
.param p1, "username" # Ljava/lang/String;
.param p2, "password" # Ljava/lang/String;
.prologue
.line 45
if-eqz p1, :cond_0 # if (username == null) goto cond_0
if-eqz p2, :cond_1 # if (password == null) goto cond_1
.line 48
const-string v0, "admin"
invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0
if-eqz v0, :cond_2 # if (!username.equals("admin")) goto cond_2
.line 49
const-string v0, "admin123"
invoke-virtual {p2, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0
if-eqz v0, :cond_2 # if (!password.equals("admin123")) goto cond_2
.line 50
const/4 v0, 0x1 # v0 = true
return v0 # return true
:cond_0
:cond_1
.line 46
const/4 v0, 0x0 # v0 = false
return v0 # return false
:cond_2
.line 48
const/4 v0, 0x0 # v0 = false
return v0 # return false
.end method
修改方案 1:直接返回 true
.method private validateLogin(Ljava/lang/String;Ljava/lang/String;)Z
.registers 4
.param p1, "username" # Ljava/lang/String;
.param p2, "password" # Ljava/lang/String;
.prologue
# 移除所有验证逻辑,直接返回 true
const/4 v0, 0x1 # v0 = true
return v0 # return true
.end method
修改方案 2:修改条件判断
.method private validateLogin(Ljava/lang/String;Ljava/lang/String;)Z
.registers 4
.param p1, "username" # Ljava/lang/String;
.param p2, "password" # Ljava/lang/String;
.prologue
.line 45
if-eqz p1, :cond_0
if-eqz p2, :cond_1
.line 48
const-string v0, "admin"
invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0
# 修改:将 if-eqz 改为 if-nez(反转逻辑)
if-nez v0, :cond_2 # if (username.equals("admin")) goto cond_2
.line 49
const-string v0, "admin123"
invoke-virtual {p2, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0
# 修改:将 if-eqz 改为 if-nez(反转逻辑)
if-nez v0, :cond_2 # if (password.equals("admin123")) goto cond_2
# 修改:这里改为返回 false(原本返回 true)
const/4 v0, 0x0
return v0
:cond_0
:cond_1
.line 46
const/4 v0, 0x0
return v0
:cond_2
# 修改:这里改为返回 true(原本返回 false)
.line 48
const/4 v0, 0x1
return v0
.end method
推荐使用方案 1(简单直接):
编辑 login_decompiled/smali/com/example/login/LoginActivity.smali,找到 validateLogin 方法,替换为:
.method private validateLogin(Ljava/lang/String;Ljava/lang/String;)Z
.registers 4
.param p1, "username" # Ljava/lang/String;
.param p2, "password" # Ljava/lang/String;
.prologue
const/4 v0, 0x1
return v0
.end method
4.7.7 步骤 6:回编译并签名 APK
回编译:
# 回编译 APK
apktool b login_decompiled -o login_modified_unsigned.apk
# 如果成功,会显示:
# I: Building apk file...
# I: Built apk file...
签名 APK:
# 方法1:使用 apksigner(推荐)
apksigner sign --ks my-release-key.jks \
--ks-key-alias my-key-alias \
login_modified_unsigned.apk
# 方法2:使用 jarsigner(旧方法)
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \
-keystore my-release-key.jks \
login_modified_unsigned.apk my-key-alias
对齐 APK(可选但推荐):
zipalign -v 4 login_modified_unsigned.apk login_modified.apk
4.7.8 步骤 7:安装测试修改后的 APK
安装 APK:
# 卸载旧版本(如果已安装)
adb uninstall com.example.login
# 安装修改后的 APK
adb install login_modified.apk
# 如果显示 "Success",说明安装成功
测试验证:
- 启动应用
- 输入任意用户名和密码(如 "test" / "test")
- 点击登录按钮
- 预期结果: 应该显示 "Login Success!"(验证已绕过)
验证结果:
- ✅ 输入错误密码也能登录成功
- ✅ 验证逻辑已被绕过
- ✅ 应用正常运行,无崩溃
4.8 常见问题与解决方案
4.8.1 apktool 相关问题
问题 1:回编译失败 - 资源文件格式错误
症状:
W: .../res/values/strings.xml:5: error: Error parsing XML: ...
Exception in thread "main" brut.androlib.AndrolibException: ...
原因:
- XML 文件格式错误
- 资源 ID 冲突
- 特殊字符未转义
解决方案:
- 检查 XML 格式:
# 验证 XML 格式
xmllint --noout login_decompiled/res/values/strings.xml
-
修复常见错误:
- 检查标签是否闭合
- 检查特殊字符(
<,>,&)是否转义 - 检查资源 ID 是否重复
-
使用 --no-crunch 参数:
apktool b --no-crunch login_decompiled -o app.apk
问题 2:回编译失败 - Smali 语法错误
症状:
Exception in thread "main" brut.androlib.AndrolibException:
Could not smali file: ...
原因:
- Smali 语法错误
- 寄存器使用超出范围
- 方法签名错误
解决方案:
-
检查 Smali 语法:
- 验证指令格式
- 检查寄存器编号
- 验证方法签名
-
查看详细错误信息:
# apktool 会显示具体的错误位置
# 根据错误信息定位问题
- 逐步排查:
- 先注释掉修改的代码
- 逐步添加修改
- 定位问题代码
问题 3:反编译失败 - 资源混淆
症状:
W: Could not decode file, replacing by FALSE value: ...
原因:
- 资源被混淆
- 缺少框架文件
解决方案:
- 安装框架文件:
# 从设备提取框架
adb pull /system/framework/framework-res.apk
# 安装框架
apktool if framework-res.apk
# 使用框架反编译
apktool d -p framework app.apk
- 使用 -r 参数跳过资源:
apktool d -r app.apk -o output
4.8.2 jadx 相关问题
问题 1:反编译代码不完整
症状:
反编译的代码中有很多 /* unknown */ 或方法体为空
原因:
- 代码被混淆
- 控制流复杂
- jadx 无法完全还原
解决方案:
-
结合 Smali 分析:
- 使用 apktool 反编译查看 Smali
- 理解真实逻辑
- 手动还原代码
-
使用 --show-bad-code 参数:
jadx --show-bad-code app.apk
- 使用 --deobf 参数(反混淆):
jadx --deobf app.apk
问题 2:GUI 无法启动
症状:
Exception in thread "main" java.awt.HeadlessException
原因:
- 没有图形界面环境
- Java 版本不支持 GUI
解决方案:
- 使用命令行版本:
jadx -d output app.apk
- 检查 Java 版本:
java -version
# 确保使用支持 GUI 的 Java 版本
4.8.3 签名和安装问题
问题 1:APK 签名后无法安装
症状:
Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES]
原因:
- APK 未正确签名
- 签名文件损坏
解决方案:
- 使用 apksigner 验证签名:
apksigner verify --verbose app.apk
- 重新签名:
# 确保使用正确的密钥库和别名
apksigner sign --ks keystore.jks --ks-key-alias alias app.apk
问题 2:安装时签名冲突
症状:
Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE]
原因:
- 新 APK 的签名与已安装版本不同
解决方案:
- 卸载旧版本:
adb uninstall com.example.app
- 或使用相同签名:
# 使用原始 APK 的签名信息重新签名
4.8.4 代码修改问题
问题:修改后应用崩溃
症状: 应用启动后立即崩溃
原因:
- Smali 语法错误
- 寄存器使用错误
- 逻辑修改不完整
解决方案:
- 查看 logcat 日志:
adb logcat | grep -i error
-
检查修改的代码:
- 验证语法正确性
- 检查寄存器使用
- 确保逻辑完整
-
逐步回退修改:
- 先恢复原始代码
- 逐步添加修改
- 定位问题代码
4.9 本章总结
4.9.1 知识点回顾
-
apktool 工作原理
- APK 解包和回编译流程
- 资源混淆处理
- Smali 代码生成
-
jadx 反编译原理
- DEX → Java 转换算法
- 反编译准确性分析
- 代码还原的局限性
-
工具使用技巧
- apktool 命令详解
- jadx GUI 和命令行使用
- 资源文件分析
-
代码阅读技巧
- 识别关键逻辑
- 追踪数据流
- 理解混淆代码
4.9.2 实践要点
- ✅ 掌握 apktool 和 jadx 的基本使用
- ✅ 能够定位关键代码逻辑
- ✅ 能够修改 Smali 代码实现绕过
- ✅ 理解反编译的准确性和局限性
4.9.3 下一步学习
- 第 5 章:了解 Android 应用安全机制
- 第 6 章:学习动态调试技术
- 第 7 章:使用 Frida 进行动态 Hook
附录:工具命令速查表
apktool 常用命令
# 反编译
apktool d app.apk -o output
# 回编译
apktool b output -o app.apk
# 安装框架
apktool if framework-res.apk
# 查看版本
apktool -version
jadx 常用命令
# 命令行反编译
jadx -d output app.apk
# 多线程反编译
jadx -d output -j 4 app.apk
# 导出 Gradle 项目
jadx -d output -e app.apk
# 启动 GUI
jadx-gui app.apk
签名工具
# apksigner 签名
apksigner sign --ks keystore.jks app.apk
# jarsigner 签名
jarsigner -keystore keystore.jks app.apk alias
# 验证签名
apksigner verify app.apk
本章完成! 🎉
现在你已经掌握了使用 apktool 和 jadx 进行静态分析的完整流程。在下一章中,我们将学习 Android 应用的安全机制,了解应用的安全风险点。