基于JNI机制与云平台的细粒度Android应用加固方法

2021-03-21 05:12李志明刘寿春欧阳飞帆李婷婷申利民
小型微型计算机系统 2021年3期
关键词:逆向代码内存

李志明,刘寿春,欧阳飞帆,李婷婷,申利民

1(燕山大学 信息科学与工程学院,河北 秦皇岛 066004) 2(河北省计算机虚拟技术与系统集成重点实验室,河北 秦皇岛 066004) 3(河北省软件工程重点实验室,河北 秦皇岛 066004)

1 引 言

Android系统的开源特性使其快速发展的同时,也给Android应用带来了严重的安全隐患.逆向人员可轻易通过静态攻击、动态攻击以及重打包攻击等多种手段对未加固的Android应用进行逆向破解,并植入非法代码,形成恶意应用后再重新发布到应用市场[1].在用户不知情的情况下,恶意应用通过私自扣费、隐私窃取、系统破坏、自动访问不良信息等行为给用户造成损失.

通常,软件开发者采用多种防护措施保护其应用不被逆向破解.目前,主流的防护措施有代码混淆[2,3]、权限控制[4,5]以及应用加固3类,其中,应用加固是Android应用安全防护的首选措施.按照加固对象不同,应用加固可分为Dex文件加固和so文件加固两类.

Dex文件,包含Android应用的所有源代码,是逆向人员的主要攻击对象.为此,众多研究者针对Dex文件提出了很多加固方法,例如,Dex分块加载技术[6]、VMP保护技术[7]以及加壳技术[8-10]等,其中,加壳技术是最为有效且被广泛应用的方法.但是,现有加壳技术存在两点缺陷:1)将源apk文件直接放到解壳程序的资源文件中,易于被逆向人员获取;2)缺乏对解壳程序的保护,导致逆向人员可以通过重打包方式植入非法代码实现对解壳程序的篡改.尽管Dex文件加固在一定程度上提升了Android应用的安全性,但Android系统的开源性使得Dex文件在安全性方面与由c/c++编写的so文件相比存在一定差距.

JNI (Java Native Interface)机制[11],使得开发人员可以在Java代码中调用so文件中的Native函数,这样,开发人员便可将Android应用的函数代码转换为C/C++代码置于so文件中,以达到进一步保护函数代码的目的.但随着逆向人员技术水平的提高,so文件被逆向破解的情况也时有发生,因此,亟需采取相应措施对so文件进行加固防护.

从加固原理角度,so文件加固可分为Native函数混淆[12]、符号表隐藏[13]及自定义so加载器[14]3种方案.这3种方案均以整个so文件作为加固对象,存在对应用性能影响过大的问题.

综上,现有以Dex文件加固为核心的加壳技术,存在着源apk文件易被获取以及解壳程序易被篡改的问题;现有so文件加固方案以整个so文件作为加固对象,存在着加固后的Android应用运行效率下降等问题[15].

针对上述问题,融合JNI机制、动态加载技术[16]以及云平台理念,提出一种面向Android应用的安全加固模型,并对其中涉及的基于JNI机制的函数代码加固方案、基于云平台的加壳解壳方案开展研究工作,进而形成一种基于JNI机制与云平台的细粒度Android应用加固方法.该方法可使得加固应用在未显著增加Dalvik虚拟机的工作负担的前提下,具有抵御静态攻击及重打包攻击能力.

2 安全加固模型

为了便于后续描述,将面向Android应用的安全加固模型命名为RMAA(reinforce model for Android App)加固模型,其框架如图1所示.

由图1可知,RMAA加固模型对Android应用的加固环节主要集中在云平台的安全加固功能区,可有效降低逆向人员通过分析加固原理对加固应用破解的风险.此外,将源apk的加密Dex文件和签名的SHA-1值等重要文件及信息存储于云平台的持久化区,保证了校验机制的有效性及Android应用的安全性.其中,基于JNI机制的函数代码加固及基于云平台的加壳解壳技术方案是实现RMAA加固模型的关键问题,需要开展深入研究.

3 基于JNI机制的函数代码加固方案

