CTF-All-In-One

1.7.2 Dalvik 指令集

Dalvik 虚拟机

Android 程序运行在 Dalvik 虚拟机中,它与传统的 Java 虚拟机不同,完全基于寄存器架构,数据通过直接通过寄存器传递,大大提高了效率。Dalvik 虚拟机属于 Android 运行时环境,它与一些核心库共同承担 Android 应用程序的运行工作。Dalvik 虚拟机有自己的指令集,即 smali 代码,下面会详细介绍它们。

Dalvik 指令集

指令格式

Dalvik 指令语法由指令的位描述与指令格式标识来决定。

位描述约定如下:

指令格式约定如下:

寄存器

Dalvik 寄存器都是 32 位的,如果是 64 位的数据,则使用相邻的两个寄存器来表示。

寄存器有两种命名法:v 命名法和 p 命名法。如果一个函数使用到 M 个寄存器,其中有 N 个参数,那么参数会使用最后的 N 个寄存器,而局部变量使用从 v0 开始的前 M-N 个寄存器。在 v 命名法中,不管寄存器中是参数还是局部变量,都以 v 开头。而 p 命名法中,参数命名从 p0 开始,依次递增,在代码比较复杂的时候,使用 p 命名法可以清楚地区分开参数和局部变量,大多数工具使用的也是 p 命名法。

类型、方法和字段

Dalvik 字节码只有基本类型和引用类型两种。除了对象类型和数组类型是引用类型外,其余的都是基本类型:

