本帖最后由 wushaominkk 于 2018-1-15 14:45 编辑
Smali 概述我们都知道,Dalvik 虚拟机(Dalvik VM)是 Google 专门为 Android 平台设计的一套虚拟机。区别于标准 Java 虚拟机 JVM 的 class 文件格式,Dalvik VM 拥有专属的 DEX 可执行文件格式和指令集代码。smali 和 baksmali 则是针对 DEX 执行文件格式的汇编器和反汇编器,反汇编后 DEX 文件会产生.smali 后缀的代码文件,smali 代码拥有特定的格式与语法,smali 语言是对 Dalvik 虚拟机字节码的一种解释。Smali 语言起初是由一个名叫 JesusFreke 的 hacker 对 Dalvik 字节码的翻译,并非一种官方标准语言,因为 Dalvik 虚拟机名字来源于冰岛一个小渔村的名字,JesusFreke 便把 smali和 baksmali 取自了冰岛语中的“汇编器”和“反编器”。目前 Smali 是在 Google Code 上的一个开源项目。虽然主流的 DEX 可执行文件反汇编工具不少,如 Dedexer、IDA Pro 和 dex2jar+jd-gui,但 Smali 提供反汇编功能的同时,也提供了打包反汇编代码重新生成 dex 的功能,因此 Smali被广泛地用于 APP 广告注入、汉化和破解,ROM 定制等方面。
Smali 语法规范与格式Smali 是对 Dalvik 虚拟机字节码的一种解释,虽然不是官方标准语言,但所有语句都遵循一套语法规范。要了解 smali 语法规范,可以先从了解 Dalvik 虚拟机字节码的指令格式开始。3.1 Dalvik 虚拟机字节码指令格式在 Android 4.0 源码 Dalvik/docs 目录下提供了一份文档 instruction-formats.html,里面详细列举了 Dalvik 虚拟机字节码指令的所有格式.
Dalvik 虚拟机字节码的类型、方法和字段的表示方法3.2.1 类型Dalvik 字节码有两种类型,基本类型和引用类型。对象和数组是引用类型,其它都是基本类型。
Dalvik 字节码类型描述符
描述符 类型
- V void,只能用于返回值类型
- Z boolean
- B byte
- S short
- C charI intJ long(64 位)
- F floatD double(64 位)
- L Java 类类型
- [ 数组类型
每个 Dalvik 寄存器都是 32 位大小,对于小于或者等于 32 位长度的类型来说,一个寄存器就可以存放该类型的值,而像 J、D 等 64 位的类型,它们的值是使用相邻两个寄存器来存储的,如 v0 与 v1、v3 与 v4 等。 Java 中的对象在 smali 中以 Lpackage/name/ObjectName;的形式表示。前面的 L 表示这是一个对象类型,package/name/表示该对象所在的包,ObjectName 是对象的名字,“;”表示对象名称的结束。相当于 java 中的 package.name.ObjectName。例 如:Ljava/lang/String;相当于 java.lang.String。
“[”类型可以表示所有基本类型的数组。[I 表示一个整型一维数组,相当于 java 中的int[]。对于多维数组,只要增加[就行了,[[I 相当于 int[][],[[[I 相当于 int[][][]。注意每一维的最多 255 个。对象数组的表示:[Ljava/lang/String;表示一个 String 对象数组。
方法
方法调用的表示格式:Lpackage/name/ObjectName;->MethodName(III)Z。Lpackage/name/ObjectName;表示类型,MethodName 是方法名,III 为参数(在此是 3 个整型参数),Z 是返回类型(bool 型)。函数的参数是一个接一个的,中间没有隔开。一个更复杂的例子:method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;在 java 中则为:String method(int, int[][], int, String, Object[]).
字段
字段,即 java 中类的成员变量,表示格式:Lpackage/name/ObjectName;->FieldName:Ljava/lang/String; 即包名,字段名和字段类型,字段名与字段类型是以冒号“:”分隔。
两种不同的寄存器表示法
在 Dalvik 虚拟机字节码中寄存器的命名法中主要有 2 种:v 命名法和 p 命名法。假设一个函数使用到 M 个寄存器,并且该函数有 N 个入参,根据 Dalvik 虚拟机参数传递方式中的规定:入参使用最后的 N 个寄存器中,局部变量使用从 v0 开始的前 M-N 个寄存器。比如,某函数 A 使用了 5 个寄存器,2 个显式的整形参数,如果函数 A 是非静态方法,函数被调用时会传入一个隐式的对象引用,因此实际传入的参数个数是 3 个。根据传参规则,局部变量将使用前 2 个寄存器,参数会使用后 3 个寄存器。v 命名法采用小写字母“v”开头的方式表示函数中用到的局部变量与参数,所有的寄存器命名从 v0 开始,依次递增。对于上文的函数 A,v 命名法会用到 v0、v1、v2、v3、v4等 5 个寄存器,v0 与 v1 表示函数 A 的局部变量,v2 表示传入的隐式对象引用,v3 与 v4 表示实际传入的 2 个整形参数。P 命名法对函数的局部变量寄存器命名没有影响,它的命名规则是:函数的入参从 p0开始命名,依次递增。对于上文的函数 A,p 命名法会用到 v0、v1、p0、p1、p2 等 5 个寄存器,v0 与 v1 表示函数 A 的局部变量,p0 表示传入的隐式对象引用,p1 与 p2 表示实际传入的 2 个整形参数。此时,p0、p1、p2 实际上分别表示 v2、v3、v4,只是命名不一样而已。在实际的 Smali 文件中,几乎都是使用了 p 命名法,主要原因是使用 p 命名法能够通过寄存器的名字前缀就能很容易判断寄存器到底是局部变量还是函数的入参。初次学习 smali语法时容易对寄存器 p0 表示的意义出现混乱,这主要体现在静态方法和非静态方法中。其实只要理解 p 命名法的定义后就可以很清楚的理解。在 smali 语法中,在调用非静态方法时需要传入该方法所在对象的引用,因此此时 p0 表示的是传入的隐式对象引用,从 p1 开始才是实际传入的入参。但是在调用静态方法时,由于静态方法不需要构建对象的引用,因而也就不需要传入该方法所在对象的引用,因此此时从 p0 开始就是实际传入的入参。在 Dalvik 指令中使用“v 加数字”的方法来索引寄存器,如:v0、v1、v15、v255,但每条指令使用的寄存器索引范围都有限制(因为 Dalvik 指令字节码必须字节对齐),这里我们使用一个大写字母来表示 4 位数据宽度的取值范围,如:指令 move vA, vB,目的寄存器 vA可使用 v0 ~ v15 的寄存器,源寄存器 vB 可以使用 v0 ~ v15 寄存器。指令 move/from16 vAA,vBBBBB,目的寄存器 vAA 可使用 v0 ~ v255 的寄存器,源寄存器 vB 可以使用 v0 ~ v65535 寄存器。简而言之,当目的寄存器和源寄存器中有一个寄存器的编号大于 15 时,即需要加上/from16 指令才能得到正确运行。初次学习 Smali 语法时也容易对这一点不能理解,不注意就会导致 Smali 文件汇编为 dex 文件的时候出现编译错误。比如,按照前面总结的 p 命名法,当 p0 实际表示的寄存器编号大于 15 时,此时 Smali 语句 move v0,p0 就会编译出错。
在以上指令中,在部分指令助记符后添加了 jumbo 后缀,这是在 Android 4.0 开始的扩展指令,增加了寄存器和常量的取值范围。需要引起注意的是,以上指令表中形如 VA 表示寄存器范围为 v0-v15,形如 VAA 表示寄存器范围为 v0-v255,这一点在理解指令时容易被忽略而导致修改 smali 代码时编译出错。比如方法调用指令 invoke 未添加/range 时传入方法的参数列表的寄存器需要在 v0-v15 范围内,如果不在范围内需要将不合格寄存器赋值给合格寄存器,然后再调用方法。
Smali 格式结构
文件格式
无论是普通类、抽象类、接口类或者内部类,在反编译出的代码中,它们都以单独的Smali 文件来存放。每个 smali 文件头 3 行描述了当前类的一些信息,格式如下。
.class <访问权限> [修饰关键字] <类名>.super <父类名>.source <源文件名>
打开 HelloWorld.smali 文件,头 3 行代码如下。
.class public LHelloWorld;
.super Landroid/app/Activity;
.source "HelloWorld.java"
第 1 行“.class”指令指定了当前类的类名。在本例中,类的访问权限为 public,类名为“LHelloWorld;”,类名开头的 L 是遵循 Dalvik 字节码的相关约定,表示后面跟随的字符串为一个类。
第 2 行的“.super ”指令指定了当前类的父类。本例中的“LHelloWorld;”的父类为“Landroid/app/Activity;”。
第 3 行的“.source”指令指定了当前类的源文件名。经过混淆的 dex 文件,反编译出来的 smali 代码可能没有源文件信息,因此“.source”行的代码可能为空。
前 3 行代码过后就是类的主体部分了,一个类可以由多个字段或方法组成。smali 文件中字段的声明使用“.field”指令。字段有静态字段与实例字段两种。静态字段的声明格式如下。
#static fields
.field <访问权限> static [修饰关键字] <字段名>:<字段类型>
baksmali 在生成 Smali 文件时,会在静态字段声明的起始处添加“static fields”注释,Smali 文件中的注释与 Dalvik 语法一样,也是以井号“#”开头。“.field”指令后面跟着的是访问权限,可以是 public、private、protected 之一。修饰关键字描述了字段的其它属性,如synthetic。指令的最后是字段名与字段类型,使用冒号“:”分隔,语法上与 Dalvik 也是一样的。实例字段的声明与静态字段类似,只是少了 static 关键字,它的格式如下。
#instance fields
.field <访问权限> [修饰关键字] <字段名>:<字段类型>
比如以下的实例字段声明。
#instance fields
.field private btn:Landroid/widget/Button;
第 1 行的“instance fields”是 baksmali 生成的注释,第 2 行表示一个私有字段 btn,它的类型为“Landroid/widget/Button;”。如果一个类中含有方法,那么类中必然会有相关方法的反汇编代码,Smali 文件中方法的声明使用“.method”指令。方法有直接方法与虚方法两种。直接方法的声明格式如下。
#direct methods
.method <访问权限> [修饰关键字] <方法原型>
<.locals>
[.parameter]
[.prologue]
[.line]
<代码体>
.end method
“direct methods”是 baksmali 添加的注释,访问权限和修饰关键字与字段的描述相同,方法原型描述了方法的名称、参数与返回值。“.locals ”指定了使用的局部变量的个数。“.parameter”指定了方法的参数,与 Dalvik 语法中使用“.parameters”指定参数个数不同,每个“.parameter”指令表明使用一个参数,比如方法中有使用到 3 个参数,那么就会出现3 条“.parameter”指令。“.prologue”指定了代码的开始处,混淆过的代码可能去掉了该指令。“.line”指定了该处指令在源代码中的行号,同样的,混淆过的代码可能去除了行号信息。
虚方法的声明与直接方法相同,只是起始处的注释为“virtual methods”。如果一个类实现了接口,会在 smali 文件中使用“.implements”指令指出。相应的格式声明如下。
#interfaces
.implements <接口名>
“#interfaces”是 baksmali 添加的接口注释,“.implements”是接口关键字,后面的接口名是 DexClassDef 结构中 interfacesOff 字段指定的内容。如果一个类使用了注解,会在 smali 文件中使用“.annotation”指令指出。注解的格式声明如下。
#annotations
.annotation [注解属性] <注解类名>
[注解字段=值]
.endannotation
注解的作用范围可以是类、方法或字段。如果注解的作用范围是类,“.annotation”指令会直接定义在 smali 文件中,如果是方法或字段,“.annotation”指令则会包含在方法或字段定义中。例如下面的代码。
#instance fields
.field public sayWhat:Ljava/lang/String;
.annotation runtime LMyAnnoField;
info="Hellomyfriend"
.end annotation
.end field
实例字段 sayWhat 为 String 类型,它使用了 MyAnnoField 注解,注解字段 info 值为“Hellomyfriend”。将其转换为 Java 代码为:
@MyAnnoField(info="Hellomyfriend")
public String sayWhat;
如何分析和修改 Smali 代码
一个完整的的 Android 程序反编译后的代码量可能非常庞大,并且反编译后的 Smali 源码相比 java 的可读性差太多了,我们应该如何定位关键代码,分析并修改它们。
定位分析的方法
关键信息查找法
程序运行时会呈现给我们很多信息,如提示的文字内容、Log 输出的信息和 ActivityTaskRecord 等信息,那么可以从这些信息入手来定位关键的代码。
比如,我们想查找程序显示 Toast 时上下文代码,Toast 提示的文字内容则会存放到strings.xml 文件或硬编码到程序代码中,在资源文件中的字符串会有一个 id 索引,只需在反编译的代码中全文检索这个 id 即可找到显示该 Toast 的代码;如果是后者,则在反编译代码中查找这个字符串本身即可。
如在Log中分析到程序发出了一个广播,根据广播的Action字符串查找所有smali代码,也可定位到所有发出和接收该广播的多处代码位置,再逐个分析代码上下文不难定位时何处发出的广播。
对于涉及到程序 UI 逻辑的分析,通常借助 android-sdk 中的工具 hierarchyviewer 来快速分析定位是程序哪个 Activity 甚至是哪个 View 的相关代码。
代码动态调试法
Smali 代码相对复杂冗长,对于需要实现的功能,直接写 Smali 代码既费时间又容易出错,因此通常采用另一种做法,就是先把功能用 Java 源码的方式实现,然后反编译得到 Smali代码,再把 Smali 代码合并到目标代码中。
|