基于JNI机制的函数代码加固方案的流程如图2所示,其核心思想在于以修改smali指令的方式实现JNI机制,将用户选取的待加固的Java层函数与加固后so文件中相应Native函数建立映射关系,以达到细粒度化加固函数代码安全性的目的.

由图2可知,基于JNI机制的函数代码加固方案的优势有两点:1)细粒度的核心函数代码加固方式可有效地减少Dalvik虚拟机的额外工作负担,从而使得加固应用对系统资源的消耗不会有明显增加;2)通过JNI机制实现Java层函数与so文件中相应Native函数间建立映射关系,达到了Dex文件加固与so文件加固的有机结合目的.

为了实现基于JNI机制的函数代码加固方案,需对其中涉及的Dex文件类及成员函数抽取算法、基于section的细粒度so文件加固方法、基于JNI机制的函数映射等问题展开研究工作.

3.1 Dex文件类及其成员函数信息抽取算法

Dex文件中类及其成员函数信息抽取的目的,在于获取源apk文件中类及其成员函数信息,并将获取到的信息存储于自定义的ClassInfoMap和MethodInfoMap结构体中,以供用户在其中选择其需要加固的核心函数.

针对Dex文件结构[17],提出Dex文件类及其成员函数信息抽取的流程如图3所示,其步骤如下:

1)提取DexHeader结构体中的mapOff属性值,并根据mapOff属性值找到DexMapList后,转至2).

2)判断是否遍历DexMapList集合中的所有DexMapItem结构体实例,若是转至11),否则转至3).

3)在DexMapList集合中获取一个DexMapItem结构体实例.

4)判断该DexMapItem结构体实例中的type属性值与kDexTypeClassDefItem值是否相同,若相同,则根据其offset属性值找到描述类信息的DexClassDef结构体实例,转至5),否则转至2).

图2 基于JNI机制的函数代码加固流程Fig.2 Flow of the function code reinforcement based on JNI mechanism

5)将该DexClassDef结构体实例中的classIds、accessFlags和sourceFilesIdx属性值存放于自定义ClassInfoMap结构体实例的相应属性中,完成类信息的提取,转至6).

图3 Dex文件类及成员函数信息抽取流程Fig.3 Extraction process of Dex file class and member function information

6)根据该DexClassDef结构体实例的classDataoff属性值找到DexClassData结构体实例,转至7).

7)读取DexClassData结构体实例中的direcMethods属性值找到DexMethod集合,转至8).

8)是否遍历DexMethod集合中所有DexMethodId结构体实例,若是转至2),否则转至9).

9)获取一个DexMethod集合中的DexMethodId结构体实例,转至10).

10)将该DexMethodId结构体实例中的nameIdx、protoIdx和returnIdx属性值提取并存放于自定义MethodInfoMap结构体实例的相应属性中,转至8).

11)结束.

3.2 基于section的细粒度so文件加固方法

现有so文件加固的方法,多以整个so文件作为加固对象.这种方式,使得加固应用运行时给Dalvik虚拟机造成较大的工作负荷,并以应用性能下降的形式展现出来.为此,提出一种基于section的细粒度so文件加固方法.该方法选取so文件中承载Native函数的section作为基本加固对象,从而实现对so文件的细粒度加固.

3.2.1 自定义section声明

若用户需要将so文件中的某个Native函数置于特定的section中,则需在用于编译so文件的源文件中,在特定函数名的前面添加__attribute__((section("sectionName")))用以完成编译后so文件中自定义section的声明.

3.2.2 自定义section加固

若要对自定义section进行加固,需要首先确定自定义section在so文件中的位置.so文件头部结构[17]中的e_shoff属性给出了第一个section的相对位置.

在section结构[17]中,sh_name、sh_size属性分别代表section的名称及大小,且各个section在so文件中的存放位置是相邻的,因此,第i+1个section的起始位置为第i个section的偏移量加上其sh_size属性的值.

在准确提取section地址的基础上,形成对单个自定义section加固的步骤如下.

1)读取so文件头部结构中的e_shstrnd属性值,检索其中是否存在自定义section名称,若有,则转至2);若无,则转至6).

2)读取so文件头部结构中的e_shoff属性值,并定位到第一个section的位置.

