smali语言

2024-03-20

前言

希望看这篇前先去看看https://wsxk.github.io/android%E9%80%86%E5%90%91%E5%AD%A6%E4%B9%A0/
这是我2年前初学android时总结的文章,感觉还是能让你对android有一个基本的概念
最近做android题略感力不从心,或许是之前基础没学好,导致逆向看代码比较痛苦的缘故(anyway,java都没学好呢,hhh
现在就从在之前写过的文章的基础上再往前精进

0. java的重要机制

建议先学习一下java的基本语法和概念,虽然不学无所谓,但是还是建议学一下,方便你之后了解什么是smali
另外java的一些奇特的机制,比如反射,还是有必要了解的,不然android逆向会有障碍
当然,描述一些常见的字符运算就没有必要了,这里直接说一些java的比较重要的特性
0.1 首先说比较重要的一点,java中的char类型,也是占用2个字节,java在内存中总是使用Unicode编码
所以即使是char a = 'A';,在内存中的表示也为\x0041,也正因如此,char a = '中';这样的语句也是合法的

另一个我关注的点是java当中的反射机制
0.2 反射:Java的反射是指程序在运行期可以拿到一个对象的所有信息。

1. JVM为每个加载的class及interface创建了对应的Class实例来保存class及interface的所有信息;
注意,这里的Class是一种名叫Class的class

虽然很绕,但是其定义如下:

public final class Class {
    private Class() {} //值得一提的是 因为定义为private,所以只有JVM有权调用这个函数
}
2. 获取一个class对应的Class实例后,就可以获取该class的所有信息;
这是因为JVM在运行中如果需要用到某个类型A,就会将保存A类型信息的文件A.class加载到JVM中,并创建一个Class实例来保存A类型的所有信息。
包括类名、包名、父类、实现的接口、所有方法、字段等

3. 通过Class实例获取class信息的方法称为反射(Reflection);

具体而言,通过Class实例获取class信息的方法有三种

package java_code;
public class Main {
    public static void main(String[] args){
        try {
            String  a = "wsxk";
            Class cls1 = a.getClass(); // 方法一
            Class cls2 = String.class; // 方法二
            Class cls3 = Class.forName("java.lang.String"); // 方法三
            printClassInfo(Class.class);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    
    static void printClassInfo(Class cls) {
        System.out.println("Class name: " + cls.getName());
        System.out.println("Simple name: " + cls.getSimpleName());
        if (cls.getPackage() != null) {
            System.out.println("Package name: " + cls.getPackage().getName());
        }
        System.out.println("is interface: " + cls.isInterface());
        System.out.println("is enum: " + cls.isEnum());
        System.out.println("is array: " + cls.isArray());
        System.out.println("is primitive: " + cls.isPrimitive());
    }
}

4. JVM总是动态加载class,可以在运行期根据条件来控制加载class

反射机制的出现,很大程度是服务于java底层机制或底层api的,比如某个函数func是用来迭代数组里的元素的,然而数组里的元素的类型是可以是int,float…..,然而根据类型来将func写成funcA,funcB,…又费事,这时就可以通过获取该类型的Class,调用其提供的方法来进行迭代了
对于反射的使用,想要有更多了解,可以看看https://www.liaoxuefeng.com/wiki/1252599548343744/1255945147512512

1. smali语言

所谓smali语言,其实是Davlik字节码
可以用JVMjava字节码的关系类比Daviliksmali
Davalik其实是运行于android系统中的虚拟机,用于运行java程序

1.1 smali语法

首先从java中的常见语法开始说明其对应的smali语法

1.1.1 基本类型

1.1.2 对象

如果了解java的话,我们会知道java除了基本类型(如int)等,还有一种object类型,他们属于引用类型
Object类型,即引用类型的对象,在引用时,使用L开头,后面紧接着的是完整的包名,比如:java.lang.String,对应的Smali语法则是 Ljava/lang/String

1.1.3 数组

数组其实在java中也属于引用类型
一维数组在类型的左边加一个方括号,比如:[I 等同于Java的 int[ ] [F 等同于Java的 float[ ]
每多一维就加一个方括号,最多可以设置255维。

1.1.4 方法声明及调用

给个例子

Lpackage/name/ObjectName;->MethodName(III)Z
  1. Lpackage/name/ObjectName; 这是声明的具体类型,即java中的类
  2. ->MethodName(III)Z 调用名为MethodName的方法,其中I和Z上文提到过,为int和boolean的smali表示,所以这个函数的参数是3个int型,返回值的类型为boolean

由于方法的参数列表没有使用逗号这样的分隔符进行划分,所以只能从左到右,根据类型定义来区分参数个数。
再列几个实例来让大伙体会体会:

/**下列代码均为java中String类的内置方法**/

java方法:public char charAt(int index){...}
Davilk描述:Ljava/lang/String;->charAt(I)C

java方法:public void getChars(int srcBegin,int srcEnd,char dst[],int dstBegin){...}
Davilk描述:Ljava/lang/String;->getChars(II[CI)V

java方法:public boolean equals(Object anObject){...}
Davilk描述:Ljava/lang/String;->equals(Ljava/lang/Object)Z

问题来了,一个类的构造方法调用函数名称统一为<init>

1.1.5 寄存器声明及使用

在Smali中,如果需要存储变量,必须先声明足够数量的寄存器,1个寄存器可以存储32位长度的类型,比如Int,而两个寄存器可以存储64位长度类型的数据,比如Long或Double。
声明可使用的寄存器数量的方式为:.registers N,N代表需要的寄存器的总个数,同时,还有一个关键字 .local ,它用于声明非参数的寄存器个数(包含在registers声明的个数当中),也叫做本地寄存器,只在一个方法内有效,但不常用,一般使用registers即可。

  1. 本地寄存器,用 v0 , v1 , v2 …表示
  2. 参数寄存器(parameter register),用 p0 , p1 , p2 , …表示
  3. .registers 用来表明方法中 参数寄存器+本地寄存器 的值
  4. .local 用来表明方法中本地寄存器的值
  5. 在实例函数中,p0代指“this”,p1表示函数的第一个参数,p2代表函数中的第二个参数…,
  6. 在static函数中,p0就是第一个参数… 因为static不用传this指针

举个例子

/**java中的某个函数**/
MyObject->myMethod(int p1, float p2, boolean p3)

method LMyObject;->myMethod(IFZ)V
/**对于这个方法,最少需要5个寄存器,p0代表this,p1为int,p2+p3表示float,p4代表boolean**/

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

1.1.6 Dalvik指令集

给出https://source.android.com/devices/tech/dalvik/dalvik-bytecode#instructions官方指导的指令集
需要注意,这里的 move/from16 v1, v2中的16,指的是寄存器的索引是16位,即v32252这种,Davik中的寄存器默认都是四字节大小,即使遇到long,double这种需要8字节的类型,默认取vn,vn+1两个寄存器共同表示
注意,这里的return v1,是吧v1寄存器的值,放入 “寄存器组”(Register Group)的结构当中,该结构专门用来存放返回值
const指令里的4,16就指的是立即数的位宽了
virtual调用protect和public方法,super调用父类方法,direct调用private方法,static调用静态方法,interface调用接口方法
下面进入举例环节(可能某个类中没有该方法,只是为了简单介绍原理,请不要细究😀

/**调用了String类中的fun方法(我虚构的),静态函数没有参数,该函数的返回值是Boolean值**/
invoke-static {}, Ljava/lang/String;->fun()Z

/**sget-object获取对象**/
sget-object v0, Ljava/lang/String;->shareHandler:Landroid/os/Handler;
/**调用该对象的方法**/
invoke-virtual {v0, v3}, Landroid/os/Handler;->removeCallbacksAndMessages(Ljava/lang/Object;)V

/**调用函数后,把返回值移动到v1中**/
const/4 v2, 0x0
invoke-virtual {p0, v2}, Ljava/lang/String;->getPreferences(I)Landroid/content/SharedPreferences;
move-result-object v1

至于判断指令,其实是比较简单的,就不细说了

if-eq vA, VB, cond_** 如果vA等于vB则跳转到cond_**相当于if (vA==vB)
if-ne vA, VB, cond_** 如果vA不等于vB则跳转到cond_**相当于if (vA!=vB)
if-lt vA, VB, cond_** 如果vA小于vB则跳转到cond_**相当于if (vA<vB)
if-le vA, VB, cond_** 如果vA小于等于vB则跳转到cond_**相当于if (vA<=vB)
if-gt vA, VB, cond_** 如果vA大于vB则跳转到cond_**相当于if (vA>vB)
if-ge vA, VB, cond_** 如果vA大于等于vB则跳转到cond_**相当于if (vA>=vB)
 
if-eqz vA, :cond_** 如果vA等于0则跳转到:cond_** 相当于if (VA==0)
if-nez vA, :cond_** 如果vA不等于0则跳转到:cond_**相当于if (VA!=0)
if-ltz vA, :cond_** 如果vA小于0则跳转到:cond_**相当于if (VA<0)
if-lez vA, :cond_** 如果vA小于等于0则跳转到:cond_**相当于if (VA<=0)
if-gtz vA, :cond_** 如果vA大于0则跳转到:cond_**相当于if (VA>0)
if-gez vA, :cond_** 如果vA大于等于0则跳转到:cond_**相当于if (VA>=0)

Dalvik指令集中还有最重要的一环,对象的属性取值(get)和 赋值(put)
举个例子:

/**把100放到p0(即Lcom/coderyuan/smali/MainActivity对象的mIntA字段中,该字段为整数**/
const/16 v0, 0x64
iput v0, p0, Lcom/coderyuan/smali/MainActivity;->mIntA:I
/**其中指令的前缀是 数组(array)、实例(instance)和静态(static)三种,对应的缩写前缀就是a、i、s**/