使用钩子技术改进Android程序安全性(上篇)

译文
移动开发
在本文中,我想和诸位分享一些近阶段的研究成果;具体地说是,使用钩子技术来提供一种简单有效的保护方案以应对某些针对Android应用的离线攻击。

一、 简介

在Android开发世界中,开发人员通常利用第三方库(例如游戏引擎,数据库引擎或移动支付引擎)来开发他们的应用程序。通常情况下,这些第三方库是闭源代码库,因此开发商不能更改它们。有时,第三方库会给应用程序带来一定的安全问题。例如,用于调试目的内部日志打印可能会在用户登录和付款时泄漏凭据信息,或者是游戏引擎中一些存储在本地的明文形式的资源和脚本有可能轻易为攻击者所获得。

[[167107]]

在本文中,我想和诸位分享一些近阶段的研究成果;具体地说是,使用钩子技术来提供一种简单有效的保护方案以应对某些针对Android应用的离线攻击。

二、 Android应用中普遍存在的安全问题

(一) Android应用打包概述

Android应用程序通常是用Java编程语言编写的。当开发人员有高性能需求或低级API访问时,他们可以使用C/C++代码并编译为本机库,然后通过Java本机接口(JNI)调用它们。之后,Android SDK工具就会把所有已编译的代码、数据和资源文件打包到Android包(APK)中。

Android应用程序是以APK格式打包和发行的,其实这是一个标准的ZIP文件格式,可以使用任何ZIP工具解压缩。一旦解压缩,APK文件可能包含以下文件夹和文件(参考图1)︰

1.META-INF目录

  • MANNIFEST.MF:清单文件
  • CERT.RSA:应用程序证书
  • CERT.SF:相应于MANNIFEST.MF文件的资源和SHA-1 Digest清单

2.classes.dex:编译成DEX文件格式的Java类,为Dalvik虚拟机所理解和执行

3.lib:该目录包含特定于处理器的软件层的已编译代码,其下一般还包括如下子目录:

  • armeabi:包含所有基于ARM*处理器的编译代码
  • armeabi-v7a:包含所有基于ARMv7及以上版本处理器的编译代码
  • x86:包含基于Intel® x86处理器的编译代码
  • mips:包含基于MIPS处理器的编译代码

4.assets:该目录下包含应用程序资源,可以通过AssetManager来检索这个目录

5.AndroidManifest.xml: Android配置文件,描述了程序的名称、版本、访问权限、应用程序引用的库文件等

6.res:所有应用程序资源都放置在此目录下

7.resources.arsc:该文件中包含预编译资源

 图1:一个典型的Android APK包中的内容

一旦程序包被安装在用户设备上,它的文件将被提取并放置在以下目录中:

1.整个应用程序的包文件复制到路径/data/app

2.Classes.dex文件被提取和优化,并将优化后的文件复制到路径/data/dalvik-cache

3.本机库被提取并复制到路径/data/app-lib/<package-name>

4. 创建一个名为 /data/data/<package-name>的文件夹并分配给应用程序用以存储其私有数据。

(二) Android开发中的风险意识

通过在上一节中分析的文件夹和文件结构,作为开发人员必须应该知道应用程序中存在的几个弱点。攻击者可以利用这些弱点获得大量的有价值的信息。

例如,第一个脆弱点是,应用程序往往都把游戏引擎所使用的原始数据资源存储在assets文件夹中。这包括音频和视频材料、游戏逻辑脚本文件以及精灵和场景的纹理资源。因为Android应用程序的包并不加密,所以攻击者可以从应用程序商店或另一个Android设备中通过获得对应的包以后进而很容易地得到这些资源。

另一个易受攻击点是,针对根设备和外部存储的脆弱的文件访问控制。攻击者可以通过受害者设备的根特权来获取应用程序的私有数据文件,或者把应用程序数据写入例如SD卡这类外部存储上。如果不很好保护私有数据,攻击者可以从该文件中获得如用户帐户和密码等信息。

最后,调试信息可能是可见的。如果开发人员忘记在发布应用程序之前注释掉有关调试代码,攻击者可以通过使用Logcat工具来检索调试输出信息。

三、 钩子技术概述

(一) 何谓钩子

钩子是一系列用于更改代码技术的术语,用于修改原始代码运行序列的行为,其方式是通过在运行时代码段中插入一定的指令来实现。图2展示了钩子技术的基本实现流程。

 图2:钩子可以更改程序的运行顺序

在这篇文章中,将研究两种类型的钩子技术:

1.符号表重定向

通过分析动态链接库的符号表,我们可以找到所有的外部调用函数Func1() 的重定位地址。然后,我们把每个重定位地址修改到挂钩函数Hook_Func1()的起始地址(请参见图3)。

 图3:符号表重定向流程示意图

2.内联重定向

与符号表重定向必须修改每一个重定向地址不同的是,内联钩子只覆盖我们想要钩住的目标函数的起始字节(见图4)。内联重定向比符号表重定向更健壮,因为它在任何时候只修改一次。缺点是,如果在应用程序中的任何地方调用原始函数,那么它还会执行被钩住的函数中的代码。所以,我们在重定向函数中必须仔细地标识调用者。

 图4︰内联重定向的流程示意图

四、 实现钩子

因为Android操作系统基于Linux*内核,因此许多Linux的研究技术都适用于安卓系统。本文中给出的详细示例就是基于Ubuntu * 12.04.5 LTS。

