最近在学习Android 移动安全逆向方面,逆向首先要看懂代码,Android4.4之前一直使用的是 Dalivk虚拟机,而Smali是用于Dalivk的反汇编程序的实现。
Smali 支持注解,调试信息,行数信息等基本Java的基本特性,可以说是很接近Java编译再JVM上的中间语言,一般用来做Android程序的逆向工程。
1.Smali文件结构
一个Smali文件对应的是一个Java的类,更准确的说是一个.class文件,如果有内部类,需要写成ClassName$InnerClassA,ClassName$InnerClassB...这样的形式
2.基本类型
3.对象
Object类型,即引用类型的对象,在引用时,使用L开头,后面紧接着的是完整的包名,比如:java.lang.String,对应的Smali语法则是 Ljava/lang/String
4.数组
一维数组在类型的左边加一个方括号,比如:[I 等同于Java的 int[] ,[F 等同于Java的 float[] ,每多一维就加一个方括号,最多可以设置255维。
5.方法声明及调用
官方Wiki中给出的Smali引用方法的模板如下:
Lpackage/name/ObjectName;->MethodName(III)Z
第一部分:Lpackage/name/ObjectName;用于声明具体的类型,以便JVM寻找
第二部分:MethodName(III)Z,其中 MethodName 为具体的方法名,()中的字符,表示了参数数量和类型,即3个int型参数,Z为返回值的类型,返回Boolean类型
由于方法的参数列表没有使用逗号这样的分隔符进行划分,所以只能从左到右,根据类型定义来区分参数个数。
如果需要调用构造方法,则MethodName为:<init>
6.寄存器声明及使用
在Smali中,如果需要存储变量,必须先声明足够数量的寄存器,1个寄存器可以存储32位长度的类型,比如Int,而两个寄存器可以存储64位长度类型的数据,比如Long或Double。
声明可使用的寄存器数量的方式为:.registers N,N代表需要的寄存器的总个数,同时,还有一个关键字 .local ,它用于声明非参数的寄存器个数(包含在registers声明的个数当中),也叫做本地寄存器,只在一个方法内有效,但不常用,一般使用registers即可。
那么,如何确定需要使用的寄存器的个数?
由于非static方法,需要占用一个寄存器以保存this指针,那么这类方法的寄存器个数,最低就为1,如果还需要处理传入的参数,则需要再次叠加,此时还需要考虑Double
Float这种需要占用两个寄存器的参数类型,举例来看:
如果一个Java方法声明如下:
myMethod(int p1, float p2, boolean p3)
那么对应的Smali则为:
method LMyObject;->myMethod(IJZ)V
此时,寄存器的对应情况如下:
那么最少需要的寄存器个数则为:5
如果方法体内含有常量、变量等定义,则需要根据情况增加寄存器个数,数量只要满足需求,保证需要获取的值不被后面的赋值冲掉即可,方法有:存入类中的字段中(存入后,寄存器可被重新赋值),或者长期占用一个寄存器
7.Dalvik指令集
如果需要使用Smali编写程序,还需要掌握常用的Dalvik虚拟机指令,其合集称为Dalvik指令集。这些指令有点类似x86汇编的指令,但指令更多,使用也非常简单方便。最
尽的介绍,可以参考Android官方的Dalvik相关文档:https://source.android.com/devices/tech/dalvik/dalvik-bytecode#instructions
一般的指令格式为:[op]-[type](可选)/[位宽,默认4位] [目标寄存器],[源寄存器](可选),比如:move v1,v2,move-wide/from16 v1,v2
这里也列举一些常用的指令,并结合Smali进行说明:
- 移位操作:
此类操作常用于赋值
- 返回操作:
用于返回值,对应Java中的return语句
- 常量操作:
用于声明常量,比如字符串常量(仅声明,String a = “abc”这种语句包含声明和赋值)
- 调用操作:
用于调用方法,基本格式:invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB
,其中,BBBB代表方法引用(参见上面介绍的方法定义及调用),vC~G为需要的参数,根据顺序一一对应
- 判断操作:
判断操作用来比较一个寄存器中的值,是否与目标寄存器中的值相等或不等,对应Java中的if语句,格式为:if-[test] v1,v2, [condition],其衍生操作还有专门与0进行比较的if-[test]z v1, [condition],其中[condition]为符合判断结果后的跳转条件,需要提前定义好。判断操作也通常和goto配合使用,用来实现循环或者if-else语句
- 属性操作:
属性操作的分为:取值(get)和赋值(put)
目标类型分为:数组(array)、实例(instance)和静态(static)三种,对应的缩写前缀就是a、i、s
长度类型分为:默认(什么都不写)、wide(宽,64位)、object(对象)、boolean、byte、char、short(后面几种就不解释了,和Java一致)
指令格式:[指令名] [源寄存器], [目标字段所在对象寄存器], [字段指针],示例代码如下,操作是为int型的类成员变量mIntA赋值为100:
const/16 v0, 0x64
iput v0, p0, Lcom/coderyuan/smali/MainActivity;->mIntA:I
下面列出用于实例字段的指令,其中i都可以换成a或者s,分别用于操作数组字段或者静态字段
- 其他指令:
除以上介绍的几种基本的Dalvik指令外,Dalvik还支持值类型转换(如:int转float,double转float等)、基本运算(数学运算、逻辑运算、自增)两种指令集,这里只列举一些常用的指令,其他的可以参考上面提到的Google官方文档