3)根据so文件头部结构中的e_shnum属性值得到该so文件中存在的section数量.

4)以e_shnum属性值为终止条件,按照前述section位置的计算方法逐一比对section名称与自定义section的名称是否匹配.若匹配,则将自定义section的内容读取到临时变量enc_content中,并将该section的位置偏移量及其大小分别存储于define_section_offset及define_section_size变量中.

5)采用异或算法[18]对enc_content变量中的内容进行加密,并用加密后的内容替换so文件中自定义section的内容.

6)结束.

3.2.3 自定义section解密

欲对加固后的自定义section解密,则需先确定自定义section在内存中的地址空间.so文件被加载到内存的起始地址是不固定的,因此,so文件在内存起始地址的获取是确定自定义section在内存中地址空间的关键所在.

so文件被加载至内存后,Android系统会自动向proc//maps目录下的内存镜像文件中写入so文件在内存中的相关信息,并可通过“cat maps|grep xxx.so”命令完成对名称为xxx的so文件在内存中相关信息的检索,从而获取xxx.so文件的内存首地址.

在上述基础上,定义满足A≤X≤B的内存段为自定义section在内存空间的地址范围,记为[A,B]={X|A≤X≤B},其中A为首地址,B为尾地址,则自定义section在内存中地址范围的计算方式如式(1)所示.

[A,B]={X|sr+es+dso≤x≤sr+es+dso+dss}

(1)

式中,sr表示so文件在内存中的起始地址,es表示so文件的头部结构大小(固定为52字节),dso表示自定义section在so文件中的位置偏移量,dss表示自定义section的大小.

通过以上分析,形成对单个自定义section的解密步骤如下.

1)通过“cat maps|grep”命令得到so文件在内存的相关信息,并获取该文件在内存中的首地址,赋值给式(1)中sr变量.

2)将对自定义section加固步骤中,记录的define_section_offset和define_section_size变量分别赋值给式(1)中dso变量和dss变量.

3)按式(1)计算自定义section在内存中的地址空间范围.

4)根据第3)步的计算结果,提取自定义section内容,并将其存放于临时变量dec_content中.

5)采用异或算法对dec_content变量中的内容进行解密,并用解密后的内容替换内存中so文件的自定义section的内容.

6)结束.

3.3 基于JNI机制的函数映射

为使Android应用能够正常调用so文件中的Native函数,则需建立Java层函数与so文件中相应Native函数的映射关系.本文采用修改smali指令[17]实现JNI机制的方式建立函数映射关系.

为了叙述方便,假设用户选择的待加固函数为MainActivity类中的getString()函数,则建立Java层getString()函数与so文件中getString()函数之间映射关系的步骤如下.

1)在反编译apk文件后形成的smali文件夹下查找MainActivity.smali文件.

2)在MainActivity.smali文件中,根据正则表达式const(.method)(.*?)*getString.*(.end method).查找getString()函数的smali指令.

3)删除smali指令中getString()函数的功能实现部分.

4)在MainActivity.smali文件的getString()函数功能实现部分,添加相应smali指令,实现利用JNI机制调用加固后so文件中的getString()函数.修改smali指令会导致所需寄存器数量发生变化,因此,在修改smali指令后需要重新计算并修改该函数所需的寄存器数量[17].

经过以上步骤,Java层getString()函数的功能实现代码转移到了so文件的相应函数中,并建立起了二者之间映射关系.至此,即使逆向人员利用apktool、jadx-ju等工具对源apk文件进行反编译,也不能直接获得getString()函数的代码内容.由此可见,该方法可使得Android应用Java层函数代码的安全性得以提高.

4 基于云平台的加壳解壳方案

针对现有加壳解壳方案中源apk文件易于获取及缺乏对解壳程序保护的问题,在RMAA加固模型基础上,融合签名校验及动态加载技术,提出一种基于云平台的加壳解壳方案,其流程如图4所示.

图4 基于云平台的加壳解壳方案流程Fig.4 Flow of the shell and unshell solution based on cloud platform

4.1 平台端应用加壳程序设计

