【Android逆向工程】第3章:Java 字节码与 Smali 语法基础

目录

  • [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
  • 在非静态方法中,p0this,实际参数从 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_Xtry_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

推荐编辑器:

  1. VS Code + Smali 语法高亮插件
  2. Sublime Text + Smali 插件
  3. Android Studio(内置 Smali 支持)

VS Code 配置:

json 复制代码
// .vscode/settings.json
{
    "files.associations": {
        "*.smali": "smali"
    }
}

安装 Smali 插件:

  1. 打开 VS Code
  2. 搜索 "Smali" 插件
  3. 安装 "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
  • ✅ 应用正常运行,无崩溃

如果修改失败:

  1. 检查 Smali 语法是否正确
  2. 检查寄存器使用是否正确
  3. 检查方法签名是否匹配
  4. 查看 logcat 日志排查错误

3.8 常见问题与解决方案

3.8.1 寄存器相关问题

问题 1:寄存器数量限制导致编译失败

症状:

复制代码
Error: Invalid register: v65536

原因:

  • 寄存器数量超过了限制
  • 寄存器编号错误

解决方案:

  1. 检查 .registers 指令声明的寄存器数量
  2. 确保使用的寄存器编号在声明范围内
  3. 减少局部变量的使用

示例:

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:方法参数寄存器编号错误

症状:

方法调用时参数传递错误

原因:

  • 在非静态方法中,p0this,参数从 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_Xtry_end_X 必须配对
  • catch 指令必须在 try_end_X 之后
  • move-exception 必须在 catch 块的第一条指令

3.8.3 编译和回编译问题

问题 1:baksmali 反编译失败

症状:

复制代码
Error: Invalid DEX file

解决方案:

  1. 检查 DEX 文件是否损坏
  2. 使用正确的 API 级别:--api-level 28
  3. 更新 baksmali 到最新版本
问题 2:smali 编译失败

症状:

复制代码
Error: Invalid register
Error: Invalid instruction

解决方案:

  1. 检查 Smali 语法是否正确
  2. 检查寄存器使用是否超出范围
  3. 检查指令格式是否正确
  4. 查看详细的错误信息定位问题
问题 3:回编译后 APK 无法安装

症状:

复制代码
Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES]

解决方案:

  1. APK 必须签名才能安装
  2. 使用 apksignerjarsigner 签名
  3. 使用 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 代码修改问题

问题:修改后逻辑错误

症状:

应用崩溃或行为异常

解决方案:

  1. 仔细分析原始逻辑
  2. 确保修改后的逻辑完整
  3. 注意寄存器的一致性
  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 知识点回顾

  1. Java 字节码 vs DEX 字节码

    • JVM 使用栈,Dalvik 使用寄存器
    • DEX 格式更紧凑,适合移动设备
  2. Smali 语法基础

    • 寄存器系统(v0-vN, p0-pN)
    • 数据类型表示
    • 方法调用约定
  3. Smali 指令集

    • invoke-* 系列:方法调用
    • if-* 系列:条件跳转
    • move-* 系列:数据移动
    • const-* 系列:常量加载
  4. 类、方法、字段定义

    • 访问修饰符
    • 异常处理

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 代码。在下一章中,我们将学习如何使用工具进行静态分析实战。

相关推荐
大傻^10 小时前
LangChain4j Spring Boot Starter:自动配置与声明式 Bean 管理
java·人工智能·spring boot·spring·langchain4j
沐硕10 小时前
《基于改进协同过滤与多目标优化的健康饮食推荐系统设计与实现》
java·python·算法·fastapi·多目标优化·饮食推荐·改进协同过滤
愣头不青10 小时前
560.和为k的子数组
java·数据结构
共享家952710 小时前
Java入门(String类)
java·开发语言
Georgewu10 小时前
如何判断应用在鸿蒙卓易通或者出境易环境下?
android·harmonyos
l软件定制开发工作室10 小时前
Spring开发系列教程(34)——打包Spring Boot应用
java·spring boot·后端·spring·springboot
0xDevNull10 小时前
Spring Boot 循环依赖解决方案完全指南
java·开发语言·spring
爱丽_10 小时前
GC 怎么判定“该回收谁”:GC Roots、可达性分析、四种引用与回收算法
java·jvm·算法
bbq粉刷匠10 小时前
Java--多线程--单例模式
java·开发语言·单例模式
随风,奔跑10 小时前
Spring MVC
java·后端·spring