Android - 逆向学习 - Smali代码 - 基础知识

简介

Smali是用于Dalvik(Android虚拟机)的反汇编程序实现,汇编工具(将Smali代码汇编为dex文件)为smali.jar,与之对应的baksmali.jar则是反汇编程序(下载地址),官方所说的基于Jasmin/dedexer语法,实际根不知道是什么鬼……

Smali支持注解、调试信息、行数信息等基本Java的基本特性,可以说是很接近Java编译在JVM上的中间语言了,一般用来做Android程序的逆向工程,还可以。。搞搞小名堂

个人认为Smali只是用于做反汇编的一种语言实现,如果可以,自己也能定义一套这样的语言,实现反汇编的效果

Smali基础

下面的内容涉及一些Smali编程的结构和基本语法,这些基本语法,在使用Smali修改App逻辑时需要用到

Smali文件结构

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

基本类型

类型关键字 对应Java中的类型说明
V void,只能用于返回类型
Z boolean
B byte
S short
C char
I int
J long (64 bits)
F float
D double (64 bits)

对象

Object类型,即引用类型的对象,在引用时,使用L开头,后面紧接着的是完整的包名,比如:java.lang.String对应的Smali语法则是Ljava/lang/String

数组

数组定义比较有意思,一维数组在类型的左边加一个方括号,比如:[I等同于Java的int[],每多一维就加一个方括号,最多可以设置255维。。。

方法声明及调用

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

    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即可

示例:

.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方法声明如下:

myMethod(int p1, float p2, boolean p3)

那么对应的Smali则为:

method LMyObject;->myMethod(IJZ)V

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

寄存器名称 对应的引用
p0 this
p1 int型的p1参数
p2, p3 float型的p2参数
p4 boolean型的p3参数

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

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

Dalvik指令集

如果需要使用Smali编写程序,还需要掌握常用的Dalvik虚拟机指令,其合集称为Dalvik指令集。这些指令有点类似x86汇编的指令,但指令更多,使用也非常简单方便。最详尽的介绍,可以参考Android官方的Dalvik相关文档:https://source.android.com/devices/tech/dalvik/dalvik-bytecode#instructions

一般的指令格式为:[op]-[type](可选)/[位宽,默认4位] [目标寄存器],[源寄存器](可选),比如:move v1,v2move-wide/from16 v1,v2

这里也列举一些常用的指令,并结合Smali进行说明:

  • 移位操作:

此类操作常用于赋值

指令 说明
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语句中的异常

- 返回操作:

用于返回值,对应Java中的return语句

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

- 常量操作:

用于声明常量,比如字符串常量(仅声明,String a = “abc”这种语句包含声明和赋值)

指令 说明
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中的方法

- 判断操作:

判断操作用来比较一个寄存器中的值,是否与目标寄存器中的值相等或不等,对应Java中的if语句,格式为:if-[test] v1,v2, [condition],其衍生操作还有专门与0进行比较的if-[test]z v1, [condition],其中[condition]为符合判断结果后的跳转条件,需要提前定义好。判断操作也通常和goto配合使用,用来实现循环或者if-else语句

指令 说明
if-eq v1,v2 判断两个寄存器中的值是否相等
if-ne v1,v2 判断两个寄存器中的值是否不相等
if-lt v1,v2 判断v1寄存器中的值是否小于v2寄存器中的值(lt == less than)
if-ge v1,v2 判断v1寄存器中的值是否大于或等于v2寄存器中的值(ge == great than or equals)
if-gt v1,v2 判断v1寄存器中的值是否大于v2寄存器中的值(gt == great than)
if-le v1,v2 判断v1寄存器中的值是否小于或等于v2寄存器中的值(le == less than or equals)

需要注意的是,在Java中编写的if语句,往往在对应的Smali中,会变成相反的判断逻辑,如下面所示:

    private void test() {
        int a = 0;
        int b = 1;
        String result;
        if (a > b) {
            result = "a great than b";
        } else {
            result = "a less than or equals b";
        }
    }

上面的Java代码逻辑很简单——一个很简单的if语句,为了在Smali中看的更清楚,我只做了字符串赋值操作。下面是对应的Smali代码:

.method private test()V
    .registers 4

    .line 24
    const/4 v0, 0x0

    .line 25
    .local v0, "a":I
    const/4 v1, 0x1

    .line 27
    .local v1, "b":I
    if-le v0, v1, :cond_7

    .line 28
    const-string v2, "a great than b"

    .line 28
    .local v2, "result":Ljava/lang/String;
    goto :goto_9

    .line 30
    .end local v2    # "result":Ljava/lang/String;
    :cond_7
    const-string v2, "a less than or equals b"

    .line 32
    .restart local v2    # "result":Ljava/lang/String;
    :goto_9
    return-void
.end method

仔细观察可以发现:

  • 属性操作:

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

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

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

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

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代码是进行的是最基本的类成员变量的赋值、取值操作

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

# 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获取类成员变量的方法,比较接近函数调用,只不过没有函数调用时的参数

  • 其他指令:

除以上介绍的几种基本的Dalvik指令外,Dalvik还支持值类型转换(如:int转float,double转float等)、基本运算(数学运算、逻辑运算、自增)两种指令集,这里只列举一些常用的指令,其他的可以参考上面提到的Google官方文档

指令 说明
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寄存器

Smali能干什么?

虽然我们了解了Smali的基本语法,但一般不会直接编写Smali来进行功能开发,这样成本过高,而了解Smali的目的,是为了做Android的逆向工程,如:分析APP的原理、漏洞检测,当然,也可以对一些APP做一些小改动(最好不要做一些伤天害理、违法乱纪、损人不利己的事),具体如何进行逆向,我将在下一篇Smali相关的文章中进一步介绍!

 

其他代码

Smali代码的数据类型

B---byte
C---char
D---double
F---float
J---long
I---int
S---short
V---void
Z---boolean
[XXX---array
LXXX/YYY---object
L代表这个数据类型是一个类,如
Ljava/lang/String代表一个String类

Smali的函数定义

Func-Name (Para-Type1Para-Type2Para-Type3...) Return-Type
例子:
1)Hello()V
void Hello();
2)Hello(IIC)V
void Hello(int,int,char);
3)Hello(Z[S[ILjava/Lang/String;[[J)Ljava/Lang/String
String Hello(boolean,short,int,String,long[][]);

基本语法

.field private isFlag:Z 定义变量
.method 方法
.parameter/param 方法参数(用P寄存器来代替传入的参数)
.prologue 方法开始
.line 123 方法位于第123行
invoke-supper 调用父函数
const/height16 v0,0x7f03 把0x7f03赋值给v0
invoke-direct 调用函数
return-void 函数返回void
.end method 函数结束
new-instance 创建实例
input-object 对象赋值
iget-object 调用对象
invoke-static 调用静态函数
.loacls 使用V类型寄存器的个数
.annotation 内部类的表示

注意以上语法中所有前面带有"."的类型都可以理解为注释,在这种类型上下断点程序是无法中断的。
在调用函数的时候如果没有static关键字,p0都代表this指针。
使用put的赋值顺序是从左向右,而使用get语法的赋值是从右向左。

条件跳转

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_**

其他的方法可以使用AndroidKiller进行学习,只要将鼠标悬停在对应的Smali汇编上就可以看到具体的含义。

寄存器
本地寄存器用V开头数字结尾,如v0、v1等,参数寄存器则使用p开头,如p0、p1等。特别注意p0不一定是第一个参数。在非静态函数中p0代表this。

“By:部分资源食用方法 安装软件后,将坡姐补丁扔进安装根目录运行 Over”
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容资源!
3. 如果你也有好源码或者教程,可以到审核区发布,分享有金币奖励和额外收入!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需!
7. 本站不保证所提供下载的资源的准确性、安全性和完整性,源码仅供下载学习之用!
8. 如用于商业或者非法用途,与本站无关,一切后果请用户自负!
9. 本站所有解压密码均为:www.ak0.cn,如遇到无法解压的请联系管理员
AKE小栈 » Android - 逆向学习 - Smali代码 - 基础知识

Leave a Reply