平台端应用加壳程序的主要功能为,将源apk的Dex文件加密并存储于云平台的持久化区,以供Android端应用解壳程序下载;将源apk的剩余文件融合于源解壳程序的Dex文件中形成新Dex文件,并对新Dex文件的相关参数值进行修正;采用Jarsigner工具对加固应用apk文件进行重签名,并将该签名的SHA-1值存放于持久化区(见图1).

Dex文件头部结构中的checksum、signature及file_size是描述Dex文件规格信息的重要属性,也是Dalvik虚拟机评判Dex文件是否可以被正常加载的依据.将源apk剩余资源文件与源解壳程序Dex文件融合后形成的新Dex文件的内容及规格势必发生变化,因此,需要根据新Dex文件的规格对上述3个属性的值进行修正.

4.2 Android端应用解壳程序设计

Android端应用解壳程序的功能为,首先,从加固应用apk文件的Dex文件中提取源apk剩余文件;其次,通过云平台的服务接口下载源apk加密Dex文件,并利用AES算法对其解密;再次,将源apk剩余文件与解密后的源apk的Dex文件重新整合为源apk文件;最后,利用动态加载技术将系统控制权转交给源apk文件,使源apk文件按原逻辑正常运行.基于此,设计的解壳程序流程如图5所示,其中,解壳程序反篡改、动态加载及其相关置换是其中的关键环节.

4.2.1 解壳程序反篡改方案

在现有Android应用加壳解壳方案中,通常,应用的签名校验逻辑存放在Android应用Java层,逆向人员可通过API分析法[17]等手段找到签名校验逻辑的代码位置,然后删除签名校验逻辑或者将校验结果改为真,从而实现对解壳程序的逆向篡改.

针对上述问题,提出一种基于云平台技术的解壳程序反篡改方案,具体流程如下.

首先以加固应用签名的SHA-1值为参数访问云平台提供下载源apk加密Dex文件的服务接口,若该SHA-1值与云平台保存的SHA-1值一致则签名校验通过,允许下载源apk的Dex文件,否则退出加固应用.

图5 解壳程序流程Fig.5 Flow of the unshell

由此可见,在基于云平台的加壳解壳方案中,若逆向人员通过非法手段对解壳程序进行逆向篡改后,由于篡改前后解壳程序的签名SHA-1值发生了变化,导致无法从云平台下载源apk加密Dex文件,从而有效防范加固应用的非法运行.

4.2.2 动态加载及其相关置换

由图5可知,为了使源apk文件能够正常运行,需要利用反射机制置换Dalvik虚拟机的默认类加载器为自定义类加载器,以达到动态加载源apk文件的目的.

在动态加载源apk文件之后,Dalvik虚拟机将源apk文件加载至解壳程序所在的进程中,但由于此时内存中Application实例是解壳程序的Applicaiton对象,Dalvik虚拟机无法找到源apk文件的入口.因此,需要将此时的Application实例置换为源apk文件的Application对象.

此外,为了使源apk文件能够正常运行,还需要利用反射机制置换Resource类中的mAssetsManager实例为自定义AsseManager对象,以保证Dalvik虚拟机可以正常访问源apk文件的资源文件.此处,自定义AsseManager对象是AssetsManager类的一个实例,其addAssetPath()方法可以获得相应资源文件的访问路径.

5 实验验证

在RMAA加固模型总体框架的指引下,在基于JNI机制的函数代码加固方案及基于云平台的加壳解壳方案的研究基础上,设计并开发了RMAA加固系统原型,并从基于section的细粒度so文件加固有效性、基于JNI机制的函数映射有效性,以及应用加固前后性能变化3个方面对基于JNI机制与云平台的细粒度Android应用加固方法的有效性进行验证.

5.1 基于section的细粒度so文件加固有效性

以自主开发的用于人体健康风险评测的Android应用healthEvl.apk文件作为测试用例,并输入RMAA加固系统.在RMAA加固系统完成对healthEvl.apk文件的反编译、类及其成员函数的信息抽取环节后,以用户选择用于疾病风险评估的Java层disRiskByMeridian函数为例,验证基于section的细粒度so文件加固的有效性.

利用IDAPro逆向工具反汇编加固前后的so文件,分别得到如图6、图7所示的逆向视图.

图6 so文件加固前的逆向视图Fig.6 Reverse view of so file before reinforcing

