欢迎光临
我们一直在努力

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

目录

  • 3.1 Java 字节码与 DEX 字节码
  • 3.2 Smali 语法基础
  • 3.3 Smali 指令集详解
  • 3.4 Smali 中的类、方法和字段
  • 3.5 工具使用:字节码转换与分析
  • 3.6 代码对照:Java → 字节码 → Smali
  • 3.7 实战案例:修改 Smali 代码绕过验证
  • 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 的区别

特性JVMDalvik VM
字节码格式 .class .dex
虚拟机类型 栈式虚拟机 寄存器虚拟机
文件组织 每个类一个文件 多个类合并到一个文件
指令集 基于栈操作 基于寄存器操作
内存占用 较大 较小(优化后)
执行方式 解释执行/JIT 解释执行/JIT/AOT

3.1.4 指令集映射关系

Java 字节码示例:

// Java 源码
int a = 10;
int b = 20;
int c = a + b;

对应的 Java 字节码(javap -c):

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 代码:

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 代码:

public int add(int a, int b) {
int result = a + b;
return result;
}

对应的 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 无返回值
引用类型

类类型:

Ljava/lang/String; # String 类
Landroid/app/Activity; # Activity 类

数组类型:

[I # int[]
[[I # int[][]
[Ljava/lang/String; # String[]

完整类型示例:

// Java 代码
String[] names;
int[][] matrix;

# Smali 代码
.field names:[Ljava/lang/String;
.field matrix:[[I

3.2.3 方法调用约定

Smali 中有多种方法调用指令,用于不同的调用场景。

invoke-virtual(虚方法调用)

用途: 调用实例方法,支持多态

语法:

invoke-virtual {参数寄存器}, 类名;->方法名(参数类型)返回类型

示例:

// Java 代码
String str = "Hello";
int len = str.length();

# Smali 代码
const-string v0, "Hello"
invoke-virtual {v0}, Ljava/lang/String;->length()I
move-result v1 # 将返回值存储到 v1

invoke-static(静态方法调用)

用途: 调用静态方法

语法:

invoke-static {参数寄存器}, 类名;->方法名(参数类型)返回类型

示例:

// Java 代码
int max = Math.max(10, 20);

# 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 方法

语法:

invoke-direct {参数寄存器}, 类名;->方法名(参数类型)返回类型

示例:

// Java 代码
String str = new String("Hello");

# 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(接口方法调用)

用途: 调用接口方法

语法:

invoke-interface {参数寄存器}, 接口名;->方法名(参数类型)返回类型

invoke-super(父类方法调用)

用途: 调用父类方法

语法:

invoke-super {参数寄存器}, 父类名;->方法名(参数类型)返回类型

调用指令对比表:

指令用途多态支持性能
invoke-virtual 实例方法 较慢
invoke-static 静态方法
invoke-direct 构造函数/私有方法
invoke-interface 接口方法 最慢
invoke-super 父类方法

3.2.4 条件跳转指令

条件跳转指令用于实现 if-else、循环等控制流。

基本条件跳转

相等比较:

if-eq vA, vB, :label # if (vA == vB) goto label
if-ne vA, vB, :label # if (vA != vB) goto label

大小比较:

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

零值比较:

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 代码
if (a == b) {
return true;
} else {
return false;
}

# 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 代码
public void test() {
String str = "Hello";
int len = str.length();
System.out.println(len);
}

.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 代码
int result = Math.max(10, 20);

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 代码
public int compare(int a, int b) {
if (a > b) {
return 1;
} else if (a < b) {
return 1;
} else {
return 0;
}
}

.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 指令:

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 代码
int a = 10;
int b = a;
int c = getValue();

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 指令:

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 代码
int a = 10;
int b = 1000;
long c = 100000L;
String str = "Hello";

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 其他常用指令

算术运算

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

逻辑运算

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

数组操作

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

字段操作

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 代码:

package com.example;
public class MyClass {
// …
}

Smali 代码:

.class public Lcom/example/MyClass;
.super Ljava/lang/Object;
.source "MyClass.java"

类修饰符:

.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 代码:

public int add(int a, int b) {
return a + b;
}

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 代码:

public class MyClass {
private int value;
public static String name;
}

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 代码:

try {
int result = 10 / 0;
} catch (ArithmeticException e) {
e.printStackTrace();
}

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

基本用法:

# 编译单个文件
javac HelloWorld.java

# 编译多个文件
javac *.java

# 指定输出目录
javac -d build/ src/**/*.java

# 指定 classpath
javac -cp libs/*.jar src/**/*.java

示例:

# 创建测试文件
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):

# 转换单个 .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):

# 转换 .class 文件
d8 Test.class –output .

# 转换整个目录
d8 build/classes/**/*.class –output .

# 指定 Android API 级别
d8 –lib android.jar –output . build/classes/**/*.class

验证 DEX 文件:

# 使用 dexdump 查看 DEX 内容
dexdump -d classes.dex | head -50

3.5.3 使用 baksmali 反编译为 Smali

安装 baksmali:

# 下载 baksmali
wget https://github.com/JesusFreke/smali/releases/download/v2.5.2/baksmali-2.5.2.jar

# 或使用 Homebrew (macOS)
brew install smali

基本用法:

# 反编译 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/

查看反编译结果:

# 查看目录结构
tree smali_output/

# 查看特定类的 Smali 代码
cat smali_output/com/example/Test.smali

3.5.4 使用 smali 编译回 DEX

基本用法:

# 编译 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

验证编译结果:

# 使用 dexdump 验证
dexdump -d classes_new.dex | head -50

3.5.5 使用文本编辑器阅读和修改 Smali

推荐编辑器:

  • VS Code + Smali 语法高亮插件
  • Sublime Text + Smali 插件
  • Android Studio(内置 Smali 支持)
  • VS Code 配置:

    // .vscode/settings.json
    {
    "files.associations": {
    "*.smali": "smali"
    }
    }

    安装 Smali 插件:

  • 打开 VS Code
  • 搜索 “Smali” 插件
  • 安装 “Smali” 或 “Smali Language Support”

  • 3.6 代码对照:Java → 字节码 → Smali

    3.6.1 示例 1:字符串拼接

    Java 源码:

    public class StringTest {
    public static String concat(String a, String b) {
    return a + b;
    }
    }

    Java 字节码(javap -c):

    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 代码:

    .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

    逐行解释:

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

    public class ConditionTest {
    public static int max(int a, int b) {
    if (a > b) {
    return a;
    } else {
    return b;
    }
    }
    }

    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 代码:

    .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

    逐行解释:

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

    public class LoopTest {
    public static int sum(int n) {
    int result = 0;
    for (int i = 0; i < n; i++) {
    result += i;
    }
    return result;
    }
    }

    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

    逐行解释:

    # 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:

    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

    # 使用 Android Studio 或命令行编译
    # 这里假设已经编译为 app.apk

    3.7.2 反编译 APK

    步骤 1:使用 apktool 反编译

    apktool d app.apk -o app_decompiled

    步骤 2:查看 Smali 代码

    cat app_decompiled/smali/com/example/test/LoginActivity.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 分析验证逻辑

    关键代码分析:

    .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 中的调用:

    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

    .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 方法中:

    # 原始代码
    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,移除验证

    .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

    apktool b app_decompiled -o app_modified.apk

    步骤 2:签名 APK

    # 生成密钥库(如果还没有)
    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(旧方法):

    jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \\
    -keystore my-release-key.jks \\
    app_modified.apk my-key-alias

    步骤 3:对齐 APK(可选但推荐)

    zipalign -v 4 app_modified.apk app_modified_aligned.apk

    步骤 4:安装测试

    # 卸载旧版本(如果已安装)
    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 指令声明的寄存器数量
  • 确保使用的寄存器编号在声明范围内
  • 减少局部变量的使用
  • 示例:

    # 错误:声明了 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 开始

    解决方案:

    # 非静态方法
    .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 代码:

    try {
    // code
    } catch (Exception e) {
    // handle
    }

    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
  • # 完整流程
    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 查看错误日志
  • 调试技巧:

    # 添加日志输出(需要导入 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 代码。在下一章中,我们将学习如何使用工具进行静态分析实战。

    赞(0)
    未经允许不得转载:171主机测评 » 【Android逆向工程】第3章:Java 字节码与 Smali 语法基础
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址