2017-09-29 11:55:51
阅读:3923次
点赞(0)
收藏
来源: 安全客
作者:非虫
作者:非虫
预估稿费:600RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
简介
长期从事Android SO动态库分析时,时常会做一些重复性较高的工作。例如,SO库中的Java_com_xxx_yyy()等一系统与Java层桥接的方法,逆向它们时,通常需要做如下工作:
IDA Pro载入SO,完成第一次的反编译。
导入jni.h头文件,引入JNINativeInterface与JNIInvokeInterface结构体信息。
设置Java_com_xxx_yyy()类型方法的前两个参数为JNIEnv* env与jobject thiz。
如果有F5插件,则进行一次Force call type。
......。
将这些工作自动化,可以大大的提高逆向分析的工作效率。基于IDA Pro提供的脚本与插件系统,可以很方便的完成以上前3项工作。下面,我们一步步来打造一个SO自动化逆向分析的工具。
目标细化
在开始完成一个工具前,需要将这些需要解决的问题进行一次量化分析。
首先,如何定位需要处理的SO库方法?由于Java_com_xxx_yyy()类型方法与Java层进行桥接,在java层代码中必定会有它的声明。所有的这些方法在Java代码中会有一个native属性,只需要遍历Java层的代码,获取所有的native方法即可。
其次,不同的方法有不同的参数类型,签名的不同,该如何处理?为了让工具实现起来过于复杂,我们只处理Java中内置的数据类型,自定义的数据类型统一使用jobject进行处理与表示。
最后,就是将获取到的Java层的所有native方法信息与IDA Pro中的相应的方法进行一一的对应,并进行方法的自动化类型处理,这就需要用到IDA Pro的脚本功能。
功能实现
明确了以上的3个步骤后,下面来动手一一的完成它。
解析native方法
为了快速的解析native方法,我最先想到的是使用grep命令(系统为macOS)。首先,使用JD-GUI反编译APK,导出所有的Java源文件,然后在命令行下执行:
$grep'native'-r./java_dir-hpublicnativeStringstringFromJNI();或者执行如下命令:
$grep-Eo'^(|public|private|protected).*native.*;'-r./java_dir-hpublicnativeStringstringFromJNI();不错,都能够正确获取到native方法,虽然输出的前面会有一个JD-GUI反编译带的空格。
为了让windows的用户,即使在不安装Mingw或其他的linux模拟环境的情况下,也能够正确的获取到方法,我还是决定使用python来写一个生成方法签名信息的脚本。就即名叫make_sig.py好了。
Python的便捷,让我可以很方便地在命令行下测试re模块的正则表达式,如下:
$python Python2.7.10(default,Feb72017,00:08:15) [GCC4.2.1CompatibleAppleLLVM8.0.0(clang-800.0.34)]ondarwin Type"help","copyright","credits"or"license"formoreinformation. >>>importre >>>l="publicstaticnativelongnativeLoadMaster(StringparamString,byte[]paramArrayOfByte1,String[]paramArrayOfString,byte[]paramArrayOfByte2);" >>>rr=re.match('^(|public|private|protected).*native(.*)(.*)[(](.*)[)];',l) >>>print"{}".format(rr.group(0)) publicstaticnativelongnativeLoadMaster(StringparamString,byte[]paramArrayOfByte1,String[]paramArrayOfString,byte[]paramArrayOfByte2); >>>print"{}".format(rr.group(1)) >>>print"{}".format(rr.group(2)) long >>>print"{}".format(rr.group(3)) nativeLoadMaster >>>print"{}".format(rr.group(4)) StringparamString,byte[]paramArrayOfByte1,String[]paramArrayOfString,byte[]paramArrayOfByte2OK,正则表达式弄对了!可以正确的解析一条native方法的所有信息:返回值、方法名、签名。我这里不打算展开如何编写正则表达式,因为我觉得很多人应该会了,如果你对于正则表达式不太熟,建议你到这个链接快速的学习一下:https://github.com/zeeshanu/learn-regex 。
下面的代码片断是解析一个目录下所有的文件,找到native方法并保存到指定的文件中:
defmake_sig_file(java_src_dir,sig_file): f=file(sig_file,'w+') forparent,dirnames,filenamesinos.walk(java_src_dir): forfilenameinfilenames: #print"file:"+os.path.join(parent,filename) filepath=os.path.join(parent,filename) withopen(filepath)aso: content=o.read() forminre.finditer('(|public|private|protected).*native(.*)(.*)[(](.*)[)];',content): rr=re.match('package(.*?);.*?class([^\s]+)',content,re.S) pkg_name=rr.group(1) class_name=rr.group(2) func_name=m.group(3) print'func_name:',func_name print'pkg_name:',pkg_name print'class_name:',class_name full_func_name='Java_'+pkg_name+'_'+class_name+'_'+func_name full_func_name=full_func_name.replace('.','_') #print'full_func_name:',full_func_name full_method_sig=m.group(0) full_method_sig=full_method_sig.replace(func_name,full_func_name).strip() #printfull_method_sig f.write(full_method_sig+'\n') f.close()这段代码不需要太多的解释,os.walk会遍历一个目录中所有文件信息,对于目录中的第一个文件,使用open打开后,调用re.finditer来匹配native方法,打到就把它写入到sig_file指定的文件名中。
更多的代码参看makesig.py的文件内容,对于很多人,你只需要知道执行
make_sig.pyxxx_outmethod_sig.txt就可以生成methodsig.txt方法签名文件了。xxx_out为JD-GUI导出的APK的Java源码目录。
Java数据类型处理
好了,到现在已经取到了所有的native方法信息,现在需要对这些方法的签名进行处理。
所有的native方法支持的数据类型在jni.h头文件中都有定义,该文件可以在Android NDK任意系统版本的include目录下找到。在文件的开头就有这么一段:
typedefuint8_tjboolean;/*unsigned8bits*/ typedefint8_tjbyte;/*signed8bits*/ typedefuint16_tjchar;/*unsigned16bits*/ typedefint16_tjshort;/*signed16bits*/ typedefint32_tjint;/*signed32bits*/ typedefint64_tjlong;/*signed64bits*/ typedeffloatjfloat;/*32-bitIEEE754*/ typedefdoublejdouble;/*64-bitIEEE754*/既然最后的处理代码使用Python来写,咱也不含糊,先弄一个jni_types如下:
jni_types={ 'boolean':'jboolean', 'byte':'jbyte', 'char':'jchar', 'short':'jshort', 'int':'jint', 'long':'jlong', 'float':'jfloat', 'double':'jdouble', 'string':'jstring', 'object':'jobject', 'void':'void' }然后,写一个Java层方法类型转换成JNI类型的方法,代码如下:
defget_jnitype(java_type): postfix='' jtype=java_type.lower() ifjtype.endswith('[]'): postfix='Array' jtype=jtype[:-2] tp='' ifjtypenotinjni_types: tp='jobject' else: tp=jni_types[jtype]+postfix returntp小小的测试一下:
deftest_jnitype(): printget_jnitype('int') printget_jnitype('Int') printget_jnitype('long') printget_jnitype('Long') printget_jnitype('void') printget_jnitype('String') printget_jnitype('String[]') printget_jnitype('boolean') printget_jnitype('ArrayList<String>') printget_jnitype('Object[]') printget_jnitype('byte[]') printget_jnitype('FileEntry')输出如下:
jint jint jlong jlong void jstring jstringArray jboolean jobject jobjectArray jbyteArray jobject稳!单个方法的签名解析没问题了,那将整个方法的类型转化为JNI接受的类型也没多大问题,代码如下:
defget_args_type(java_args): iflen(java_args)==0: return'JNIEnv*env,jobjectthiz' jargs=java_args.lower() args=jargs.split(',') #print'argcount:',len(args) full_arg='JNIEnv*env,jobjectthiz,' i=1 forjava_arginargs: java_type=java_arg.split('')[0] full_arg+=get_jnitype(java_type) full_arg+='arg' full_arg+=str(i) full_arg+=',' i+=1 returnfull_arg[:-2]最后是编写get_jni_sig方法,实现一个Java的native方法签名转成IDA Pro能接受的签名信息。具体看代码,这里就不占篇幅了。
自动化设置方法信息
前两步没问题,到这里问题就不大了。下面是写IDAPython代码,来完成一个jni_helper.py脚本工具。
首先是IDA Pro分析SO时候,并不会自动的导入JNINativeInterface与JNIInvokeInterface结构体信息。这就需要自己来完成了。
JNINativeInterface的方法字段忒多,我不打算自己手写,容易出错还效率低下。我使用IDA Pro的导出功能,点击菜单File->Produce File->DUMP typeinfo to IDC file...,然后一个idc文件,然后复制IDC中的内容,简单修改就完成了add_jni_struct()方法,代码如下:
defadd_jni_struct(): ifBADADDR==GetStrucIdByName("JNINativeInterface"): AddStrucEx(-1,"JNINativeInterface",0) id=GetStrucIdByName("JNINativeInterface") AddStrucMember(id,"reserved0",0,0x25500400,0XFFFFFFFF,4,0XFFFFFFFF,0,0x000002) AddStrucMember(id,"reserved1",0X4,0x25500400,0XFFFFFFFF,4,0XFFFFFFFF,0,0x000002) ...... AddStrucMember(id,"GetDirectBufferAddress",0X398,0x25500400,0XFFFFFFFF,4,0XFFFFFFFF,0,0x000002) AddStrucMember(id,"GetDirectBufferCapacity",0X39C,0x25500400,0XFFFFFFFF,4,0XFFFFFFFF,0,0x000002) #SetStrucAlign(id,2) idc.Eval('SetStrucAlign({},2);'.format(id)) ifBADADDR==GetStrucIdByName("JNIInvokeInterface"): AddStrucEx(-1,"JNIInvokeInterface",0) id=GetStrucIdByName("JNIInvokeInterface") AddStrucMember(id,"reserved0",0,0x25500400,0XFFFFFFFF,4,0XFFFFFFFF,0,0x000002) AddStrucMember(id,"reserved1",0X4,0x25500400,0XFFFFFFFF,4,0XFFFFFFFF,0,0x000002) AddStrucMember(id,"reserved2",0X8,0x25500400,0XFFFFFFFF,4,0XFFFFFFFF,0,0x000002) AddStrucMember(id,"DestroyJavaVM",0XC,0x25500400,0XFFFFFFFF,4,0XFFFFFFFF,0,0x000002) AddStrucMember(id,"AttachCurrentThread",0X10,0x25500400,0XFFFFFFFF,4,0XFFFFFFFF,0,0x000002) AddStrucMember(id,"DetachCurrentThread",0X14,0x25500400,0XFFFFFFFF,4,0XFFFFFFFF,0,0x000002) AddStrucMember(id,"GetEnv",0X18,0x25500400,0XFFFFFFFF,4,0XFFFFFFFF,0,0x000002) AddStrucMember(id,"AttachCurrentThreadAsDaemon",0X1C,0x25500400,0XFFFFFFFF,4,0XFFFFFFFF,0,0x000002) #SetStrucAlign(id,2) idc.Eval('SetStrucAlign({},2);'.format(id)) #idaapi.run_statements('autoid;id=GetStrucIdByName("JNIInvokeInterface");SetStrucAlign(id,2);')比较有趣的是,在IDC中有这么一句:
SetStrucAlign(id,2)这个SetStrucAlign()方法在IDAPython中并没有,要想调用它,可以使用如下方法:
idc.Eval('SetStrucAlign({},2);'.format(id))导入完成后,下一步的工作是获取SO中所有的Java_com_xxx_yyy()类型的方法信息,这个好办,代码如下:
addr=get_code_seg() symbols=[] forfunceainFunctions(SegStart(addr)): functionName=GetFunctionName(funcea) symbols.append((functionName,funcea))symbols现在存放了所有的方法,只需要判断是否以“Java_”开头就能区分native方法了。
接着是读取前面生成的方法签名文件,读取它的所有方法签名信息,这里我使用如下方法:
sig_file=AskFile(0,'*.*','opensigfile')AskFile()方法会弹出一个文件选择框来让用户选择生成的文件,我觉得这种交互比直接写死文件路径要优雅,虽然这里会让你参与进来,可能会使你烦燥。
我们传入获取到的第一条Java方法签名给上一步的get_jni_sig()方法,来生成对应的JNI方法签名。最后,调用SetType()来设置它的方法签名信息。
至此,所有的工作都完成了。完整的工程见:https://github.com/feicong/jni_helper 。
本文由 安全客 原创发布,如需转载请注明来源及本文地址。
本文地址:http://bobao.360.cn/learning/detail/4489.html