简容, 黎桐辛, 周渊, 李舟军, 韩心慧
(1. 北京航空航天大学 计算机学院,北京 100191; 2. 北京大学 计算机科学技术研究所,北京 100080; 3. 国家计算机网络应急技术处理协调中心,北京 100085)
目前Android操作系统已经成为全球市场上所占份额最高的移动终端系统,基于Android操作系统的应用程序数量也逐年增多. 由于Android平台的开放性,应用程序能够较容易的被逆向分析、修改破解、重新打包[1]. 为了防止软件被逆向破解或攻击利用,许多开发者采取了应用加壳的方式[2],对程序关键代码进行加密、隐藏,极大地增加了逆向分析的难度,从而达到对程序保护的效果.
但是,以Android平台为目标的恶意软件也利用了加壳技术的特性,对自身携带的恶意代码进行隐藏,用于躲避杀毒引擎的检测和安全研究人员的分析[3-4]. 同时,加壳后的应用也无法利用静态分析工具准确检测应用内部的安全隐患[5]. 因此,实现自动化通用脱壳,在程序分析和恶意软件检测等方面有重要意义.
本文设计并实现了一种多层次的自动化通用脱壳系统,能够应对目前市场上主流加壳服务,还原程序的代码逻辑. 提出了多粒度的数据还原方案,保证数据还原的完整性和有效性. 对应用市场上的加壳程序进行安全性评估,得到了新的发现,证明了脱壳系统的实际应用价值.
本文基于Dalvik虚拟机设计并实现了一种多层次自动化通用脱壳系统. 该系统将应用程序的启动执行过程划分为不同的3个主要层次,在不同层次对加壳应用的dex文件进行不同粒度的数据转储(Dump),并最终对dex文件的头部信息,索引结构区,数据区进行还原,生成完整dex文件.
图1展示了脱壳系统的整体架构. 它包含了多层次状态监控,多粒度数据转储和Dex文件重构3个模块. 由于目前的加壳技术对dex文件的解密不再是一个单一阶段的过程,而是随着dex文件的加载与执行逐步解密,因此,在不同时刻内存中的dex文件所具备的数据也有所不同. 为此,在加壳程序运行的每一个层次设置监控点,跟踪加壳程序的执行过程. 依照数据类型的不同,对dex文件的数据进行不同粒度的区分,并根据监控点的反馈信息,获取当前运行层次下dex文件对应的数据内容中的有效部分. 最后,对获取到的有效数据进行dex文件重构,实现自动化的脱壳流程.
该系统以加壳后的应用程序作为输入,通过动态执行加壳程序,输出脱壳后包含完整代码的dex文件,在脱壳过程中,不需要使用者对加壳服务有逆向经验或对程序进行额外的人工预处理,同时,由于该系统直接基于Dalvik虚拟机实现,不会受到加壳服务完整性检查和反调试措施的影响,自动化程度较高.
图1 脱壳系统整体框架Fig.1 Architecture of the unpacker system
为了监控壳代码的行为,本文设计了多层次的状态监控方案,该方案不需要对虚拟机整体运行做跟踪,而是在程序执行的不同层次,选取尽可能少的监控点,用于反映壳代码在执行过程中的关键行为.
1.1.1 层次划分
脱壳系统将程序的启动执行划分为以下3个层次.
① Dex文件加载:Dalvik虚拟机可以通过文件,或二进制字节流的形式,将dex文件加载至内存,并生成相应的DexFile结构来代表被加载的dex文件.
② 类加载:Dalvik虚拟机中使用ClassObject结构代表已加载的类. 根据类加载方式的不同,可以分为显式加载和隐式加载.
③ 方法执行:当类中的方法被调用时,Dalvik虚拟机将从类加载信息中生成对应的DexMethod结构来代表该方法. 方法执行的过程,即为虚拟机对字节码解释执行的过程.
1.1.2 监控点的选取
根据上述层次划分,在每一层次选取监控点,用于获取在该层次下壳代码的行为信息. 在选取监控点时,应当考虑到:选取的监控点数量应该尽量少,以减小系统整体的性能开销;选取的监控点应覆盖当前层次中所有的路径调用情况;选取的监控点不能是导出函数或系统提供的API,以防被加壳程序检测或修改. 根据上述规则,对每一层次的监控点选取如下.
① 监控dex文件加载:为避免dex文件的重复加载,Dalvik虚拟机将已加载的dex文件保存在全局变量gDvm.userDexFiles中. 选取了addToDexFileTable函数作为dex文件加载的监控点. 该函数是将dex文件增加至gDvm.userDexFiles中的唯一途径,因此通过监控该函数,能够得到壳代码加载dex文件的信息.
② 监控类加载:当一个类从未被加载过时,最终将由native层的Dalvik_dalvik_system_DexFile_defineClassNative函数完成从dex文件加载指定类的任务. 选取该函数作为类加载行为的监控点.
③ 监控类方法执行:当应用程序启动时,Dalvik虚拟机解释器以android.app.ActivityThread类的静态成员函数main为入口点执行,在执行过程中遇到函数调用时,会因函数属于Java层还是Native层而出现4种不同的情况. 表1展示了这4种情况下虚拟机所使用的跳转方法. 可以看到,除去Native层到Native层的调用之外,都能够在每一个方法被调用前,监控到该方法的调用信息. 由于Native层的代码不属于dex文件中由Java编译而来的字节码,因此可以不用处理这一类情况.
表1 Dalvik虚拟机函数调用的4种类型Tab.1 Four types of function calls in Dalvik virtual machine
在传统的脱壳方案中,往往会指定一个特定的脱壳时机(通常为加壳程序的第一个Activity被创建时),当这个时机到达后,脱壳程序认为此时壳代码已将dex文件在内存中完全解密,并一次性转储dex文件,完成整个脱壳流程. 实际上,以这种方式得到的dex文件虽然能够被正常反编译,但通常会出现部分数据缺失或偏移错误的情况,这是因为基于特定时机后dex文件完全解密的假设本身可能是不准确的. 为了解决这个问题,使用了多粒度数据转储的方案,来保证内存数据的准确性.
Dex文件由多项不同类型数据结构组成,且通过加载基址加上数据偏移的方式来计算数据存放位置,因此,通常需要根据偏移值进行多次索引,才能获取相应数据内容. 例如,存放于DexHeader的stringIdsOff代表了字符串类型数据结构DexStringId的起始偏移,通过DexStringId中stringDataOff成员的值,可以获得MUTF-8编码的实际字符串内容. 除字符串信息外,dex文件还包含了类型信息、原型信息、字段信息、方法信息、类信息以及依赖信息等部分. 根据上述索引方式,可以在知道dex文件内存基址的情况下,解析所有数据所在内存地址,并获取内容. 然而,并非所有的内容都是有效的,在何时去获取数据内容,取决于壳代码逐级解密的时机,例如,只有当类中的方法被执行时,虚拟机才会去获取方法中的字节码,壳代码可以选择在此时解密出类方法的字节码.
从1.2.1节所述的层次划分来看,不同时刻对于dex文件中的数据获取的准确程度是不同的. 根据数据获取的粒度不同,将数据分为以下3类. ① dex文件的基本属性,如版本标识和内存映射长度;② dex文件中各项数据结构以及具体数据中与类方法无关的部分;③ 类方法部分. 每一个类方法均由DexMethod结构表示,其中包含了可被虚拟机执行的字节码数据.
根据状态监控反馈的信息,该模块将动态的获取不同粒度下的数据内容. 多粒度数据转储基于这样一个准则,即数据转储发生在Dalvik虚拟机即将使用该数据之前,在数据转储到虚拟机正常使用数据的过程中,壳代码将没有机会对该数据进行动态修改.
1.2.1 Dex文件基本属性获取
当监测到dex文件的加载行为时,将获取该dex文件的基本属性. 具体而言,通过监测的addToDexFileTable函数获得指向内存中dex文件的pDexOrJar指针,并利用该指针得到dex文件对应内存代表的DexFile结构. 此时dex文件中的各项数据结构对应的内容不一定是有效的,因此仅获取脱壳所需要的一些基本属性,包括:dex文件在内存中的映射长度,dex文件是否已被优化为odex格式,dex文件的版本标识和字节序标记.
1.2.2 Dex文件数据结构获取
当监测到类加载行为时,将获取dex文件各项数据结构的信息. 具体而言,通过监测的Dalvik_dalvik_system_DexFile_defineClassNat-ive函数,获得当前加载类所使用的ClassLoader和当前类所在的dex文件信息,然后利用ClassLoader主动加载该dex文件中的所有类,使得壳代码在类中注入的静态代码块得到执行. 此时,对于dex文件中不属于类方法的数据,在类被加载至内存后将处于解密状态,获取当前dex文件每一类数据结构的个数和对应的偏移值,根据偏移值计算出数据的内存地址,并依据数据结构中的成员定义,获取数据的真实内容. 需要注意的是,虽然此时dex文件中类方法相关数据不一定处于解密状态,但仍然会在此时根据相关偏移去解析和获取类方法数据.
1.2.3 类方法的动态更新
如1.2.2节所述,当完成类加载操作后,类方法数据可能仍处于加密状态,这是由于部分壳代码采取了方法替换的方式,将原有类方法替换为壳代码,并在类方法被调用时动态解密. 为了应对这种情况,在类方法调用时进行类方法的动态更新.
当监测到方法调用行为时,通过函数局部变量Method,获得当前被调用方法的函数签名,所属类以及其指令集,如果该方法所属类位于已加载的非系统dex文件集合中,则获取该方法包含的所有数据. 由于在程序运行期间,并非所有的方法均有机会得到执行,可以使用Monkey、UI Automator等动态测试工具,发送随机事件,如键盘输入、屏幕点击、手势滑动等,提高动态代码覆盖率.
重构dex,指的是将多粒度数据转储获得的内存数据重新汇编成可供静态分析工具分析的完整dex文件,并写入外部存储设备. 进行dex重构时,通过广度优先遍历的方式,依照dex文件标准规范,以dex文件头部为根节点,重构dex文件中的各项数据结构. 由于dex文件重构的数据完全来源于数据转储的内容,因此dex文件重构的时机尤为重要,如果在脱壳时仅进行单一阶段的重构,或者在壳代码对原有数据进行解密之前就进行重构操作,都将导致还原的dex文件不完整. 为此,设计了一套数据更新规则,保证dex文件重构的完整性和准确性.
壳代码在内存中释放原有dex文件是一个逐级操作的过程. 壳代码在对dex文件进行动态修改时,必将产生相应数据结构属性或内容的变化. 重构dex时,需要考虑相关数据的变化,正确更新,得到准确的dex文件.
图2展示了一个类方法在逐级解密时的变化路线,其中实线箭头代表壳代码在执行方法前,由于动态修改操作将Native方法还原成Java方法,虚线箭头代表壳代码在方法执行完毕后,重新将该方法标记为Native方法. 虽然通过监控方法调用,能够感知到壳代码的修改行为,但壳代码的重新加密操作,使得会从内存中获得同一方法的不同数据,在重构dex文件时,需要选择其中的正确部分.
图2 解密过程中类方法在内存中的变化Fig.2 Class methods transformation during decryption
为此,通过设计单向的数据更新规则,规定了在程序运行中可被接受的数据变化操作,当变化操作出现时,对dex文件中对应的数据结构进行更新. 由于整个更新过程是单向的,因此壳代码尝试重新加密或破坏原本有效数据的行为将不会导致数据更新. 以图2为例,仅接受一个方法从Native类型转换成Java类型的行为,并认为该转换是方法的解密过程,而当壳代码重新将方法标记为Native类型时,将不会更新之前已获取的方法数据. 对于其他类型的数据,也设计了与之类似的单向转换规则.
为了验证脱壳系统的有效性,选取了市场上主流加壳厂商的加壳产品进行脱壳测试. 从应用市场中,选取了爱加密,阿里,百度,梆梆,360,腾讯6家厂商加壳的应用程序各10个,使用脱壳系统对其进行脱壳处理.
从如下两个标准来衡量脱壳的有效性.
① 脱壳系统给出的还原后dex文件,能够使用BakSmali、JEB等反编译工具进行处理,以证明dex文件能够满足后续静态分析的需要.
② 还原失败的类方法在所有方法中的比例. 还原失败的类方法,指的是在脱壳过程中,被壳代码替换成Native方法,且未能成功还原为字节码的类方法. 这一比例应当尽可能低.
最终的测试结果如表2所示. 由于加壳程序中本身存在真实的Native函数,因此通过人工分析的方法,排除了这部分真实Native函数,并给出了所有Native方法在总方法数的比例,以及还原失败的类方法占总方法数的比例.
表2 加壳应用样本脱壳测试结果Tab.2 Unpack results of packed application samples
可以看到,针对目前主流加壳服务,脱壳系统还原的dex文件均能用于后续静态分析,且还原失败类方法比例均在0.3%以下,表明本脱壳系统具有良好的通用性和有效性.
为了测试脱壳系统所带来的额外性能开销,在型号为Nexus 5的智能手机上,使用了CF-Bench来进行对比试验. 通过在安装有脱壳系统和未安装脱壳系统的情况下,分别运行CF-Bench 10次并取平均值,得到最终结果如图3所示. 可以看到,脱壳系统引入的额外性能开销约为11.7%,在可以接受的范围之内.
同时,选取了drizzleDumper和DexHunter这两项提供了开源代码的脱壳方案,与本文的脱壳系统进行实验对比. 为判断脱壳后dex文件的完整性和准确性,从互联网上选取了5个开源的Android应用程序进行编译,并利用加壳服务进行加壳处理.
在使用上述脱壳工具进行脱壳后,将能被正常反编译,且其中包含的方法代码与对应开源项目能一一对应的dex文件视作成功脱壳. 最终的测试结果如表3所示. drizzleDumper通过特征搜索来定位dex文件,无法处理破坏了dex文件头部的情况;DexHunter需要人为确定脱壳目标文件的名称和路径,同时对于不属于类数据的部分进行了连续的内存转储,使得最后的dex文件存在不能正常反编译的情况. 因此,针对目前的加壳服务,本文提出的系统能达到更好的脱壳效果.
目前,静态检测领域存在大量优秀的分析工具,如基于流分析的ScanDroid,基于代码相似性的ViewDroid[6]等等. 脱壳系统与静态分析工具相结合,通常被应用到恶意代码的检测和分析领域,并取得了良好的效果. 本文从另一个角度出发,利用脱壳系统,对应用市场中使用了加壳服务的程序进行了安全性评估.
应用程序在被加壳后,其文件结构会发生不同程度的变化. 加壳服务通常会在APK文件中加入动态加载库,并通过JNI接口调用其中的函数. 通过分析不同类型的加壳服务,归纳了这些加壳服务自身的动态库名称,如表4所示.
Janus是一个移动应用安全分析社区化平台[7],它收集了主流应用市场中应用程序的信息,并提供了一种自定义的规则语言来对数据库进行检索. 通过搜索检测APK文件中是否包含上述特征动态库,来判断应用程序是否使用了加壳服务,以及加壳服务的具体名称,然后选取部分加壳程序下载分析.
历史下载量是应用市场排名算法中的一个重要影响因素. 根据特征匹配结果,随机选取了下载量处于不同区间的加壳应用程序共3 500个作为实验样本,实验样本收集于2017年5月. 为了确保下载量的准确性,将应用程序在百度、腾讯、360三家应用市场中被下载次数的平均值作为最终的下载量. 表5展示了这些应用下载量的分布情况.
表5 加壳应用样本下载量分布Tab.5 Download distribution of packed application samples
在软件开发过程中,由于开发人员的疏忽,可能为应用程序引入潜在的安全问题. 这些安全问题可能导致程序在某些极端边界条件下产生异常运行,也有可能被恶意攻击者利用,造成更为严重的后果. 为了批量分析应用程序中的安全问题,使用了AndroBugs[8]框架作为Android应用程序漏洞扫描的基础工具. 待扫描的应用程序被分为以下3类.
① 加壳后的应用:从应用市场上下载的加壳程序样本;② 脱壳后的应用:使用脱壳系统还原出原有dex文件后,重新打包的应用程序;③ 未加壳的应用:针对每一个加壳程序样本,选取出下载量偏差在10%以内的不加壳应用程序.
根据安全问题的严重程度,AndroBugs划分出了4种不同的安全等级:① Critical:代码符合某些已知的安全问题特征,需要进一步排查;② Warning:可能存在的安全隐患;③ Notice:检测到存在某些敏感操作,提供了额外的信息以供分析;④ Info:没有检测到安全问题.
需要注意的是,虽然并非所有被标记为Critical的代码都存在被攻击的安全漏洞,但是扫描结果的统计信息能够有效地对应用程序进行整体安全评估.
图4展示了对上述3类应用程序进行漏洞扫描后,其不同安全等级的统计对比信息. 从实验数据可以看出,加壳程序被脱壳后,暴露出了更多的安全问题. 同时,下载量大致相同的应用程序中,加壳应用往往比未加壳应用存在更多的安全问题. 脱壳后的加壳程序样本Critical标记统计数目是未加壳程序样本的约1.6倍. 通过进一步的人工分析,总结出产生该结果的两个主要因素.
图4 应用程序漏洞扫描结果对比Fig.4 Application vulnerability scan results comparison
① 壳本身所引入的潜在风险. 加壳服务不可避免的需要在原有应用程序中引入自身代码,为了实现代码的解密和动态加载,部分壳代码使用了较为敏感的API,例如Runtime.getRuntime().exec来执行特定命令,或者使用mprotect函数在内存中映射可读可写可执行的区域. 同时,加壳程序会通过修改APK中的AndroidManifest.xml文件,注册新的组件,并且获取额外的运行权限,以保障各项服务的正常运行. 例如,为了收集运行时产生的异常或者崩溃信息,需要增加读取日志、外部存储读写和网络通信等权限. 额外权限的提升,使得应用程序一旦被攻击,也就能够使得恶意代码在一个较高权限的环境中运行,增加了恶意代码的威胁程度.
② 开发者对于加壳服务的过于信任,导致在采用加壳服务后,在一定程度上忽视了安全编码的重要性. 加壳服务的核心目的在于对程序的代码和资源进行保护,防止暴露核心敏感逻辑和二次篡改. 虽然加壳提高了对应用程序进行分析的难度,但是却无法修复程序中潜在的安全问题.
以下3类是加壳程序样本脱壳后,增长数目较多的威胁类型.
① 安全套接层SSL的不合理使用. SSL为数据在不可信网络中的安全传输提供了保障. 许多应用程序均使用了包含SSL子层的通信协议(例如HTTPS)完成与服务器之间的数据传输. 但是错误的调用相关函数,或者对参数的设置不合理,将导致整个通信过程不再安全. 例如,在扫描的脱壳后样本中,有67%设置了ALLOW_ALL_HOSTNAME_VERIFIER属性,使得应用在与远程服务器建立连接时,不会检查SSL证书的Common Name,任何持有有效证书的攻击者将能进行中间人攻击[9],在通信过程中进行数据的窃取和伪造.
② 文件属性全局可读或全局可写. Android系统沙箱机制保证了每个应用程序无法随意访问其他应用程序的资源. 然而当应用程序错误的设置自身私有数据属性时,将极大降低沙箱隔离的有效性,导致其它任何程序都能读取或改写其文件内容. 脱壳后样本中有73%设置了MODE_WORLD_READABLE或MODE_WORLD_WRITEABLE文件属性.
③ WebView中使用addJavascriptInterface带来的威胁. 应用程序经常使用WebView类来在应用中内置一个浏览器组件,同时也可以通过addJavascriptInterface函数来往WebView中注入Java对象,这使得JavaScript代码能调用注入Java对象中的公有方法. 在Android 4.2以及之前的系统中,攻击者能利用这个特性,通过反射的方式访问被注入Java对象的所有公有域,甚至以应用程序的权限执行任意代码[10].
综上所述,对应用加壳虽然起到了防止应用被逆向破解和恶意窜改的作用,但由于壳代码本身不会改变应用原本的代码逻辑,存在于应用内的安全问题并不能得到修补,开发者不能因使用了加壳服务而忽视了安全编码的重要性. 同时,加壳本身也为应用引入了一定风险,一旦应用被恶意攻击,所造成的危害也会更大. 这与通常认为加壳提升了安全的直觉是相矛盾的.
本文设计并实现了一种多层次的自动化通用Android脱壳系统,能正确还原出加壳应用中被加密的代码逻辑,供静态分析工具使用. 实验表明,该系统能对市面上主流加壳服务保护后的应用进行正确脱壳. 利用该系统,本文对市场上被加壳的应用程序进行安全性评估,证明了脱壳系统的实际应用价值. 实验发现加壳应用比未加壳应用存在更多的安全问题,其原因在于加壳技术对原应用程序引入了更多的安全风险,及开发者可能过于信任加壳服务后忽视了安全编码的重要性. 加壳厂商和程序开发者应引起重视,进一步提高相关代码的安全性.