【Android逆向工程】第4章:静态分析工具实战(apktool + jadx)

128 阅读22分钟

第4章:静态分析工具实战(apktool + jadx)

目录


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 等)
    ↓
输出目录(包含所有可编辑文件)

详细步骤:

  1. ZIP 解压

    • 使用 Java 的 ZIP API 解压 APK
    • 提取所有文件到临时目录
  2. AndroidManifest.xml 解码

    • 读取二进制 AXML 格式
    • 解析字符串池和资源 ID
    • 转换为可读的 XML 格式
  3. resources.arsc 解析

    • 解析资源索引表
    • 提取资源类型、名称、值
    • 生成可编辑的资源文件
  4. DEX 反编译

    • 使用 baksmali 将 DEX 反编译为 Smali
    • 处理类、方法、字段定义
    • 生成 .smali 文件
  5. 资源文件处理

    • 解码 9-patch 图片
    • 处理 XML 布局文件
    • 提取 drawable、values 等资源

4.1.3 资源混淆处理

资源混淆问题:

某些加固工具会对资源进行混淆:

  • 资源名称混淆(如 res/layout/a.xml
  • 资源 ID 重新映射
  • resources.arsc 结构修改

apktool 的处理方法:

  1. 资源 ID 修复

    • 分析 resources.arsc 结构
    • 重建资源 ID 映射表
    • 修复引用关系
  2. 资源名称还原

    • 从 resources.arsc 提取原始名称
    • 恢复文件目录结构
    • 修复 XML 中的资源引用
  3. 9-patch 图片处理

    • 识别 9-patch 图片格式
    • 提取 padding 信息
    • 正确解码和编码

4.1.4 回编译流程

apktool 的重新编译(回编译)流程:

反编译目录
    ↓
1. 编译 Smali → DEX
    ↓
2. 编码 XML → AXML
    ↓
3. 编码 resources.arsc
    ↓
4. 处理资源文件
    ↓
5. 打包为 ZIP (APK)
    ↓
输出 APK 文件

关键步骤:

  1. Smali 编译

    • 使用 smali 工具编译 .smali 文件
    • 合并为 classes.dex
    • 验证 DEX 格式
  2. 资源编码

    • 将 XML 编码为 AXML
    • 重建 resources.arsc
    • 更新资源 ID
  3. APK 打包

    • 创建 ZIP 结构
    • 添加所有文件
    • 生成 APK

4.2 apktool 使用详解

4.2.1 apktool 安装

Windows 安装:

  1. 下载 apktool:

  2. 将两个文件放到同一目录(如 C:\tools\apktool\

  3. 添加到系统 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不反编译 DEXapktool 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: ...

解决方案:

  1. 检查 XML 文件格式是否正确
  2. 检查资源 ID 是否冲突
  3. 验证资源引用是否正确

问题 2:Smali 语法错误

症状:

Exception in thread "main" brut.androlib.AndrolibException: ...

解决方案:

  1. 检查 Smali 语法是否正确
  2. 检查寄存器使用是否超出范围
  3. 验证方法签名是否正确

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 源码

关键步骤:

  1. DEX 解析

    • 读取 DEX Header
    • 解析字符串表、类型表、方法表
    • 提取字节码指令
  2. 控制流分析

    • 构建基本块(Basic Block)
    • 分析跳转关系
    • 构建控制流图
  3. 类型推断

    • 分析变量类型
    • 推断方法返回类型
    • 恢复泛型信息(如果可能)
  4. 代码生成

    • 将指令序列转换为 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 安装:

  1. 下载 jadx-1.4.7.zip
  2. 解压到任意目录(如 C:\tools\jadx\
  3. 运行 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反编译失败时使用 fallbackjadx -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+BF3
  • 查找引用: 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:搜索字符串

步骤:

  1. 在 jadx 中使用全局搜索(Ctrl+Shift+F

  2. 搜索关键字符串:

    • 错误消息:"Login Failed"、"Invalid Password"
    • API 端点:"/api/login"、"api.example.com"
    • 常量值:"admin"、"secret"
  3. 查看搜索结果,定位相关代码

示例:

// 搜索 "Login Failed"
// 找到以下代码:
if (!checkPassword(password)) {
    showMessage("Login Failed");
    return;
}
// 可以定位到验证逻辑
方法 2:追踪方法调用链

步骤:

  1. 找到入口方法(如 onCreateonClick

  2. 追踪方法调用:

    • 查看调用了哪些方法
    • 分析参数传递
    • 理解返回值处理
  3. 构建调用链图

示例调用链:

onCreate()
    ↓
checkPassword()
    ↓
encrypt()
    ↓
sendToServer()
方法 3:分析异常处理

步骤:

  1. 查找 try-catch 块
  2. 分析捕获的异常类型
  3. 理解错误处理逻辑

示例:

try {
    String result = api.login(username, password);
    // 成功处理
} catch (NetworkException e) {
    // 网络错误处理
} catch (AuthException e) {
    // 认证错误处理
}

4.5.2 追踪数据流

变量赋值追踪

步骤:

  1. 找到关键变量(如 passwordtoken
  2. 追踪变量的赋值:
    • 从哪里获取(用户输入、网络请求、本地存储)
    • 经过哪些处理(加密、编码、转换)
    • 最终如何使用(验证、发送、存储)

示例:

// 1. 获取用户输入
String input = editText.getText().toString();

// 2. 加密处理
String encrypted = encrypt(input);

// 3. 发送到服务器
api.login(username, encrypted);

// 追踪流程:input → encrypt() → encrypted → api.login()
参数传递追踪

步骤:

  1. 找到方法调用
  2. 追踪参数来源:
    • 参数从哪里来
    • 经过哪些处理
    • 如何传递

示例:

// 方法调用
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;
    }
}

对比分析:

  • ✅ 循环逻辑完全一致
  • ✅ 变量作用域正确
  • ⚠️ 变量名全部改变(resulti, arrayiArr, ii2

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);
    }
}

对比分析:

  • ✅ 逻辑正确还原
  • ❌ 类名和方法名仍然是混淆后的名称
  • ✅ 字符串常量正确还原(可以用于分析)

改进方法:

  1. 结合上下文分析

    • 查看类的使用场景
    • 分析方法功能
    • 推断原始名称
  2. 字符串分析

    • 查找字符串常量
    • 分析字符串用途
    • 推断类和方法功能
  3. 重命名(在 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:搜索字符串

  1. 在 jadx 中按 Ctrl+Shift+F 打开全局搜索
  2. 搜索 "Login Failed"
  3. 找到相关代码位置

方法 2:分析代码结构

  1. 打开 LoginActivity.java
  2. 找到 onCreate 方法
  3. 查看 onClick 监听器
  4. 找到 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:分析校验流程

验证逻辑分析:

  1. 输入检查: 检查用户名和密码是否为 null
  2. 用户名验证: username.equals("admin")
  3. 密码验证: password.equals("admin123")
  4. 结果返回: 两个条件都满足才返回 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",说明安装成功

测试验证:

  1. 启动应用
  2. 输入任意用户名和密码(如 "test" / "test")
  3. 点击登录按钮
  4. 预期结果: 应该显示 "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 冲突
  • 特殊字符未转义

解决方案:

  1. 检查 XML 格式:
# 验证 XML 格式
xmllint --noout login_decompiled/res/values/strings.xml
  1. 修复常见错误:

    • 检查标签是否闭合
    • 检查特殊字符(<, >, &)是否转义
    • 检查资源 ID 是否重复
  2. 使用 --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 语法错误
  • 寄存器使用超出范围
  • 方法签名错误

解决方案:

  1. 检查 Smali 语法:

    • 验证指令格式
    • 检查寄存器编号
    • 验证方法签名
  2. 查看详细错误信息:

# apktool 会显示具体的错误位置
# 根据错误信息定位问题
  1. 逐步排查:
    • 先注释掉修改的代码
    • 逐步添加修改
    • 定位问题代码
问题 3:反编译失败 - 资源混淆

症状:

W: Could not decode file, replacing by FALSE value: ...

原因:

  • 资源被混淆
  • 缺少框架文件

解决方案:

  1. 安装框架文件:
# 从设备提取框架
adb pull /system/framework/framework-res.apk

# 安装框架
apktool if framework-res.apk

# 使用框架反编译
apktool d -p framework app.apk
  1. 使用 -r 参数跳过资源:
apktool d -r app.apk -o output

4.8.2 jadx 相关问题

问题 1:反编译代码不完整

症状: 反编译的代码中有很多 /* unknown */ 或方法体为空

原因:

  • 代码被混淆
  • 控制流复杂
  • jadx 无法完全还原

解决方案:

  1. 结合 Smali 分析:

    • 使用 apktool 反编译查看 Smali
    • 理解真实逻辑
    • 手动还原代码
  2. 使用 --show-bad-code 参数:

jadx --show-bad-code app.apk
  1. 使用 --deobf 参数(反混淆):
jadx --deobf app.apk
问题 2:GUI 无法启动

症状:

Exception in thread "main" java.awt.HeadlessException

原因:

  • 没有图形界面环境
  • Java 版本不支持 GUI

解决方案:

  1. 使用命令行版本:
jadx -d output app.apk
  1. 检查 Java 版本:
java -version
# 确保使用支持 GUI 的 Java 版本

4.8.3 签名和安装问题

问题 1:APK 签名后无法安装

症状:

Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES]

原因:

  • APK 未正确签名
  • 签名文件损坏

解决方案:

  1. 使用 apksigner 验证签名:
apksigner verify --verbose app.apk
  1. 重新签名:
# 确保使用正确的密钥库和别名
apksigner sign --ks keystore.jks --ks-key-alias alias app.apk
问题 2:安装时签名冲突

症状:

Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE]

原因:

  • 新 APK 的签名与已安装版本不同

解决方案:

  1. 卸载旧版本:
adb uninstall com.example.app
  1. 或使用相同签名:
# 使用原始 APK 的签名信息重新签名

4.8.4 代码修改问题

问题:修改后应用崩溃

症状: 应用启动后立即崩溃

原因:

  • Smali 语法错误
  • 寄存器使用错误
  • 逻辑修改不完整

解决方案:

  1. 查看 logcat 日志:
adb logcat | grep -i error
  1. 检查修改的代码:

    • 验证语法正确性
    • 检查寄存器使用
    • 确保逻辑完整
  2. 逐步回退修改:

    • 先恢复原始代码
    • 逐步添加修改
    • 定位问题代码

4.9 本章总结

4.9.1 知识点回顾

  1. apktool 工作原理

    • APK 解包和回编译流程
    • 资源混淆处理
    • Smali 代码生成
  2. jadx 反编译原理

    • DEX → Java 转换算法
    • 反编译准确性分析
    • 代码还原的局限性
  3. 工具使用技巧

    • apktool 命令详解
    • jadx GUI 和命令行使用
    • 资源文件分析
  4. 代码阅读技巧

    • 识别关键逻辑
    • 追踪数据流
    • 理解混淆代码

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 应用的安全机制,了解应用的安全风险点。