Android 逆向之 Smali 语法


基本概念

APK

APK其实就是一个ZIP压缩包,将APK后缀改成ZIP后就可以解压出APK内部文件。

Dalvik字节码

Dalvik是google专门为Android操作系统设计的一个虚拟机,经过深度的优化。虽然Android上的程序是使用java来开发的,但是Dalvik和标准的java虚拟机JVM还是两回事。Dalvik VM是基于寄存器的,而JVM是基于栈的;Dalvik有专属的文件执行格式dex(dalvik executable),而JVM则执行的是java字节码。Dalvik VM比JVM速度更快占用空间更少

如何逆向

反编译为Smali工程

# weicools in ~/Downloads/AndroidReverse [14:30:45]
$ pwd
/Users/weicools/Downloads/AndroidReverse

# weicools in ~/Downloads/AndroidReverse [14:30:47]
$ ls
abc.apk

# 1. apktool反编译apk
# weicools in ~/Downloads/AndroidReverse [14:30:54]
$ apktool d --only-main-classe abc.apk

# weicools in ~/Downloads/AndroidReverse [14:30:47]
$ ls
abc    abc.apk

# 2. 重新打包Apk
# weicools in ~/Downloads/AndroidReverse [14:31:02] C:130
$ apktool b abc

# 3. 签名Apk
# weicools in ~/Downloads/AndroidReverse [14:31:13] C:130
$ jarsigner -verbose -keystore custom.keystore -signedjar abc_signed.apk /abc/dist/abc.apk custom

# weicools in ~/Downloads/AndroidReverse [14:31:20] C:130
$ cd filemagic

# 4. 查看反编译生成的工程目录
# weicools in ~/Downloads/AndroidReverse/filemagic on git:master o [14:31:33]
$ ls
AndroidManifest.xml res                 smali_classes4
smali               smali_classes5
apktool.yml         lib                 smali_classes2      assets              smali_classes3

查看源码

使用 jadx-gui 打开对呀Apk即可,加固的Apk需要先脱壳 才能查看源码

分析思路

代码可以通过 jadx-gui 查看,但是这个工具查代码并不方便,所以还是推荐在 jadx-gui开启反混淆并把源码保存为Gradle工程,然后使用Android Studio查看,。

  1. 首先看这个类有没有静态方法静态代码块,因为这类代码会在对象初始化前运行,可能在这里加载so文件,或者是加密校验等操作。
  2. 再看看这个类的构造方法
  3. 最后看生命周期方法

Smali语法

smali就是Dalvik VM内部执行的核心代码,它有自己的一套语法。要了解 smali 语法规范,可以先从了解 Dalvik 虚拟机字节码的指令格式开始。3.1 Dalvik 虚拟机字节码指令格式在 Android 4.0 源码 Dalvik/docs 目录下提供了一份文档 instruction-formats.html,里面详细列举了 Dalvik 虚拟机字节码指令的所有格式。

指令类型

指令 功能
.field private isFlag:z 定义变量
.method 方法
.parameter 方法参数
.prologue 方法开始
.line 12 此方法位于12行
invoke-super 调用父类方法
const/high16 v0,0x7fo3 把0x7fo3赋值给v0
invoke-direct 调用函数
return-void 函数返回void
.end method 函数结束
new-instance 创建实例
input-object 对象赋值
iget-object 调用对象
Invoke-static 调用静态函数

数据类型

