[Android][Security] 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速度更快,占用空间更少。

如何逆向

查看资源文件

  1. 把要解压的APK拷贝到APKs目录

  2. 使用

    apktool d APKs/xxx.apk -o dir
    

    将APK解压到当前目录,得到完整的资源文件。

    虽然通过解压的方式也可以得到资源文件目录,但是那样得到的xml文件并无法阅读。

  3. 打包回APK

    apktool b APKs/xxx –o file.apk
    

    将文件恢复到apk

  4. 重新签名

    jarsigner -verbose -keystore demo.keystore -storepass demopass -signedjar signed.apk XimalayaNew.apk demoAlias
    
  5. 解出来的文件结构

    AndroidManifest.xml kotlin              smali               unknown
    apktool.yml         lib                 smali_classes2
    assets              original            smali_classes3
    build               res                 smali_classes4
    

    这样解出来可以看到有4个smali文件夹,里面都是smali文件。关于smali是这次的主角,后面再详细说。

查看源码

  1. 若要查看源码,将xxx.apk命名为xxx.zip,使用unzip命令解压,得到dex文件,目录结构如下:

    AndroidManifest.xml classes2.dex        javax               publicsuffixes.gz
    META-INF            classes3.dex        kotlin              push_version
    assets              classes4.dex        lib                 res
    classes.dex         com                 miui_push_version   resources.arsc
    

    这里也有xml文件,可以试着打开看看,发现这些文件打开是乱码。

  2. 使用dex2jar工具逆向dex文件:

    % d2j-dex2jar.sh *.dex
    dex2jar classes.dex -> ./classes-dex2jar.jar
    Detail Error Information in File ./classes-error.zip
    Please report this file to http://code.google.com/p/dex2jar/issues/entry if possible.
    dex2jar classes2.dex -> ./classes2-dex2jar.jar
    dex2jar classes3.dex -> ./classes3-dex2jar.jar
    dex2jar classes4.dex -> ./classes4-dex2jar.jar
    

    看到每个dex文件都生成了对应的jar文件。

  3. JD-gui工具打开这些jar文件,可以看到对应的源码。

分析思路

代码可以通过JD-GUI查看,但是这个工具查代码并不方便,所以还是推荐把smali文件导入到编辑器中,在编辑器里查找要看的关键词,然后再回到JD-GUI查看源码。两者结合一起去看是效率最高的。

JD-GUI看的代码有很多是混淆过的,但是一些系统回调方法是不能混淆的,比如onCreate

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

Smali

smali就是Dalvik VM内部执行的核心代码。它有自己的一套语法。

指令

指令 功能
.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;

函数

函数公式为:

Func-Name (Para-Type1Para-Type2Para-Type3...)Return-Type

参数之间没有间隔。

举例:

  • 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)

文件分析

.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-3行定义的是基本信息:这是一个由WMWActivity.java编译得到的smali文件(第3行),它是com.disney.WMW这个package下的一个类(第1行),继承自com.disney.common.BaseActivity(第2行)。

5-6行定义的是接口信息:这个WMWActivity实现了一个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 fields和instance fields均为成员变量,格式是:

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

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

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

获取的指令有:iget、sget、iget-boolean、sget-boolean、iget-object、sget-object等,

操作的指令有:iput、sput、iput-boolean、sput-boolean、iput-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-direct,invoke-virtual,另外还有invoke-static、invoke-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-xxxxx/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)

不建议在程序原有的方法上增加大量逻辑,这样可能会出现很多寄存器方面的错误导致编译失败。比较好的方法是:把想要增加的逻辑先用java写成一个apk,然后把这个apk反编译成smali文件,随后把反编译后的这部分逻辑的smali文件插入到目标程序的smali文件夹中,然后再在原来的方法上采用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 指令进行比较两个操作数的大小

未完成


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