语法 含义
V void
Z boolean
B byte
S short
C char
I int
J long
F float
D double
L 对象类型
[ 数组类型

Dalvik 使用方法名、类型参数和返回值来描述一个方法。方法格式如下:

Lpackage/name/ObjectName;->MethodName(III)Z

例如把下面的 Java 代码转换成 smali:

# Java
String method(int, int [][], int, String, Object[])

# smali
.method method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
.end method

字段格式如下:

Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;

空操作指令

空操作指令的助记符为 nop,值为 00,通常用于对齐代码。

数据操作指令

数据操作指令为 move,原型为 move destination, source

返回指令

基础字节码为 return

数据定义指令

基础字节码为 const

锁指令

用在多线程程序中对同一对象操作。

实例操作指令

数组操作指令

异常指令

跳转指令

有三种跳转指令:无条件跳转(goto)、分支跳转(switch)和条件跳转(if)。

比较指令

对两个寄存器的值进行比较,格式为 cmpkind vAA, vBB, vCC,其中 vBB 和 vCC 寄存器是需要比较的两个寄存器或两个寄存器对,比较的结果放到 vAA 寄存器。指令集中共有5条比较指令:

字段操作指令

用于对对象实例的字段进行读写操作。对普通字段与静态字段操作有两种指令集,分别是 iinstanceop vA, vB, field@CCCCsstaticop vAA, field@BBBB。扩展为 iinstanceop/jumbo vAAAA, vBBBB, field@CCCCCCCsstaticop/jumbo vAAAA, field@BBBBBBBB

普通字段指令的指令前缀为 i,静态字段的指令前缀为 s。字段操作指令后紧跟字段类型的后缀。

方法调用指令

用于调用类实例的方法,基础指令为 invoke,有 invoke-kind {vC, vD, vE, vF, vG}, meth@BBBBinvoke-kind/range {vCCCC .. vNNNN}, meth@BBBB 两类。扩展为 invoke-kind/jumbo {vCCCC .. vNNNN}, meth@BBBBBBBB 这类指令。

根据方法类型的不同,共有如下五条方法调用指令:

方法调用的返回值必须使用 move-result* 指令来获取,如:

invoke-static {}, Landroid/os/Parcel;->obtain()Landroid/os/Parcel;
move-result-object v0

数据转换指令

格式为 unop vA, vB,vB 寄存器或vB寄存器对存放需要转换的数据,转换后结果保存在 vA 寄存器或 vA寄存器对中。

数据运算指令

包括算术运算符与逻辑运算指令。

数据运算指令有如下四类:

第一类指令可归类为:

smali 语法

类声明:

.class <访问权限> [修饰关键字] <类名>
.super <父类名>
.source <源文件名>

字段声明:

# static fields
.field <访问权限> static [修饰关键字] <字段名>:<字段类型>

# instance fields
.field <访问权限> [修饰关键字] <字段名>:<字段类型>

方法声明:

# direct methods
.method <访问权限> [修饰关键字] <方法原型>
    [.locals]
    [.param]
    [.prologue]
    [.line]
<代码体>
.end method

# virtual methods
.method <访问权限> [修饰关键字] <方法原型>
    [.locals]
    [.param]
    [.prologue]
    [.line]
<代码体>
.end method

需要注意的是,在一些老教程中,会看到 .parameter,表示使用的寄存器个数,但在最新的语法中已经不存在了,取而代之的是 .param,表示方法参数。

接口声明:

# interfaces
.implements <接口名>

注释声明:

# annotations
.annotation [注释属性] <注释类名>
    [注释字段 = 值]
.end annotation

循环语句

# for
Iterator<对象> <对象名> = <方法返回一个对象列表>;
for(<对象> <对象名>:<对象列表>){
    [处理单个对象的代码体]
}

# while
Iterator<对象> <迭代器> = <方法返回一个迭代器>;
while(<迭代器>.hasNext()){
    <对象> <对象名> = <迭代器>.next();
    [处理单个对象的代码体]
}

比如下面的 Java 代码:

public void encrypt(String str) {
    String ans = "";
    for (int i = 0 ; i < str.length(); i++){
        ans += str.charAt(i);
    }
    Log.e("ans:", ans);
}

对应下面的 smali:

# public void encrypt(String str) {
.method public encrypt(Ljava/lang/String;)V
.locals 4
.parameter p1, "str"    # Ljava/lang/String;
.prologue

# String ans = "";
const-string v0, ""
.local v0, "ans":Ljava/lang/String;

# for (int i  0 ; i < str.length(); i++){
# int i=0 =>v1
const/4 v1, 0x0
.local v1, "i":I
:goto_0     # for_start_place

# str.length()=>v2
invoke-virtual {p1}, Ljava/lang/String;->length()I
move-result v2

# i<str.length()
if-ge v1, v2, :cond_0

# ans += str.charAt(i);
# str.charAt(i) => v2
new-instance v2, Ljava/lang/StringBuilder;
invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v2

#str.charAt(i) => v3
invoke-virtual {p1, v1}, Ljava/lang/String;->charAt(I)C
move-result v3

# ans += v3 =>v0
invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder;
move-result-object v2
invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0

# i++
add-int/lit8 v1, v1, 0x1
goto :goto_0

# Log.e("ans:", ans);
:cond_0
const-string v2, "ans:"
invoke-static {v2, v0}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
return-void
.end method

switch 语句

public void encrypt(int flag) {
        String ans = null;
        switch (flag){
            case 0:
                ans = "ans is 0";
                break;
            default:
                ans = "noans";
                break;
        }
        Log.v("ans:", ans);
    }

对应下面的 smali:

# public void encrypt(int flag) {
.method public encrypt(I)V
    .locals 2
    .param p1, "flag"    # I
    .prologue

# String ans = null;
    const/4 v0, 0x0
    .local v0, "ans":Ljava/lang/String;

# switch (flag){
    packed-switch p1, :pswitch_data_0   # pswitch_data_0指定case区域的开头及结尾

# default: ans="noans"
    const-string v0, "noans"

# Log.v("ans:", ans)
    :goto_0
    const-string v1, "ans:"
    invoke-static {v1, v0}, Landroid/util/Log;->v(Ljava/lang/String;Ljava/lang/String;)I
    return-void

# case 0: ans="ans is 0"
    :pswitch_0            # pswitch_<case的值>
    const-string v0, "ans is 0"
    goto :goto_0          # break
    nop
    :pswitch_data_0 #case区域的结束
    .packed-switch 0x0    # 定义case的情况
        :pswitch_0   #case 0
    .end packed-switch
.end method

根据 switch 语句的不同,case 也有两种方式:

# packed-switch
packed-switch p1, :pswitch_data_0
...
:pswitch_data_0
.packed-switch 0x0
    :pswitch_0
    :pswitch_1

# spase-switch
sparse-switch p1,:sswitch_data_0
...
sswitch_data_0
.sparse-switch
    0xa -> : sswitch_0
    0xb -> : sswitch_1 # 字符会转化成数组

try-catch 语句

public void encrypt(int flag) {
    String ans = null;
    try {
        ans = "ok!";
    } catch (Exception e){
        ans = e.toString();
    }
    Log.d("error", ans);
}

对应的下面的 smali:

# public void encrypt(int flag) {
.method public encrypt(I)V
    .locals 3
    .param p1, "flag"    # I
    .prologue

# String ans = null;
    const/4 v0, 0x0
    .line 20
    .local v0, "ans":Ljava/lang/String;

# try { ans="ok!"; }
    :try_start_0    # 第一个try开始,
    const-string v0, "ok!"
    :try_end_0      # 第一个try结束(主要是可能有多个try)
    .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0

# Log.d("error", ans);
    :goto_0
    const-string v2, "error"
    invoke-static {v2, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
    return-void

# catch (Exception e){ans = e.toString();}
    :catch_0        #第一个catch
    move-exception v1
    .local v1, "e":Ljava/lang/Exception;
    invoke-virtual {v1}, Ljava/lang/Exception;->toString()Ljava/lang/String;
    move-result-object v0
    goto :goto_0
.end method

更多资料