静态分析是探索Android程序内幕的一种最多见的方法,它与动态调试双剑合璧,帮助分析人员解决分析时遇到的各种“疑难”问题。固然,静态分析技术自己须要分析人员具有较强的代码理解能力,这些都须要在平时的开发过程当中不断地积累经验,很难想象一个连Android应用程序源码都看不懂的人去逆向分析Android程序。html
静态分析(Static Analysis)是指在不运行代码的状况下,采用词法分析、语法分析等各类技术手段对程序文件进行扫描并生成程序的反汇编代码,而后阅读反汇编代码来掌握程序功能的一种技术。在实际的分析过程当中,彻底不运行程序是不太可能的,分析人员时常须要先运行目标程序来寻找程序的突破口。静态分析强调的是静态,在整个分析的过程当中,阅读反汇编代码是主要的分析工做。生成反汇编代码的工具称为反汇编工具或反编译工具,选择一个功能强大的反汇编工具不只能得到更好的反汇编效果,并且也能为分析人员节省很多时间。在Android的逆向分析中,常见的工具备APkTool、baksmail、dex2jar和Editor等工具,具体分析时须要灵活的使用这些工具。java
静态分析Android程序有两种方法:一种方法是阅读反汇编生成的Dalvik字节码,可使用IDA Pro分析dex文件,或者使用文本编辑器阅读baksmali反编译生成的smali文件;另外一种方法是阅读反汇编生成的Java源码,可使用dex2jar生成jar文件,而后再使用jd-gui阅读jar文件的代码。android
在逆向一个Android软件时,因为软件的复杂性,若是盲目的进行分析,可能须要阅读成千上万行的反汇编代码才能找到程序的关键点,这无疑是浪费时间的表现,本小节将介绍如何快速的定位程序的关键代码。程序员
每一个apk文件中都包含有一个AndroidManifest.xml文件,它记录着软件的一些基本信息。包括软件的包名、运行的系统版本、用到的组件等。而且这个文件被加密存储进了apk文件中,在开始分析前,有必要先反编译apk文件对其进行解密。反编译apk的工具使用前面章节介绍过的Apktool。Apktool提供了反编译与打包apk文件的功能。本小节使用到的实例程序为crackme0502.apk,按照前面使用Apktool的步骤,在命令提示符下输入“apktool d crackme0502.apk”便可反编译成功。固然,也能够借鉴一些一键反编译工具。性能优化
咱们知道,一个Android程序由一个或多个Activity以及其它组件组成,每一个Activity都是相同级别的,不一样的Activity实现不一样的功能。每一个Activity都是Android程序的一个显示“页面”,主要负责数据的处理及展现工做,在Android程序的开发过程当中,程序员不少时候是在编写用户与Activity之间的交互代码。bash
每一个Android程序有且只有一个主Activity(隐藏程序除外,它没有主Activity),它是程序启动的第一个Activity。例如,打开crackme0502文件夹下的AndroidManifest.xml文件,其中有以下片段的代码。数据结构
<activity android:label="@string/title_activity_main" android:name=". MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
复制代码
在程序中使用到的Activity都须要在AndroidManifest.xml文件中手动声明,声明Activity使用activity标签,其中android:label指定Activity的标题,android:name指定具体的Activity类,“.MainActivity”前面省略了程序的包名,完整类名应该为com.droider.crackme0502. MainActivity,intent-filter指定了Activity的启动意图,android.intent.action.MAIN表示这个Activity是程序的主Activity。 android.intent.category.LAUNCHER表示这个Activity能够经过LAUNCHER来启动。若是AndroidMenifest.xml中,全部的Activity都没有添加android.intent.category.LAUNCHER,那么该程序安装到Android设备上后,在程序列表中是不可见的,一样的,若是没有指定android.intent.action.MAIN,Android系统的LAUNCHER就没法匹配程序的主Activity,所以该程序也不会有图标出现。app
在反编译出的AndroidManifest.xml中找到主Activity后,能够直接去查看其所在类的OnCreate()方法的反汇编代码,对于大多数软件来讲,这里就是程序的代码入口处,全部的功能都从这里开始获得执行,咱们能够沿着这里一直向下查看,追踪软件的执行流程。框架
若是须要在程序的组件之间传递全局变量,或者在Activity启动以前作一些初始化工做,就能够考虑使用Application类了。使用Application时须要在程序中添加一个类继承自android.app.Application,而后重写它的OnCreate()方法,在该方法中初始化的全局变量能够在Android其它组件中访问,固然前提条件是这些变量具备public属性。最后还须要在AndroidManifest.xml文件的Application标签中添加“android:name”属性,取值为继承自android.app.Application的类名。编辑器
鉴于Application类比程序中其它的类启动得都要早,一些商业软件将受权验证的代码都转移到了该类中。例如,在OnCreate()方法中检测软件的购买状态,若是状态异常则拒绝程序继续运行。所以,在分析Android程序过程当中,咱们须要先查看该程序是否具备Application类,若是有,就要看看它的OnCreate()方法中是否作了一些影响到逆向分析的初始化工做。
若是盲目的对一个Android程序进行分析,可能须要阅读成千上万行的反汇编代码才能找到程序的关键点,这无疑是浪费时间的表现。
所谓信息反馈法,是指先运行目标程序,而后根据程序运行时给出的反馈信息做为突破口寻找关键代码。在第2章中,咱们运行目标程序并输入错误的注册码时,会弹出提示“无效用户名或注册码”,这就是程序反馈给咱们的信息。一般状况下,程序中用到的字符串会存储在String.xml文件或者硬编码到程序代码中,若是是前者的话,字符串在程序中会以id的形式访问,只需在反汇编代码中搜索字符串的id值便可找到调用代码处;若是是后者的话,在反汇编代码中直接搜索字符串便可。
这种定位代码的方法与信息反馈法相似。在信息反馈法中,不管程序给出什么样的反馈信息,终究是须要调用Android SDK中提供的相关API函数来完成的。好比弹出注册码错误的提示信息就须要调用Toast.MakeText().Show()方法,在反汇编代码中直接搜索Toast应该很快就能定位到调用代码,若是Toast在程序中有多处的话,可能须要分析人员逐个甄别。
顺序查见解是指从软件的启动代码开始,逐行的向下分析,掌握软件的执行流程,这种分析方法在病毒分析时常常用到。
代码注入法属于动态调试方法,它的原理是手动修改apk文件的反汇编代码,加入Log输出,配合LogCat查看程序执行到特定点时的状态数据。这种方法在解密程序数据时常用,详细的内容会在本书的第8章介绍。
栈跟踪法属于动态调试方法,它的原理是输出运行时的栈跟踪信息,而后查看栈上的函数调用序列来理解方法的执行流程,这种方法的详细内容会在本书的第8章介绍。
Method Profiling(方法剖析)属于动态调试方法,它主要用于热点分析和性能优化。该功能除了能够记录每一个函数占用的CPU时间外,还可以跟踪全部的函数调用关系,并提供比栈跟踪法更详细的函数调用序列报告,这种方法在实践中可帮助分析人员节省不少时间,也被普遍使用,
使用Apktool反编译apk文件后,会在反编译工程目录下生成一个smali文件夹,里面存放着全部反编译出的smali文件,这些文件会根据程序包的层次结构生成相应的目录,程序中全部的类都会在相应的目录下生成独立的smali文件。如上一节中程序的主Activity名为com.droider.crackme0502. MainActivity,就会在smali目录下依次生成com\droider\ crackme0502目录结构,而后在这个目录下生成MainActivity.smali文件。
smali文件的代码一般状况下比较长,并且指令繁多,在阅读时很难用肉眼捕捉到重点,若是有阅读工具可以将特殊指令(例如条件跳转指令)高亮显示,势必会让分析工做事半功倍,为此笔者专门为文本编辑器Notepad++编写了smali语法文件来支持高亮显示与代码折叠,并以此做为smali代码的阅读工具。
不管是普通类、抽象类、接口类或者内部类,在反编译出的代码中,它们都以单独的smali文件来存放。每一个smali文件都由若干条语句组成,全部的语句都遵循着一套语法规范。在smali文件的头3行描述了当前类的一些信息,格式以下。
.class <访问权限> [修饰关键字] <类名>
.super <父类名>
.source <源文件名>
打开MainActivity.smali文件,头3行代码以下。
.class public Lcom/droider/crackme0502/MainActivity;
.super Landroid/app/Activity;
.source "MainActivity.java"
复制代码
第1行“.class”指令指定了当前类的类名。在本例中,类的访问权限为public,类名为“Lcom/droider/crackme0502/MainActivity;”,类名开头的L是遵循Dalvik字节码的相关约定,表示后面跟随的字符串为一个类。
第2行的“.super”指令指定了当前类的父类。本例中的“Lcom/droider/crackme0502/ MainActivity;”的父类为“Landroid/app/Activity;”。
第3行的“.source”指令指定了当前类的源文件名。
前3行代码事后就是类的主体部分了,一个类能够由多个字段或方法组成。smali文件中字段的声明使用“.field”指令。字段有静态字段与实例字段两种。静态字段的声明格式以下。
# static fields
.field <访问权限> static [修饰关键字] <字段名>:<字段类型>
复制代码
baksmali在生成smali文件时,会在静态字段声明的起始处添加“static fields”注释,smali文件中的注释与Dalvik语法同样,也是以井号“#”开头。“.field”指令后面跟着的是访问权限,能够是public、private、protected之一。修饰关键字描述了字段的其它属性,如synthetic。指令的最后是字段名与字段类型,使用冒号“:”分隔,语法上与Dalvik也是同样的。
实例字段的声明与静态字段相似,只是少了static关键字,它的格式以下。
# instance fields
.field <访问权限> [修饰关键字] <字段名>:<字段类型>
复制代码
好比如下的实例字段声明。
# instance fields
.field private btnAnno:Landroid/widget/Button;
复制代码
第1行的“instance fields”是baksmali生成的注释,第2行表示一个私有字段btnAnno,它的类型为“Landroid/widget/Button;”。
若是一个类中含有方法,那么类中必然会有相关方法的反汇编代码,smali文件中方法的声明使用“.method”指令。方法有直接方法与虚方法两种。直接方法的声明格式以下。
# 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”指令指出。相应的格式声明以下。
# interfaces
.implements <接口名>
复制代码
“# interfaces”是baksmali添加的接口注释,“.implements”是接口关键字,后面的接口名是DexClassDef结构中interfacesOff字段指定的内容。
若是一个类使用了注解,会在smali文件中使用“.annotation”指令指出。注解的格式声明以下。
# annotations
.annotation [注解属性] <注解类名>
[注解字段 = 值]
.end annotation
复制代码
注解的做用范围能够是类、方法或字段。若是注解的做用范围是类,“.annotation”指令会直接定义在smali文件中,若是是方法或字段,“.annotation”指令则会包含在方法或字段定义中。例以下面的代码。
# instance fields
.field public sayWhat:Ljava/lang/String;
.annotation runtime Lcom/droider/anno/MyAnnoField;
info = "Hello my friend"
.end annotation
.end field
复制代码
实例字段sayWhat为String类型,它使用了com.droider.anno.MyAnnoField注解,注解字段info值为“Hello my friend”。将其转换为Java代码以下所示:
@ com.droider.anno MyAnnoField(info = "Hello my friend")
public String sayWhat;
复制代码
IDA Pro是目前很棒的静态反编译软件,是反编译者不可缺乏的利器、巨酷的反编译软件,它能够更好的反汇编和更有深层分析.能够快速到达指定代码位置。
IDA Pro从6.1版本开始,提供了对Android的静态分析与动态调试支持。包括Dalvik指令集的反汇编、原生库(ARM/Thumb代码)的反汇编、原生库(ARM/Thumb代码)的动态调试等。具体可查看IDA Pro官方的更新日志,连接以下:www.hex-rays.com/ products/ ida/6.1/index.shtml。
以5.2节的crackme0502.apk为例,首先解压出classes.dex文件,而后打开IDA Pro,将classes.dex拖放到IDA Pro的主窗口,会弹出加载新文件对话框,如图5-4所示,IDA Pro解析出了该文件属于“Android DEX File”,保持默认的选项,点击OK按钮,稍等片刻IDA Pro就会分析完dex文件。
此时,dex文件开头的0x70个字节就会格式化显示,效果如图5-7所示。一样,读者能够手动对dex文件中其它的结构进行整理,如DexHeader下面的DexStringId结构。
CODE:0002CFCC Method 2589 (0xa1d):
CODE:0002CFCC public android.database.Cursor
CODE:0002CFCC android.support.v4.widget.SimpleCursorAdapter.swapCursor(
CODE:0002CFCC android.database.Cursor p0) #方法声明
CODE:0002CFCC this = v2 #this引用
CODE:0002CFCC p0 = v3 #第一个参数
CODE:0002CFCC invoke-super {this, p0}, <ref ResourceCursorAdapter.
swapCursor(ref) imp. @ _def_ResourceCursorAdapter_
swapCursor@LL>
CODE:0002CFD2 move-result-object v0
CODE:0002CFD4 iget-object v1, this, SimpleCursorAdapter_mOriginalFrom
CODE:0002CFD8 invoke-direct {this, v1}, <void SimpleCursorAdapter.
findColumns(ref) SimpleCursorAdapter_findColumns@VL>
CODE:0002CFDE
CODE:0002CFDE locret:
CODE:0002CFDE return-object v0
CODE:0002CFDE Method End
复制代码
IDA Pro的反汇编代码使用ref关键字来表示非Java标准类型的引用,如方法第1行的invoke-super指令的前半部分以下。
invoke-super {this, p0}, <ref ResourceCursorAdapter.swapCursor(ref)
复制代码
前面的ref是swapCursor()方法的返回类型,后面括号中的ref是参数类型。
后半部分的代码是IDA Pro智能识别的。IDA Pro能智能识别Android SDK的API函数并使用imp关键字标识出来,如第1行的invoke-super指令的后半部分以下。
imp. @ _def_ResourceCursorAdapter_swapCursor@LL
复制代码
imp代表该方法为Android SDK中的API,@后面的部分为API的声明,类名与方法名之间使用下划线分隔。
IDA Pro能识别隐式传递过来的this引用,在smali语法中,使用p0寄存器传递this指针,此处因为this取代了p0,因此后面的寄存器命名都依次减了1。
IDA Pro能识别代码中的循环、switch分支与Try/Catch结构,并能将它们以相似高级语言的结构形式显示出来,这在分析大型程序时对了解代码结构有很大的帮助。具体的代码反汇编效果读者能够打开5.2节使用到的SwitchCase.apk与TryCatch.apk的classes.dex文件自行查看。
使用IDA Pro定位关键代码的方法总体上与定位smali关键代码差很少。
第一种方法是搜索特征字符串。首先按下快捷键CTRL+S打开段选择对话框,双击STRINGS段跳转到字符串段,而后点击菜单项“Search→text”,或者按下快捷键ALT+T,打开文本搜索对话框,在String旁边的文本框中输入要搜索的字符串后点击OK按钮,稍等片刻就会定位到搜索结果。不过目前IDA Pro对中文字符串的显示与搜索都不支持,若是字符串中的中文字符显示为乱码,须要编写相关的字符串处理插件来解决,这个工做就交给读者去完成了。
第二种方法是搜索关键API。首先按下快捷键CTRL+S打开段选择对话框,双击第一个CODE段跳转到数据起始段,而后点击菜单项“Search→text”,或者按下快捷键ALT+T,打开文本搜索对话框,在String旁边的文本框中输入要搜索的API名称后点击OK按钮,稍等片刻就会定位到搜索结果。若是API被调用屡次,能够按下快捷键CTRL+T来搜索下一项。
第三种方法是经过方法名来判断方法的功能。这种办法比较笨拙,对于混淆过的代码,定位关键代码比较困难。好比,咱们知道crackme0502.apk程序的主Activity类为MainActivity,因而在Exports选项卡页面上输入Main,代码会自动定位到以Main开头的所在行,如图5-9所示,可粗略判断出每一个方法的做用。
为了让读者看到一种常见的Android程序的保护手段,这里更换一下破解思路。经过图5-10可发现,MainActivity$SNChecker.isRegistered()方法实际上返回一个Boolean值,经过判断它的返回值来肯定注册码是否正确。如今的问题是,若是该程序是一个大型的Android软件,并且调用注册码判断的地方可能不止一处,这种状况时,一般有两种解决方法:第一种是使用IDA Pro的交叉引用功能查找到全部方法被调用的地方,而后修改全部的判断结果;第二种方法是直接给isRegistered()方法“动手术”,让它的结果永远返回为真。很显然,第二种方法解决问题更利落,并且一劳永逸。
下面尝试使用这种方法进行破解,首先按下空格键切换到反汇编视图,发现直接修改方法的第二条指令为“return v9 ”便可完成破解,对应机器码为“0F 09”,将其修改完成后从新修复与签名,安装测试发现程序启动后就当即退出了。这时最早怀疑的是程序是否修改正确,使用IDA Pro从新导入修改过的classes.dex文件,发现修改的地方没错,看来是程序采起了某种保护措施!回想一下前面提到的两种程序退出方法: Context的finish()方法与android.os.Process的killProcess()方法,按下快捷键CTRL+S并双击CODE回到代码段,接着按下快捷键ALT+T搜索finish与killProcess,最后在MyApp类的onCreate()方法中找到了相应的调用,查看相应的反汇编代码,发现这段代码使用Java的反射机制,手工调用isRegistered()方法检查字符串“11111”是否为合法注册码,若是是或者调用isRegistered()失败都说明程序被修改过,从而调用killProcess()来杀死进程。明白了保护手段,解决方法就简单多了,直接将两处killProcess()的调用直接nop掉(修改相应地方的指令为0)就能够了。
对于Android恶意软件分析人员来讲,提起Androguard应该不会感到陌生,Androguard提供了一组工具包来辅助分析人员快速鉴别与分析APK文件。
在分析大型软件时,为了弄清程序的结构框架,须要花费掉大量的时间与精力来阅读smali代码,这无疑是分析成本的一大开销。然而,Android程序大多数状况下是采用Java语言开发的,传统意义上的Java反汇编工具依然可以派上用场。
dex2jar的官网是https://sourceforge.net/projects/dex2jar/,目前最新版本为020.0,将下载下来的dex2jar压缩包解压,而后将解压后的文件夹添加到系统的PATH环境变量中,在命令提示符下输入如下命令:
d2j-dex2jar xxx.apk
复制代码
稍等片刻就会在同目录下生成一个jar文件。dex2jar是一个工具包,除了提供dex文件转换成jar文件外,还提供了一些其它的功能,每一个功能使用一个bat批处理或sh脚原本包装,只需在Windows系统中调用bat文件、在Linux系统中调用sh脚本便可。
d2j-apk-sign用来为apk文件签名。命令格式:d2j-apk-sign xxx.apk。
d2j-asm-verify用来验证jar文件。命令格式:d2j-asm-verify -d xxx.jar。
d2j-dex2jar用来将dex文件转换成jar文件。命令格式:d2j-dex2jar xxx.apk
d2j-dex-asmifier用来验证dex文件。命令格式:d2j-dex-asmifier xxx.dex。
d2j-dex-dump用来转存dex文件的信息。命令格式:d2j-dex-dump xxx.apk out.jar。
d2j-init-deobf用来生成反混淆jar文件时的初始化配置文件。
d2j-jar2dex用来将jar文件转换成dex文件。命令格式:d2j-jar2dex xxx.apk。
d2j-jar2jasmin用来将jar文件转换成jasmin格式的文件。命令格式:d2j-jar2jasmin xxx.jar
d2j-jar-access用来修改jar文件中的类、方法以及字段的访问权限。
d2j-jar-remap用来重命名jar文件中的包、类、方法以及字段的名称。
d2j-jasmin2jar用来将jasmin格式的文件转换成jar文件。命令格式:d2j-jasmin2jar dir
dex2jar为d2j-dex2jar的副本。
dex-dump为d2j-dex-dump的副本。
复制代码
为了达到源码级的反编译效果,可使用Java反编译工具JAD将jar文件转换成Java源文件,目前JAD官网已经没法访问,能够经过http://www.varaneckas.com/jad/下载到JAD的可执行文件。
在这里,笔者推荐使用jd-gui。jd-gui是一款用 C++ 开发的 Java 反编译工具,支持Windows、Linux和苹果Mac OS三个平台。jd-gui是免费的,并且反编译效果不错,该工具省掉了将jar文件转换成Java源文件的步骤,直接以源码的形式显示jar文件中的内容,能够从官方免费获取。
除了反编译功能外,jd-gui还带有强大的搜索功能,在主界面按下快捷键CTRL+F,会在程序的状态栏显示一个搜索工具条,输入要搜索的内容,当前打开的反编译窗口会高亮显示搜索结果。除此以外,点击菜单项“Search→Search”会弹出搜索对话框,搜索框列举出了isRegistered()方法在哪些文件中被引用过。