本发明属于信息安全领域,涉及逆向工程、移动终端安全,具体涉及一种针对Android加固应用的多点Hook逆向方法。
背景技术:
近三年,移动互联网以爆发形态迅猛发展,各种品牌的智能终端销量也随着以指数形态高升,据《Strategy Analytics 2015Q1》统计,2014年全球智能手机出货量年均增长30%,达13亿台。
在移动互联网兴起的大背景下,Android平台在占有率方面霸占整个移动互联网市场。调研机构Strategy Analytics最新的第三季度报告中显示,Android以81.2%的市场占有率稳居移动操作系统市场之首。出货量为2.68亿,去年同期为2.06亿,市场份额由81.4%上升至83.6%。
在中国的移动互联网市场来看,随着Android系统的兴起,市场上适用于Android的满足消费者各种功能的应用软件层出不穷。由阿里巴巴发布的《2015上半年度中国手机应用发展趋势研究报告》(Android版)中显示,Android平台App在中国市场,受消费者的不同需求,个别功能类型的App数量呈现出爆发式的增长,也体现其在未来的发展趋势。
然而,随着Android平台应用软件的爆发涌现,也带来了各种各样的问题。智能终端应用程序中,不乏包含了缺陷代码、漏洞甚至是恶意代码,致使用户的隐私信息、个人财产面临着泄露、损失的威胁。在国内知名互联网安全厂商360的《2015年第二季度中国手机安全状况报告》中,2015年第二季度,安卓移动平台新增恶意程序样本550万个,比2015年第一季度增长了141万个。平均每天新增手机恶意程序样本近6.04万个,累计检测到移动端用户感染恶意程序6573万人次,平均每天恶意程序感染量达到了72.2万人次。据2015年第二季度移动端恶意程序新增量和感染量统计,根据中国反网络病毒联盟的分类标准,360互联网安全中心在2015年第二季度监测的移动平台恶意程序的分类统计中可见,2015年第二季度安卓平台新增恶意程序主要是资费消耗,占比高达80.5%;其次为恶意扣费(14.5%)和隐私窃取(4.5%),这三类恶意程序占总量的99.5%,其余类型的恶意程序新增量占到0.5%。
随着恶意软件的不断发展,恶意软件对自身的保护技术也不断增强,各类恶意软件通过对自身的APK安装文件进行加固,实现了一定程度上的反分析、反逆向,导致对恶意软件进行技术分析的难度也越来越大,为了能够准确识别、分析、处理各类恶意软件,对于加固的 Android APK进行逆向分析已经成为一种重要的恶意软件分析检测手段。
目前普遍采用的脱壳及APK逆向方法主要为静态分析和动态分析法,静态分析法通过对APK文件自身进行分析,从而得到其dex程序文件实现分析;动态分析法则针对加壳、加固的APK无法静态分析这一特点,在程序运行时动态地从内存中dump得到dex的方式来进行分析,但是目前各类加固技术已经实现了运行时的代码动态加载,使得普通的动态分析法在获取dex程序文件时所获得dex文件是不完整、甚至完全错误的。
技术实现要素:
本发明针对传统的Android应用分析方法,在面对动态加固技术时逆向分析所获取到的安卓应用APK无法安装,及运行dex文件中函数代码不正确或者为空等问题,提出了一种针对Android加固应用的多点Hook逆向方法。
具体步骤如下:
步骤一、针对某个被测的Android应用,利用Dalvek虚拟机加载到内存中;
步骤二、在被测应用程序加载的过程中,选取被测应用程序若干不同的函数入口点同时进行Hook操作,对每个函数分别加载对应的Hook点;
不同的函数入口包括:app.attachbaseContext、app.onCreat和Activity.onCreat;
步骤三、当虚拟机执行到不同函数时,借助该函数所加的Hook点获取该函数被调用的类结构ClassLoader;
步骤四、通过各个类结构ClassLoader获取各个函数以及函数所在类在内存中的偏移位置,并得到程序各个函数的dex源码;
ClassLoader包含了该函数的详细信息,该函数所在类的详细信息以及指针;指针指向的是该函数以及函数所在类的位置,用偏移量体现;该位置保存的是该函数的dex源码;
步骤五、将被测应用程序各个函数的dex源码组成dex文件,在内存中进行修复完善;
具体步骤如下:
步骤501、对被测应用程序的dex文件进行解析,遍历存有类信息的头部结构索引DexClassDef;
头部结构索引DexClassDef包括:被测应用各个类对应的条目class_def_items,被测应用的基本头部信息和被测应用的全局数据;
通过分析各个类的条目class_def_items获取被测应用各个类的相关信息,及各个类的内存地址偏移class_data_off;
步骤502、对头部结构索引DexClassDef的信息分别进行保存;
将各条目class_def_items保存到索引临时存储区域buffer_classdef中;将基本头部信息保 存到到buffer_header中,将全局数据保存到buffer_data中;
步骤503、根据各个类的地址偏移class_data_off,获取各个类实际代码,并将获取到的代码保存至数据临时存储区域buffer_classdata中;
步骤504、判断各个类读取过程中,是否有某个类的地址偏移class_data_off指向了超出应用内存空间的非法地址;如果有,调用类定义构造方法重新构造该类的结构索引classdefine,并对该类地址进行修正,以正确地址向buffer_classdef进行回写,并修正buffer_classdef中对应的class_data_off,进入步骤505;否则,保持不变;
步骤505、对索引临时存储区域buffer_classdef、数据临时存储区域buffer_classdata、应用基本头部信息buffer_header、应用数据buffer_data四个buffer区进行拼装,在内存中形成临时dex文件;
步骤506、调用校验和计算函数计算整个临时dex文件的校验和,并签名;
调用校验和计算函数dexComputeChecksum计算整个临时dex文件的校验和,并回填到临时dex文件应用基本头部信息结构buffer_header中的checksum,然后用安全哈希算法计算整个临时dex的哈希值sha1回填到buffer_header的signature。
步骤507、将签名后的内存临时dex文件导出到文件,即得到了修复完善的dex代码。
步骤六、从内存中dump修复完善后的dex文件,即获得了被测应用的完整dex文件。
本发明的优点在于:
1、一种针对Android加固应用的多点Hook逆向方法,在待分析应用运行过程中动态对其中的多个函数入口点进行Hook,可以规避传统技术无法完整、正确获取程序dex的弊端,得到传统分析方法难以得到的完整dex代码。
2、一种针对Android加固应用的多点Hook逆向方法,传统动态分析方法在程序启动时dump的代码是错误或函数体为空的,而本方法在函数执行时动态获取其真实的代码保存位置,使得最终获取的代码是正确、完整的。
3、一种针对Android加固应用的多点Hook逆向方法,使得传统方法获取到的dex头部结构是错误的,导致逆向得到的dex无法重新打包执行,而本方法通过对dex的修复,保证逆向得到的dex与原有dex在功能上能够实现完全一致,以确保逆向的可靠性和有效性。
4、一种针对Android加固应用的多点Hook逆向方法,解决了传统逆向方法恢复出的dex存在功能缺失、安装时无法通过系统校验、无法正常安装和运行等严重问题。
附图说明
图1为本发明一种针对Android加固应用的多点Hook逆向方法的流程图;
图2为本发明将被测应用程序各个函数的dex源码进行修复完善的流程图;
图3为本发明Android应用启动示意图;
图4为本发明Hook技术示意图;
图5为本发明dex逆向与修复流程示意图;
图6为本发明传统dex逆向方法所得到的dex示意图;
图7是本发明dump获取dex文件的具体方法示意图;
图8是本发明dex文件在内存中的结构示意图;
图9是本发明ClassObject在内存中的结构示意图;
图10为传统的APP逆向方式得到的dex示意图;
图11为本发明逆向得到的dex示意图;
图12为本发明与旧逆向方法所得到dex文件对比图。
具体实施方式
下面将结合附图对本发明作进一步的详细说明。
目前,获取app源码的方法一般为静态分析或者单点Hook,导致获取的app源码不完整,或者无法恢复成可用的app;多点hook所选取的点虽然具有一定的任意性,但是为了对app的各执行分支、函数、类进行覆盖,采用对较优的函数入口点进行hook,通过多点Hook逆向方法,对加固过的Android应用在不同的位置进行Hook,并根据不同位置获取的ClassLoader进一步获取到dex在内存中的偏移位置,得到程序各部分的dex源码,在最后对所获取的所有dex进行整理组合和修复,最终获取完整的应用dex,使Android逆向分析的应用面得到了极大地扩展,达到了对于各类Android应用动态加固方法都能够进行有效分析的水平,具有适用类型广、结果完整可靠等优势,对于Android恶意应用、加固应用的检测具有极为重要的意义。
如图1所示,具体步骤如下:
步骤一、针对某个被测的Android应用,利用Dalvek虚拟机加载到内存中;
Android应用程序运行于Dalvik虚拟机中,Dalvik虚拟机与传统的Java虚拟机作用相同,可以看作Dalvik虚拟机是Java虚拟机的移动版本,所有Android程序都借助虚拟机运行在Android系统进程里,每个进程对应一个Dalvik实例。将待分析的Android应用加载,进入Android平台的Dalvek虚拟机,虚拟机自动对待分析的Android应用进行解压和加载,将待分析Android应用的完整结构加载入内存中。
Android系统启动加载完内核后,首先执行init进程,对设备进行初始化,然后读取init.rc文件,并启动系统中重要外部程序Zygote。Zygote进程是Android所有进程的孵化器进程,它启动后首先初始化Dalvik虚拟机,然后启动system_server并进入Zygote模式,通过socket 等候命令。当执行一个Android应用程序时,system_server进程通过Binder IPC方式发送命令给Zygote,Zygote收到命令后通过fork自身创建一个Dalvik虚拟机的实例来执行应用程序的入口函数,程序启动完成。
Zygote提供了三中创建进程的方法:
Fork(),创建一个Zygote进程;
ForkAndSpecialize(),创建一个非Zygote进程;
ForkSystemServer(),创建一个系统服务进程。
其中,Zygote进程可以再fork()出其他进程,非Zygote进程则不能fork其他进程,而系统服务进程在终止后它的子进程也必须终止。
当程序fork成功后,执行的工作就交给了Dalvik虚拟机。Dalvik虚拟机首先通过loadClassFromDex函数完成类的装载,每个类成功解析后都会拥有一个ClassObject类型的数据结构存储在运行时环境中,虚拟机使用gDvm.loadedClasses全局哈希表来存储与查询所有装载进来的类,随后,字节码验证器对装入的代码进行校验,接着查找并装载main方法,随后初始化解释器并执行字节码流。
通过研究Android应用程序启动源码,Android应用程序app的具体启动流程如图3所示,具体为:
步骤101、用户向Android系统发送启动应用程序的指令,Android系统接收到指令,通过系统Application Framework层的Launcher组件向常驻服务ActivityManagerService发送启动请求;
发送启动应用程序的指令通常由用户点击或是终端指令启动;
步骤102、ActivityManagerService在接收到请求后查询系统中已启动应用程序列表该应用是否已启动,若发现列表中不存在此应用程序,则未启动,向Zygote进程发送一个创建应用程序进程的请求;否则,进入步骤103;
步骤103、Zygote进程收到请求以后将自身克隆(Fork)出一个子进程A,同时创建一个ActivityThread对象,并且将A进程的入口函数替换为ActivityThread的入口函数;
子进程A包括了一个Dalvik VM实例和相关底层JNI接口,相对于JVM而言,DVM并不提供一个运行时容器,它提供的只是一个用于共享的进程,Android系统中所有的应用程序运行都是独立的,OS级别的进程,直接受到OS层面的资源控制以及调度的影响,只是他们共享Zygote的预加载的类。
步骤104、Zygote将创建的子进程A启动,进程A实例化一个ActivityThread,进入ActivityThread的入口函数,为主线程创建一个消息队列,用于驱动应用程序;
步骤105、进程A调用attach()方法对其初始化,把需要启动的应用信息写入到ActivityThread实例中;
写入的信息包括应用名称、所含组件、资源文件路径、库文件路径以及用于加载、解析应用的加载器等;
步骤106、完成对ActivityThread实例的初始化后,主线程进入消息循环;
调用Application类的中的方法attachBaseContext调整应用程序启动的上下文环境,使用系统默认加载器加载应用程序中的Classes.dex文件;
步骤107、进程A的主线程进入应用程序的主入口,启动应用程序app;
一般情况下应用程序的主入口为Application类中的onCreate()方法。
步骤二、在被测应用程序加载的过程中,选取被测应用程序若干不同且重要的函数入口点同时进行Hook操作,对每个函数分别加载对应的Hook点;
在android中可以通过ptrace附加进程,然后向远程进程注入so库,从而达到监控以及远程进程关键函数挂钩。目前,android上的注入hook基本上都是基于修改got表,从而达到hook拦截效果。比如,android中binder通信封装在libbinder.so里面,libbinder.so中和binder驱动打交道是通过系统调用ioctl,因此拦截ioctl并解析其中的参数,就可以获取binder通信过程中的一些关键隐私信息。然而,ioctl是libc.so中导出的一个函数符号,真正实现ioctl函数功能是在libc.so中。以源码为例,libc.so中ioctl实现分为两个部分:
1._ioctl.S代码
2.ioctl代码
从上面分析可以知道ioctl是通过调用汇编实现的_ioctl完成实际功能。
相对于修改got表,还可以通过内联hook实现函数hook功能。inline hook就是在函数实现的库中,通过篡改函数执行的汇编指令而到达目的。比如假设test函数,汇编代码如下:
stmfd sp!,{r4,r5,r6,r7}
ldmfd ip,{r4,r5,r6}
str r5,[r1,#-4]
str r6,[r1,#-8]
ldr r7,=_NR_xx
swi#0
movs r0,r0
beq 1f
如果需要inline hook该test函数的话,就需要分析该函数汇编码,找到切入点,篡改函数的指令,比如在该函数中将stmfd sp!,{r4,r5,r6,r7}指令篡改为ldr xx。当然,inline hook并不是那么容易,在inline hook过程中需要确定代码是thum指令还是arm指令,并且还得保证篡改了指令之后执行hook函数后堆栈是平衡的,这样才能完成hook之前函数的功能。
下面以一个图说明inline hook。如图4所示,左边为需要hook的函数A1,A2,A3表示func函数的实现指令。中间表示拦截func函数hook,右边一栏表示存储func函数前几条指令A1,以及回到执行指令A2的跳转指令。因此,如果需要内联hook func函数,首先需要在远程进程中申请空间,该空间会存储func函数的前几条指令,以及跳转回func函数后续指令的跳转 指令。其次,需要将func函数地址重定向到hook函数。
当应用执行func函数时候,由于之前被重定向到hook函数,因此会首先执行hook的代码,在hook函数中会处理传进来的参数,处理完成之后,跳转到右边A1处,此处A1执行相当于执行func函数的A1,A1执行完之后接着执行跳转指令跳转到func A2处执行。因此,一次中转之后,func函数被完整的执行。
修改got表和内联hook比较:
1.修改got表拦截比内联hook拦截容易,只需要知道elf文件中调用外部符号的地址。
2.inline hook实现拦截的信息比修改got表丰富。比如修改got表,拦截应用中的connect函数只能拦截应用通过java层访问网络得请求链接,然而如果应用自己通过jni方式调用socket connect方法,将会不能拦截到。如果采用内联hook,很显然,connect函数最终是需要执行实现connect功能的指令,而指令实现库已经被hook,所以能达到拦截目的。
选取针对Android应用程序启动过程中以及运行过程中有较大概率的多个函数入口点进行Hook,以保证可以覆盖到应用中所有类的dex代码,如app.attachbaseContext、app.onCreat、Activity.onCreat等函数,使得应用中每个类的代码都可以被Hook所覆盖到,可以做到对各类动态保护技术的无障碍分析,然后在下一步中可以使用ClassLoader取得其dex代码;
步骤三、当虚拟机执行到不同函数时,借助该函数所加的Hook点获取该函数被调用的类结构ClassLoader;
利用所加Hook点获取ClassLoader。ClassLoader为JVM虚拟机中类的加载器,通过ClassLoader,JVM将类的字节码加载进JVM容器中开始运行,而Android应用本质上是一种特殊的Java文件,其虚拟机运行的程序文件为dex文件,故而在Android系统中使用了一种dexClassLoader,通过该加载器,Android虚拟机实现了将dex文件进行加载并运行的功能。由于在前一步已经对应用的各个函数入口点进行了Hook操作,获取到了相应类的Hook点,当虚拟机执行到不同的函数时,通过相关Hook点,获取到到该函数被调用时有关类的dexClassLoader,其中,包含了函数和所在类的详细信息。
对Android相关源码进行分析后得出Android平台ClassLoader关系,如图5所示:
在Android平台上,对一个应用程序类的加载(dex文件加载),默认使用PathClassLoader,完成加载和解析。通过分析可知,其父类为BaseClassLoader,在其实现过程中,涉及到DexPathList类,这个类是调用dexfile的native实现完成对dex文件的加载和findClass,并且构造Element结构数组,将进程运行时所需要的lib、res以及dexfile信息进行记录。BaseClassLoader继承自java类ClassLoader,从而继承ClassLoader的回调逻辑,实现了对类的查找、解析等逻辑。
步骤四、通过各个类结构ClassLoader获取各个函数以及函数所在类在内存中的偏移位置,并得到程序各个函数的dex源码;
借助在前述步骤中获取的dexClassLoader,可以得到其中所保存的运行时内存dex地址,进而可以得到dex在内存中的保存位置。
在Android平台上的ClassLoader,都是基于ClassLoader的特定继承关系实现的,如图6所示,从ClassLoader中可以提取出dex文件在内存中的偏移,即图6中所示的mCookie。在每一个ClassLoader中都有这种结构树,其中存在一DexPathList类的成员,DexPathList类是Android平台中在应用运行启动运行过程中,为ClassLoader提供库地址、资源地址、以及存储被加载文件的信息。这个类的成员中较为重要的是一个Element型数组,其中保存着所加载的文件的索引和类型标记,针对dex文件二言,则是存储了一个java层的DexFile类对象,通过DexFile类完成加载和解析dex的工作,返回dex加载后结构所在的内存地址即mCookie。
通过对Android应用启动过程的分析可知在ClassLoader加载dex后,需要将ClassLoader指针填充到mBoundApplication.info中。简而言之,通过取得这个成员变量,就可以得到dex在内存中的地址。
事实上,通过实验在app运行中各个节点进行hook,取得了上述结构中的mCookie,即内存中各部分dex代码的保存位置。
步骤五、将被测应用程序各个函数的dex源码组成dex文件,在内存中进行修复完善;
对于内存中所获取的各个部分dex代码进行拼合,得到整个应用的dex,由于某些Android加固APP使用了内存动态dex还原技术。Elf在使用了上述方法后,使得dex在内存中可控,可以做到实时加载,也可以做到分片,由于待分析应用可能是经过动态保护的,所以该dex可能是不完整或者各个段代码偏移是错误的,故需要对得到的dex文件进行修复,具体的修复技术为:根据dex源码分析,实现dex的反解析,在Header中计算各个类的实际偏移,然后重构Header偏移值、进行类以及方法体回填、计算Sha1回填,从而修复dex,修复后的dex可保证与原dex在功能上保持一致,提高逆向分析结果的可靠性和有效性。
具体来说,应用程序软件在加载dex运行时,会动态修改方法的属性,而且现在的加固方案会对dex的codeItem进行隐藏、移位等处理,以防止连读段dump的传统逆向方式。所以需要在Dex-Reconstruction子模块中,对目标DexFile各个成员数据进行遍历,修复完整的dex数据后,写入到文件,从而得到dex文件,具体如下。
如图2所示,具体步骤如下:
步骤501、对被测应用程序的dex文件进行解析,遍历存有类信息的头部结构索引DexClassDef;
头部结构索引DexClassDef包括:被测应用各个类对应的条目class_def_items,被测应用的基本头部信息和被测应用的全局数据;
通过分析各个类的条目class_def_items获取被测应用各个类的相关信息,及各个类的内存地址偏移class_data_off;
步骤502、对头部结构索引DexClassDef的信息分别进行保存;
将各条目class_def_items保存到索引临时存储区域buffer_classdef中;将基本头部信息保存到到buffer_header中,将全局数据保存到buffer_data中;
步骤503、根据各个类的地址偏移class_data_off,获取各个类实际代码,并将获取到的代码保存至数据临时存储区域buffer_classdata中;
根据class_data_off,将所有的class_data_items保存到buffer_classdata;
步骤504、判断各个类读取过程中,是否有某个类的地址偏移class_data_off指向了超出应用内存空间的非法地址;如果有,调用类定义构造方法重新构造该类的结构索引classdefine,并对该类地址进行修正,以正确地址向buffer_classdef进行回写,并修正buffer_classdef中对应的class_data_off,进入步骤505;否则,保持不变;
如果存在class_data_off指向外部区域,对该class进行classdefine,找到其位置拷贝回到buffer_classdata中,并修正buffer_classdef中对应的class_data_off。
拷贝DexFile文件中DexClassDef结构前部分到buffer_header中,拷贝后部分到buffer_data中。
遍历buffer_classdata中所有的code_item_off,确认其是否在DexFile的空间之内,并进一步确认其accessFlag值是否合法。
步骤505、对索引临时存储区域buffer_classdef、数据临时存储区域buffer_classdata、应用基本头部信息buffer_header、应用数据buffer_data四个buffer区进行拼装,在内存中形成临时dex文件;
步骤506、调用校验和计算函数计算整个临时dex文件的校验和,并签名;
调用校验和计算函数dexComputeChecksum计算整个临时dex文件的校验和,并回填到临时dex文件应用基本头部信息结构buffer_header中的校验和checksum,然后用安全哈希算法计算整个临时dex的哈希值sha1回填到buffer_header的signature。
步骤507、将签名后的内存临时dex文件导出到文件,即得到了修复完善的dex代码。
总的来说,对dex文件进行解析,对存有类信息的ClassDef结构内容进行遍历读取,从中抽取codeoff,并存储下来,直到遍历结束,获得所有Class信息。至此我们获得了dex中所有的类。然后对dex文件中其他部分进行遍历,获得整体结构。重新计算dex的checksum和sha1值,进行回填。最后将所有数据依次写出到文件,得到dex。
步骤六、从内存中dump修复完善后的的dex代码,即获得了待分析应用的完整dex
dump获取dex文件的具体方法如下:
通过Cookie值可以获得Dex在内存的映射文件结构,取得其类以及各变量信息,将其写到向储存器中的文件保存为dex,这三个步骤如图7所示,依次获取DexOrJar,RawDexFile,DvmDex以及DexFile,然后对获取到的dex文件结构进行重新构造,最终得到dex文件的二进制字节码输出。
dex文件被opt优化处理后,被映射到内存中,dvm中定义了多个文件结构,形成了dex文件在内存中的结构,如图8所示,包括DexOrJar、RawDexFile、DvmDex以及DexFile,依次属于包涵关系:其中DexFile就是dex文件的格式定义,从严格意义上讲,DexFile是dex经过opt优化后的odex文件格式定义,dex文件格式已在第二章中进行分析,为避免赘述,仅给出其的定义源码:
在dvm解析dex时,通过DvmDex生成ClassObject,使得程序运行,ClassObject是Java类在虚拟机中的实体,也就是源码中所定义的类的表现,也是Android应用程序的基本单元,其包涵了某一类的方法表、成员表、代码信息等。换而言之,ClassObject是类在内存中的真实形态,其在内存中的结构如图9所示,其中Object为ClassObject的父类结构,Method部分为类的方法列表,包括普通方法、虚方法等,StaticField为静态成员变量,直接保存变量值,InstField为实例成员变量,保存有变量的内存偏移地址;DvmDex作为中间载体将,ClassObject与DexFile连接起来。通过解析DexFile根据结构中各成员找到其在内存中的数据,因此根据DexOrJar位置,得到DexFile并在内存中对其进行解析,此时根据DexFile动态得到的数据,即可得到dex文件。
下面使用一则新老逆向方法进行对比,以展示本方法的优势。
对于某Android加固APP,使用传统的APP逆向方式所得到的dex进行反汇编后得到的代码如图10所示,可以从图中看出,所还原的方法体为空,显示其还原的dex文件存在不完整的问题,该dex无法正常运行,对于后续的研究也没有任何意义。
而使用本发明所述逆向方法所还原得到的dex如图11所示,方法体完整,显示了其还原效果可靠有效,dex可以正常运行,还原成功。
如图12所示,为使用Hex Compare工具对新旧逆向方法所得到的dex文件进行差别对比的结果,其中左侧显示了文件对比后不同的部分,可以看出有较大不同,结合前述传统app逆向方法所得到的不完整方法体结果,可以表明本发明所介绍的新式逆向方法与传统方法相比有较大的优势。