Smali 语法

QROM逆向工程之 - Smali文档结构介绍

在逆向工程适配过程中,直接打交道的就是smali文件,所以明确smali文档结构,对以后的适配过程有很大的帮助。在适配的过程可以根据smali文件的格式反应对应的java源文件,其实samli文件在读起来和java其实差不多,只是java源文件很容易看清代码执行的逻辑,而smali只能进行局部的判断。在机型适配的过程中根据可以相关指令判获取类的继承信息,实现的接口信息,字段的权限和类型,方法的权限参数类型和返回值。

Smali格式结构

文件结构

无论是普通类、抽象类、接口类或者内部类,反编译出来的代码中,都是以单独的Smali文件存放的,每个文件的的前三行描述改类的信息,包括权限信息、继承信息和源文件信息。#号之后为注释内容格式,格式如下。


1
2
3
.class public Lcom/android/server/SystemServer; #.class <访问权限> [修饰关键字] <类名>
.super Ljava/lang/Object; #<父类名>
.source "SystemServer.java" #<源文件名>
  • 第一行“.class”指令指定了当前类的类名。在本例中,类的访问权限为 public,类名为
    “Lcom/android/server/SystemServer;”,类名开头的 L 是遵循 Dalvik 字节码的相关约定,表示后面跟随的字符串为
    一个类
  • 第 2 行的“.super ”指令指定了当前类的父类。本例中的“SystemServer;”的父类为
    “Lcom/android/server/SystemServer”
  • 第 3 行的“.source”指令指定了当前类的源文件名。经过混淆的 dex 文件,反编译出来
    的 smali 代码可能没有源文件信息,因此“.source”行的代码可能为空。

Smali 文件中字段的声明使用“.field”指令。字段有静态字段与实例字段两种。静态字段的声明格式如下。


1
2
3
4
#static fields
.field <访问权限> static [修饰关键字] <字段名>:<字段类型
#instance fields
.field <访问权限> [修饰关键字] <字段名>:<字段类型

比如说下面的例子,最上面的是静态字段的声明,下面是实例字段的声明 ,其中[ ]中为可选项

1
2
3
4
5
6
7
8
9
# static fields
.field private static final ENCRYPTED_STATE:Ljava/lang/String; = "1"
.field private static final ENCRYPTING_STATE:Ljava/lang/String; = "trigger_restart_min_framework"
.field private static final TAG:Ljava/lang/String; = "SystemServer"
# instance fields
.field mContentResolver:Landroid/content/ContentResolver;


Smali中的方法,方法有直接方法和虚方法,其中直接方法也就是java中的私有方法。这种方法在内部类的访问中会生成对应的access方法,在下面章节介绍。方法的基本结构如下。


1
2
3
4
5
6
7
8
9
#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”关键字声明,由于一个类可以实现多个接口所以可以出现多个”.implements”。
在QROM逆向工程适配机型过程中由于厂商对某些接口类进行扩展,导致在适配机型过程中导致某些方法没有实现而导致无法开机。这时候就可以根据实现的接口去补一些没有实现的方法 ,格式如下。

1
2
#interfaces
.implements <接口名>


除此之外如果一个类使用了注解,会在samli文件中使用”.annotation”指令支出。注解的作用范围可以是类 、方法、或者字段。在适配中由于工具自动插桩的过程,可能产生对一个类的存在两个注解,虽然可以回编译回去,但是dexopt释放dex文件的过程中出错直接影响开机。在apktool1.5.2之前apktool回编译jar的过程中不做检查,但是apktool 2.0.0之后就会校验,这里只简单提一下

  • 类的注解

    1
    2
    3
    4
    #annotations
    .annotation [注解属性] <注解类名>
    [注解字段=值]
    .endannotation
  • 字段的注解

    1
    2
    3
    4
    5
    #instance fields
    .field public sayWhat:Ljava/lang/String;
    .annotation runtime LMyAnnoField;
    info="Hellomyfriend"
    .end annotat

类的结构

