怎么得到smali文件?
apk文件通过apktool反编译出来的都有一个smali文件夹,里面都是以.smali结尾的文件。
Smali是什么?
Smali是安卓系统里的Java虚拟机(Dalvik)所使用的一种.dex格式文件的汇编器,而Baksmali是反汇编器。其语法是一种宽松式的Jasmin/dedexer语法。
Smali语言其实就是一种面向Dalvik的汇编语言(汇编语言是一种用于电子计算机、微处理器、微控制器,或其他可编程器件的低级语言。在不同的设备中,汇编语言对应着不同的机器语言指令集。使用汇编语言编写的源代码,然后通过相应的汇编程序将它们转换成可执行的机器代码。这一过程被称为汇编过程。),我们用smali语言写的代码通过汇编器就转换成Dalvik可以执行的dex文件。
对应的有两个工具,smali.jar将Smali文件转换为dex文件,baksmali.jar则是将dex文件转换为smali文件。apktool能反编译出smali文件就是因为它里面用了baksmali工具。
为什么要学习Smali?
smali语法是逆向的基础,它能帮助我们理解别人代码的逻辑(当然我们也可以用dex2jar, 更方便查看代码),但是当我们要修改别人的代码逻辑,或者加上自己的逻辑,最终是要修改smali文件的,如果我们不懂smali语法,就无从下手。
如何学习?
smali的语法细节太多了,各种指令,看的人头疼,我们其实也不需要全都学会,因为现在有工具帮我们了。
AndroidStudio有个插件: Java2Smali,方便快捷把java代码转换为smali代码。有了这个工具,我们可以结合它在实践中学习smali语法,同时它也可以帮我们省很多事,当我们想在别人的逻辑中增加代码时,我们可以把自己的代码先用这个工具转成smali语句, 然后做微调就行了。
当然我们还是需要学习一些基础语法,了解一些常用指令。
如果想完整学习smali语法,可以看看Android软件安全与逆向分析这本书,网上的很多资料也是来源于这本书。
Dalvik是基于寄存器结构的,在Dalvik字节码中,寄存器都是32位的,能够支持任何类型,64位类型(Long/Double)用2个寄存器表示;Dalvik字节码有两种类型:原始类型;引用类型(包括对象和数组)。
原始类型
简写 | 含义 |
---|---|
B | byte |
C | char |
D | double (64 bits) |
F | float |
I | int |
J | long (64 bits) |
S | short |
V | void 只能用于返回值类型 |
Z | boolean |
对象类型
形式:Lxxx/yyy/zzz;
L
表示这是一个对象类型
xxx/yyy
是该对象所在的包
zzz
是对象名称
;
标识对象名称的结束
如:Ljava/lang/String;
数组类型
形式:[XXX
[I
表示一个int型的一维数组,相当于int[]
增加一个维度增加一个[
,如[[I
表示int[][]
数组每一个维度最多255个;
对象数组表示也是类似
如:String数组的表示是[Ljava/lang/String
方法
形式:Lxxx/yyy/zzz;->methodName(Lxxx/yyy/zzz;Lxxx/yyy/zzz;I)Z
Lxxx/yyy/zzz
表示对象的类型
methodName
表示方法名
Lxxx/yyy/zzz;Lxxx/yyy/zzz;I
表示三个参数,两个对象类型,一个整型(方法的参数是一个接一个的,中间没有隔开)
Z
返回值类型,这里是boolean类型
如:Log.i("xxx", msg);
转换成smali语句之后是
Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I
字段
形式:Lxxx/yyy/zzz;->FieldName:Lxxx/yyy/zzz;
Lxxx/yyy/zzz;
表示对象的类型
FieldName
表示字段名称
Lxxx/yyy/zzz
表示字段类型
如:ff = "aa";
转换后是 Lcom/example/reforceapk/MyLog;->ff:Ljava/lang/String
寄存器和变量
Dalvik变量都是存放在寄存器中的,寄存器的命名方法有两种,v命名法和p命名法,一般我们都用p命名法。
p命名法:寄存器采用v和p来命名,v表示本地寄存器,p表示参数寄存器。
如果一个非静态方法有两个本地变量,有三个参数,需要的寄存器关系如下:
v0 第一个本地寄存器
v1 第二个本地寄存器
p0 // 指代调用这个方法的this对象。
p1 第一个参数
p2 第二个参数
p3 第三个参数如果是静态方法,那么就不需要this对象了,需要的寄存器是v0, v1, p0, p1, p2。
.registers
使用这个指令指定方法中寄存器的总数
.locals
使用这个指定表明方法中非参寄存器的总数,放在方法的第一行。
常用指令
.field private isFlag:z — 定义变量
.method — 方法
.prologue — 方法开始
.end method — 方法结束
.parameter — 方法参数
.line 12 — 此方法位于第12行
invoke-super — 调用父函数
invoke-direct — 调用函数
invoke-static — 调用静态函数
const/high16 v0, 0x7f03 — 把0x7f03赋值给v0
return-void — 函数返回void
new-instance — 创建实例
move-result v0 — 将上一个invoke类型的指令操作的非对象结果赋值给v0寄存器
move-result-object v0 — 将上一个invoke类型指令操作的对象赋值给v0寄存器
字段操作指令
对于实例字段和静态字段有两类指令:
iget, iput 对实例字段进行读,写
sget, sput 对静态字段进行读,写
会根据不同类型添加不同的后缀:
iget,iget-object,iget-boolean,iget-byte,iget-char,iget-short
iput,iput-object,iput-boolean,iput-byte,iput-char,iput-short
获取static fields的指令示例:
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
上句中sget-object指令把out这个变量获取并放到v0寄存器中。
获取instance fields的指令与static fields的类似,需要指明对象所属的实例。示例:
iget-object v0, p0, Lcom/example/reforceapk/MyLog;->ff:Ljava/lang/String;
上句iget-object指令比sget-object多了一个参数p0,就是该变量所在类的实例,在这里就是p0即“this”。
条件跳转指令
“if-eq vA, vB, :cond_x” — 如果vA等于vB则跳转到:cond_x
“if-ne vA, vB, :cond_x” — 如果vA不等于vB则跳转到:cond_x
“if-lt vA, vB, :cond_x” — 如果vA小于vB则跳转到:cond_x
“if-ge vA, vB, :cond_x” — 如果vA大于等于vB则跳转到:cond_x
“if-gt vA, vB, :cond_x” — 如果vA大于vB则跳转到:cond_x
“if-le vA, vB, :cond_x” — 如果vA小于等于vB则跳转到:cond_x
“if-eqz vA, :cond_x” — 如果vA等于0则跳转到:cond_x
“if-nez vA, :cond_x” — 如果vA不等于0则跳转到:cond_x
“if-ltz vA, :cond_x” — 如果vA小于0则跳转到:cond_x
“if-gez vA, :cond_x” — 如果vA大于等于0则跳转到:cond_x
“if-gtz vA, :cond_x” — 如果vA大于0则跳转到:cond_x
“if-lez vA, :cond_x” — 如果vA小于等于0则跳转到:cond_x