Android SO静态库自动化逆向分析。历久从事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 -h public native String stringFromJNI();
或许履行以下敕令:
$ grep -Eo '^( |public|private|protected).* native .*;' -r ./java_dir -h public native String stringFromJNI();
不错,都可以或许准确获得到native办法,固然输入的后面会有一个JD-GUI反编译带的空格。
为了让Windows的用户,即使在不装置Mingw或其他的Linux模仿情况的情况下,也可以或许准确的获得到办法,我照样决议应用Python来写一个天生办法署名信息的剧本。就即名叫make_sig.py好了。
Python的便捷,让我可以或许很便利地在敕令行下测试re模块的正则表达式,以下:
$ python
Python 2.7.10 (default, Feb 7 2017, 00:08:15)
[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.34)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import re
>>> l = " public static native long nativeLoadMaster(String paramString, byte[] paramArrayOfByte1, String[] paramArrayOfString, byte[] paramArrayOfByte2);"
>>> rr = re.match('^( |public|private|protected).* native (.*) (.*)[(](.*)[)];', l)
>>> print "{}".format(rr.group(0))
public static native long nativeLoadMaster(String paramString, 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))
String paramString, byte[] paramArrayOfByte1, String[] paramArrayOfString, byte[] paramArrayOfByte2
OK,正则表达式弄对了!可以或许准确的剖析一条native办法的一切信息:前往值、办法名、署名。我这里不盘算睁开若何编写正则表达式,因为我感到很多人应当会了,假如你对付正则表达式不太熟,倡议你到这个链接疾速的进修一下:https://github.com/zeeshanu/learn-regex 。
上面的代码片段是剖析一个目次下一切的文件,找到native办法并保留到指定的文件中:
def make_sig_file(java_src_dir, sig_file):
f = file(sig_file, 'w+')
for parent, dirnames, filenames in os.walk(java_src_dir):
for filename in filenames:
#print "file: " + os.path.join(parent, filename)
filepath = os.path.join(parent, filename)
with open(filepath) as o:
content = o.read()
for m in re.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()
#print full_method_sig
f.write(full_method_sig + '\n')
f.close()
这段代码不必要太多的说明,os.walk会遍历一个目次中一切文件信息,对付目次中的第一个文件,应用open关上后,挪用re.finditer来婚配native办法,打到就把它写入到sig_file指定的文件名中。
更多的代码参看makesig.py的文件内容,对付很多人,你只必要晓得履行
make_sig.py xxx_out method_sig.txt
就能够或许天生methodsig.txt办法署名文件了。xxx_out为JD-GUI导出的APK的Java源码目次。
Java数据范例处置
好了,到如今曾经取到了一切的native办法信息,如今必要对这些办法的署名停止处置。
一切的native办法支撑的数据范例在jni.h头文件中都有界说,该文件可以或许在Android NDK随意率性体系版本的include目次下找到。在文件的开首就有这么一段:
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
既然末了的处置代码应用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范例的办法,代码以下:
def get_jnitype(java_type):
postfix = ''
jtype = java_type.lower()
if jtype.endswith('[]'):
postfix = 'Array'
jtype = jtype[:-2]
tp = ''
if jtype not in jni_types:
tp = 'jobject'
else:
tp = jni_types[jtype] + postfix
return tp
小小的测试一下:
def test_jnitype():
print get_jnitype('int')
print get_jnitype('Int')
print get_jnitype('long')
print get_jnitype('Long')
print get_jnitype('void')
print get_jnitype('String')
print get_jnitype('String[]')
print get_jnitype('boolean')
print get_jnitype('ArrayList')
print get_jnitype('Object[]')
print get_jnitype('byte[]')
print get_jnitype('FileEntry')
输入以下:
jint
jint
jlong
jlong
void
jstring
jstringArray
jboolean
jobject
jobjectArray
jbyteArray
jobject
稳!单个办法的署名剖析没成绩了,那将全部办法的范例转化为JNI接收的范例也没多大成绩,代码以下:
def get_args_type(java_args):
if len(java_args) == 0:
return 'JNIEnv* env, jobject thiz'
jargs = java_args.lower()
args = jargs.split(', ')
#print 'arg count:', len(args)
full_arg = 'JNIEnv* env, jobject thiz, '
i = 1
for java_arg in args:
java_type = java_arg.split(' ')[0]
full_arg += get_jnitype(java_type)
full_arg += ' arg'
full_arg += str(i)
full_arg += ', '
i += 1
return full_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()办法,代码以下:
def add_jni_struct():
if BADADDR == 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))
if BADADDR == 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('auto id; 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 = []
for funcea in Functions(SegStart(addr)):
functionName = GetFunctionName(funcea)
symbols.append((functionName, funcea))
symbols如今存放了一切的办法,只必要断定能否以“Java_”开首就能够辨别native办法了。
接着是读取后面天生的办法署名文件,读取它的一切办法署名信息,这里我应用以下办法:
sig_file = AskFile(0, '*.*', 'open sig file')
AskFile()办法会弹出一个文件抉择框来让用户抉择天生的文件,我感到这类交互比间接写死文件门路要优雅,固然这里会让你介入出去,能够会使你烦燥。
咱们传入获得到的第一条Java办法署名给上一步的get_jni_sig()办法,来天生对应的JNI办法署名。末了,挪用SetType()来设置它的办法署名信息。