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

相关推荐
毕设源码-朱学姐14 小时前
【开题答辩全过程】以 工厂能耗分析平台的设计与实现为例,包含答辩的问题和答案
java·vue.js
一笑的小酒馆14 小时前
Android launcher3实现简单的负一屏功能
android
xuyin120415 小时前
【Android】Flow基础知识和使用
android
Spring AI学习15 小时前
Spring AI深度解析(9/50):可观测性与监控体系实战
java·人工智能·spring
李新_16 小时前
基于Markwon封装Markdown组件
android·aigc·markdown
java1234_小锋16 小时前
Spring IoC的实现机制是什么?
java·后端·spring
xqqxqxxq17 小时前
背单词软件技术笔记(V2.0扩展版)
java·笔记·python
消失的旧时光-194317 小时前
深入理解 Java 线程池(二):ThreadPoolExecutor 执行流程 + 运行状态 + ctl 原理全解析
java·开发语言
哈哈老师啊17 小时前
Springboot学生综合测评系统hxtne(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
java·数据库·spring boot