徐玄骥,董帅克,张智斌
(1.昆明理工大学,云南 昆明 650600;2.蚂蚁科技集团股份有限公司,浙江 杭州 310000)
代码对于普通开发者而言是非常重要的知识产权。为了保护代码资源,防止其被随意剽窃,开发者通常会使用代码混淆这一技术。然而,代码混淆无异于一把双刃剑,虽然能有效防止开发者的代码被恶意使用,但也极大程度上增加了安全分析师的分析成本。此外,恶意软件开发者还可以利用代码混淆来隐藏自己的真实意图,从而逃脱自动分析工具的扫描。与传统领域的应用相比,因为Java 语言天生的易于逆向的特征,代码混淆技术对于安卓(Android)应用具有更重要的意义。
Android 操作系统是使用人数最多的移动操作系统。统计信息[1]显示,截至2020 年12 月,谷歌官方应用程序商店(Google Play)共有超过350 万款应用可供下载,月平均下载人次超过500 万。因此,对Android 应用的代码混淆技术使用情况进行调研具有很重要的意义。一方面,开发者可以了解到目前主流的代码混淆技术,并判断是否要为自己的应用添加相应的保护。另一方面,安全分析师可以了解到不同种类的应用在代码混淆技术的使用方式上的异同,从而设计出更好的代码分析 工具。
目前,学术界已经有部分针对Android 代码混淆机制的研究。文献[2]提出了针对Dalvik 字节码的混淆方法,并针对市面上主流的反病毒软件进行了测试,观察其对混淆的抵抗能力。文献[3]提出了一种开源的黑盒混淆工具,并提供了包括重签名、随机清单、重命名等多种混淆方法,从而可以帮助开发者方便快捷地为应用添加代码混淆。文献[4]提出了一种针对Android 应用的控制流混淆方法,通过插入多余控制流以及对原有控制流进行压扁操作,从而提升了代码被逆向破解的难度。文献[5]提出了一种基于Dalvik 字节码抽取以及重新映射的混淆方法,通过将Dalvik 操作码重新映射成新操作码进而封装成SO 文件,可以显著提高逆向工程的成本。以往的学术文献,往往关注如何对Android应用的现有混淆技术进行修改,而忽略了从全局的角度去发掘Android 应用整体对代码混淆使用情况的分析。
基于此,设计并实现了一套针对Android 应用的代码混淆扫描框架,并对标识符重命名、字符串加密、Java 反射以及应用加壳4 种常见的代码混淆手段设计了相应的检测算法。将检测算法运行在超过10万款Android应用上,并对统计结果进行了分析。
Android应用程序包(Android Aapplication Package,APK)文件包含了一款Android 应用的全部内容,通常由res、assets、lib、META-INF 这4 个路径和AndroidManifest.xml、classes.dex、resources.arsc 这3 个文件组成。它们的具体用途如下。
(1)res:此路径下存放了Android 的资源文件,这些资源文件将会被映射到.R 文件当中。
(2)assets:此路径与res 路径功能类似,用来存放APK 的静态资源文件。与res 路径不同的是,开发者可以在此路径下创建任意深度的子路径,并且存放任意文件类型的文件。
(3)lib:此路径下存放着为不同CPU 平台编译的文件,通常是各种库文件,如.so 文件等。
(4)META-INF:此路径用来存放一款应用的签名信息,可以用来校验应用的完整性。
(5)AndroidManifest.xml:Android 应用的清单文件,用来存放应用的基本信息,包括名称,版本,所需权限以及组件等。每个Android 应用都只包含一个清单文件。
(6)classes.dex:存放Android 应用中的所有类信息。此文件可以被Android Dalvik 虚拟机理解并执行。
(7)resources.arsc:此文件存放应用中的资源文件以及其对应的ID。
1.2.1 标识符重命名
在软件开发过程中,开发者通常赋予变量名较多的语义信息,以保证程序的较高可读性。然而,充足的语义也为逆向者提供了方便,在变量名的帮助下,逆向者可以轻易理解原作者的编程意图,窃取其中重要的实现,从而对开发者的知识产权产生威胁。
标识符重命名通过将变量中的语义信息抹除,可以有效增加逆向者的攻击成本,从而被广泛使用。如下代码片段表明,经过标识符重命名之后,在没有上下文的环境下,逆向者很难弄清类a 的实际含义。
1.2.2 字符串加密
由于应用中的字符串包含了非常多的语义信息,逆向者通常会将其作为理解程序语义的突破口。如1.2.1 小节程序片段表明,即便混淆了程序中出现的标识符名称,有经验的逆向者仍然可以依靠字符串来猜测函数的功能。因此,字符串加密被作为一种有效的混淆手段。在Android 应用开发的过程中,字符串加密可应用在多个阶段,包括Java 代码编译阶段、Java 字节码转Dex 文件阶段等。
对于恶意代码开发者来说,字符串加密能够有效地抹除程序语义信息,抵御部分基于硬编码特征的扫描工具的检测。假如加密算法实现得足够复杂,还能够有效地增加逆向过程的时间成本。下面的代码展示了字符串加密的例子。
1.2.3 Java 反射
Java 反射机制是一种具有与类进行动态交互能力的一种机制,是Java 语言的常用手段,能够使得开发者在程序运行时了解类、方法和变量的信息,动态地创建类的实例或调用方法。反射强调动态交互,通过运行时加载,在需要时可以随时随意的利用反射这种机制来进行操作。在Android 程序开发领域,Java 反射通常在需要访问隐藏属性或者调用方法来改变原来程序的逻辑中使用,常用来调用标注有hidden 注解的应用程序编程接口(Application Programming Interface,API)。
以下为使用Java 反射的一个简单示例:
反射在普通软件中的使用与恶意软件存在较大的差异。普通软件通常会利用反射进行Java 本地接口(Java Native Interface,JNI)调用或后向兼容性的检测,恶意软件则会利用反射去隐藏控制流,从而抵抗静态分析工具的检测,或使用反射将原本正常的函数调用变得十分臃肿,加强对逆向者的 干扰。
1.2.4 应用加壳
加壳是一种广泛应用的代码保护技巧。在加壳之后,原应用将会被加密并被称为“壳”的新应用所替代。当用户在屏幕点击应用时,新应用将被启动,在启动的过程中同时完成解密与还原操作,还原完成后再将程序控制权转移给原应用。在实际生产环境中,“壳”的解密过程通常较为复杂,难以通过静态分析的方式将加密后的原应用解密,进而可以大大增加逆向工程的成本与开销。
图1 展示了本文所使用的扫描框架的设计原理。为了增加扫描结果的覆盖性,从3 种不同的数据源中爬取Android 应用程序,使用Androguard 工具[6]对其进行解包。无法解包成功的应用实例将被剔除。在解包之后,使用自行设计的4 种混淆手段检测方法对应用进行扫描,并对扫描结果进行手工分析与汇总。
图1 扫描框架设计
通过对多种Android 代码混淆工具进行调研,研究发现,使用了标识符重命名的Android 程序在标识符的分布上会与未混淆的Android 程序产生较大差异。为了准确地表示这种差异,设计了基于3-Gram[7]词频的标识符重命名检测算法,其具体流程如下。
(1)数据预处理:针对某Android 应用,提取其全部标识符名称,组成集合Set_id。
(2)特征生成:针对Set_id 中的所有标识符,使用3-Gram 方法抽取其所有元组,统计各元组的出现频次,组成固定长度的向量并作归一化。
(3)模型训练:从开源仓库F-Droid 下载3 147 款未经混淆的Android 应用程序组成集合Set_app,使用两种不同的混淆器(ProGuard 和DashO)对Set_app 中的应用程序的标识符进行重命名,进而生成了未经混淆与经过混淆的两种正负样本集合。对样本集合中的应用程序进行数据预处理与特征生成得到特征集合Set_feat,再依靠特征集合Set_feat训练支持向量机(Support Vector Machine,SVM)分类器。
训练出来的SVM 分类器可以判断某Android 应用是否进行了标识符重命名混淆。
由1.2.2 小节可知,加密能够将Android 应用中的字符串变得随机,并且难以理解。而字符串的随机性可以用信息熵来描述。加密后的字符串较之普通字符串,往往拥有更高的信息熵。
信息熵的通用计算公式为:
式中,N表示事件的个数,Pi表示事件i的发生概率。
在本系统的实现中,抽取了一个Android 应用中出现的所有字符串并将其合并。假设合并后的字符串为S中出现的字符种类共为N,Pi表示第i个字符Xi出现的概率,则,其中S.count(Xi)表示Xi在S中出现的次数,S.length表示字符串的长度。最终通过式(1)计算获得信息熵。
接下来,进一步复用了2.1 小节展示的思路,针对一款Android 应用抽取其全部字符串并组合,计算其信息熵,并使用信息熵来训练SVM 分类器。
Java 提供了多种反射API 来实现不同的目的,这是Java语言的一种高级特征。本文主要关注[Class.forName()→getMethod()→invoke()]操作序列,一方面因为这种操作序列的Java 反射操作中最常见的模式,另一方面这种操作序列能够隐性地转移程序的控制流,从而作为一种混淆手段躲避静态扫描工具的检测。在检测到Java 反射的使用后,对解包产生的Smali 文件进行了进一步的分析,通过后向切片的方法获取Java 反射调用的真实对象,并分析其调用意图。
应用加壳通常有较高的开发门槛,因此开发者往往选择专门厂商提供的应用加壳服务,如梆梆加固、360 加固等。由于应用加壳功能通常作为一种云服务出售,使用某厂商加壳功能的应用在代码层面通常具有相似的特征。基于此,人工分析了若干应用加壳厂商,并从中抽取了相应的静态特征进行扫描,若某应用命中了这条特征,则可以认为它使用了该厂商的应用加壳服务。本文选取的特征如 表1 所示。
表1 常用应用加壳服务及其对应的静态特征
为了使扫描的结果更具有代表性,从3 种不同渠道收集了共114 560 款Android 应用。数据集囊括了来自Google 官方市场和中国第三方市场的Android 应用以及恶意软件。具体的数量信息如表2所示。
表2 数据集详情
表3 展示了数据集中标识符重命名的扫描结果。可以看出,中国第三方市场和恶意软件中的Android 应用使用标识符重命名的频率更高。而Google 官方市场中仅有不到一半的Android 应用使用了标识符重命名。对此现象的解释是,Google 官方市场针对软件剽窃提采取了更为严格的监管措施,从而使得对于软件开发者来说代码混淆并不是必须的工作。
表3 标识符重命名使用情况
在进一步人工判断了恶意软件与正常软件针对标识符重命名的不同策略后发现,恶意软件倾向于使用更复杂的命名策略,比如使用相似的字母组成不同的字符串,如Ill1II 与ll1II1 中使用了l、I、1这3 种不同的字符。除此之外,恶意软件还常常利用Java 的重载特性,用毫无关联的词汇替代原有的标识符。这种情况对安全分析师造成了较强的干扰。
表4 展示了数据集中字符串加密的扫描情况。可以看到,Google 官方市场与中国第三方应用市场中均极少使用字符串加密,而恶意软件中使用字符串加密的样例占比超过5&。
表4 字符串加密使用情况
通过进一步手工分析了使用字符串加密的恶意软件样例,发现字符串加密通常与标识符重命名结合起来,通过将字符串的加解密函数名更改为毫无关联的词组,从而误导安全分析师。比如,在软件com.solodroid.materialwallpaper 中,字符串的解密函数被命名为NavigationItem;->getDrawable()。从字面分析,其意义可能为获取图像资源的函数。
表5 展示了数据集中的Android 应用对Java 反射的使用情况。可以看出,反射作为一种Java 语言的高级用法,在各种类型的应用中被使用的比例较为接近。
表5 Java 反射使用情况
进一步分析不同类型的Android 应用使用反射调用的真实目标,发现恶意应用倾向于使用Java 反射调用敏感的API,或使用Java 反射将普通的调用臃肿化,从而增加逆向分析师的分析成本。而普通应用则通常只考虑应用程序的兼容性。这种方法的差异可以从表6 中清晰地看出。
表6 Java 反射调用频率最高的前两位使用情况
扫描结果显示,在114 560 款Android 应用当中,共有7 408 款应用使用了加壳技术,占比百分之6.5&。具体到各厂商提供的加壳服务的数据如表7 所示。
表7 应用加壳使用情况
在进一步的人工分析当中发现,使用了加壳技术的应用非常难于自动化分析,往往需要通过虚拟机或人工脱壳,才能够获取原应用文件,此举通常能够比较好地保护开发者的代码资源。
在本文中,为了了解代码混淆在Android 应用上的使用情况。采集了来自3 种不同来源的Android应用程序,并构建了一个拥有10 余万不同类型Android 应用的数据库。除此之外,设计了一套代码混淆扫描框架,并将其部署于收集的数据集上。针对标识符重命名、字符串加密、Java 反射以及应用加壳4 种不同的代码混淆技巧,进行自动化扫描和人工分析。扫描结果显示,不同来源的Android应用在代码混淆的使用上差别较大。在实际应用中,开发者应该考虑更多地使用更高级的代码混淆技巧,如字符串加密与应用加壳等,从而更好地保护自己的代码资源。