(一) 内联式重定向

创建内联重定向的最简单方法是在函数的起始地址处插入JMP指令。当代码调用目标函数时,它将立即跳至重定向函数中。请参阅图5中所给的示例。

在主进程中,代码运行func1()来处理一些数据,然后返回到主进程。这里,func1()的起始地址是0xf7e6c7e0。

 图5:内联挂钩中使用前五个字节的函数来插入JMP指令

内联钩子注入过程会将地址中的前五个字节的数据替换成0xE9 E0 D7 E6 F7 。这个过程将创建一个跳转指令,此指令会跳转到地址0xF7E6D7E0,而这个地址正好是函数my_func1()的入口。于是,所有对 func1()的代码调用都将被重定向到my_func1()。输入到my_func(1)的数据经过一个预处理阶段,然后将处理过的数据传递给func1()来完成原始过程。图6展示了钩住func1()后的代码运行序列,而接下来的图7展示了建立钩子后func1()的伪C代码。

 图6:使用钩子:在func1()中插入my_func1()

使用此方法,原始代码不会意识到数据处理流程的变化。但是,更多的处理代码被追加到原始函数func1()中。开发人员可以使用这种技术在运行时添加程序补丁。

 图7:使用钩子——图6的伪C代码

(二) 符号表重定向

相对于内联重定向,符号表重定向更复杂。有关钩子代码必须解析整个符号表,处理所有可能的情况,一个接一个地搜索和替换重定位函数的地址。DLL(动态链接库)中的符号表将非常不同,这取决于使用了什么样的编译器参数以及开发人员调用外部函数的方式。

为了研究有关符号表的所有情况,需要创建包含两个使用不同编译器参数的动态库的测试工程,它们分别是:

1. 位置独立代码(PIC)对象:libtest_PIC.so

2. 非PIC对象:libtest_nonPIC.so

图8给出了测试程序的代码执行流程,以及libtest1()/libtest2()的源代码(注意:它们几乎具有完全相同的功能,除了使用不同的编译器参数编译外),还有程序的输出。

 图8︰测试项目的软件工作流程

函数printf()用于实现钩子,它是打印信息到控制台的最常用的函数。它定义在文件stdio.h中,而函数代码位于库文件glibc.so中。

在libtest_PIC和libtest_nonPIC库中,使用了三个外部函数调用约定:

1.直接函数调用

2.间接函数调用

  • 本地函数指针
  • 全局函数指针

 图9:libtest1()的代码

 图10:libtest2()的代码,与libtest1()相同

 图11:测试程序的输出结果

五、 libtest_nonPIC.so库中的非PIC代码研究

一个标准的DLL对象文件是由多个节(section)组成。每一节都有它自己的作用和定义。例如,Rel.dyn节中就包含了动态重定位表信息。文件的节信息可以通过命令 objdump -D libtest_nonPIC.so反编译得到。

在库文件libtest_nonPIC.so的重定位节rel.dyn中(见图12),共有四个地方包含了函数printf()的重定位信息。动态重定位节中的每个条目包括以下类型:

1.偏移量Offset中的值标识要调整的对象的位置。

2.类型字段Type标识重定位类型。R_386_32对应于把符号的32位绝对地址置于指定内存位置的重定位数据,而R_386_PC32则对应于把符号的32位PC相对地址置于指定内存位置的重定位数据。

3.Sym部分指向被引用的符号的索引。

图13展示了函数libtest1()的生成的汇编代码。有红色标记的printf()的入口地址在图12中重定位节rel.dyn中被标记出来。

 图12:库文件libtest_nonPIC.so的重定位节信息 

 图13︰libtest1()的反汇编代码以非PIC格式编译

为了把函数printf()重定向到另一个称为hooked_printf()的函数,挂钩函数把hooked_printf()的地址写入这四个偏移地址。

 图14:语句printf("libtest1: 1st call to the original printf()\n");的工作流程

 图15:语句global_printf1("libtest1: global_printf1()\n");的工作流程

 图16:语句local_printf("libtest1: local_printf()\n");的工作流程

如图14-16所示的,当链接器把动态库加载到内存时,它首先找到重新定位的符号printf的名称,然后将printf的真实地址写入相应的地址(偏移量0x4b5、 0x4c2、0x4cf和0x200c)。这些相应的地址在重定位节rel.dyn中定义。之后,libtest1()中的代码便可以正确地跳转到printf()函数处。

【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】

责任编辑:李英杰 来源: 51CTO
相关推荐

2016-06-12 11:53:27

2013-06-05 10:37:06

OracleJava安全

2021-06-07 14:54:42

Chrome强化扩展下载安全

2017-07-27 19:35:34

2020-10-08 10:15:15

LynisLinux系统安全

2018-10-18 14:07:01

2015-06-15 10:48:25

2009-02-12 09:55:28

2009-11-30 09:41:38

2011-11-03 09:41:35

Android签名安全性

2023-05-18 14:43:35

2009-12-10 10:20:04

2013-02-18 16:12:55

2011-02-13 14:36:35

2022-06-22 09:00:00

安全编程语言工具

2020-09-10 16:30:05

AI人工智能

2021-12-13 06:36:14

SpoolSploit安全漏洞渗透测试

2010-09-16 11:14:50

2012-12-21 15:24:53

Android 上的安全性

2015-07-17 13:16:06

点赞
收藏

51CTO技术栈公众号