通过 Hooking 技术提高 Android 应用程序的安全性:第 1 部分





5.00/5 (2投票s)
在本文中,我将分享一些使用 Hooking 技术进行的研究,以针对 Android 应用程序中的某些离线攻击提供简单有效的保护解决方案。
Intel® Developer Zone 为跨平台应用程序开发提供工具和操作指南,以及平台和技术信息、代码示例和同行专家的经验,以帮助开发人员创新和取得成功。加入我们的社区,了解 Android、Internet of Things、Intel® RealSense™ Technology 和 Windows,下载工具,获取开发套件,与志同道合的开发人员分享想法,并参与黑客马拉松、竞赛、路演和本地活动。
目录
- Android 中的常见安全风险
- Android 应用程序和包概述
- Android 开发中的风险意识
- Hooking 技术概述
- 什么是 Hooking?
- 实现 Hooking
- 内联重定向
- 符号表重定向
- libtest_nonPIC.so 中非 PIC 代码的研究
- 转到第二部分 ››
在 Android* 开发领域,开发人员通常利用第三方库(例如游戏引擎、数据库引擎或移动支付引擎)来开发他们的应用程序。通常,这些第三方库是闭源库,因此开发人员无法更改它们。有时第三方库会给应用程序带来安全问题。例如,用于调试的内部日志打印可能会在登录和支付过程中泄露用户凭据,或者攻击者可以轻松获取存储在游戏引擎本地的明文资源和脚本。
在本文中,我将分享一些使用 Hooking 技术进行的研究,以针对 Android 应用程序中的某些离线攻击提供简单有效的保护解决方案。
Android 中的常见安全风险
Android 应用程序和包概述
Android 应用程序通常使用 Java* 编程语言编写。当开发人员需要请求性能或低级 API 访问时,他们可以使用 C/C++ 编写代码并编译成原生库,然后通过 Java Native Interface (JNI) 调用它。之后,Android SDK 工具会将所有编译的代码、数据和资源文件打包成 Android Package (APK)。
Android 应用程序以 APK 格式打包和分发,这是一种标准的 ZIP 文件格式。可以使用任何 ZIP 工具将其解压。解压后,APK 文件可能包含以下文件夹和文件(参见图 1)
- META-INF 目录
- MANIFEST.MF — manifest 文件
- CERT.RSA — 应用程序的证书
- CERT.SF — 资源列表和 MANIFEST.MF 文件中相应行的 SHA-1 摘要
- classes.dex — 以 Dalvik 虚拟机可理解的 DEX 文件格式编译的 Java 类
- lib — 包含特定于处理器软件层的已编译代码的目录,包含以下子目录
- armeabi — 为所有基于 ARM* 的处理器编译的代码
- armeabi-v7a — 为所有基于 ARMv7 及更高版本的处理器编译的代码
- x86 — 为 Intel® x86 处理器编译的代码
- mips — 为 MIPS 处理器编译的代码
- assets — 包含应用程序资源的目录,可以通过
AssetManager
检索 - AndroidManifest.xml — 额外的 Android manifest 文件,描述应用程序的名称、版本、访问权限、引用的库文件
- res — 放置所有应用程序资源的地方
- resources.arsc — 包含预编译资源的文件
一旦包安装在用户的设备上,其文件将被提取并放置在以下目录
- 整个应用包文件被复制到 /data/app
- classes.dex 被提取并优化,然后优化后的文件被复制到 /data/dalvik-cache
- 原生库被提取并复制到 /data/app-lib/<package-name>
- 创建一个名为 /data/data/<package-name> 的文件夹,并分配给应用程序存储其私有数据
Android 开发中的风险意识
通过分析上一节给出的文件夹和文件结构,应用程序存在几个开发人员应该注意的易受攻击点。攻击者可以通过利用这些弱点获得大量有价值的信息。
一个易受攻击点是应用程序将原始数据存储在“asset”文件夹中,例如游戏引擎使用的资源。这包括音频和视频素材、游戏逻辑脚本文件以及精灵和场景的纹理资源。由于 Android 应用包未经加密,攻击者可以通过从应用商店或其他 Android 设备获取包来轻松获取这些资源。
另一个易受攻击点是已 root 设备和外部存储的文件访问控制薄弱。攻击者可以通过受害者设备的 root 权限获取应用程序的私有数据文件,或者应用程序数据被写入外部存储,如 SD 卡。如果私有数据没有得到很好的保护,攻击者就可以从文件中获取一些信息,例如用户帐户信息和密码。
最后,调试信息可能可见。如果开发人员在发布应用程序之前忘记注释掉相关的调试代码,攻击者就可以使用 Logcat 检索调试输出。
Hooking 技术概述
什么是 Hooking?
Hooking 是一系列代码修改技术的统称,用于通过在运行时将指令插入代码段来改变原始代码运行顺序的行为(图 2 概述了 Hooking 的基本流程)。
本文研究了两种 Hooking 技术
- 符号表重定向
通过分析动态链接库的符号表,我们可以找到外部调用函数 Func1()
的所有重定位地址。然后,我们将每个重定位地址修补为 Hooking 函数 Hook_Func1()
的起始地址(参见图 3)。
- 内联重定向
与必须修改每个重定位地址的符号表重定向不同,内联 Hooking 只会覆盖我们要 Hook 的目标函数的起始字节(参见图 4)。内联重定向比符号表 Hooking 更健壮,因为它一次只做一个更改。缺点是,如果原始函数在应用程序的任何地方被调用,它也会执行被 Hook 函数中的代码。因此,我们必须在重定向函数中仔细识别调用者。
实现 Hooking
由于 Android OS 基于 Linux* 内核,因此 Linux 的许多研究也适用于 Android。此处详细介绍的示例基于 Ubuntu* 12.04.5 LTS。
内联重定向
创建内联重定向最简单的方法是在函数的起始地址插入一个 JMP 指令。当代码调用目标函数时,它会立即跳转到重定向函数。请参见图 5 所示的示例。
在主进程中,代码运行 func1()
来处理一些数据,然后返回主进程。func1()
的起始地址是 0xf7e6c7e0。
内联 Hooking 注入过程将地址中的前五个字节替换为 0xE9 E0 D7 E6 F7。该过程创建了一个跳转指令,该指令将跳转到地址 0xF7E6D7E0,即称为 my_func1()
的函数的入口。所有对 func1()
的代码调用都将重定向到 my_func1()
。传递给 my_func(1)
的数据经过预处理阶段,然后将处理后的数据传递给 func1()
以完成原始过程。图 6 显示了 Hooking func1()
后的代码运行顺序。图 7 给出了 Hooking func1()
后的伪 C 代码。
使用此方法,原始代码不会意识到数据处理流程的改变。但是,更多的处理代码已附加到原始函数 func1()
。开发人员可以使用此技术在运行时向函数添加补丁。
符号表重定向
与内联重定向相比,符号表重定向更为复杂。相关的 Hooking 代码必须解析整个符号表,处理所有可能的情况,逐个搜索并替换重定位函数地址。DLL(动态链接库)中的符号表将根据使用的编译器参数以及开发人员如何调用外部函数而有很大差异。
为了研究关于符号表的所有情况,创建了一个包含两个使用不同编译器参数编译的动态库的测试项目
- 位置无关代码 (PIC) 对象 — libtest_PIC.so
- 非 PIC 对象 — libtest_nonPIC.so
图 8-11 展示了测试程序的代码执行流程,libtest1()/libtest2() 的源代码,它们是完全相同的函数,只是使用了不同的编译器参数编译,以及程序的输出。
使用 printf()
函数进行 Hooking。它是将信息打印到控制台最常用的函数。它定义在 stdio.h 中,函数代码位于 glibc.so 中。
在 libtest_PIC 和 libtest_nonPIC 库中,使用了三种外部函数调用约定
- 直接函数调用
- 间接函数调用
- 本地函数指针
- 全局函数指针
libtest_nonPIC.so 中非 PIC 代码的研究
标准的 DLL 对象文件由多个节组成。每个节都有其自身的角色和定义。.rel.dyn 节包含动态重定位表。可以使用 objdump –D libtest_nonPIC.so 命令反汇编文件的节信息。
在 libtest_nonPIC.so 的重定位节 .rel.dyn(参见图 12)中,有四个包含 printf()
函数重定位信息的地方。动态重定位节中的每个条目都包含以下类型
- Offset 中的值标识要调整的 对象内的位置。
- Type 字段标识重定位类型。R_386_32 是一种重定位,它将符号的绝对 32 位地址放置到指定的内存位置。R_386_PC32 是一种重定位,它将符号的 PC 相关 32 位地址放置到指定的内存位置。
- Sym 部分引用所引用符号的索引。
图 13 显示了函数 libtest1()
生成的汇编代码。用红色标记的 printf()
的入口地址在图 12 的 .rel.dyn 重定位节中指定。
为了将 printf()
重定向到另一个名为 hooked_printf()
的函数,Hooking 函数应将 hooked_printf()
的地址写入这四个偏移地址。
如图 14-16 所示,当链接器将动态库加载到内存时,它首先找到重定位符号 printf 的名称,然后将 printf 的实际地址写入相应的地址(偏移量 0x4b5、0x4c2、0x4cf 和 0x200c)。这些对应的地址定义在 .rel.dyn 重定位节中。之后,libtest1()
中的代码就可以正确地跳转到 printf()
。