APK其实就是一个ZIP压缩包,将APK后缀改成ZIP后就可以解压出APK内部文件。
Dalvik是google专门为Android操作系统设计的一个虚拟机,经过深度的优化。虽然Android上的程序是使用java来开发的,但是Dalvik和标准的java虚拟机JVM还是两回事。Dalvik VM是基于寄存器的,而JVM是基于栈的;Dalvik有专属的文件执行格式dex(dalvik executable),而JVM则执行的是java字节码。Dalvik VM比JVM速度更快,占用空间更少。
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文件。
JD-gui
工具打开这些jar
文件,可以看到对应的源码。
代码可以通过JD-GUI
查看,但是这个工具查代码并不方便,所以还是推荐把smali文件导入到编辑器中,在编辑器里查找要看的关键词,然后再回到JD-GUI
查看源码。两者结合一起去看是效率最高的。
JD-GUI
看的代码有很多是混淆过的,但是一些系统回调方法是不能混淆的,比如onCreate
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
参数之间没有间隔。
举例:
.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 <clinit>()V
.locals 3
.prologue
//...
return-void
.end method
.method public constructor <init>()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
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实例)。
.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{
//...
}
}
# 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”的指令操作。
sget-object v0, Lcom/disney/WMW/WMWActivity;->PREFS_INSTALLATION_ID:Ljava/lang/String;
sget-object就是用来获取变量值并保存到紧接着的参数的寄存器中,在这里,把上面出现的PREFS_INSTALLATION_ID
这个String成员变量获取并放到v0这个寄存器中,注意:前面需要该变量所属的类的类型,后面需要加一个冒号和该成员变量的类型,中间是“->”表示所属关系。
指令与static fields的基本一样,只是由于不是static变量,不能仅仅指出该变量所在类的类型,还需要该变量所在类的实例。看例子:
iget-object v0, p0, Lcom/disney/WMW/WMWActivity;->_view:Lcom/disney/common/WMWView;
可以看到iget-object指令比sget-object多了一个参数,就是该变量所在类的实例,在这里就是p0即“this”。
指令使用和上述类似,不细述。
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中的函数和成员变量一样也分为两种类型,但是不同成员变量中的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()返回的整型。
.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输出这个值,这样我们就能看到具体是哪一行的问题了。
何为插桩,引用一下wiki的解释:程序插桩,最早是由J.C. Huang 教授提出的,它是在保证被测程序原有逻辑完整性的基础上在程序中插入一些探针(又称为“探测仪”),通过探针的执行并抛出程序运行的特征数据,通过对这些数据的分析,可以获得程序的控制流和数据流信息,进而得到逻辑覆盖等动态信息,从而实现测试目的的方法。
插桩思路是,比如有些应用为防止被修改,会在开启的时候检查签名,签名结果为false的时候就会退出应用。所以就要定位检查的函数,然后通过log把目标值打印出来。
补充一份实例:
先写一个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 <init>()V
.registers 1
.prologue
.line 10
invoke-direct {p0}, Ljava/lang/Object;-><init>()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
,因为这个指令是获取方法的返回值,所以一般这么加代码:
V
之后可以加入V
,那么在move-result-object
命令之后可以加入然后打包签名安装运行,可以看到我们要的log
> adb logcat -s WOW
16:12:55.443 26400 26400 D WOW : Code running in here.
一般不会大量修改代码,而是会改一些关键逻辑。比如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 (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)
不建议在程序原有的方法上增加大量逻辑,这样可能会出现很多寄存器方面的错误导致编译失败。比较好的方法是:把想要增加的逻辑先用java写成一个apk,然后把这个apk反编译成smali文件,随后把反编译后的这部分逻辑的smali文件插入到目标程序的smali文件夹中,然后再在原来的方法上采用invoke的方式调用新加入的逻辑。这样的话不管加入再多的逻辑,也只是修改了原程序的几行代码而已。
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 指向当前指令地址
ADD 加指令
SUB 减指令
STR 把寄存器内容存到栈上
LDR 把栈上内容载入一个寄存器中
.W 是一个可选指令宽度说明符。不会影响为此指令的行为,它只是确保生成32位指令。
BL 执行函数调用,并把使lr指向调用者的下一条指令,即函数的返回地址
BLX 同上,但是在ARM和thumb指令集间切换
CMP 指令进行比较两个操作数的大小
未完成