由图6可知,反汇编未加固前的so文件,可显示disRiskByMeridian()函数的完整汇编代码;逆向人员可以通过对汇编代码的分析掌握disRiskByMeridian()函数的实现过程.由图7可知,对so文件中承载disRiskByMeridian()函数的section加密后,再次反汇编加固后的so文件时发生错误,且无法得到disRiskByMeridian()函数的汇编代码.由此,可验证基于section的细粒度so文件加固的有效性.

图7 so文件加固后的逆向视图Fig.7 Reverse view of so file after reinforcing

5.2 基于JNI机制的函数映射有效性

以RMAA加固系统加固healthEvl.apk文件中的ReasoningCenter类的disRiskByMeridian函数得到的reinforce.apk文件为例,验证基于JNI机制的函数映射有效性.在验证reinforce.apk应用功能正常的基础上,利用jadx-gui逆向工具反编译healthEvl.apk和reinforce.apk文件,分别得到如图8和图9所示的逆向视图.

图8 healthEvl.apk逆向视图Fig.8 Reverse view on healthEvl.apk

对比图8与图9可知,healthEvl.apk经加固后,disRiskByMeridian()函数的函数体由实现代码变为了JNIManager.disRiskByMeridian(),从而验证了函数映射机制的有效性.

5.3 应用加固前后性能对比

从理论角度分析,经RMAA加固系统加固后的应用,在apk文件大小、内存占用量及启动时间等方面均会有所增加.apk文件大小增加的主要原因是在原有apk文件基础上增加了解壳程序代码和so文件;加固应用在解壳过程中,对源apk的Dex文件解密以及对源apk文件的动态加载环节需要开辟额外的内存空间,因此,在加固应用的启动阶段,内存占用量及启动时间均会明显增加.此外,下载Dex文件时消耗在网络传输上的时间是启动时间增加的主要影响因素.

图9 reinforce.apk逆向视图Fig.9 Reverse view on reinforce.apk

为了测试应用加固前后实际的apk文件大小、启动阶段内存占用量及启动时间等方面的变化,从开源社区Gitee下载5个未加固的apk文件.在经RMAA加固系统加固过程中,每个应用均选取1个函数进行基于JNI机制的函数代码加固.5个应用加固前后apk文件大小、启动阶段内存占用量、启动时间的变化情况如表1所示.

由表1可知,加固应用apk文件的大小增量变化平缓,稳定在0.2MB左右,启动阶段内存增量稳定在8MB左右,启动时间增量在500ms以下.由此可见,在内存增量方面,对于目前存储容量为几十乃至上百GB级别的Android终端的影响可以忽略不计;在启动时间增量方面,用户是难以感知的.

表1 应用加固前后性能指标对比Table 1 Comparison of performance indexes before and after reinforcing

6 结 论

本文针对现有Android应用加固技术或方法的不足,提出一种基于JNI机制与云平台的细粒度Android应用加固方法.该方法采用基于JNI机制的函数代码加固方案实现了so文件的细粒度加固,从而使得逆向人员不能通过逆向工具轻易获取应用的核心函数代码,且加固应用并未显著增加Dalvik虚拟机工作负担;采用基于云平台的加壳解壳方案,通过解壳程序运行时以加固应用签名的SHA-1值为参数访问云平台提供下载源apk加密Dex文件的服务接口方式,有效解决了源apk文件易被获取以及解壳程序易被篡改的问题.通过RMAA加固系统验证了基于JNI机制与云平台的细粒度Android应用加固方法的有效性,且加固应用的启动时间、内存占用量等并未显著增加.

在后续的研究工作中,可采用机器学习手段开展根据smali指令语义自动生成相应so文件的研究工作,从而进一步提升该加固方法的自动化程度.

猜你喜欢
逆向代码内存
逆向而行
对外直接投资逆向技术溢出的碳排放效应
笔记本内存已经在涨价了,但幅度不大,升级扩容无须等待
“春夏秋冬”的内存
神秘的代码
一周机构净增(减)仓股前20名
重要股东二级市场增、减持明细
内存搭配DDR4、DDR3L还是DDR3?
近期连续上涨7天以上的股
上网本为什么只有1GB?