符号 类型
B byte
C char
D double
F float
I int
J long
S short
V void-只用于返回值类型
Z boolean
[XXX Array
Lxxx/yyy Object

数组:

在基本类型前加上前中括号“[”,例如int数组和float数组分别表示为:[I、[F

对象:

以L作为开头,格式是LpackageName/objectName;

String对象在smali中为:Ljava/lang/String;

类里面的内部类:LpackageName/objectName$subObjectName;

函数形式

函数公式为:

FuncName (ParamType1ParamType2ParamType3...)ReturnType

参数之间没有间隔。举例:

foo ()V --> void foo()

foo (III)Z --> boolean foo(int, int, int)

foo (Z[I[ILjava/lang/String;J)Ljava/lang/String; --> String foo(boolean, int[], int[], String, long)

文件分析 - TODO

.class public Lcom/disney/WMW/WMWActivity; 
.super Lcom/disney/common/BaseActivity;
.source "WMWActivity.java"

# interfaces
.implements Lcom/burstly/lib/ui/IBurstlyAdListener;

# annotations
.annotation system Ldalvik/annotation/MemberClasses;
    value = {
        Lcom/disney/WMW/WMWActivity$MessageHandler;,
        Lcom/disney/WMW/WMWActivity$FinishActivityArgs;
    }
.end annotation


# static fields
.field private static final PREFS_INSTALLATION_ID:Ljava/lang/String; = "installationId"
//...


# instance fields
.field private _activityPackageName:Ljava/lang/String;
//...


# direct methods
.method static constructor ()V
    .locals 3

    .prologue
    //...

    return-void
.end method

.method public constructor ()V
    .locals 3

    .prologue
    //...

    return-void
.end method

.method static synthetic access$100(Lcom/disney/WMW/WMWActivity;)V
    .locals 0
    .parameter "x0"

    .prologue
    .line 37
    invoke-direct {p0}, Lcom/disney/WMW/WMWActivity;->initIap()V

    return-void
.end method

.method static synthetic access$200(Lcom/disney/WMW/WMWActivity;)Lcom/disney/common/WMWView;
    .locals 1
    .parameter "x0"

    .prologue
    .line 37
    iget-object v0, p0, Lcom/disney/WMW/WMWActivity;->_view:Lcom/disney/common/WMWView;

    return-object v0
.end method

//...

#virtual methods
.method public captureScreen()V
    .locals 4

    .prologue
    //...

    goto :goto_0
.end method

.method public didScreenCaptured()V
    .locals 6

    .prologue
    //...

    goto :goto_0
.end method

smali寄存器

Dalvik VM与JVM的最大的区别之一就是Dalvik VM是基于寄存器的。基于寄存器是什么意思呢?也就是说,在smali里的所有操作都必须经过寄存器来进行:

本地寄存器v开头数字结尾的符号来表示,如v0、v1、v2、…

参数寄存器p开头数字结尾的符号来表示,如p0、p1、p2、…

特别注意的是,p0不一定是函数中的第一个参数:

  • 非static函数中,p0代指this,p1表示函数的第一个参数,p2代表函数中的第二个参数…
  • static函数中p0才对应第一个参数(因为Java的static方法中没有this方法)。

本地寄存器没有限制,理论上是可以任意使用的,下面是例子:

const/4 v0, 0x0
iput-boolean v0, p0, Lcom/disney/WMW/WMWActivity;->isRunning:Z

在上面的两句中,使用了v0本地寄存器,并把值0x0存到v0中,然后第二句用iput-boolean这个指令把v0中的值存放到com.disney.WMW.WMWActivity.isRunning这个成员变量中。

即相当于:this.isRunning = false;(上面说过,在非static函数中p0代表的是this,在这里就是com.disney.WMW.WMWActivity实例)。

smali中的继承、接口、包信息

.class public Lcom/disney/WMW/WMWActivity; 
.super Lcom/disney/common/BaseActivity;
.source "WMWActivity.java"

# interfaces
.implements Lcom/burstly/lib/ui/IBurstlyAdListener;

# annotations
.annotation system Ldalvik/annotation/MemberClasses;
    value = {
        Lcom/disney/WMW/WMWActivity$MessageHandler;,
        Lcom/disney/WMW/WMWActivity$FinishActivityArgs;
    }
.end annotation

第1行: .class 是com.disney.WMW这个package下的一个类

第2行: .super 继承自com.disney.common.BaseActivity

第3行: .source 是一个由WMWActivity.java编译得到的smali文件

第5-6行: .implements 实现了一个com.burstly.lib.ui这个package下(一个广告SDK)的IBurstyAdListener接口

第8-14行: 定义了内部类:它有两个成员内部类——MessageHandler和FinishActivityArgs,内部类将在后面小节中会有提及

所以对应的Java代码大概是这样:

class WMWActivity extends BaseActivity implements IBurstlyAdListener{
    //...
    class MessageHandler {
        //...
    }
    class FinishActivityArgs{
        //...
    }
}

smali中的成员变量

# static fields
.field private static final PREFS_INSTALLATION_ID:Ljava/lang/String; = "installationId"
//...


# instance fields
.field private _activityPackageName:Ljava/lang/String;
//...

上面定义的static fieldsinstance fields均为成员变量,格式是:

.field public/private [static] [final] varName:<类型>

然而static fields和instance fields还是有区别的,当然区别很明显,那就是static fields是static的,而instance则不是。

根据这个区别来获取这些不同的成员变量时也有不同的指令。

获取的指令有:iget/sgetiget-boolean/sget-booleaniget-object/sget-object等,

操作的指令有:iput/sputiput-boolean/sput-booleaniput-object/sput-object等。

没有“-object”后缀的表示操作的成员变量对象是基本数据类型,带“-object”表示操作的成员变量是对象类型,特别地,boolean类型则使用带“-boolean”的指令操作。

获取static fields指令示例
sget-object v0, Lcom/disney/WMW/WMWActivity;->PREFS_INSTALLATION_ID:Ljava/lang/String;

sget-object 就是用来获取变量值并保存到紧接着的本地寄存器中,在这里,把上面出现的PREFS_INSTALLATION_ID这个String成员变量获取并放到v0寄存器中,注意:前面需要该变量所属的类的类型,后面需要加一个冒号和该成员变量的类型,中间是 -> 表示所属关系。

获取instance fields指令示例

指令与static fields的基本一样,只是由于不是static变量,不能仅仅指出该变量所在类的类型,还需要该变量所在类的实例。看例子:

iget-object v0, p0, Lcom/disney/WMW/WMWActivity;->_view:Lcom/disney/common/WMWView;

可以看到iget-object指令比sget-object多了一个参数,就是该变量所在类的实例,在这里就是p0即this

获取array指令: aget和aget-object

指令使用和上述类似,不细述。

put指令使用和get指令相似
const/4 v3, 0x0
sput-object v3, Lcom/disney/WMW/WMWActivity;->globalIapHandler:Lcom/disney/config/GlobalPurchaseHandler;

相当于:this.globalIapHandler = null;(null = 0x0)

.local v0, wait:Landroid/os/Message;
const/4 v1, 0x2
iput v1, v0, Landroid/os/Message;->what:I

相当于:wait.what = 0x2;(wait是Message的实例)

smali中的函数调用

smali中的函数和成员变量一样也分为两种类型,但是不同成员变量中的static和instance之分,而是direct和virtual之分。那么direct method和virtual method有什么区别呢?

直白地讲,direct method就是private函数,其余的public和protected函数都属于virtual method。所以在调用函数时,有invoke-directinvoke-virtual,另外还有invoke-staticinvoke-super以及invoke-interface等几种不同的指令。当然其实还有invoke-XXX/range指令的,这是参数多于4个的时候调用的指令,比较少见,了解下即可。

invoke-static

顾名思义就是调用static函数的,因为是static函数,所以比起其他调用少一个参数

invoke-static {}, Lcom/disney/WMW/UnlockHelper;->unlockCrankypack()Z

这里注意到invoke-static后面有一对大括号“{}”,其实是调用该方法的实例+参数列表,由于这个方法既不需参数也是static的,所以{}内为空,再看一个例子:

const-string v0, "fmodex"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

这个是调用static void System.loadLibrary(String)来加载NDK编译的so库用的方法,同样也是这里v0就是参数”fmodex”了。

invoke-super

调用父类方法用的指令,在onCreate、onDestroy等方法都能看到,略。

invoke-direct

调用private函数的,例如

invoke-direct {p0}, Lcom/disney/WMW/WMWActivity;->getGlobalIapHandler()Lcom/disney/config/GlobalPurchaseHandler;

这里GlobalPurchaseHandler getGlobalIapHandler()就是定义在WMWActivity中的一个private函数,如果修改smali时错用invoke-virtual或invoke-static将在回编译后程序运行时引发一个常见的VerifyError

invoke-virtual

用于调用protected或public函数,同样注意修改smali时不要错用invoke-direct或invoke-static,例子

sget-object v0, Lcom/disney/WMW/WMWActivity;->shareHandler:Landroid/os/Handler;
invoke-virtual {v0, v3}, Landroid/os/Handler;->removeCallbacksAndMessages(Ljava/lang/Object;)V

v0是shareHandler:Landroid/os/Handler,v3是传递给removeCallbackAndMessage方法的Ljava/lang/Object参数就可以了。

invoke-xxx/range

当方法的参数多于5个时(含5个),不能直接使用以上的指令,而是在后面加上“/range”,使用方法也有所不同:

invoke-static/range {v0 .. v5}, Lcn/game189/sms/SMS;->checkFee(Ljava/lang/String;Landroid/app/Activity;Lcn/game189/sms/SMSListener;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z

刚才看到的例子都是“调用函数”这个操作而已,貌似没有取函数返回的结果的操作?

在Java代码中调用函数和返回函数结果是一条语句完成的,而在smali里则需要分开来完成,在使用上述指令后,如果调用的函数返回非void,那么还需要用到move-result(返回基本数据类型)和move-result-object(返回对象)指令:

const/4 v2, 0x0
invoke-virtual {p0, v2}, Lcom/disney/WMW/WMWActivity;->getPreferences(I)Landroid/content/SharedPreferences;
move-result-object v1

v1保存的就是调用getPreferences(int)方法返回的SharedPreferences实例

invoke-virtual {v2}, Ljava/lang/String;->length()I
move-result v2

v2保存的则是调用String.length()返回的整型。

smali中函数实体分析

.method protected onDestroy()V
    .locals 0

    .prologue
    .line 277
    invoke-super {p0}, Lcom/disney/common/BaseActivity;->onDestroy()V

    .line 279
    return-void
.end method

这是onDestroy()函数,它的作用大家都知道。首先看到函数内第一句:.local 0,这句话很重要,标明了你在这个函数中最少要用到的本地寄存器的个数。这里由于只需要调用一个父类的onDestroy()处理,所以只需要用到p0,所以使用到的本地寄存器数为0。如果不清楚这个规则,很容易在植入代码后忘记修改.local 的值,那么回编译后运行时将会得到一个VerifyError错误,而且极难发现问题所在。我正是被这个问题困扰了很多次,最后研究发现.local的值有这个规律,于是在文档查证了一下果然是这个问题。例如我往onDestroy()增加一句:this.existed = true; 那么应该改为(注意修改.local的值为1——使用到了v0这一个本地寄存器):

.method protected onDestroy()V
    .locals 1

    .prologue
    .line 277
    const/4 v0, 0x1

    iput-boolean v0, p0, Lcom/disney/WMW/WMWActivity;->exited:Z

    invoke-super {p0}, Lcom/disney/common/BaseActivity;->onDestroy()V

    .line 279
    return-void
.end method

另外注意到.line这个标识,它是标注了该代码在原Java文件中的行数,Dalvik VM运行到.line XX时就将这个值存起来,如果在这一行运行时出错了,就往catLog输出这个值,这样我们就能看到具体是哪一行的问题了。

smali插桩

何为插桩,引用一下wiki的解释:程序插桩,最早是由J.C. Huang 教授提出的,它是在保证被测程序原有逻辑完整性的基础上在程序中插入一些探针(又称为“探测仪”),通过探针的执行并抛出程序运行的特征数据,通过对这些数据的分析,可以获得程序的控制流和数据流信息,进而得到逻辑覆盖等动态信息,从而实现测试目的的方法。

插桩思路是,比如有些应用为防止被修改,会在开启的时候检查签名,签名结果为false的时候就会退出应用。所以就要定位检查的函数,然后通过log把目标值打印出来。

  1. 写一个打印log的静态类

  2. 将其转换成smali文件

  3. 把文件放入工程里

  4. 在要打印log的地方添加如下代码:

    invoke-static {v1}, Lcom/softard/MyLog;->Log(Ljava/lang/Object;)V
  5. 重新打包APK,运行,就可以看到打印结果

补充一份实例:先写一个Log类:

package com.softard.xxxx;

import android.util.Log;

public class LogUtil {
    public static final String TAG = "WOW";

    public static void print() {
        Log.d(TAG, "Code running in here.");
    }
}

然后Android Studio将java代码转换成smali

.class public Lcom/softard/xxxx/LogUtil;
.super Ljava/lang/Object;
.source "LogUtil.java"


# static fields
.field public static final TAG:Ljava/lang/String; = "WOW"


# direct methods
.method public constructor ()V
    .registers 1

    .prologue
    .line 10
    invoke-direct {p0}, Ljava/lang/Object;->()V

    return-void
.end method

.method public static print()V
    .registers 2

    .prologue
    .line 14
    const-string v0, "WOW"

    const-string v1, "Code running in here."

    invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

    .line 15
    return-void
.end method

然后把LogUtil.smali文件放到反编译后的smali文件夹下的根目录。放根目录是为了绕过包名的影响,方便调用,所以LogUtil.smali文件的包名要去掉:

.class public Lcom/softard/xxxx/LogUtil; -> .class public LLogUtil;

然后在目标位置调用打印方法:

invoke-virtual {p1, v0}, Landroid/view/View;->setOnClickListener(Landroid/view/View$OnClickListener;)V

invoke-static {}, LLogUtil;->print()V  <-在此调用

.line 51
invoke-static {p0}, Lcom/softard/rxdemo/demo/Chapter9;->practice1(Landroid/content/Context;)V

加代码的时候要注意,要找对地方加,就是在上个方法调用完后添加,比如invoke-virtual invoke-static等。而且这些指令后面不能有move-result-object,因为这个指令是获取方法的返回值,所以一般这么加代码:

  • 在invoke-static/invoke-virtual 指令返回类型是V之后可以加入
  • 在invoke-static/invoke-virtual 指令返回类型不是V,那么在move-result-object命令之后可以加入

然后打包签名安装运行,可以看到我们要的log

> adb logcat -s WOW
16:12:55.443 26400 26400 D WOW     : Code running in here.

smali修改

一般不会大量修改代码,而是会改一些关键逻辑。比如if,有时候修改一个判断就可以达到逻辑跳转的目的。

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 (vAvB)
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)

不建议在程序原有的方法上增加大量逻辑,这样可能会出现很多寄存器方面的错误导致编译失败。比较好的方法是:

  1. 把想要增加的逻辑先用java写成一个apk
  2. 然后把这个apk反编译成smali文件,随后把反编译后的这部分逻辑的smali文件插入到目标程序的smali文件夹中
  3. 然后再在原来的方法上采用invoke的方式调用新加入的逻辑。这样的话不管加入再多的逻辑,也只是修改了原程序的几行代码而已。

汇编ARM指令

ARM指令中寻址方式

  • 立即数寻址

    也叫立即寻址,是一种特殊寻址方式。操作数本身包含在指令中,只要取出指令也就取到了操作数,该操作数叫立即数,对应寻址方式叫做立即寻址。

MOV R0, #64; R0←64
  • 寄存器寻址

    利用寄存器中的数值作为操作数,也称为寄存器直接寻址。

ADD R0, R1, R2; R0←R1+R2
  • 寄存器间接寻址

    把寄存器中的值作为地址,通过这个地址去取得操作数,操作数本身存放在存储器中。

LDR R0,[R1]; R0←[R1]
  • 寄存器偏移寻址

    这是ARM指令集特有的寻址方式,它是在寄存器寻址得到操作数后再进行位移操作,得到最终操作数。

MOV R0,R2,LSL #3; R0←R2*8, R2的值左移3位,结果赋值给R0
  • 寄存器基址变址寻址

    是在寄存器间接寻址的基础上扩展来的。它将寄存器中的值与指令中给出的地址偏移量相加,从而得到一个地址,通过这个地址取得操作数。

LDR R0,[R1, #4]; R0←[R1+4] 将R1的内容加上4形成操作数地址,取得的操作数存入寄存器R0中
  • 多寄存器寻址

    可以一次完成多个寄存器值的传送

LDMIA R0,{R1,R2,R3,R4}; R1←[R0], R2←[R0+4], R3←[R0+8], R4←[R0+12]
  • 堆栈寻址

    堆栈按先进后出工作,使用堆栈指针SP指示当前的操作位置,堆栈指针总是指向栈顶

STMFD SP!, {R1 - R7, LR} 将R1-R7 LR压入堆栈。满递减堆栈

LDMED SP!,{R1 - R7, LR} 将堆栈中的数据取回到R1-R7,LR寄存器。空递减堆栈

ARM中寄存器

  • R0-R3: 用于函数参数及返回值的传递
  • R4-R6,R8,R10-R11: 没有特殊规定,就是普通的通用寄存器
  • R7: 栈帧指针(Frame Pointer)指向前一个保存的栈帧和链接寄存器(link register lr)在栈上的地址
  • R9: 操作系统保留
  • R12: IP intra-procedure scratch
  • R13: SP stack pointer 栈顶指针
  • R14: link register 存放函数的返回地址
  • R15: pogram counter 指向当前指令地址

ARM常用指令

  • ADD 加指令
  • SUB 减指令
  • STR 把寄存器内容存到栈上
  • LDR 把栈上内容载入一个寄存器中
  • .W 是一个可选指令宽度说明符。不会影响为此指令的行为,它只是确保生成32位指令。
  • BL 执行函数调用,并把使lr指向调用者的下一条指令,即函数的返回地址
  • BLX 同上,但是在ARM和thumb指令集间切换
  • CMP 指令进行比较两个操作数的大小

Reference


文章作者: Weikeet
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Weikeet !
评论
  目录