安卓平台是个多进程同时运行的系统,它还缺少合适的动态分析接口。因此,在安卓平台上进行全面的动态分析具有高难度和挑战性。已有的研究大多是针对一些安全问题的分析方法或者框架,无法为实现更加灵活、通用的动态分析工具的开发提供支持。此外,很多研究只是针对单进程的分析,在安卓平台多个应用进程协作完成事务的情境下,则无法进行很好的分析。
目录
[5 实例分析](#5 实例分析)
[5.1 覆盖率程序分析实例](#5.1 覆盖率程序分析实例)
[5.1.1 注入与分析实现](#5.1.1 注入与分析实现)
[5.1.2 Grinderbench 测试](#5.1.2 Grinderbench 测试)
[5.1.3 安卓应用情景测试](#5.1.3 安卓应用情景测试)
[5.2 权限使用检测实例](#5.2 权限使用检测实例)
[5.2.1 注入与分析实现](#5.2.1 注入与分析实现)
[5.2.2 结果以及分析](#5.2.2 结果以及分析)
[5.3 本章小结](#5.3 本章小结)
5 实例分析
5.1 覆盖率程序分析实例
测试程序的覆盖率,是一种评价测试活动覆盖产品代码比例的指标。代码覆盖率能够直接反映一个程序执行路径(比如某测试用例)的有效性。在 JVM 上已经有不少工具可以测试代码的覆盖率,比如 Emma和 Jacoco。JaCoCo 如今也能支持在安卓平台测试代码覆盖率,然而 JaCoCo 测试需要与安卓的部署系统Gradle 相结合,以在部署应用的时候预注入代码覆盖的相关逻辑和依赖的 JaCoCo类库。由于 JaCoCo 的功能已经固定在它复杂的代码体系里,要扩展并不容易。
5.1.1 注入与分析实现
由于方法属于类,分支属于方法,因此找到分支的覆盖情况的过程中也能获取类以及方法覆盖的情况。为此如何获取分支覆盖的数据是本实例重要的一点。在 Java 语言中,分支的产生只有两种情况,一种是条件分支语句,一种是 Switch分支语句(并不包含异常处理语句),这与 JaCoCo 的实现原理相同。为了识别某一次方法调用中分支覆盖的情况,可以静态得从遍历该方法内的所有字节码指令,获取方法的所有分支,在运行时记录每一个遍历到的分支。
下图表示了本实例在方法进入和方法退出时候进行的注入。本文利用了DiSL 的@SyntheticLocal 变量。这个变量会被 DiSL 加到目标程序的方法里,变为一个方法局部变量,以供分析使用。本案例中,创建了两个该种类的变量:encounterBranch 和 branches,分别表示是否在分支语句内,以及方法内分支覆盖率的统计信息(采取 boolean 数组的形式,分别对应一个方法内 b local 个分支)。
@SyntheticLocal
public static boolean encounterBranch = false;
@SyntheticLocal
public static boolean [] branches;
@Before (marker = BodyMarker.class, order = 2)
public static void onMethodEntry (CodeCoverageContext c) {
branches = new boolean [c.getMethodBranchCount ()];
}
@After (marker = BodyMarker.class, order = 2)
public static void onMethodExit (CodeCoverageContext c) {
CodeCoverageAnalysisStub.commitBranch (
c.thisClassName(), c.thisMethodName(), branches);
}
通过两个实验来验证本实例分支覆盖率分析的正确性。第一个实验,通过与已有的 JaCoCo 分支覆盖率测试工具进行比较以及对 Grinderbench这个轻量级 Java 测试套件实验来验证本案例分析结果的正确性;第二个实验,通过对一个安卓应用程序的四个操作情景进行测试,统计并验证四种情境下几个核心方法的覆盖率情况与代码情况是否吻合。
@AfterReturning (marker = BranchInsnMarker.class, order = 1)
public static void afterBranchInstr (CodeCoverageContext c)
{
if (encounterBranch) {
branches [c.getIndex ()] = true;
encounterBranch = false;
}
}
@Before (marker = SwitchInsnMarker.class, order = 1)
public static void beforeSwitch () {
encounterBranch = true;
}
@AfterReturning (marker = BranchLabelMarker.class, order = 1)
public static void afterBranchLabel (CodeCoverageContext c)
{
if (encounterBranch) {
branches [c.getIndex ()] = true;
encounterBranch = false;
}
}
5.1.2 Grinderbench 测试
Grinderbench 是一个 JVM 上的简单的测试套件,其包括 Parallel、kXML、PNGdecoding、Chess 和 Crypto 五个测试,涉及多线程、CPU 密集、I/O 密集等测试类型。在不添加任何参数的情况下,该测试会依次进行这五个测试。本案例就是测量这种模式下的程序覆盖率。为了能够在安卓平台上运行 Grinderbench,本文利用 dx 工具,将 Grinderbench 的 jar 文件转换成 DVM 下可以执行的 jar 文件,并通过 adb 工具将该 jar 包发送到安卓系统上。这样就可以在安卓系统上通过 dalvikvm来新建一个 DVM 进程从而进行测试。
在 JVM 上,首先利用 JaCoCo 工具,来测试上述的运行模式下程序的覆盖率测试结果;紧接着,在本框架下于安卓 Dalvik 虚拟机中执行分支覆盖率测试。由于这部分分析没有安卓独有的事件,因而可以与原 JVM 上的 ShadowVM 模型兼容。可以直接在 JVM 上利用相同的分析代码执行覆盖率测试。
实验结果如表 5-1 所示。第一段数据是 JaCoCo 分别在 JVM 和 DVM 上对GrinderBench 进行测试的结果汇总情况,二者完全一致。第二段是本文开发的覆盖率分析分别在JVM和DVM上的结果,可以发现无论是在JVM上还是DVM上,覆盖到的分支、方法和类的数目均与 JaCoCo 一致。然而在总数上与 JaCoCo 并不完全相同。
因此可以看出基于本文框架,不仅能够方便的开发动态分析,还可以兼有跨平台的可移植性,而分析结果也能正确的反映程序的实际执行。
5.1.3 安卓应用情景测试
DroidBench测试集合中的 ImplicitFlow4 应用来检测本分析在安卓应用中的测试情况。ImplicitFlow4 应用是一个简易的安卓程序,它含有一个动组件,组件中包含了用户名输入、密码输入的文本框和登陆的按钮。用户输入用户名密码以后点击按钮会触发程序的判断操作。该应用的具体代码参考DroidBench 的 github 源代码库。
本案例分析的目标是目标应用中的两个重要方法checkUsernamePassword 以及 lookup,用户输入错误的用户名或者密码都会触发不同的程序运行路径从而造成不同的分支覆盖结果。本案例采取了四组用户数据作为输入:
1-用户名错误;
2-用户名正确、密码错误;
3-用户名正确,密码正确;
4-用户依次按照前三个情况输入。
经过试验,这四种情况下的覆盖情况如下所示。通过直接阅读源代码,可以验证该结果的正确性。
5.2 权限使用检测实例
安卓系统采取权限控制的方式控制应用的访问能力。应用在安装时会提示用户需要的权限,用户并没有办法了解这些赋予的权限会被如何使用。加上安卓存在很多利用 Intent 的跨进程消息,通过静态分析很难完全识别出所有的敏感函数调用。
5.2.1 注入与分析实现
先介绍本案例用户定义的分析事件,即注入部分需要插入的事件。为此首先需要监控 ActivityManagerService 类的 checkPermission 方法。
public class IPCAnalysis extends RemoteIPCAnalysis {
/* Analysis Event Handler Starts */
public void permissionUsed (
Context ctx, int tid, ShadowString permissionName) {
List<ThreadState> callers = ThreadState.getCallers (ctx,tid);
for(ThreadState caller:callers){
caller.addPermission(permissionName.toString ());
}
}
public void boundaryStart (
Context ctx, int tid, ShadowString boundaryName) {
ThreadState state = ThreadState.get (ctx, tid);
state.pushBoundary (boundaryName.toString ());
}
public void boundaryEnd (
Context ctx, int tid, ShadowString boundaryName) {
ThreadState state = ThreadState.get (ctx, tid);
state.popBoundary (boundaryName.toString ());
}
/* Analysis Event Handler Ends */
ActivityManagerService.checkPermission 方 法 接 受 三 个 参 数 : (Stringpermission, int pid, int uid),其中 permission 参数是一个字符串,用来表示不同的权 限 的 名 称 , pid 与 uid 则 分 别 表 示 调 用 进 程 的 进 程 号 与 用 户 id 。ActivityManagerService类的checkPermission方法一般在System Server进程中被调用,而且一般都是响应来自别的进程的 API 调用,因此它处在 IPC Binder 调用的第二与第三个事件之间。本案例利用该方法来捕获System Server所有的权限验证。其次,为了在分析过程检测到权限使用时能够还原出目标应用进程的对应线程的栈情况,需要在分析端为每个分析进程的每个线程维护一个栈。这个实现原理如下,只要记录下每次进入一个方法以及离开一个方法,就能知道任意时刻的线程的栈信息。在注入端,可以通过监控目标应用中的所有类的方法入口以及方法出口,产生相应分析事件,这样在分析服务器就可以动态得掌握当前分析事件处在什么样的运行时栈当中。注入部分的代码此处略去,可以参考图 5-3 中的分析响应代码。
由于在分析中并不是所有的分析都需要完整的还原 IPC 事件的顺序,因此框架并不直接提供事件同步功能。而本案例需要严格的控制事件顺序,也展示了框架的灵活性和可扩展性。
@Override
public void onRequestSent (
TransactionInfo info, NativeThread client, Context ctx) {
ThreadState clientState = ThreadState.get (client);
clientState.recordRequestSent(client,info);
}
@Override
public void onRequestReceived (
TransactionInfo info, NativeThread client,
NativeThread server, Context ctx) {
ThreadState clientState = ThreadState.get (client);
clientState.waitForRequestSent (info);
ThreadState serverState = ThreadState.get (server);
serverState.recordRequestReceived(client, server, info);
}
@Override
public void onResponseSent (
TransactionInfo info, NativeThread client, NativeThread
server, Context ctx) {
ThreadState serverState = ThreadState.get (server);
serverState.recordResponseSent(client, server, info);
}
@Override
public void onResponseReceived (
TransactionInfo info, NativeThread client,
NativeThread server, Context ctx) {
ThreadState serverState = ThreadState.get (server);
serverState.waitForResponseSent (info, client);
ThreadState clientState = ThreadState.get (client);
if(clientState.getPermissionCount()>0){
IPCLogger.reportPermissionUsage (clientState);
clientState.clearPermissions ();
}
clientState.recordResponseReceived(client, server, info);
}
}
5.2.2 结果以及分析
为了验证分析的有效性,本文通过一个简单的实例,来展示本实例的功能。本案例选取 DroidBench 测试程序组中的 Reflection_Reflection4 程序作为测试程序。该程序只包含了一个活动组件,在启动应用后活动组件 onCreate 会自动被调用。
应用程序 => Phone进程 => System Server 进程 => Phone进程 => 应用程序。
通过本文的框架,首先 System Server 进程中当调用 ActivityManagerService的 checkPermission 方法检测 READ_PHONE_STATE 权限时,分析中会识别所有的上层调用方(Phone 进程的某个线程以及应用进程的某个线程),并告知它们在这段 IPC 调用中这个权限被使用了。那么最后应用进程执行完 IPC 方法后,分析端收到 onResponseReceived 事件,即收到 getDeviceId 的 IPC 调用返回值的时候,它就知道了该 IPC 调用造成了 READ_PHONE_STATE 权限的使用。
分析端得到的分析结果如图 5-7 所示,第一段信息表明了在程序执行到ConcreteClass 的 foo 方 法 里 , 调 用 TelephoneManager.getDeviceId 的 时 候READ_PHONE_STATE 权限被使用了。类似的,第二段信息则表明在 Concrete 类的 bar 方法中,对 SmsManager.sendTextMessage 的调用引起了 SEND_SMS 以及WAKE_LOCK 权限的使用。从结果中也可以发现,一个 API 调用可能引起多个权限的使用,用户可以进一步在分析中筛选自己关注的那些权限。
5.3 本章小结
首先介绍了基于本框架实现的一个覆盖测试分析,通过运行 GrinderBench 以及在执行安卓应用这两种情景,验证了覆盖率分析的正确性,并说明了一些分析可以跨平台复用。接着通过一个检测安卓权限使用的案例,介绍了用户如何将分析与Binder 事件联合使用,以进行多进程分析。通过这两个例子充分展示了本框架强大的功能性,以及用户友好的分析编写方式。