目录
- [3.1 Java 字节码与 DEX 字节码](#3.1 Java 字节码与 DEX 字节码)
- [3.2 Smali 语法基础](#3.2 Smali 语法基础)
- [3.3 Smali 指令集详解](#3.3 Smali 指令集详解)
- [3.4 Smali 中的类、方法和字段](#3.4 Smali 中的类、方法和字段)
- [3.5 工具使用:字节码转换与分析](#3.5 工具使用:字节码转换与分析)
- [3.6 代码对照:Java → 字节码 → Smali](#3.6 代码对照:Java → 字节码 → Smali)
- [3.7 实战案例:修改 Smali 代码绕过验证](#3.7 实战案例:修改 Smali 代码绕过验证)
- [3.8 常见问题与解决方案](#3.8 常见问题与解决方案)
3.1 Java 字节码与 DEX 字节码
3.1.1 Java 字节码(.class)简介
Java 字节码是 Java 源代码编译后的中间表示形式,运行在 Java 虚拟机(JVM)上。
编译流程:
Java 源码 (.java)
↓ javac
Java 字节码 (.class)
↓ JVM
机器码执行
Java 字节码特点:
- 平台无关的中间代码
- 基于栈的虚拟机(Stack-based VM)
- 指令集相对简单
- 面向对象设计
3.1.2 DEX 字节码(.dex)简介
DEX(Dalvik Executable)是 Android 平台上的字节码格式,针对移动设备进行了优化。
转换流程:
Java 源码 (.java)
↓ javac
Java 字节码 (.class)
↓ dx/d8
DEX 字节码 (.dex)
↓ Dalvik/ART VM
机器码执行
DEX 字节码特点:
- 多个 .class 文件合并为一个 .dex
- 基于寄存器的虚拟机(Register-based VM)
- 指令集更紧凑
- 优化了内存使用
3.1.3 Dalvik 虚拟机与 JVM 的区别
| 特性 | JVM | Dalvik VM |
|---|---|---|
| 字节码格式 | .class | .dex |
| 虚拟机类型 | 栈式虚拟机 | 寄存器虚拟机 |
| 文件组织 | 每个类一个文件 | 多个类合并到一个文件 |
| 指令集 | 基于栈操作 | 基于寄存器操作 |
| 内存占用 | 较大 | 较小(优化后) |
| 执行方式 | 解释执行/JIT | 解释执行/JIT/AOT |
3.1.4 指令集映射关系
Java 字节码示例:
java
// Java 源码
int a = 10;
int b = 20;
int c = a + b;
对应的 Java 字节码(javap -c):
java
0: bipush 10 // 将 10 压入栈
2: istore_1 // 弹出栈顶值,存储到局部变量 1 (a)
3: bipush 20 // 将 20 压入栈
5: istore_2 // 弹出栈顶值,存储到局部变量 2 (b)
6: iload_1 // 加载局部变量 1 (a) 到栈
7: iload_2 // 加载局部变量 2 (b) 到栈
8: iadd // 弹出两个值,相加,结果压入栈
9: istore_3 // 弹出栈顶值,存储到局部变量 3 (c)
对应的 Smali 代码:
smali
const/16 v0, 0xa # 将 10 加载到寄存器 v0
const/16 v1, 0x14 # 将 20 加载到寄存器 v1
add-int v2, v0, v1 # v2 = v0 + v1
关键区别:
- JVM:使用栈,需要 push/pop 操作
- Dalvik:使用寄存器,直接操作寄存器
3.2 Smali 语法基础
3.2.1 寄存器系统
Smali 使用寄存器来存储局部变量和方法参数,这是与 Java 字节码最大的区别。
寄存器命名规则
局部变量寄存器:
v0,v1,v2, ...vN:局部变量寄存器- 从 0 开始编号
- 通常用于存储方法内的局部变量
参数寄存器:
p0,p1,p2, ...pN:参数寄存器p0在非静态方法中通常是this引用p1,p2, ... 是方法的实际参数
寄存器数量限制:
- 单个方法最多可以使用 65536 个寄存器(理论值)
- 实际使用中,寄存器数量受方法复杂度限制
- 寄存器数量过多会导致编译失败
寄存器使用示例
Java 代码:
java
public int add(int a, int b) {
int result = a + b;
return result;
}
对应的 Smali 代码:
smali
.method public add(II)I
.registers 4 # 使用 4 个寄存器
# 寄存器分配:
# p0 = this (非静态方法的 this 引用)
# p1 = a (第一个参数)
# p2 = b (第二个参数)
# v0 = result (局部变量)
.prologue
.line 1
add-int v0, p1, p2 # v0 = p1 + p2
return v0 # 返回 v0
.end method
⚠️ 重要提示:
- 参数寄存器从
p0开始,不是p1 - 在非静态方法中,
p0是this,实际参数从p1开始 - 在静态方法中,参数从
p0开始
3.2.2 数据类型
基本类型
| Java 类型 | Smali 类型 | 大小 | 说明 |
|---|---|---|---|
boolean |
Z |
1 字节 | 布尔值 |
byte |
B |
1 字节 | 字节 |
short |
S |
2 字节 | 短整型 |
char |
C |
2 字节 | 字符 |
int |
I |
4 字节 | 整型 |
long |
J |
8 字节 | 长整型 |
float |
F |
4 字节 | 单精度浮点 |
double |
D |
8 字节 | 双精度浮点 |
void |
V |
- | 无返回值 |
引用类型
类类型:
smali
Ljava/lang/String; # String 类
Landroid/app/Activity; # Activity 类
数组类型:
smali
[I # int[]
[[I # int[][]
[Ljava/lang/String; # String[]
完整类型示例:
java
// Java 代码
String[] names;
int[][] matrix;
smali
# Smali 代码
.field names:[Ljava/lang/String;
.field matrix:[[I
3.2.3 方法调用约定
Smali 中有多种方法调用指令,用于不同的调用场景。
invoke-virtual(虚方法调用)
用途: 调用实例方法,支持多态
语法:
smali
invoke-virtual {参数寄存器}, 类名;->方法名(参数类型)返回类型
示例:
java
// Java 代码
String str = "Hello";
int len = str.length();
smali
# Smali 代码
const-string v0, "Hello"
invoke-virtual {v0}, Ljava/lang/String;->length()I
move-result v1 # 将返回值存储到 v1
invoke-static(静态方法调用)
用途: 调用静态方法
语法:
smali
invoke-static {参数寄存器}, 类名;->方法名(参数类型)返回类型
示例:
java
// Java 代码
int max = Math.max(10, 20);
smali
# Smali 代码
const/16 v0, 0xa # 10
const/16 v1, 0x14 # 20
invoke-static {v0, v1}, Ljava/lang/Math;->max(II)I
move-result v2 # max 结果存储到 v2
invoke-direct(直接方法调用)
用途: 调用构造函数、私有方法、final 方法
语法:
smali
invoke-direct {参数寄存器}, 类名;->方法名(参数类型)返回类型
示例:
java
// Java 代码
String str = new String("Hello");
smali
# Smali 代码
new-instance v0, Ljava/lang/String;
const-string v1, "Hello"
invoke-direct {v0, v1}, Ljava/lang/String;-><init>(Ljava/lang/String;)V
invoke-interface(接口方法调用)
用途: 调用接口方法
语法:
smali
invoke-interface {参数寄存器}, 接口名;->方法名(参数类型)返回类型
invoke-super(父类方法调用)
用途: 调用父类方法
语法:
smali
invoke-super {参数寄存器}, 父类名;->方法名(参数类型)返回类型
调用指令对比表:
| 指令 | 用途 | 多态支持 | 性能 |
|---|---|---|---|
invoke-virtual |
实例方法 | ✅ | 较慢 |
invoke-static |
静态方法 | ❌ | 快 |
invoke-direct |
构造函数/私有方法 | ❌ | 快 |
invoke-interface |
接口方法 | ✅ | 最慢 |
invoke-super |
父类方法 | ❌ | 快 |
3.2.4 条件跳转指令
条件跳转指令用于实现 if-else、循环等控制流。
基本条件跳转
相等比较:
smali
if-eq vA, vB, :label # if (vA == vB) goto label
if-ne vA, vB, :label # if (vA != vB) goto label
大小比较:
smali
if-lt vA, vB, :label # if (vA < vB) goto label
if-le vA, vB, :label # if (vA <= vB) goto label
if-gt vA, vB, :label # if (vA > vB) goto label
if-ge vA, vB, :label # if (vA >= vB) goto label
零值比较:
smali
if-eqz vA, :label # if (vA == 0) goto label
if-nez vA, :label # if (vA != 0) goto label
if-ltz vA, :label # if (vA < 0) goto label
if-lez vA, :label # if (vA <= 0) goto label
if-gtz vA, :label # if (vA > 0) goto label
if-gez vA, :label # if (vA >= 0) goto label
示例:
java
// Java 代码
if (a == b) {
return true;
} else {
return false;
}
smali
# Smali 代码
if-eq p1, p2, :equal # if (a == b) goto equal
const/4 v0, 0x0 # v0 = false
return v0 # return false
:equal
const/4 v0, 0x1 # v0 = true
return v0 # return true
3.3 Smali 指令集详解
3.3.1 invoke-* 系列(方法调用)
invoke-virtual
完整示例:
java
// Java 代码
public void test() {
String str = "Hello";
int len = str.length();
System.out.println(len);
}
smali
.method public test()V
.registers 3
.prologue
const-string v0, "Hello" # v0 = "Hello"
invoke-virtual {v0}, Ljava/lang/String;->length()I
move-result v1 # v1 = str.length()
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(I)V
return-void
.end method
invoke-static
java
// Java 代码
int result = Math.max(10, 20);
smali
const/16 v0, 0xa # v0 = 10
const/16 v1, 0x14 # v1 = 20
invoke-static {v0, v1}, Ljava/lang/Math;->max(II)I
move-result v2 # v2 = Math.max(10, 20)
3.3.2 if-* 系列(条件判断)
完整 if-else 示例
java
// Java 代码
public int compare(int a, int b) {
if (a > b) {
return 1;
} else if (a < b) {
return -1;
} else {
return 0;
}
}
smali
.method public compare(II)I
.registers 3
.param p1, "a" # I
.param p2, "b" # I
.prologue
if-gt p1, p2, :check_less # if (a > b) goto check_less
const/4 v0, 0x1 # v0 = 1
return v0 # return 1
:check_less
if-lt p1, p2, :equal # if (a < b) goto equal
const/4 v0, -0x1 # v0 = -1
return v0 # return -1
:equal
const/4 v0, 0x0 # v0 = 0
return v0 # return 0
.end method
3.3.3 move-* 系列(数据移动)
常用 move 指令:
smali
move vA, vB # vA = vB (32位)
move-wide vA, vB # vA = vB (64位,用于 long/double)
move-object vA, vB # vA = vB (对象引用)
move-result vA # vA = 方法返回值 (32位)
move-result-wide vA # vA = 方法返回值 (64位)
move-result-object vA # vA = 方法返回值 (对象)
move-exception vA # vA = 异常对象
示例:
java
// Java 代码
int a = 10;
int b = a;
int c = getValue();
smali
const/16 v0, 0xa # v0 = 10 (a)
move v1, v0 # v1 = v0 (b = a)
invoke-static {}, Lcom/example/Test;->getValue()I
move-result v2 # v2 = getValue() (c)
3.3.4 const-* 系列(常量加载)
常用 const 指令:
smali
const/4 vA, #+B # vA = 符号扩展的 4 位立即数 (-8 到 7)
const/16 vA, #+B # vA = 符号扩展的 16 位立即数 (-32768 到 32767)
const vA, #+B # vA = 32 位立即数
const-wide/16 vA, #+B # vA = 符号扩展的 16 位立即数 (long)
const-wide/32 vA, #+B # vA = 符号扩展的 32 位立即数 (long)
const-wide vA, #+B # vA = 64 位立即数 (long)
const/high16 vA, #+B # vA = 0xBBBB0000 (高16位)
const-string vA, string # vA = 字符串引用
const-class vA, type # vA = 类对象引用
示例:
java
// Java 代码
int a = 10;
int b = 1000;
long c = 100000L;
String str = "Hello";
smali
const/16 v0, 0xa # v0 = 10
const/16 v1, 0x3e8 # v1 = 1000
const-wide/32 v2, 0x186a0 # v2 = 100000L
const-string v4, "Hello" # v4 = "Hello"
3.3.5 其他常用指令
算术运算
smali
add-int vA, vB, vC # vA = vB + vC
sub-int vA, vB, vC # vA = vB - vC
mul-int vA, vB, vC # vA = vB * vC
div-int vA, vB, vC # vA = vB / vC
rem-int vA, vB, vC # vA = vB % vC
逻辑运算
smali
and-int vA, vB, vC # vA = vB & vC
or-int vA, vB, vC # vA = vB | vC
xor-int vA, vB, vC # vA = vB ^ vC
shl-int vA, vB, vC # vA = vB << vC
shr-int vA, vB, vC # vA = vB >> vC
ushr-int vA, vB, vC # vA = vB >>> vC
数组操作
smali
new-array vA, vB, type # vA = new type[vB]
array-length vA, vB # vA = vB.length
aget vA, vB, vC # vA = vB[vC]
aput vA, vB, vC # vB[vC] = vA
字段操作
smali
iget vA, vB, field # vA = vB.field
iput vA, vB, field # vB.field = vA
sget vA, field # vA = static_field
sput vA, field # static_field = vA
3.4 Smali 中的类、方法和字段
3.4.1 类定义
Java 代码:
java
package com.example;
public class MyClass {
// ...
}
Smali 代码:
smali
.class public Lcom/example/MyClass;
.super Ljava/lang/Object;
.source "MyClass.java"
类修饰符:
smali
.class public final Lcom/example/MyClass; # public final class
.class public abstract Lcom/example/MyClass; # public abstract class
.class public Lcom/example/MyClass; # public class
3.4.2 方法定义
Java 代码:
java
public int add(int a, int b) {
return a + b;
}
Smali 代码:
smali
.method public add(II)I
.registers 4
.param p1, "a" # I
.param p2, "b" # I
.prologue
.line 1
add-int v0, p1, p2
return v0
.end method
方法修饰符:
| Java 修饰符 | Smali 表示 |
|---|---|
public |
.method public |
private |
.method private |
protected |
.method protected |
static |
.method public static |
final |
.method public final |
synchronized |
.method public synchronized |
3.4.3 字段定义
Java 代码:
java
public class MyClass {
private int value;
public static String name;
}
Smali 代码:
smali
.class public Lcom/example/MyClass;
.super Ljava/lang/Object;
# 实例字段
.field private value:I
# 静态字段
.field public static name:Ljava/lang/String;
字段修饰符:
| Java 修饰符 | Smali 表示 |
|---|---|
public |
.field public |
private |
.field private |
protected |
.field protected |
static |
.field public static |
final |
.field public final |
3.4.4 异常处理
Java 代码:
java
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
e.printStackTrace();
}
Smali 代码:
smali
.method public test()V
.registers 3
.prologue
.line 1
:try_start_0
const/16 v0, 0xa
const/4 v1, 0x0
div-int v0, v0, v1
:try_end_0
.catch Ljava/lang/ArithmeticException; {:try_start_0 .. :try_end_0} :catch_0
return-void
:catch_0
move-exception v0
invoke-virtual {v0}, Ljava/lang/ArithmeticException;->printStackTrace()V
return-void
.end method
⚠️ 重要提示:
try_start_X和try_end_X标记 try 块的开始和结束catch指令指定捕获的异常类型和处理位置move-exception将异常对象移动到寄存器
3.5 工具使用:字节码转换与分析
3.5.1 使用 javac 编译 Java 源码
基本用法:
bash
# 编译单个文件
javac HelloWorld.java
# 编译多个文件
javac *.java
# 指定输出目录
javac -d build/ src/**/*.java
# 指定 classpath
javac -cp libs/*.jar src/**/*.java
示例:
bash
# 创建测试文件
cat > Test.java << 'EOF'
public class Test {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
EOF
# 编译
javac Test.java
# 查看生成的 .class 文件
ls -la Test.class
3.5.2 使用 dx/d8 转换为 DEX
dx 工具(旧版,Android SDK Build Tools < 28):
bash
# 转换单个 .class 文件
dx --dex --output=classes.dex Test.class
# 转换整个目录
dx --dex --output=classes.dex build/classes/
# 包含外部库
dx --dex --output=classes.dex --libs=android.jar build/classes/
d8 工具(新版,Android SDK Build Tools >= 28):
bash
# 转换 .class 文件
d8 Test.class --output .
# 转换整个目录
d8 build/classes/**/*.class --output .
# 指定 Android API 级别
d8 --lib android.jar --output . build/classes/**/*.class
验证 DEX 文件:
bash
# 使用 dexdump 查看 DEX 内容
dexdump -d classes.dex | head -50
3.5.3 使用 baksmali 反编译为 Smali
安装 baksmali:
bash
# 下载 baksmali
wget https://github.com/JesusFreke/smali/releases/download/v2.5.2/baksmali-2.5.2.jar
# 或使用 Homebrew (macOS)
brew install smali
基本用法:
bash
# 反编译 DEX 文件
java -jar baksmali-2.5.2.jar d classes.dex -o smali_output/
# 指定 API 级别(重要!)
java -jar baksmali-2.5.2.jar d classes.dex -o smali_output/ --api-level 28
# 反编译 APK 中的 DEX
java -jar baksmali-2.5.2.jar d app.apk -o smali_output/
查看反编译结果:
bash
# 查看目录结构
tree smali_output/
# 查看特定类的 Smali 代码
cat smali_output/com/example/Test.smali
3.5.4 使用 smali 编译回 DEX
基本用法:
bash
# 编译 Smali 目录为 DEX
java -jar smali-2.5.2.jar a smali_output/ -o classes_new.dex
# 指定 API 级别
java -jar smali-2.5.2.jar a smali_output/ -o classes_new.dex --api-level 28
验证编译结果:
bash
# 使用 dexdump 验证
dexdump -d classes_new.dex | head -50
3.5.5 使用文本编辑器阅读和修改 Smali
推荐编辑器:
- VS Code + Smali 语法高亮插件
- Sublime Text + Smali 插件
- Android Studio(内置 Smali 支持)
VS Code 配置:
json
// .vscode/settings.json
{
"files.associations": {
"*.smali": "smali"
}
}
安装 Smali 插件:
- 打开 VS Code
- 搜索 "Smali" 插件
- 安装 "Smali" 或 "Smali Language Support"
3.6 代码对照:Java → 字节码 → Smali
3.6.1 示例 1:字符串拼接
Java 源码:
java
public class StringTest {
public static String concat(String a, String b) {
return a + b;
}
}
Java 字节码(javap -c):
java
public static java.lang.String concat(java.lang.String, java.lang.String);
Code:
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: aload_0
8: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
11: aload_1
12: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: invokevirtual #5 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
18: areturn
Smali 代码:
smali
.class public LStringTest;
.super Ljava/lang/Object;
.method public static concat(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
.registers 4
.param p0, "a" # Ljava/lang/String;
.param p1, "b" # Ljava/lang/String;
.prologue
.line 3
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v0, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
return-object v0
.end method
逐行解释:
smali
# 1. 创建 StringBuilder 实例
new-instance v0, Ljava/lang/StringBuilder;
# v0 = new StringBuilder()
# 2. 调用构造函数
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
# StringBuilder(v0).<init>()
# 3. 调用 append 方法,添加第一个字符串
invoke-virtual {v0, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
# v0.append(p0)
move-result-object v0
# v0 = 返回值(StringBuilder 对象)
# 4. 调用 append 方法,添加第二个字符串
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
# v0.append(p1)
move-result-object v0
# v0 = 返回值
# 5. 调用 toString 方法
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
# v0.toString()
move-result-object v0
# v0 = 返回的字符串
# 6. 返回结果
return-object v0
# return v0
3.6.2 示例 2:条件判断
Java 源码:
java
public class ConditionTest {
public static int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}
}
Java 字节码:
java
public static int max(int, int);
Code:
0: iload_0
1: iload_1
2: if_icmple 7
5: iload_0
6: ireturn
7: iload_1
8: ireturn
Smali 代码:
smali
.method public static max(II)I
.registers 2
.param p0, "a" # I
.param p1, "b" # I
.prologue
.line 3
if-gt p0, p1, :cond_0 # if (a > b) goto cond_0
return p0 # return a
:cond_0
return p1 # return b
.end method
逐行解释:
smali
# 1. 比较 a 和 b,如果 a > b,跳转到 :cond_0
if-gt p0, p1, :cond_0
# if (p0 > p1) goto :cond_0
# 2. 如果条件不满足(a <= b),执行这里,返回 a
return p0
# return p0
# 3. 标签:如果 a > b,跳转到这里
:cond_0
# 4. 返回 b
return p1
# return p1
3.6.3 示例 3:循环
Java 源码:
java
public class LoopTest {
public static int sum(int n) {
int result = 0;
for (int i = 0; i < n; i++) {
result += i;
}
return result;
}
}
Smali 代码:
smali
.method public static sum(I)I
.registers 3
.param p0, "n" # I
.prologue
.line 3
const/4 v0, 0x0 # v0 = result = 0
const/4 v1, 0x0 # v1 = i = 0
:goto_0
if-ge v1, p0, :cond_0 # if (i < n) goto cond_0
return v0 # return result
:cond_0
add-int/2addr v0, v1 # result += i
add-int/lit8 v1, v1, 0x1 # i++
goto :goto_0 # goto 循环开始
.end method
逐行解释:
smali
# 1. 初始化 result = 0
const/4 v0, 0x0
# v0 = 0
# 2. 初始化 i = 0
const/4 v1, 0x0
# v1 = 0
# 3. 循环标签
:goto_0
# 4. 检查循环条件:if (i < n)
if-ge v1, p0, :cond_0
# if (v1 >= p0) goto :cond_0 (如果 i >= n,退出循环)
# 注意:if-ge 是 "if greater or equal",所以这里是反向逻辑
# 5. 如果循环结束,返回 result
return v0
# return v0
# 6. 循环体标签
:cond_0
# 7. 执行循环体:result += i
add-int/2addr v0, v1
# v0 = v0 + v1
# 8. i++
add-int/lit8 v1, v1, 0x1
# v1 = v1 + 1
# 9. 跳回循环开始
goto :goto_0
# goto :goto_0
3.7 实战案例:修改 Smali 代码绕过验证
3.7.1 创建测试应用
步骤 1:编写 Java 代码
创建 LoginActivity.java:
java
package com.example.test;
import android.app.Activity;
import android.os.Bundle;
import android.widget.EditText;
import android.widget.Toast;
public class LoginActivity extends Activity {
private static final String CORRECT_PASSWORD = "admin123";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 简化:直接验证
if (checkPassword("admin123")) {
showMessage("Login Success!");
} else {
showMessage("Login Failed!");
}
}
private boolean checkPassword(String password) {
return password.equals(CORRECT_PASSWORD);
}
private void showMessage(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
}
步骤 2:编译为 APK
bash
# 使用 Android Studio 或命令行编译
# 这里假设已经编译为 app.apk
3.7.2 反编译 APK
步骤 1:使用 apktool 反编译
bash
apktool d app.apk -o app_decompiled
步骤 2:查看 Smali 代码
bash
cat app_decompiled/smali/com/example/test/LoginActivity.smali
反编译后的 Smali 代码:
smali
.class public Lcom/example/test/LoginActivity;
.super Landroid/app/Activity;
.field private static final CORRECT_PASSWORD:Ljava/lang/String; = "admin123"
.method protected onCreate(Landroid/os/Bundle;)V
.registers 3
.param p1, "savedInstanceState" # Landroid/os/Bundle;
.prologue
.line 12
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
.line 15
const-string v0, "admin123"
invoke-direct {p0, v0}, Lcom/example/test/LoginActivity;->checkPassword(Ljava/lang/String;)Z
move-result v0
if-eqz v0, :cond_0
.line 16
const-string v0, "Login Success!"
invoke-direct {p0, v0}, Lcom/example/test/LoginActivity;->showMessage(Ljava/lang/String;)V
:goto_0
return-void
:cond_0
.line 18
const-string v0, "Login Failed!"
invoke-direct {p0, v0}, Lcom/example/test/LoginActivity;->showMessage(Ljava/lang/String;)V
goto :goto_0
.end method
.method private checkPassword(Ljava/lang/String;)Z
.registers 3
.param p1, "password" # Ljava/lang/String;
.prologue
.line 23
const-string v0, "admin123"
invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0
return v0
.end method
.method private showMessage(Ljava/lang/String;)V
.registers 3
.param p1, "message" # Ljava/lang/String;
.prologue
.line 27
const/4 v0, 0x0
invoke-static {p0, p1, v0}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
move-result-object v0
invoke-virtual {v0}, Landroid/widget/Toast;->show()V
return-void
.end method
3.7.3 分析验证逻辑
关键代码分析:
smali
.method private checkPassword(Ljava/lang/String;)Z
.registers 3
.param p1, "password" # Ljava/lang/String;
.prologue
.line 23
const-string v0, "admin123" # v0 = "admin123"
# 调用 password.equals("admin123")
invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0 # v0 = 返回值(true/false)
return v0 # return v0
.end method
在 onCreate 中的调用:
smali
invoke-direct {p0, v0}, Lcom/example/test/LoginActivity;->checkPassword(Ljava/lang/String;)Z
move-result v0 # v0 = checkPassword 的返回值
if-eqz v0, :cond_0 # if (v0 == 0) goto cond_0
# 如果返回 false (0),跳转到失败分支
# 如果返回 true (非0),继续执行成功分支
3.7.4 修改 Smali 代码绕过验证
方法 1:修改 checkPassword 方法,始终返回 true
smali
.method private checkPassword(Ljava/lang/String;)Z
.registers 3
.param p1, "password" # Ljava/lang/String;
.prologue
.line 23
# 原始代码(注释掉)
# const-string v0, "admin123"
# invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
# move-result v0
# return v0
# 修改:直接返回 true
const/4 v0, 0x1 # v0 = true
return v0 # return true
.end method
方法 2:修改条件判断,反转逻辑
在 onCreate 方法中:
smali
# 原始代码
if-eqz v0, :cond_0 # if (v0 == 0) goto cond_0
# 修改为:始终跳转到成功分支
# if-eqz v0, :cond_0
goto :cond_1 # 直接跳转到成功分支(需要添加标签)
# 或者修改条件判断
if-nez v0, :cond_0 # if (v0 != 0) goto cond_0 (反转逻辑)
方法 3:直接修改 onCreate,移除验证
smali
.method protected onCreate(Landroid/os/Bundle;)V
.registers 3
.param p1, "savedInstanceState" # Landroid/os/Bundle;
.prologue
.line 12
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
# 移除所有验证代码,直接显示成功消息
const-string v0, "Login Success!"
invoke-direct {p0, v0}, Lcom/example/test/LoginActivity;->showMessage(Ljava/lang/String;)V
return-void
.end method
3.7.5 回编译并测试
步骤 1:回编译 APK
bash
apktool b app_decompiled -o app_modified.apk
步骤 2:签名 APK
bash
# 生成密钥库(如果还没有)
keytool -genkey -v -keystore my-release-key.jks \
-keyalg RSA -keysize 2048 -validity 10000 \
-alias my-key-alias
# 签名 APK
apksigner sign --ks my-release-key.jks \
--ks-key-alias my-key-alias \
app_modified.apk
或者使用 jarsigner(旧方法):
bash
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \
-keystore my-release-key.jks \
app_modified.apk my-key-alias
步骤 3:对齐 APK(可选但推荐)
bash
zipalign -v 4 app_modified.apk app_modified_aligned.apk
步骤 4:安装测试
bash
# 卸载旧版本(如果已安装)
adb uninstall com.example.test
# 安装修改后的 APK
adb install app_modified_aligned.apk
# 运行应用,验证绕过效果
3.7.6 验证修改效果
预期结果:
- ✅ 无论输入什么密码,都应该显示 "Login Success!"
- ✅ checkPassword 方法始终返回 true
- ✅ 应用正常运行,无崩溃
如果修改失败:
- 检查 Smali 语法是否正确
- 检查寄存器使用是否正确
- 检查方法签名是否匹配
- 查看 logcat 日志排查错误
3.8 常见问题与解决方案
3.8.1 寄存器相关问题
问题 1:寄存器数量限制导致编译失败
症状:
Error: Invalid register: v65536
原因:
- 寄存器数量超过了限制
- 寄存器编号错误
解决方案:
- 检查
.registers指令声明的寄存器数量 - 确保使用的寄存器编号在声明范围内
- 减少局部变量的使用
示例:
smali
# 错误:声明了 3 个寄存器,但使用了 v3
.method test()V
.registers 3
const/4 v3, 0x1 # 错误!v3 超出范围
.end method
# 正确:声明足够的寄存器
.method test()V
.registers 4
const/4 v3, 0x1 # 正确
.end method
问题 2:方法参数寄存器编号错误
症状:
方法调用时参数传递错误
原因:
- 在非静态方法中,
p0是this,参数从p1开始 - 在静态方法中,参数从
p0开始
解决方案:
smali
# 非静态方法
.method public test(Ljava/lang/String;I)V
.registers 3
.param p1, "str" # 第一个参数是 p1(p0 是 this)
.param p2, "num" # 第二个参数是 p2
.end method
# 静态方法
.method public static test(Ljava/lang/String;I)V
.registers 2
.param p0, "str" # 第一个参数是 p0
.param p1, "num" # 第二个参数是 p1
.end method
3.8.2 异常处理问题
问题:try-catch 块表示方式
Java 代码:
java
try {
// code
} catch (Exception e) {
// handle
}
Smali 表示:
smali
:try_start_0
# try 块代码
:try_end_0
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
# 正常流程继续
return-void
:catch_0
move-exception v0
# catch 块代码
return-void
⚠️ 重要提示:
try_start_X和try_end_X必须配对catch指令必须在try_end_X之后move-exception必须在 catch 块的第一条指令
3.8.3 编译和回编译问题
问题 1:baksmali 反编译失败
症状:
Error: Invalid DEX file
解决方案:
- 检查 DEX 文件是否损坏
- 使用正确的 API 级别:
--api-level 28 - 更新 baksmali 到最新版本
问题 2:smali 编译失败
症状:
Error: Invalid register
Error: Invalid instruction
解决方案:
- 检查 Smali 语法是否正确
- 检查寄存器使用是否超出范围
- 检查指令格式是否正确
- 查看详细的错误信息定位问题
问题 3:回编译后 APK 无法安装
症状:
Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES]
解决方案:
- APK 必须签名才能安装
- 使用
apksigner或jarsigner签名 - 使用
zipalign对齐 APK
bash
# 完整流程
apktool b app_decompiled -o app_unsigned.apk
apksigner sign --ks keystore.jks app_unsigned.apk
zipalign -v 4 app_unsigned.apk app_final.apk
3.8.4 代码修改问题
问题:修改后逻辑错误
症状:
应用崩溃或行为异常
解决方案:
- 仔细分析原始逻辑
- 确保修改后的逻辑完整
- 注意寄存器的一致性
- 使用 logcat 查看错误日志
调试技巧:
smali
# 添加日志输出(需要导入 Log 类)
const-string v0, "TAG"
const-string v1, "Debug message"
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
3.9 本章总结
3.9.1 知识点回顾
-
Java 字节码 vs DEX 字节码
- JVM 使用栈,Dalvik 使用寄存器
- DEX 格式更紧凑,适合移动设备
-
Smali 语法基础
- 寄存器系统(v0-vN, p0-pN)
- 数据类型表示
- 方法调用约定
-
Smali 指令集
- invoke-* 系列:方法调用
- if-* 系列:条件跳转
- move-* 系列:数据移动
- const-* 系列:常量加载
-
类、方法、字段定义
- 访问修饰符
- 异常处理
3.9.2 实践要点
- ✅ 理解寄存器系统是学习 Smali 的关键
- ✅ 掌握指令集,能够阅读 Smali 代码
- ✅ 能够修改 Smali 代码实现逻辑绕过
- ✅ 注意参数寄存器的编号规则
3.9.3 下一步学习
- 第 4 章:使用 apktool 和 jadx 进行静态分析
- 第 6 章:学习动态调试技术
- 第 7 章:使用 Frida 进行动态 Hook
附录:Smali 指令速查表
数据移动指令
| 指令 | 说明 | 示例 |
|---|---|---|
move vA, vB |
移动 32 位值 | move v0, v1 |
move-wide vA, vB |
移动 64 位值 | move-wide v0, v2 |
move-object vA, vB |
移动对象引用 | move-object v0, v1 |
move-result vA |
移动方法返回值 | move-result v0 |
常量加载指令
| 指令 | 说明 | 示例 |
|---|---|---|
const/4 vA, #+B |
加载 4 位常量 | const/4 v0, 0x5 |
const/16 vA, #+B |
加载 16 位常量 | const/16 v0, 0x100 |
const-string vA, string |
加载字符串 | const-string v0, "Hello" |
方法调用指令
| 指令 | 说明 | 示例 |
|---|---|---|
invoke-virtual |
虚方法调用 | invoke-virtual {v0}, Ljava/lang/String;->length()I |
invoke-static |
静态方法调用 | invoke-static {}, Ljava/lang/System;->currentTimeMillis()J |
invoke-direct |
直接方法调用 | invoke-direct {v0}, Ljava/lang/String;-><init>()V |
条件跳转指令
| 指令 | 说明 | 示例 |
|---|---|---|
if-eq vA, vB, :label |
相等跳转 | if-eq v0, v1, :equal |
if-ne vA, vB, :label |
不等跳转 | if-ne v0, v1, :not_equal |
if-lt vA, vB, :label |
小于跳转 | if-lt v0, v1, :less |
if-gt vA, vB, :label |
大于跳转 | if-gt v0, v1, :greater |
本章完成! 🎉
现在你已经掌握了 Smali 语法的基础知识,能够阅读和修改 Smali 代码。在下一章中,我们将学习如何使用工具进行静态分析实战。