1 长生剑 - Smali Instrumentation

0x01 长生剑


长生剑是把神奇的剑,为白玉京所配,剑名取意来自于李白的诗:“仙人抚我顶,结发受长生。”长生剑是七种武器系列的第一种武器,而笔者接下来所要介绍的调试方法也是我最先学习的调试方法,而且这种方法就像长生剑同样,简单并一直都有很好的效果。这种方法就是Smali Instrumentation,又称Smali 插桩。使用这种方法最大的好处就是不须要对手机进行root,不须要指定android的版本,若是结合一些tricks的话还会有意想不到的效果。java

0x02 Smali/baksmali


作安卓逆向最早接触到的东西确定就是smali语言了,smali最先是由Jasmin提出,随后jesusfreke开发了最有名的smali和baksmali工具将其发扬光大,几乎dex上全部的静态分析工具都是在这个项目的基础上创建的。什么?你没据说过smali和baksmali?你只用过Apktool?若是你仔细阅读了Apktool官网的说明你就会发现,Apktool其实只是一个将各类工具结合起来的懒人工具而已。而且笔者建议从如今起就抛弃Apktool吧。缘由以下:首先,Apktool更新并无smali/baksmali频繁,smali/baksmali更新后要过非长久的时间才会合并到Apktool中,在这以前你可能须要忍受不少诡异的bug。其次,Apktool在反编译或者重打包dex的时候,若是发生错误,仅仅只会提供错误的exception信息而已,但若是你使用smali/baksmali,工具会告诉你具体的出错缘由,会对重打包后的调试有巨大的帮助。最后,不少apk为了对付反调试会在资源文件中加入不少junk code从而使得Apktool的解析崩溃掉,形成反编译失败或者没法重打包。但若是你仅对classes.dex操做就不会有这些问题了。android

学习smali最好的方法就是本身先用java写好程序,再用baksmali转换成smali语句,而后对照学习。好比下面就是java代码和用baksmali反编译事后的smali文件的对照分析。git

MZLog类主要是用Log.d()输出调试信息,Java代码以下:github

package com.mzheng;
 
public class MZLog {
 
    public static void Log(String tag, String msg)
    {
        Log.d(tag, msg);
    }
 
    public static void Log(Object someObj)
    {
        Log("mzheng", someObj.toString());
    }
 
    public static void Log(Object[] someObj)
    {
        Log("mzheng",Arrays.toString(someObj));
    }
 
}

对应的smali代码以下:数组

.class public Lcom/mzheng/MZLog; # class的名字
.super Ljava/lang/Object;  #这个类继承的对象
.source "MZLog.java" # java的文件名
 
 
# direct methods #直接方法
.method public constructor <init>()V  #这是class的构造函数实现
    .registers 1 #这个方法所使用的寄存器数量
 
    .prologue  # prologue并无什么用
    .line 7 #行号
invoke-direct {p0}, Ljava/lang/Object;-><init>()V #调用Object的构造方法,p0至关于"this" 指针
    return-void #返回空
.end method
 
.method public static Log(Ljava/lang/Object;)V # Log(Object)的方法实现
    .registers 3
    .param p0, "someObj"    # Ljava/lang/Object; 参数信息
 
    .prologue
    .line 16
    const-string v0, "mzheng"  #给v0赋值”mzheng”
 
    invoke-virtual {p0}, Ljava/lang/Object;->toString()Ljava/lang/String; #调用toString()函数
 
    move-result-object v1 #将toString()的结果保存在v1
 
    invoke-static {v0, v1}, Lcom/mzheng/MZLog;->Log(Ljava/lang/String;Ljava/lang/String;)V #调用MZLog的另外一个Log函数,参数是v0和v1
 
    .line 17
    return-void
.end method
 
