Espydroid+: 对 Android App 进行精准的反射分析
本文介绍2019年 Computers & Security 期刊的一篇文章,题目是《Espydroid+: Precise reflection analysis of android apps》,DOI信息在这里。
文章以恶意程序用于逃逸检测的反射 API 入手,提出一种叫做 Espydroid+ 的混合分析方法。其中,静态分析主要用来分析调用反射 API 的程序路径,动态分析主要负责按上述路径执行。此外, Espydroid+ 还提供了智能 UI 探索、反射 API 改写等功能。
概览
Android 恶意软件使用反射(reflection) API 获取用户数据,盗窃个人信息。这些恶意软件可以通过使用参数混淆、加密等方法逃避基于静态分析的检测。动态分析是一种检测运行时恶意行为的可行方案,但动态分析通常会带来大量无用的程序分支,它们与反射 API 的分析无关,而且会严重影响动态分析的效率。
在本文中,作者提出一种叫做 Espydroid+
的混合分析(hybrid analyze)方法,以克服上述问题。Espydroid+
结合了反射引导的静态切片(Reflection Guided Static Slicing, RGSS),它是一种有效的方法,通过修剪(prune)不相关的程序路径来处理大量程序路径的探索,并确保结果路径在次动态分析中得到执行。作者观察到 Espydroid+
成功移除了数据集总路径的 59.91% ,且没有丢失任何语义信息,因此认为 Espydroid+
是快速有效的。
简介
自 Android 系统发布以来,研究界和反病毒行业不断发现和报告安卓应用的各种安全问题。从 Android app 的增长速度来看,人工分析恶意软件是不可行的。恶意软件的作者每时每刻都会在恶意 app 中加入各种先进的技术,以阻碍自动化工具的分析。
反射(reflection)和运行时绑定(run-time binding)是恶意软件作者常用的几种主要技术。有文献分别指出观测样本中的 57.08% 、 49.13% 与 76.4% 使用反射。这些统计数据促使作者开发一个更精确和可扩展的解决方案来分析反射 API 。
基于静态分析的方法有几个固有的限制。首先,静态分析会遗漏许多反射目标,因为恶意软件作者通常会将反射与运行时依赖代码结合起来。这包括对反射 API 的参数使用数组索引、子串、加密、多态等。其次,基于类型推理的静态分析会产生很多假阳性,导致精度不高。
基于动态分析的解决方案通过执行 app 来观察其行为,改善了这种情况下的分析。然而,其缺点是在动态分析过程中存在大量的路径需要探索。因此,动态分析受到路径爆炸的挑战,无法达到理想的覆盖率。
为解决静态分析与动态分析的问题,一些混合分析方法被提出。但它们都存在一些不足: Tamiflex
易丢失隐藏在复杂GUI操作后面的目标; MIRROR
采用了 Java 程序分析的方法,但由于异步与事件驱动机制,用于传统 Java 的方法并不适合 Android 。
Android 自身提供 Monkey
作为 Android SDK的一部分,用于在测试中生成一定数量的随机事件。 Monkey
的局限在于:
- 事件是随机的,因此,可能与测试目标不相关
- 产生很多重复的事件
- 产生正确的文本输入的概率很低
- 没有保证覆盖面
此外,动态分析工具不能解决用户输入带来的问题,几乎所有基于动态分析的工具均不能处理依赖于用户输入的 app 。
这篇文章提出 Espydroid+
,联合动态分析与作者设计的 RGSS 静态分析技术。 RGSS 通过删除所有不会导致调用任何反射的组件,生成一个优化的 app ,以确保反射 API 在运行时得到执行。文章的主要贡献如下:
- 提出
Espydroid+
,一种用于Android应用中反射分析的混合分析方法,与现有的静态方法相比,能够精确地处理混淆的反射调用。 Espydroid+
使用 RGSS 对 app 进行剪枝。在动态分析前, RGSS 去除不含反射 API 的不相关路径,生成一个切片 app,减少了接下来Espydroid+
的动态分析阶段面临的状态空间爆炸的影响。- 文章对
Espydroid+
使用由 660 个 app 组成的数据集进行测试。 RGSS 移除了总路径的 59.91% 。Espydroid+
在精度与找回率两方面均体现出优于其它方案的效果。
代表案例
文章通过下面的例子说明了反射 API 的运行时绑定如何限制了最先进的静态分析方法。
如图,这个 app 共有 6 个 Activity
组件、 1 个 Service
组件、 1 个 Receiver
组件和 2 个 Java 类。 A.3
引发了 B
组件的 onCreate()
方法被调用,之后根据 B.4-B.6
又调用了 clz
类的 meth
变量中存储的方法。但是由于变量做了编码,其值需要在运行时中获取。这使得基于字符串和类型推理的静态分析方法都被欺骗了。该方法实际调用了 J
类的 write_sf()
方法。
上例显示了目前最先进的方法在分析中存在的问题:
- 反射目标的绑定过晚。这使静态分析解决方案受挫,因为它们在这种情况下无法发现正确的参数值。
- 组件间通信( ICC )。 ICC 在 Android 中有广泛的应用,它解决了对其它组件代码的依赖问题。 ICC 动态加载类与方法名,而现有方法不能准确识别。
- 条件约束。部分恶意软件家族使用条件约束实现反模拟器运行,以逃避检测。如检测
BRAND
值,其一般为设备生产厂商名,但模拟器为generic
或generic_86
。 - 复杂 GUI 。一些恶意 app 将恶意代码隐藏在复杂的 GUI 操作后面,以逃避基于
Monkey
的检测。
上述讨论强调了开发能够克服这些复杂问题的方法的必要性,以确保 Android app 的隐私。
Espydroid+
Espydroid+
的主要贡献是提出了一种更好的剪枝算法,通过剪去没有反射调用的组件,使得更多的反射调用被执行,同时不影响精确度。此外, Espydroid+
添加了用于处理逃避分析( anti-analyse )的解决方法。
下图展示了 Espydroid+
的结构,主要包括3个阶段。最重要的是 RGSS 阶段,将重点说明。
RGSS
剪枝过程主要针对 GUI 组件(即 Activity
)进行。这一阶段生成一个切片后的 app,间接提高目标 API 的可达性。文章描述了一种粗粒度的组件级切片方法,除了对不包含任何相关 API 的组件进行断链外,不会从任何组件中删除任何代码。
文章将 app 表示为 5 元组状态图 $(Q, \sum, \delta, q_0, F)$ ,其中
- 每个状态 $q \in Q$ 代表 app 的类
- 每个输入事件 $e \in (\sum)$ 是一条指令,它的执行会使 app 跳转至下一个状态
- $\delta$ 表示$e$ 执行时两个状态之间的转移
- $q_0$ 是起始状态,是 app 的启动
Activity
,对每个 app 唯一,在manifest
文件中注明 - 所有拥有反射调用的状态是终状态 $F$
文章通过消除不相关的转移来生成一个缩小的状态图,并将其称为反射引导状态图( Reflection Guided State Diagram , RGSD )。文章首先描述了一种从 Android app 中建模标准状态图的方法,然后设计了一种从原始状态图生成 RGSD 的算法,共分5个步骤。
步骤 1 :组件转移分析 此步骤从 app 的字节码中构建状态图。在 Android 系统中,两个组件之间的通信是通过 ICC 方法进行的,这些方法是标准的、预定义的。文章使用 DialDroid
作为工具,生成状态转移图。下图是以“代表案例”中的 app 为例的生成结果。
步骤 2 :提取终状态 在处理类是否存在来自其中间代码的 RPC 调用的同时,文章也会对反射API进行解析,并将结果以 “CN-MN” 的形式存储,其中 MN 是包含反射 API 的 CN 类的一个方法。在解析反射 API 时, Android 的所有框架类都被排除在外。这些类是状态图的终状态( $F$ ),并作为下一步的输入。对于上例, $F = { A, B, C, D }$ 。
步骤 3 :状态图转化为 RGSD 删除不能到达终状态的所有转移。
上述算法描述了这一步骤。上一步骤的结果,即状态转移图与 $F$ 作为本步骤的输入。 processQueue
队列用于维护即将处理的状态。初始状态下,将 $F$ 添加到 processQueue
中,因为后向分析是从终状态开始的。 RQ
用于存储相关状态, Rδ
用于存储相关转移。遍历从处理初始化的 processQueue
开始,一直持续到队列变空。空队列表示处理已经结束。该算法每次从 processQueue
中取出( deque )一个元素,存储在 CQ
中,并找到它的前级元素(第 6 行)。如果节点有前级元素,那么所有还没有出现在队列中的前级元素,都会被排到 processQueue
。这个检查是为了确保在遍历过程中不会产生循环。这些前级元素被添加到 RQ
列表中。每一个前级元素和 CQ
之间的所有转移都是相关的,所以它们也被添加到 Rδ
中(第 9-15 行)。
在路径计算过程中,会遇到一些既不是初始状态( $q_0$ ),也不是没有被调用的状态。这意味着从启动 Activity
中没有通往这些状态的路径。目标是在动态分析过程中不遗漏任何这样的路径。因此,文章准备了一个单独的列表, UnlinkedStates
,包含了所有这样的状态,这些状态没有任何从初始状态的路径,但它们或者是终状态,或者是通往终状态的路径(第 18-20 行)。
最后,移除所有不在 Rδ
中且具有 activity 目标的转移(第 23-27 行)。下图给出了步骤 1 结果经步骤 3 处理后的结果。
步骤 4 :约束处理 主要针对一些恶意软件家族的反分析约束,例如示例代码的 A.6
检测了 Build Brand
。
上述算法解释了处理约束的方法。首先找出每个转移的根方法( root method ),这里根方法指的是包含过渡方法的最上层的调用者。之后,使用控制流图计算从根方法的第一条语句到当前转移的控制流路径,判断其中的程序分支(主要是 If
、 For
与 While
语句)应该如何选择。重写 For
与 While
语句时可能导致程序无限循环,文章采用 Soot
工具的 LoopFinder
解决此问题。输出示例如下图。
步骤 5 :明确无关联组件的意图 本步骤主要处理步骤 3 生成的 UnlinkedStates
中的状态。它们主要由 receiver
与 service
这些非 GUI 组件组成。 Android 的事件驱动机制导致其存在,由于其主要由系统事件驱动执行,在动态分析中,可能由于没有相应事件而导致其逃避检测。文章提供 Dynodroid
工具作为解决方法之一,它通过观察 app 监听的事件,通过在检测时产生同样的事件,使得这些组件得到执行。
智能 UI 探索
这部分工作主要是作者基于以前工作的提高。集中在两个方面:
- 在没有输入正确的文本时, app 就会终止执行,这是不希望出现的。因此,文章建议生成有效的文本输入,例如:电子邮件地址、日期、电话号码等。通过维护有效输入的数据库,提供特定内容的输入,该数据库与 app 中这些输入的可能标签提示相关联(而不是随机生成一个输入)。
- 使用错误检测与处理模块。在执行过程中,由于无效输入或事件、数据不足等原因导致 app 异常终止的 widget 被记录下来,并确保在后续运行中忽略错误的
view
组件。这样就避免了不必要的、不成功的 app 执行。
改造反射 API
此阶段通过语义上等价的非反射方式(即传统的Java调用)来构造使用反射的代码语句,并在原来的 app 中使用这些语句。根据 Soot
的内建 Jimple
表达完成。经过这一步骤, app 可以经由其它静态分析方法工具正确地检测。 示例中A.12
的 Jimple
代码表示如下:
1 | $r9 = new com.example.read; |
Espydroid+ 的不足
最后,作者总结了 Espydroid+
的两点不足:
- 不能处理包含非 Android Native 代码的 app 与基于 JavaScript 的 app 。
Espydroid+
主要基于对 Java 字节码的分析,若 app 不以 Java 字节码实现,则不能实现检测效果。 - 无法分析那些实现了防止被修改或被修改后功能不正常的机制的 app 。动态分析的基础是 app 的正常运行。一些 app 在运行前检查其代码的完整性,或者经过修改后 app 运行异常,均不能进行动态分析。