颜慧颖,周振吉,吴礼发,洪征,孙贺
基于符号执行的Android原生代码控制流图提取方法
颜慧颖,周振吉,吴礼发,洪征,孙贺
(解放军理工大学指挥信息系统学院,江苏南京 210000)
提出了一种基于符号执行的控制流图提取方法,该方法为原生库中的函数提供了符号执行环境,对JNI函数调用进行模拟,用约束求解器对符号进行求解。实现了控制流图提取原型系统CFGNative。实验结果表明,CFGNative可准确识别样例中所有的JNI函数调用和原生方法,并能够在可接受的时间内达到较高的代码覆盖率。
控制流图;Android应用软件;原生代码;符号执行
Android系统的普及使Android应用的数量和种类呈爆发式增长。据Statista的数据统计,2017年3月,仅Google play上的Android应用就达2.8×106多个,比2015年增长了近1×106个[1]。面对海量的Android应用,安全人员可能需要分析程序寻找漏洞或判断程序是否有恶意行为;开发者倾向于复用已有的程序模块。而大多数情况下,分析者接触到的都是编译后的应用,需要分析程序安装包(APK,Android package)中的代码文件(字节码文件和原生库文件)获取有用的信息。因此,人们对高效的Android应用分析方法和技术的需求更加迫切。
控制流图在程序逆向和分析领域中应用十分广泛。编译后的程序丢失了很多源代码中数据和代码结构的信息,间接跳转、不可执行的数据块、代码段对齐等使反汇编变得困难。控制流图有助于解决这些棘手的问题[2,3]。并且控制流图是数据流分析、数据依赖分析、程序切片等其他分析方法的基础[4,5],也可作为区分恶意和非恶意代码的特征[6,7]。
传统的控制流图提取方法正被逐渐移植到Android应用程序的分析过程中。但由于Android应用程序与PC程序的结构和运行环境存在较大差别,这些方法[8~17]不能直接用于构造Android应用的控制流图。目前,针对Android应用的工作[18~20]主要关注Dalvik字节码(简称字节码)的控制流图提取方法,缺乏对原生库代码的研究。而越来越多的Android程序在原生库中完成一些重要和复杂的功能,如加密、加壳等。恶意代码也开始倾向于隐藏在原生库中以逃避检测,如“百脑虫”“蜥蜴之尾”等木马都将其恶意逻辑隐藏在原生库中,因此仅分析字节码并不能完整地了解Android应用的真实行为。针对上述问题,本文深入分析了Android应用原生代码的特点,提出了一种基于符号执行的控制流图提取方法,设计并实现了原型系统,主要贡献如下。
1) 提出了一种基于符号执行的控制流图提取方法,符号执行过程基于中间表示VEX。该方法与平台无关,可以用来分析为多种平台编译的Android原生代码。
2) 深入分析系统的JNI特性,对系统JNI相关的结构和JNI函数进行模拟,使本文方法能准确识别原生代码中的原生方法和JNI函数调用。
3) 基于angr设计并实现了用于提取Android原生库控制流图的原型系统CFGNative,该系统能自动识别导出函数和注册的原生方法,并将其作为控制流图的起点。CFGNative为用户及现有的Dalvik字节码分析工具提供了接口,以便于分析者结合CFGNative和字节码的分析工具提取全部应用的控制流图或基于该系统实现其他的程序分析方法。
2.1 Android应用和JNI函数调用
本文分析的对象是编译打包后的APK。APK中主要包含Java代码编译后生成的Dalvik字节码文件(后缀为dex),C/C++代码编译后的针对不同平台的原生库文件(后缀为so)、资源文件和AndroidManifest.xml文件。APK中的可执行文件为字节码文件和原生库文件。字节码运行在Dalvik虚拟机上,原生代码直接运行在Linux系统上。
Android应用中的字节码和原生代码可以通过JNI通信和相互调用。JNI是Java程序设计语言功能最强的特性,它允许Java类的某些方法原生实现,同时让它们能够像普通Java方法一样被调用[21]。Android的Dalvik虚拟机也支持Java的JNI特性。Dalvik字节码需调用System.loadLibrary方法加载包含原生方法的原生库文件,并且用关键字native声明原生方法,之后才能调用原生方法。
原生库按照注册规则向Dalvik虚拟机注册暴露给字节码的原生方法。原生方法需在字节码中用关键字native进行声明,Dalvik虚拟机调用原生方法时,在原生库中找到与这些声明对应的方法实体,并传递参数JNI接口指针的地址(如JNIEnv指针)和实例引用或类引用(若该原生方法是实例方法则传入实例引用,若为静态方法则传入类引用)。
原生库中的代码通过指向JNI接口的指针获取JNI函数(或JNI接口函数)的地址,然后调用JNI函数与字节码进行数据交换,如访问Java类的字段、创建类的实例、调用类和实例的方法等。JNI函数调用过程示例汇编代码如下。
.text:00000D34 MOV R4, R0
.text:00000D38 LDR R3, [R0]
.text:00000D3C BEQ loc_E60
.text:00000D40 LDR R1, = (aAndroidContent - 0xD50)
.text:00000D44 LDR R3, [R3,#0x18]
.text:00000D48 ADD R1, PC, R1 ; "android/content/Context"
.text:00000D4C BLX R3
.text:00000D50 SUBS R5, R0, #0
.text:00000D54 BEQ loc_E74
.text:00000D58 LDR R12, [R4]
.text:00000D5C MOV R0, R4
.text:00000D60 LDR R2, = (aGetsystemservi - 0xD74)
.text:00000D64 MOV R1, R5
.text:00000D68 LDR R3, = (aLjavaLangStrin - 0xD7C)
.text:00000D6C ADD R2, PC, R2 ; "getSystemService"
.text:00000D70 LDR R12, [R12,#0x84]
.text:00000D74 ADD R3, PC, R3 ; "(Ljava/lang/String;)Ljava/lang/Object;"
.text:00000D78 BLX R12
根据函数调用规约,R0传递参数JNIEnv指针,因此代码00000D38处的R3寄存器和00000D58处的R12寄存器存储的是JNIEnv的值。JNIEnv是一个接口指针,该接口结构中包含JNI函数表,通过JNIEnv和相对偏移可以访问函数表。相对JNIEnv偏移0x18的地址上存储的是JNI函数FindClass的地址。程序在00000D44处获取FindClass的地址,并通过00000D4C处的间接跳转指令调用该JNI函数得到android/content/ Context类的引用,在00000D70处获取存储在相对JNIEnv偏移0x84地址上的JNI函数GetMethodID的地址,00000D78处的间接跳转指令调用GetMethodID得到android/content/Context的getSystemService方法的引用。传统的控制流图提取方法不能解析程序中的JNI函数调用过程,因此不能直接用来提取Android原生代码的控制流图。
2.2 中间表示VEX
随着嵌入式平台的发展,Android系统的底层平台也更加多样,现在市场上主要的架构有ARM、X86、AMD64和MIPS,所以原生库可能是不同平台的目标文件,使用不同的指令集。为了使本文方法与平台无关,将原生库中的指令转化为中间表示,在中间表示上进行符号执行。
本文使用的中间表示是VEX[22]。VEX是一种较成熟的平台无关的中间语言,使用广泛,已经被实践证明能较好地兼容多种平台(包括Android系统中可能使用的几种平台)。一些经典的二进制分析平台,如Valgrid[23]、angr[24],将二进制码转化为中间表示VEX,再基于VEX进行程序分析。第3.3节将详细介绍VEX在符号执行过程中的作用。图1为32 bit ARM指令转化为VEX的示例,图1左侧是ARM指令,将寄存器R2中的值减8再存入R2寄存器。图1右侧是转化得到的VEX语句,先将R2的值赋给变量0,使变量1取值8,再将变量0和1的差赋给变量3,然后把3的值存储到寄存器R2中,最后将下一条指令的地址0x59FC8存入IP寄存器。
以原生库文件的导出函数(一般包括静态注册的原生方法一般会出现在导出函数表中)和动态注册的原生方法为符号执行的起点,控制流图的提取过程如图2所示。首先为每个起点初始化程序状态,在程序状态中模拟JNI相关结构(JNIEnv、JavaVM、JNIInvokeInterface和JNINativeInterface),将无法确定的参数初始化为符号值;然后从起点开始符号执行。遇到跳转指令时,若跳转地址是JNI相关结构中的JNI函数地址,则执行模拟JNI函数的SimProcedure,再返回符号执行过程;否则从跳转地址继续符号执行。
3.1 程序控制流图
按照冯诺依曼体系结构,无跳转指令时,程序执行完一条指令后,紧接着在内存中取下一条指令继续执行,这种执行顺序被称为顺序执行。程序的执行流程有顺序、分支和循环这3种。分支和循环都是由跳转指令破坏当前的顺序执行实现的。把连续的顺序执行的指令集合作为一个基本块,将基本块作为控制流图的节点,跳转语句导致的基本块之间的控制流迁移为控制流图的边。因此,提取控制流图的重点和难点是计算跳转地址(跳转指令实现跳转后,程序继续执行的地址)。
3.1.1 跳转指令
每种指令集中都有跳转指令,这些指令可以使程序接着执行跳转地址处的指令,而非顺序执行下一条指令。按跳转是否需要条件可将跳转指令分为强制跳转指令和条件跳转指令。一定会发生跳转的指令为强制跳转指令,如X86指令集中的CALL、JMP指令,ARM指令集中的B、BL指令,以及直接给PC赋值的指令,如LDR PC、Expr。当满足特定条件才跳转的指令为条件跳转指令,如X86指令集中的JE、JNE指令,ARM指令集中的BE、BNE指令。按跳转地址的取值方式可以将跳转指令分为直接跳转指令和间接跳转指令。直接跳转指令将跳转地址直接编码到指令中,而间接跳转指令的跳转地址依赖于寄存器或内存中的值。每一条跳转指令都可以表示为jmp(,)的形式,其中,是跳转指令所在的地址,是跳转地址,是跳转的条件。控制流图的准确性和完整性很大程度上依赖于跳转地址计算的准确性。
跳转指令将PC的值置为跳转地址而非顺序执行的下一条指令地址,同时还可能改变内存或其他寄存器的值,如CALL指令在程序跳转前会将顺序执行的下一条指令的地址压入堆栈,BL指令会将顺序执行的下一条指令存储到R14寄存器中。因此可以将跳转指令抽象为改变程序状态(寄存器和内存的取值情况)的函数,第3.3.2节会进一步介绍。
3.1.2 基本块
本文将指令转化为中间表示VEX,因此控制流图的基本块是连续且顺序执行VEX语句的集合。
将中的指令全部转换为VEX语句得到的语句序列为VEX基本块。根据定义可以推断基本块(除起始块)的起始地址都是某一条跳转语句的跳转地址,基本块(除结束块)的最后一条指令都是跳转语句。只能从基本块第一条语句开始执行,且一旦开始必然顺序执行到该基本块中最后一条指令。
3.1.3 控制流图
本文的目的是分析程序控制流的迁移,并将其表示为由基本块和边构成的控制流图。
定义2 任意一个Android库文件的指令都可以被划分为多个基本块,这些基本块的集合记为。2个基本块1和2间存在数据流迁移,当且仅当
定义4没有main函数,本文从导出函数和不出现在导出表中的原生方法开始提取控制流图。给控制流图增加2个特殊节点和若干边,控制流图中其他节点和边的约束依然满足定义3,得到。
3.2 模拟JNI函数调用
如2.1节所述,原生方法通过间接跳转调用JNI函数,而传统的控制流图提取方法没有将原生方法的第一个参数解析为JNI接口指针地址,也不能解析JNI接口的结构,因此无法计算出JNI函数调用的间接跳转地址。对此,本文方法在程序状态中模拟JNI接口指针等JNI相关结构,找到向虚拟机注册的原生方法,然后将JNI接口指针的地址传递给原生方法。当符号执行遇到JNI函数调用的间接跳转指令时,即可根据模拟的JNI相关结构计算得到函数表中的JNI函数地址。另外,APK的原生库中没有JNI函数的指令,无法符号执行JNI函数,本文方法用SimProcedure代替符号执行过程。
3.2.1 模拟JNI相关结构
JNI相关结构包括JNIEnv、JavaVM、JNIInvokeInterface和JNINativeInterface。Dalvik虚拟机将JNIEnv的地址作为第一个参数传递给原生方法(JavaVM的地址是JNI_OnLoad的第一个参数)。JNIInvokeInterface和JNINativeInterface是JNI接口类型,分别包含一个函数表,函数表中包含的是JNI函数的地址。原生代码通过函数表中的函数访问虚拟机或字节码中的内容。JavaVM指向的正是JNIInvokeInterface的起始地址,JNIEnv指向的正是JNINativeInterface的起始地址,因此原生方法通过参数和相对偏移即可找到函数表中某一JNI函数表项的地址,从而得到该JNI函数的地址。JNIEnv、JavaVM、JNIInvokeInterface和JNINativeInterface在内存中的结构如图3所示。
本文分析的原生库中不包含这些结构,也不会在内存中构造该结构,因此,在符号执行前需要在程序状态的内存中模拟该结构。在内存中开辟一块未使用的空间给JavaVM、JNIEnv、JNIInvokeInterface和JNINativeInterface,在开辟的空间中填入任意未使用地址空间中的地址,执行过程中自动将JavaVM和JNIEnv的地址作为参数传递给原生方法。
3.2.2 SimProcedure模拟JNI函数
除了模拟JNI相关结构,还需要模拟接口函数表中的JNI函数。在真实的Android原生库运行环境中,原生方法通过Dalvik虚拟机调用JNI函数,本文符号执行过程中既无虚拟机也无JNI函数,需要用SimProcedure对JNI函数的功能进行抽象和模拟,并且用SimProcedure hook JNI函数的地址,使执行到JNI函数时,相应的SimProcedure能被调用。
SimProcedure是指用来模拟JNI函数的一类函数,概括了JNI函数的程序逻辑,是程序执行过程中实现JNI函数对程序状态的改变以及相应控制流图节点构造的实体。SimProcedure不是原生库中的代码,因此其构造的节点只用于记录该处JNI函数调用的信息,不包含真实代码对应的VEX语句。
JNIInvokeInterface和JNINativeInterface的函数表中的每个函数都对应一个SimProcedure。有的JNI函数会返回字节码的类、对象、字段、方法等的引用,后续JNI函数调用中可能会使用这些返回值。而执行环境中不存在字节码中的结构,这些结构的引用只是标识它们的符号。因此,这些字节码内容的引用是全局的,且在定义SimProcedure时,应该考虑JNI函数之间的关系,使其他SimProcedure也能够准确识别这些标识。如FindClass的SimProcedure需要返回一个标识以表示类名为传入的第二个参数的类,在符号执行过程中,其他JNI函数在遇到这个标识时也应该将其解释为这个类。
先构造图3的结构,再用对应的SimProcedure hook JNI接口函数表中填入的地址。执行过程中,当原生方法调用函数表中的JNI函数时,找到的JNI函数地址实际上是被SimProcedure hook的地址,因此执行的是SimProcedure,执行完SimProcedure后返回调用JNI函数的下一条指令继续执行。
3.2.3 定位注册的原生方法
本文的符号执行要自动将JNIEnv的地址作为参数传递给原生方法,因此需要其能够识别注册的原生方法。
注册原生方法的方式分为静态注册和动态注册这2种。静态注册需要根据命名规则命名原生方法,要求用包名加上类名再加上字节码中声明的方法名来命名原生库中对应的原生方法,如应将与android.helloWorld包的MainActivity类中声明的原生方法helloFromJNI对应的原生库中的方法命名为Java_android_helloWorld_MainActivity_helloFromJNI。符号表中一般都包含静态注册的原生方法,因此可以通过符号表中函数名识别原生函数。而动态注册将原生方法与其在字节码中声明的对应关系记录到JNINativeMethod结构中,再通过调用JNINativeInterface中的RegisterNatives函数将JNINativeMethod中的原生方法注册到虚拟机。JNINativeMethod的结构如下所示。
typedef struct {const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
该结构有3个字段,分别为字节码中声明的原生方法的名称、参数和返回值的类型信息以及原生方法在原生库中的地址。若执行过程中调用了RegisterNatives函数,从参数中可得到JNINativeMethod结构的列表在内存中的地址和列表长度,解析该列表获得动态注册的原生方法,该过程由RegisterNatives函数的SimProcedure完成。
动态注册需要在字节码调用原生方法之前完成。JNI_OnLoad在原生库加载时被调用。通常在JNI_OnLoad中调用RegisterNatives,动态注册过程就会在原生库加载时完成。因此,如果原生库中实现了JNI_OnLoad方法,应该优先执行JNI_OnLoad以找到可能出现的动态注册的原生方法。
3.3 提取控制流图
本文分析的对象是Android原生库文件,而原生库不可单独执行,如果直接进行动态分析,则需要实现触发原生库执行的模块。而Android应用中调用原生方法的是字节码,因此需要搭建两层执行环境(包括Dalvik虚拟机),使环境比较复杂,并且有时需要构造复杂的输入才能触发原生库中代码的执行,分析效率较低。为此本文提出了一种基于VEX的符号执行方法,直接分析Android原生库,提取控制流图。只要构造了函数的执行环境,该方法便可单独执行某一个函数,无需从main函数开始执行。符号执行的环境由angr[25]提供,angr将指令转化为中间表示VEX,符号执行引擎根据VEX表达式和语句的语义修改程序状态,并记录下路径的约束条件,当遇到跳转语句时根据程序状态和约束条件计算跳转地址。
3.3.1 程序状态
程序状态主要包含程序在某一程序点内存、寄存器、变量(VEX中含有变量)的取值情况。将VEX中寄存器的集合记为,内存区域记为,临时变量的集合记为。符号执行的程序状态中有具体值和符号值。将具体值(如立即数0x10、地址0x400000等)的集合记为。符号值表示满足一定约束的值的集合,用符号(如、等)表示。符号值在执行中也可参与运算,如+、+0x10等。将符号值的集合记为。因此程序状态中寄存器、内存和变量取值的值域为。原生库中函数按函数调用规约接收参数,且除JavaVM和JNIEnv的地址外,其他参数在分析者没有指明的情况下都默认取符号值。只要构造一个函数被调用时的程序状态并将该状态输入符号执行引擎,就能从该函数开始符号执行,因此需要初始化原生函数和其他导出函数开始执行时的程序状态,如将参数赋值给传递参数的寄存器。
定义5 程序状态state用二元组的集合表示,实际上为内存、寄存器和变量到上的映射。
当程序状态中有二元组(,)时,则说明该状态下的取值为。
理论上是无穷且连续的,但程序执行过程中各类型变量的值一般存储在一块连续的内存区域中。将该段内存中的值作为中的一个元素,内存即可被看作离散的。另外,运行程序所需的内存空间有限,未使用的内存只是概念上的一块区域,在符号执行过程中并不分配空间以记录其状态,因此记录和读取内存是可实现的。
3.3.2 表达式和语句
符号执行是在VEX上进行的,因此需要告知执行引擎VEX表达式和语句的语义,执行引擎才能按照语义执行程序。
定义6 将表达式的集合记为,语句的集合记为。表达式的语义为表达式和程序状态到上的函数,语句的语义为语句和程序状态到新的程序状态上的函数。
VEX语句和表达式的语义要根据具体的表达式和语句定义,如表达式RdTmp(10)的语义为变量10的取值,语句WrTmp(1)=(IR)的语义为将表达式的值赋给1得到新的程序状态。VEX的表达式和语句的详细介绍参考文献[22]。
3.3.3 求解带符号的跳转地址
符号执行过程中可能存在带有符号的跳转地址。执行过程在程序状态中记录了每一条分支对符号的约束。当遇到地址带有符号的跳转指令时,用约束求解器求解该符号的约束得到符号值的范围,计算得到可能的跳转地址,然后判断这些可能的跳转地址是否为指令的起始地址,将满足条件的值列入最终跳转地址结果的集合中。
3.3.4 控制流图提取算法
基于上述讨论,控制流图提取算法的伪代码如下。
代码第3)~9)行为算法初始阶段,其中,第3)行从导出表中获取导出函数地址;第4)行获取导出函数中JNI_OnLoad的地址,若不存在则为空;第5)行识别导出函数中静态注册的原生方法;第7)行将导出函数地址加入到工作列表中。
第10)~30)行代码开始循环,从工作列表中取出地址,如果该地址为JNI函数的地址,第13)行代码调用JNI函数对应的SimProcedure,若调用的JNI函数是RegisterNatives,则可以找到动态注册的原生方法,第15)行将新找到的动态注册的原生方法的地址加入工作列表中;如果该地址不是JNI函数的地址,则执行第18)~29)行。第18)行得到起始地址为的VEX基本块。第23)行获取VEX基本块开始符号执行时的程序状态,需要为函数的起始块初始化程序状态,从程序状态集合中获取其他VEX基本块对应的程序状态。如果基本块属于原生方法,初始化程序状态时要在内存中模拟JNI相关结构。第24)行得到VEX表达式和语句的语义。第25)行为符号执行过程,第26)~29)行计算跳转地址,将以跳转地址为起点的基本块对应的程序状态加入程序状态集合,并将跳转地址加入工作列表。循环整个过程直到工作列表为空。
4.1 系统实现
为了验证本文所提方法的有效性,基于angr实现了提取Android原生库的控制流图的原型系统CFGNative,其结构如图4所示。系统中,模块loader解析输入的原生库文件,并将代码段和数据段加载到内存中;lifter从给定的地址开始识别并翻译VEX基本块;execution simulator在lifter识别的VEX基本块上进行符号执行并将计算得到的跳转地址给lifter,lifter从新的地址开始继续识别VEX基本块,迭代该过程直到没有新的VEX基本块生成;state记录执行各阶段的程序状态;core engine是符号执行的核心模块,逐条解析VEX语句的语义;JNI struct constructor在内存中构造JNIInvokeInterface和JNINativeInterface等结构;simprocedures被hook到JNI函数的地址上;constraint solver根据约束求解带符号跳转地址。
4.2 实验及结果分析
本文设计了2个实验:实验1用CFGAccurate提取本文构造的Android(源码见附录A)应用JNITest中原生库的一个控制流图,目的是检测CFGAccurate是否能准确识别静态和动态注册的原生函数和JNI函数调用。实验2对比angr的CFGAccurate、IDA和CFGNative提取控制流图的结果,目的是将CFGNative的指令覆盖率和时间耗费与现有经典分析工具进行对比,检测CFGNative增加的功能是否对其他性能造成过大影响。实验硬件配置为Intel(R) Core(TM) i7-3770处理器(8核3.40 GHz),32 GB RAM。操作系统为64 bit的Ubuntu16.04。
1) 实验1:JNITest
原生方法分为静态和动态这2种注册方式,因此JNITest中包含了2个原生方法,分别用2种不同的方式注册,同时包含多个JNI函数调用,具有一定代表性。
Java_com_example_test_MainActivity_getIMEI为静态注册的原生方法,通过JNI函数FindClass找到android.telephony.TelephonyManager类,然后调用JNI函数GetMethodID获得该类getDeviceId方法的引用,最后通过CallObjectMethod调用该方法获取设备的IMEI。该方法中的JNI函数会使用其他JNI函数返回的结果,如FindClass返回的Java类的引用会作为GetMethodID的参数,因此可以测试SimProcedure的定义是否正确。helloJNI为动态注册的原生方法,通过在JNI_ONLoad中调用RegistaerNatives进行注册,该方法返回JNI函数NewStringUTF构造的字符串“Hello from JNI”。
CFGNative提取的控制流图显示其准确识别了这2个原生方法和所有的JNI函数调用,得到的控制流图中共有8个代表JNI函数的节点,38条包含表示JNI函数节点的边。代表JNI函数调用的节点和包含这些节点的边的详细信息见附录B。
2) 实验2:对比
实验2将Android应用市场APPChina上下载量排名靠前的16个应用作为样本,过滤样本原生库中的广告包、音视频处理等第三方工具包,共收集了61个原生库文件。表4为从每个应用中收集的原生库的个数,由于有些原生库存在于多个包中,所以个数的总和大于61。从每个包中获取的具体的原生库见附录C。用CFGNative提取这些原生库的控制流图,并与当前最流行的二进制分析工具IDA和angr的CFGAccurate对比。IDA、CFGAccurate和CFGNative都以导出函数和不出现在导出函数表中的原生方法为起点构造控制流图,去掉编译过程加入的一些函数,如_gnu_ Unwind_Resume。统计每种方法提取每个原生库的控制流图平均耗费的时间和平均每个图中节点个数、边数和覆盖的指令数,结果如表2所示。
表1 样本中收集的原生库个数
表2 控制流图提取结果
CFGAccurate和IDA不具备分析JNI的能力,因此不能识别原生方法和JNI函数调用。CFGNative比其他2种方式识别了更多的节点,其中包含表示JNI函数的节点,这些节点的指令条数为0,从表2中可以看到,CFGNative比IDA和CFGAccurate覆盖了更多的指令,这是因为CFGNative识别了IDA和CFGAccurate无法识别的间接跳转,从而发现了新的基本块中的指令。实验结果表明,CFGNative不仅能够识别JNI函数调用,且在可接受的时间内具有较高的代码覆盖率。
本文提出了一种基于符号执行的控制流图提取方法,用于自动提取Android应用原生代码的控制流图。该方法可以识别原生方法和JNI函数调用,准确计算间接跳转地址,且具有较高代码覆盖率。下一步研究包括以下2个方面。
1) 符号执行用SimProcedure代替真实的JNI函数调用,因此对SimProcedure定义的准确性会影响符号执行的结果。目前对SimProcedure的定义过于依赖经验。未来工作将深入分析真实环境下JNI函数的行为并对其进行更系统的建模。
2) 符号执行过程中包含的符号值过多或约束求解器的求解能力不足都可能导致求解所得的符号值范围过大。当包含符号的跳转地址取值范围过大时,可能会产生路径爆炸问题,需要进一步研究该问题的解决方案。
#include
#include
#include
#include "jnitest.h"
#define LOGV(...) __android_log_print(ANDROID_ LOG_VERBOSE, "com.exaple.test", __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_ LOG_DEBUG, "com.exaple.test", __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_ LOG_INFO, "com.exaple.test", __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_ LOG_WARN, "com.exaple.test", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_ LOG_ERROR, "com.exaple.test", __VA_ARGS__)
JNIEnv *g_env;
JavaVM *g_vm;
jclass native_class;
#ifndef NELEM //计算结构元素个数
# define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
#endif
//静态注册的原生方法getIMEI
JNIEXPORT jstring Java_com_example_test_Main Activity_getIMEI
(JNIEnv *env, jobject mContext){
if(mContext == 0){
return (*env)->NewStringUTF(env,"[+]Error : Context is 0");
}
jclass cls_context = (*env)->FindClass (env, "android/content/Context");
if(cls_context == 0){
return (*env)->NewStringUTF(env,"[+] Error: FindClass
}
jmethodID getSystemService = (*env)-> GetMethodID(env,cls_context,"getSystemService","(Ljava/lang/String;)Ljava/lang/Object;");
if(getSystemService == 0){
return (*env)->NewStringUTF(env,"[+] Error : GetMethodID failed");
}
jfieldID TELEPHONY_SERVICE = (*env)-> GetStaticFieldID(env,cls_context,"TELEPHONY_SERVICE","Ljava/lang/String;");
if(TELEPHONY_SERVICE == 0){
return (*env)->NewStringUTF(env,"[+] Error : GetStaticFieldID failed");
}
jstring str = (jstring)(*env)->GetStatic ObjectField(env,cls_context, TELEPHONY_SERVICE);
jobject telephonymanager = ((*env)-> CallObjectMethod(env,mContext, getSystemService, str));
if(telephonymanager == 0){
return (*env)->NewStringUTF (env,"[+] Error: CallObjectMethod failed");
}
jclass cls_TelephoneManager = (*env)-> FindClass(env, "android/telephony/TelephonyManager");
if(cls_TelephoneManager == 0){
return (*env)->NewStringUTF (env,"[+] Error: FindClass TelephoneManager failed");
}
jmethodID getDeviceId = ((*env)-> GetMethodID(env,cls_TelephoneManager, "getDeviceId", "()Ljava/lang/String;"));
if(getDeviceId == 0){
return (*env)->NewStringUTF (env, "[+] Error: GetMethodID getDeviceID failed");
}
jobject DeviceID = (*env)->CallObjectMethod (env,telephonymanager,getDeviceId);
return (jstring)DeviceID;
}
//动态注册的方法helloJNI
JNIEXPORT jstring helloJNI(JNIEnv* env, jclass clazz){
const char * chs = "Hello from JNI";
return (*env)->NewStringUTF(env, chs);
}
static JNINativeMethod methods[] = {
{"helloJNI", "()Ljava/lang/String;", (void*)helloJNI}
};
jintJNI_OnLoad(JavaVM* vm, void* reserved){
if(JNI_OK != (*vm)->GetEnv(vm, (void**)&g_env, JNI_VERSION_1_6)){
return -1;}
LOGV("JNI_OnLoad()");
native_class = (*g_env)->FindClass(g_env, "com/ example/test/MainActivity");
if (JNI_OK ==(*g_env)->RegisterNatives(g_env, //动态注册原生方法
native_class, methods, NELEM(methods))){
LOGV("RegisterNatives() --> helloJNI() ok");
} else {
LOGE("RegisterNatives() --> helloJNI() failed");
return -1;}
return JNI_VERSION_1_6;
}
附录表1 JNI函数调用的节点和包含这些节点的边的信息
节点边缘
续表
节点边缘 (
附录表1中节点表示为
附录表2 样本应用中获取的原生库
包名原生库名包名原生库名 com.qihoo360.mobilesafelibnzdutil-jni-1.0.0.2002.socom.tencent.qqpimsecurelibNativeRQD.so com.tencent.mmlibmm_gl_disp.socom.baidu.homeworklibweibosdkcore.so com.baidu.inputlibbdinput_gif_v1_0_10.solibgaussblur_v1_0.socom.baidu.searchboxlibbitmaps.solibmemchunk.so com.tencent.mttlibbeso.solibbitmaps.solibblur_armv7.solibcmdsh.solibcommon_basemodule_jni.solibdaemon_lib.solibFdToFilePath.solibFileNDK.solibgif-jni.solibmemchunk.solibtencentpos.socom.tencent.qqmusiclibckey.solibdalvik_patch.solibdesdecrypt.solibexpress_verify.solibfilescanner.solibFormatDetector.solibframesequence.solibLPConvert.solibmApptracker4Dau.solibmresearch.solibNativeRQD.solibnetworkbase.solibqalmsfboot.solibqav_graphics.solibRandomUtilJni.solibtmfe30.solibweibosdkcore.solibwnsnetwork.so com.storm.smartliba.solibdaemon.solibgetuiext2.solibMMANDKSignature.solibmresearch.solibpl_droidsonroids_gif.solibpl_droidsonroids_gif_surface.solibstpinit.solibweibosdkcore.socom.UCMobilelibucinflator.so com.kugou.androidlibkgkey.solibkguo.solibLencryption.solibMMANDKSignature.solibrtmp.solibweibosdkcore.socom.sankuai.meituanlibdpencrypt.solibdpobj.solibnetworkbase.solibnh.solibPayRequestCrypt.solibRectifyCard.solibuploadnetwork.so com.baidu.BaiduMaplibcpu_features.solibgif.solibufosdk.solibweibosdkcore.socom.sina.weibolibamapv304ex.solibmemchunk.solibutility.solibweibosdkcore.so com.qiyi.videolibblur.solibdaemon.solibmediacodec.solibMMANDKSignature.solibmresearch.solibpl_droidsonroids_gif.solibrtmp.socom.pplive.androidphonelibbreakpad_util_jni.solibmeet.solibVideoSdkMd5.solibweibosdkcore.so
[1] Statista. number of apps available in leading app stores as of march 2017[EB/OL].https://www.statista.com/statistics/276623/number-of-apps-available-in-leading-app-stores/.
[2] SCHWARZ B, DEBRAY S, ANDREWS G. Disassembly of executable code revisited[C]//The Working Conference on Reverse Engineering. 2002:45-54.
[3] KRUEGEL C, ROBERTSON W, VALEUR F, et al. Static disassembly of obfuscated binaries[C]//Usenix Security Symposium. 2004: 255-270.
[4] REPS T, HORWITZ S, SAGIV M. Precise interprocedural dataflow analysis via graph reachability[C]//The 22nd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages. 1995: 49-61.
[5] HORWITZ S, REPS T, BINKLEY D. Interprocedural slicing using dependence graphs[J]. ACM Sigplan Notices, 2004, 23(7):35-46.
[6] BRUSCHI D, MARTIGNONI L, MONGA M. Detecting self-mutating malware using control-flow graph matching[C]//The International Conference on Detection of Intrusions and Malware and Vulnerability Assessment, 2006:129-143.
[7] CESARE S, XIANG Y. Malware variant detection using similarity search over sets of control flow graphs[C]//The International Conference on Trust, Security and Privacy in Computing and Communications. 2011:181-189.
[8] CIFUENTES C, VAN EMMERIK M. Recovery of jump table case statements from binary code[C]//TheInternational Workshop on Program Comprehension.1999: 192-199.
[9] MENG X, MILLER B P. Binary code is not easy[C]//The 25th International Symposium on Software Testing and Analysis. 2016: 24-35.
[10] SUTTER B D, BUS B D, BOSSCHERE K D, et al. On the Static Analysis of Indirect Control Transfers in Binaries[C]//The International Conference on Parallel & Distributed Processing Techniques & Applications. 2000:1013-1019.
[11] KINDER J, ZULEGER F, VEITH H. An abstract interpretation-based framework for control flow reconstruction from binaries[C]//The International Workshop on Verification, Model Checking, and Abstract Interpretation. 2009: 214-228.
[12] TROGER J, CIFUENTES C. Analysis of virtual method invocation for binary translation[C]//The Working Conference on Reverse Engineering. 2002: 65-74.
[13] JOHNSON R, STAVROU A. Forced-path execution for android applications on x86 platforms[C]//The International Conference on Software Security and Reliability Companion. 2013: 188-197.
[14] XU L, SUN F, SU Z. Constructing Precise Control Flow Graphs from Binaries[J]. University of California. 2012.
[15] ZHAO, J. Analyzing control flow in Java bytecode[C]//The 16th Conference of Japan Society for Software Science and Technology. 1999: 313-316.
[16] 胡刚, 张平, 李清宝,等. 基于静态模拟的二进制控制流恢复算法[J]. 计算机工程, 2011, 37(5): 276-278.
HU G, ZHANG P, LI Q B, el al. Control flow restoring algorithm for binary program based on static simulation[J]. Computer Enginerring, 2011, 37(5): 276-278.
[17] 张雁, 林英. 程序控制流图自动生成的算法[J]. 计算机与数字工程, 2010, 38(2):28-30.
ZHANG Y, LIN Y. Automatic generation algorithm of the control flow graph[J]. Computer & Digital Engineering, 2010, 38(2):28-30.
[18] ARZT S, RASTHOFER S, FRITZ C, et al. FlowDroid: precise context, flow, field, object-sensitive and lifecycle-aware taint analysis for android Apps[J]. ACM Sigplan Notices, 2014, 49(6): 259-269.
[19] LI L, BARTEL A, BISSYANDE T F, et al. Iccta: Detecting inter-component privacy leaks in android Apps[C]//The International Conference on Software Engineering. 2015: 280-291.
[20] WEI F, ROY S, OU X. Amandroid: a precise and general inter-component data flow analysis framework for security vetting of android apps[C]//The 2014 ACM SIGSAC Conference on Computer and Communications Security. 2014: 1329-1341.
[21] 辛纳(美). Android C++高级编程——使用NDK[M]. 北京: 清华大学出版社, 2013.
CINAR O. Pro Android C++ wih the NDK[M]. Beijing: Tsinghua University Press, 2013.
[22] Angr. Intermediate Representation[EB/OL]. https://docs.angr.io/ docs/ ir.html.
[23] NETHERCOTE N, SEWARD J. Valgrind: a framework for heavyweight dynamic binary instrumentation[J]. Acm Sigplan Notices, 2007, 42(6): 89-100.
[24] YAN S, WANG R, HAUSER C, et al. Firmalice-automatic detection of authentication bypass vulnerabilities in binary firmware[C]//The Network and Distributed System Security Symposium, 2015.
[25] SHOSHITAISHVILI Y, WANG R, SALLS C, et al. SOK: (State of) the art of war: offensive techniques in binary analysis[C]//Security and Privacy. 2016: 138-157.
Symbolic execution based control flow graph extraction method for Android native codes
YAN Hui-ying, ZHOU Zhen-ji, WU Li-fa, HONG Zheng, SUN He
(Institute of Command Information System, PLA University of Science and Technology, Nanjing 210000, China)
A symbolic execution based method was proposed to automatically extract control flow graphs from native libraries of Android applications. The proposed method can provide execution environments for functions in native libraries, simulate JNI function call processes and solve symbols using constraint solver. A control flow graph extraction prototype system named CFGNative was implemented. The experiment results show that CFGNative can accurately distinguish all the JNI function calls and native methods of the representative example, and reach high code coverage within acceptable time.
control flow graph, Android application, native code, symbolic execution
TP309
A
10.11959/j.issn.2096-109x.2017.00178
颜慧颖(1993-),女,江西吉安人,解放军理工大学硕士生,主要研究方向为软件安全。
周振吉(1985-),男,江苏沭阳人,博士,解放军理工大学讲师,主要研究方向为软件安全。
吴礼发(1968-),男,湖北蕲春人,博士,解放军理工大学教授、博士生导师,主要研究方向为网络安全。
洪征(1979-),男,江西南昌人,博士,解放军理工大学副教授、硕士生导师,主要研究方向为网络安全、人工智能。
孙贺(1990-),男,黑龙江齐齐哈尔人,解放军理工大学博士生,主要研究方向为软件逆向工程。
2017-05-07;
2017-06-09。
吴礼发,wulifa@vip.163.com
国家重点研发计划基金资助项目(No.2017YFB0802900);江苏省自然科学基金资助项目(No. BK20131069)
The National Key Research and Development Program of China (No.2017YFB0802900), The Natural Science Foundation of Jiangsu Province (No. BK20131069)