无论普通类、抽象类、接口类还是内部类,反编译的时候会为每个类单独生成一个 Smali
文件,但是内部类相存在相对比较特殊的地方。

  • 内部类的文件是“[ 外部类 ]$[ 内部类 ].smali”的形式来命名的,匿名内部类文件以“[ 外部类 ]$[ 数字 ].smali”来命名。
  • 内部类访问外部类的私有方法和变量时,都要通过编译器生成的“合成方法”来间接访问。
  • 编译器会把外部类的引用作为第一个参数插入到会内部类的构造器参数列表。
  • 内部类的构造器中是先保存外部类的引用到一个“合成变量”,再初始化外部类,最后才初始化自身。
  • 对于匿名内部类文件名的命名规则是“[ 外部类 ]$[ 数字 ].smali”,当然内部类也可以有内部类的,规则以此类推。这里就暴漏出逆向工程适配过程中的access方法的处理,由于baksmali在反编译过程对匿名内部的编号,当在一个类中加入私有的字段或者私有的方法,就会产生对应的access方法,当然前提是这个类有内部类,就有造成access错误的情况,在使用工具插桩的过程中就无法准确合入,这里就需要人力去合并。当然我们可以通过工具对access进格式化,对于新加的匿名内部类里面访问的外部类的方法和字段,使用的access方法,格式化处理后,自动追加到外部类中,这样可以减轻插桩的重复体力劳动。
    以下面代码 java 代码为例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class HelloWorld {
    private String mHello = "Hello World!";
    public void sayHello() {
    System.out.println("Hello!");
    }
    private void say(String s) {
    System.out.println(s);
    }
    private class InterClass {
    public InterClass(int i) { }
    void func() {
    sayHello();
    say(mHello);
    }
    }
    }

反编译后生成 HelloWorld.smali 和 HelloWorld$InterClass.smali 两个文件,其中关键代码如下:
HelloWorld 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 合成方法访问 mHello
.method static synthetic access$0(Lcom/smali/helloworld/HelloWorld;)Ljava/lang/String;
.locals 1
.parameter
.prologue
.line 7
iget-object v0, p0, Lcom/smali/helloworld/HelloWorld;->mHello:Ljava/lang/String;
return-object v0
.end method
#合成方法调用 say()
.method static synthetic access$1(Lcom/smali/helloworld/HelloWorld;Ljava/lang/String;)V
.locals 0
.parameter
.parameter
.prologue
.line 22
invoke-direct {p0, p1}, Lcom/smali/helloworld/HelloWorld;->say(Ljava/lang/String;)V
return-void
.end method

HelloWorld$InterClass.smali 文件:

1
2
3
4
5
6
7
8
9
10
11
.method public constructor <init>(Lcom/smali/helloworld/HelloWorld;I)V # 插入父类参数
.locals 0
.parameter
.parameter "i"
.prologue
.line 27
iput-object p1, p0, Lcom/smali/helloworld/HelloWorld$InterClass;->this$0:Lcom/smali/helloworld/HelloWorld; # 保存外部类的引用
invoke-direct {p0}, Ljava/lang/Object;-><init>()V #初始化父类
return-void
.end method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.method func()V
.locals 2
.prologue
.line 29
iget-object v0, p0, Lcom/smali/helloworld/HelloWorld$InterClass;->this$0:Lcom/smali/helloworld/HelloWorld; # 引用外部类
invoke-virtual {v0}, Lcom/smali/helloworld/HelloWorld;->sayHello()V #调用外部类公共方法
.line 30
iget-object v0, p0, Lcom/smali/helloworld/HelloWorld$InterClass;->this$0:Lcom/smali/helloworld/HelloWorld;
iget-object v1, p0, Lcom/smali/helloworld/HelloWorld$InterClass; ->this$0:Lcom/smali/helloworld/HelloWorld;
#调用合成方法访问外部类私有变量mHello
invoke-static {v1}, Lcom/smali/helloworld/HelloWorld; ->access$0(Lcom/smali/helloworld/HelloWorld;)Ljava/lang/String;
move-result-object v1
#调用合成方法访问外部类私有方法 say()
invoke-static {v0, v1}, Lcom/smali/helloworld/HelloWorld;->access$1(Lcom/smali/helloworld/HelloWorld;Ljava/lang/String;)V
.line 31
return-void
.end method