Android反编译之Smali语法基础

Smali语法适用于Android逆向分析,是学习Android反编译,审查app应用代码漏洞,破解app应用的基础。本文将分为多章内容,简洁明了说明Smali语法命令的各种意义。让读者快速掌握Smali语法,便利于反编译程序员对记忆不清的命令意义进行搜索。

前言

Smali语法适用于Android逆向分析,是学习Android反编译,审查app应用代码漏洞,破解app应用的基础。本文将分为多章内容,简洁明了说明Smali语法命令的各种意义。让读者快速掌握Smali语法,便利于反编译程序员对记忆不清的命令意义进行搜索。

基础语法

代码描述
.field定义变量
.method方法
.parameter方法参数
.prologue方法开始
.line 12此方法位于第12行
invoke-super调用父函数
const/high16 v0, 0x7fo3把0x7fo3赋值给v0
invoke-direct调用函数
return-void函数返回void
.end method函数结束
new-instance创建实例
iput-object对象赋值
iget-object调用对象
invoke-static调用静态函数

数据类型

代码类型
Vvoid
Zboolean
Bbyte
Sshort
Cchar
Iint
Jlong(64位)
Ffloat
Ddouble(64位)

控制条件

代码描述
if-eq vA, vB, :cond_**如果vA 等于 vB则跳转到:cond_**
if-ne vA, vB, :cond_**如果vA 不等于 vB则跳转到:cond_**
if-lt vA, vB, :cond_**如果vA 小于 vB则跳转到:cond_**
if-ge vA, vB, :cond_**如果vA 大于等于 vB则跳转到:cond_**
if-gt vA, vB, :cond_**如果vA 大于 vB则跳转到:cond_**
if-le vA, vB, :cond_**如果vA 小于等于 vB则跳转到:cond_**
if-eqz vA, :cond_**如果vA 等于 0则跳转到:cond_**
if-nez vA, :cond_**如果vA 不等于 0则跳转到:cond_**
if-ltz vA, :cond_**如果vA 小于 0则跳转到:cond_**
if-gez vA, :cond_**如果vA 大于等于 0则跳转到:cond_**
if-gtz vA, :cond_**如果vA 大于 0则跳转到:cond_**
if-lez vA, :cond_**如果vA 小于等于 0则跳转到:cond_**

成员变量

代码描述
# static fields定义静态变量的标记
# instance fields定义实例变量的标记
# direct methods定义静态方法的标记
# virtual methods定义非静态方法的标记
## 移位操作
代码描述
move v1,v2将v2中的值移入到v1寄存器中(4位,支持int型)
move/from16 v1,v2将16位的v2寄存器中的值移入到8位的v1寄存器中
move/16 v1,v2将16位的v2寄存器中的值移入到16位的v1寄存器中
move-wide v1,v2将寄存器对(一组,用于支持双字型)v2中的值移入到v1寄存器对中(4位,猜测支持float、double型)
move-wide/from16 v1,v2将16位的v2寄存器对(一组)中的值移入到8位的v1寄存器中
move-wide/16 v1,v2将16位的v2寄存器对(一组)中的值移入到16位的v1寄存器中
move-object v1,v2将v2中的对象指针移入到v1寄存器中
move-object/from16 v1,v2将16位的v2寄存器中的对象指针移入到v1(8位)寄存器中
move-object/16 v1,v2将16位的v2寄存器中的对象指针移入到v1(16位)寄存器中
move-result v1将这个指令的上一条指令计算结果,移入到v1寄存器中(需要配合invoke-static、invoke-virtual等指令使用)
move-result-object v1将上条计算结果的对象指针移入v1寄存器
move-result-wide v1将上条计算结果(双字)的对象指针移入v1寄存器
move-exception v1将异常移入v1寄存器,用于捕获try-catch语句中的异常

返回操作

代码描述
return-void返回void,即直接返回
return v1返回v1寄存器中的值
return-object v1返回v1寄存器中的对象指针
return-wide v1返回双字型结果给v1寄存器

常量操作

代码描述
const(/4、/16、/hight16) v1 xxx将常量xxx赋值给v1寄存器,/后的类型,需要根据xxx的长度选择
const-wide(/16、/32、/hight16) v1 xxx将双字型常量xxx赋值给v1寄存器,/后的类型,需要根据xxx的长度选择
const-string(/jumbo) v1 “aaa”将字符串常量”aaa”赋给v1寄存器,过长时需要加上jumbo
const-class v1 La/b/TargetClass将Class常量a.b.TargetClass赋值给v1,等价于a.b.TargetClass.class

调用操作

用于调用方法,基本格式:invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB,其中,BBBB代表方法引用(参见上面介绍的方法定义及调用),vC~G为需要的参数,根据顺序一一对应

代码描述
invoke-virtual用于调用一般的,非private、非static、非final、非构造函数的方法,它的第一个参数往往会传p0,也就是this指针
invoke-super用于调用父类中的方法,其他和invoke-virtual保持一致
invoke-direct用于调用private修饰的方法,或者构造方法
invoke-static用于调用静态方法,比如一些工具类
invoke-interface用于调用interface中的方法

文件结构

一个Smali文件对应的是一个Java的类,更准确的说是一个.class文件,如果有内部类,需要写成ClassName$InnerClassAClassName$InnerClassB…这样的形式

方法声明及调用

官方Wiki中给出的Smali引用方法的模板如下:

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

第一部分Lpackage/name/ObjectName;用于声明具体的类型,以便JVM寻找