.method public static Log(Ljava/lang/String;Ljava/lang/String;)V #Log(String, String)的方法实现
    .registers 2
    .param p0, "tag"    # Ljava/lang/String;
    .param p1, "msg"    # Ljava/lang/String;
 
    .prologue
    .line 11
    invoke-static {p0, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I #调用android API里的Log函数实现Log功能
 
    .line 12
    return-void
.end method
 
.method public static Log([Ljava/lang/Object;)V #Log(Object[])函数实现 ‘[’符号是数组的意思
    .registers 3
    .param p0, "someObj"    # [Ljava/lang/Object;
 
    .prologue
    .line 21
    const-string v0, "mzheng"
 
    invoke-static {p0}, Ljava/util/Arrays;->toString([Ljava/lang/Object;)Ljava/lang/String;  #将Object数组转换为String
 
    move-result-object v1 #转换后的结果存在v1中
 
    invoke-static {v0, v1}, Lcom/mzheng/MZLog;->Log(Ljava/lang/String;Ljava/lang/String;)V #调用Log(String, String)函数
 
    .line 22
    return-void
.end method

最后简单介绍一下smali经常使用的数据类型:编辑器

V - void
Z - boolean
B - byte
S - short
C - char
I - int
J - long (64 bits)
F - float
D - double (64 bits)

0x03 Smali插桩


若是仅仅用Smali来分析代码,效果其实不如用dex2jar和jd-gui更直观,毕竟看反编译的java代码要更容易一些。但Smali强大之处就是能够为所欲为的进行插桩操做。何为插桩,引用一下wiki的解释:程序插桩,最先是由J.C. Huang 教授提出的,它是在保证被测程序原有逻辑完整性的基础上在程序中插入一些探针(又称为“探测仪”),经过探针的执行并抛出程序运行的特征数据,经过对这些数据的分析,能够得到程序的控制流和数据流信息,进而获得逻辑覆盖等动态信息,从而实现测试目的的方法。下面我就来结合一个例子来说解一下何如进行smali插桩。函数

测试程序是一个简单的crackme (图1)。输入密码,而后点击check,若是密码正确会输出yes,不然输出no。工具

enter image description here

图1 Crackme1的界面学习

首先咱们对crackme这个apk进行解压,而后反编译。咱们会在MainActivity中看到一个getkey(String,int)函数。这个函数貌似很是复杂,咱们暂时无论。咱们首先分析一下点下button后的逻辑。咱们发现程序会经过getkey("mrkxqcroxqtskx",42)来计算出真正的密码,而后与咱们输人的密码进行比较,java代码以下:测试

public void onClick(View arg0) {  
String str = editText0.getText().toString();
if (str.equals(getkey("mrkxqcroxqtskx",42))) {
    Toast.makeText(MainActivity.this,"Yes!", Toast.LENGTH_LONG).show();
} else {
    Toast.makeText(MainActivity.this,"No!", Toast.LENGTH_LONG).show();                  }
}

这时候就是smali插桩大显身手的时候了,咱们能够经过插桩直接获取getkey("mrkxqcroxqtskx",42)这个函数的返回值,而后Log出来。这样咱们就不须要研究getkey这个函数的实现了。具体过程以下:

1 首先解压apk而后用baksmali进行反编译。

unzip crackme1.apk
java -jar baksmali-2.0.3.jar classes.dex

2 将上一节MZLog类的MZLog.smali文件拷贝到com/mzheng目录下,这个文件有3个LOG函数,分别能够输出String的值,Object的值和Object数组的值。注意,若是原程序中没有com/mzheng这个目录,你须要本身用mkdir建立一下。拷贝完后,目录结构以下:

com
└─mzheng
    │  MZLog.smali
    │
    └─crackme1
            BuildConfig.smali
            MainActivity$1.smali
            MainActivity.smali
            R$attr.smali
            R$dimen.smali
            R$drawable.smali
            R$id.smali
            R$layout.smali
            R$menu.smali
            R$string.smali
            R$style.smali
            R.smali

3 用文本编辑器打开MainActivity$1.smali文件进行插桩。为何是MainActivity$1.smali而不是MainActivity.smali呢?由于主要的判断逻辑是在OnClickListener这个类里,而这个类是MainActivity的一个内部类,同时咱们在实现的时候也没有给这个类声明具体的名字,因此这个类用$1表示。加入MZLog.smali这个文件后,咱们只须要在MainActivity$1.smali的第71行后面加上一行代码,invoke-static {v1}, Lcom/mzheng/MZLog;->Log(Ljava/lang/Object;)V,就能够输出getkey的值了。Invoke是方法调用的指令,由于咱们要调用的类是静态方法,因此使用invoke-static。若是是非静态方法的话,第一个参数应该是该方法的实例,而后依次是各个参数。具体插入状况以下:

const-string v1, "mrkxqcroxqtskx"

const/16 v2, 0x2a

# invokes: Lcom/mzheng/crackme1/MainActivity;->getkey(Ljava/lang/String;I)Ljava/lang/String;

invoke-static {v1, v2}, Lcom/mzheng/crackme1/MainActivity;->access$0(Ljava/lang/String;I)Ljava/lang/String;

move-result-object v1

############################## begin ##############################
invoke-static {v1}, Lcom/mzheng/MZLog;->Log(Ljava/lang/Object;)V
############################## end ###############################
invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

move-result v1

4 用smali.jar从新编译修改后的smali文件,把新编译的classes.dex覆盖老的classes.dex,而后再用signapk.jar对apk进行签名。几条关键指令以下:

java -jar smali.jar out
java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk

5 安装程序到android,随便输入点啥,而后点击check按钮,随后在logcat中就能够看到getkey("mrkxqcroxqtskx",42)这个函数的返回值了(图2)。

enter image description here

图2 经过logcat获取getkey的返回值

0x03 Smali修改


经过Smali/baksmali工具,咱们不光能够插桩,还能够修改apk的逻辑。几个须要注意点以下:

1. if条件判断以及跳转语句

在smali中最多见的就是if这个条件判断跳转语句了,这个判断一共有12条指令:

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)

好比咱们在crackme1里判断密码是否正确的smali代码段:

invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
 
move-result v1
 
if-eqz v1, :cond_25  # if (v1==0)
 
iget-object v1, p0, Lcom/mzheng/crackme1/MainActivity$1;->this$0:Lcom/mzheng/crackme1/MainActivity;
 
const-string v2, "Yes!"
 
invoke-static {v1, v2, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
 
move-result-object v1
 
invoke-virtual {v1}, Landroid/widget/Toast;->show()V
 
:cond_25
iget-object v1, p0, Lcom/mzheng/crackme1/MainActivity$1;->this$0:Lcom/mzheng/crackme1/MainActivity;
 
const-string v2, "No!"
 
invoke-static {v1, v2, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
 
move-result-object v1
 
invoke-virtual {v1}, Landroid/widget/Toast;->show()V

若是咱们不关心密码内容,只是但愿程序输出”yes”的话。咱们能够把if-eqz v1, :cond_25改为if-nez v1, :cond_25。这样逻辑就变为:当输错密码的时候,程序反而会输出”yes”。

2. 寄存器问题

修改Smali时有一件很重要的事情就是要注意寄存器。若是乱用寄存器的话可能会致使程序崩溃。每一个方法开头声明了registers的数量,这个数量是参数和本地变量总和。参数统一用P表示。若是是非静态方法p0表明this,p1-pN表明各个参数。若是是静态方法的话,p0-pN表明各个参数。本地变量统一用v表示。若是想要增长的新的本地变量,须要在方法开头的registers数量上增长相应的数值。

好比下面这个方法:

.method public constructor <init>()V
    .registers 1
 
    .prologue
    .line 7
    invoke-direct {p0}, Ljava/lang/Object;-><init>()V
 
    return-void
.end method

由于这不是静态方法,因此p0表明this。若是想要增长一个新的本地变量,好比v0。就须要把.registers 1改成.registers 2

3. 给原程序增长大量逻辑的办法

我很是不建议在程序原有的方法上增长大量逻辑,这样可能会出现不少寄存器方面的错误致使编译失败。比较好的方法是:把想要增长的逻辑先用java写成一个apk,而后把这个apk反编译成smali文件,随后把反编译后的这部分逻辑的smali文件插入到目标程序的smali文件夹中,而后再在原来的方法上采用invoke的方式调用新加入的逻辑。这样的话无论加入再多的逻辑,也只是修改了原程序的几行代码而已。这个思路也是不少重打包病毒惯用的伎俩,确实很是方便好用。

0x04 APK签名Tricks


当咱们在实战中,有时会碰到某些apk在内部实现了本身的签名检查。此次咱们介绍的Smali Instrumentation方法由于须要重打包,因此会改变原有的签名。固然,你能够经过修改apk把签名检查的逻辑删掉,但这又费时又费力。笔者在这里简单介绍两种很是方便的方法来解决签名检查问题。

1. Masterkey

Masterkey漏洞一共有三个,能够影响android 4.4如下版本。利用这个漏洞,咱们能够插入新的classes.dex替换掉原有的classes.dex而不须要对apk自己进行从新签名。若是apk自己有签名校验逻辑的话,利用这个漏洞来进行Smali Instrumentation简直再好不过了。首先,你须要一个android 4.4如下版本的虚拟机或者真机,而后再使用一个masterkey利用工具对apk进行exploit便可。工具下载地址在文章最后,使用的命令以下:

java -jar AndroidMasterKeys.jar -a orig.apk -z moddedClassesDex.zip -o out.apk

orig.apk是本来的apk文件,moddedClassesDex.zip是修改后的classes.dex并压缩成zip文件,out.apk就是利用Masterkey漏洞生成的新的apk文件。若是成功的话用rar打开文件会看到两个classes.dex

enter image description here

图3 Masterkey生成的apk文件有两个classes.dex文件

经过masterkey打包后的apk文件签名并不会有任何变化,这样也就不用担忧签名校验问题了。

2. 自定义ROM

签名的判断实际上是调用了android系统密码库的函数,若是咱们能够本身定制ROM的话,只须要修改AOSP源码路径下的libcore\luni\src\main\java\java\security\MessageDigest.java文件。将isEqual函数中的判断语句注释掉:

public static boolean isEqual(byte[] digesta, byte[] digestb) {
        if (digesta.length != digestb.length) {
            return false;
        }
//        for (int i = 0; i < digesta.length; i++) {
//            if (digesta[i] != digestb[i]) {
//                return false;
//            }
//        }
        return true;
}

这样的话,若是在你自定义的ROM上运行apk,不管你怎么修改classes.dex文件,都不须要关心签名问题了,系统会永远返回签名正确的。

0x05 小结


虽然如今愈来愈多的apk开始使用so文件进行逻辑处理和加固,android 4.4也加入art运行环境,但dalvik永远是android最经典的东西。若是想要学好android逆向,必定要把这部分知识学好。而且把smali研究透彻之后,会对咱们之后要讲的自定义dalvik虚拟机有很大帮助。另外文章中全部提到的代码和工具均可以在个人github下载到,地址是: https://github.com/zhengmin1989/TheSevenWeapons

相关文章
相关标签/搜索