第二部分MethodName(III)Z,其中MethodName为具体的方法名,()中的字符,表示了参数数量和类型,即3个int型参数,Z为返回值的类型,即返回Boolean类型

由于方法的参数列表没有使用逗号这样的分隔符进行划分,所以只能从左到右,根据类型定义来区分参数个数,这一点需要比较仔细来观察

如果需要调用构造方法,则MethodName为:<init>

寄存器声明及使用

在Smali中,如果需要存储变量,必须先声明足够数量的寄存器,1个寄存器可以存储32位长度的类型,比如Int,而两个寄存器可以存储64位长度类型的数据,比如Long或Double

声明可使用的寄存器数量的方式为:.registers N,N代表需要的寄存器的总个数,同时,还有一个关键字.locals,它用于声明非参数的寄存器个数(包含在registers声明的个数当中),也叫做本地寄存器,只在一个方法内有效,但不常用,一般使用registers即可

示例:

1
2
3
4
5
6
7
8
9
.method private test(I)V
.registers 4 # 声明总共需要使用4个寄存器

const-string v0, "LOG" # 将v0寄存器赋值为字符串常量"LOG"

move v1, p1 # 将int型参数的值赋给v1寄存器

return-void
.end method

结合Dalvik常用的指令进行操作,即可实现一些需要的功能

那么,如何确定需要使用的寄存器的个数?

由于非static方法,需要占用一个寄存器以保存this指针,那么这类方法的寄存器个数,最低就为1,如果还需要处理传入的参数,则需要再次叠加,此时还需要考虑Double和Float这种需要占用两个寄存器的参数类型,举例来看:

如果一个Java方法声明如下:

1
myMethod(int p1, float p2, boolean p3)

那么对应的Smali则为:

1
method LMyObject;->myMethod(IJZ)V

此时,寄存器的对应情况如下:

寄存器名称对应的引用
p0this
p1int型的p1参数
p2, p3float型的p2参数
p4boolean型的p3参数

那么最少需要的寄存器个数则为:5

如果方法体内含有常量、变量等定义,则需要根据情况增加寄存器个数,数量只要满足需求,保证需要获取的值不被后面的赋值冲掉即可,方法有:存入类中的字段中(存入后,寄存器可被重新赋值),或者长期占用一个寄存器

属性操作

属性操作的分为:取值(get)和赋值(put)

目标类型分为:数组(array)、实例(instance)和静态(static)三种,对应的缩写前缀就是a、i、s

长度类型分为:默认(什么都不写)、wide(宽,64位)、object(对象)、booleanbytecharshort(后面几种就不解释了,和Java一致)

指令格式:[指令名] [源寄存器], [目标字段所在对象寄存器], [字段指针],示例代码如下,操作是为int型的类成员变量mIntA赋值为100

1
2
3
const/16 v0, 0x64

iput v0, p0, Lcom/coderyuan/smali/MainActivity;->mIntA:I

下面列出用于实例字段的指令,其中i都可以换成a或者s,分别用于操作数组字段或者静态字段

指令描述
iget取值,用于操作int这种的值类型
iget-wide取值,用于操作wide型字段
iget-object取值,用于操作对象引用
iget-boolean取值,用于操作布尔类型
iget-byte
iget-char取值,用于操作字符类型
iget-short取值,用于操作short类型
iput赋值,用于操作int这种的值类型
iput-wide赋值,用于操作wide型字段
iput-object赋值,用于操作对象引用
iput-boolean赋值,用于操作布尔类型
iput-byte赋值,用于操作字节类型
iput-char赋值,用于操作字符类型
iput-short赋值,用于操作short类型

举例:
以下Java代码是进行的是最基本的类成员变量的赋值、取值操作

1
2
3
4
5
6
7
8
9
10
11
private String mStringA;
private int mIntA;
private Activity mActivityA;

public void fieldTest() {
mStringA = "Put String to mStringA";
mIntA = 100;
mActivityA = this;

int len = mStringA.length();
}

对应的Smali代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# instance fields
.field private mActivityA:Landroid/app/Activity;

.field private mIntA:I

.field private mStringA:Ljava/lang/String;

# virtual methods
.method public fieldTest()V
.registers 2

.line 55
const-string v0, "Put String to mStringA"

iput-object v0, p0, Lcom/coderyuan/smali/MainActivity;->mStringA:Ljava/lang/String;

.line 56
const/16 v0, 0x64

iput v0, p0, Lcom/coderyuan/smali/MainActivity;->mIntA:I

.line 57
iput-object p0, p0, Lcom/coderyuan/smali/MainActivity;->mActivityA:Landroid/app/Activity;

.line 59
iget-object v0, p0, Lcom/coderyuan/smali/MainActivity;->mStringA:Ljava/lang/String;

invoke-virtual {v0}, Ljava/lang/String;->length()I

move-result v0

.line 60
.local v0, "len":I
return-void
.end method

根据Java和Smali代码的对比,值得注意的是,Smali获取类成员变量的方法,比较接近函数调用,只不过没有函数调用时的参数

其他指令

指令描述
add-int/lit8 v1, v2, 0x1给v2寄存器+1,并存入v1寄存器(注意:lit8是对要加的常量的长度限制,如果不写,则为4位,还可选择lit16,即16位)
add-int/2addr v1, v2将v1、v2寄存器中的值相加,并赋值给v1寄存器
float-to-int v1, v2将v2寄存器中的float类型值转换为int类型,并赋值给v1寄存器

转载自:袁国正