JNI函数的Hook与快速定位 示例应用的so文件加载时机是在一些按钮被点击后才加载,注意
第一步也是要先得到JNI函数的地址,有两种方式
枚举libart的符号表,得到对应的JNI函数地址后Hook
通过计算地址的方式来Hook,比较麻烦:先得到JNEnv结构体的地址,再通过偏移得到对应JNI函数指针的地址,最后通过指针得到JNI函数的地址。需要注意的是**32位(4字节)和 64位(8字节)**的指针长度不同会导致偏移地址不同
如果用C/C++开发so文件,JNIEnv*指针变量最终都指向JNINativePoniter结构体。
要获取JNIEnv*指针变量的内存地址,Frida中提供了相应的方法来获取,部分源码如下
1 2 3 4 5 6 7 8 9 declare namespace Java { ... const vm : VM ; interface VM { ... getEnv ():Env ; tryGetEnv ():Env | null ; } }
getEnv和tryGetEnv都可以返回Frida包装后的JNIEnv对象 ,如果没有找到,前者会抛出异常错误,后者会返回一个NULL
1 2 console .log ("env:" ,JSON .stringify (Java .vm .tryGetEnv ()));
handle属性记录的是原始JNIEnv* 指针变量的内存地址,所存放的就是JNIEnv结构体的地址。利用readPointer可以将改内存的数据转化为地址
所以说,获得JNIEnv结构体的地址的最直接的方式
1 Java .vm .tryGetEnv ().handle .readPointer ()
1 2 3 4 struct JNINativeInterface { ... jstring (*NewStringUTF )(JNIEnv * ,const char*); }
env.handle的类型是NativePointer,但env的类型不是,所以不能直接env.NativePointer。但可以通过Memory.readPointer(env)或者ptr(env).NativePointer()来达到目的,推荐使用后者
枚举libart符号表来Hook 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function hook_jni ( ) { var _symbols = Process .getModuleByName ("libart.so" ).enumerateSymbols (); var newStringUtf = null ; for (let i = 0 ; i < _symbols.length ; i++) { var _symbol = _symbols[i]; if (_symbol.name .indexOf ("CheckJNI" ) == -1 && _symbol.name .indexOf ("NewStringUTF" ) != -1 ){ newStringUtf = _symbol.address ; } } Interceptor .attach (newStringUtf, { onEnter : function (args ) { console .log ("newStringUtf args: " , args[1 ].readCString ()); }, onLeave : function (retval ) { console .log ("newStringUtf retval: " , retval); } }); } hook_jni ();
这些api在so层hook都讲过的,都能看懂,只是这过滤了一下包含CheckJNI的符号
再来介绍一下Process.pointerSize这个方法,返回当前进程的指针大小,这样就不用可以去区别32位和64位指针大小的区别了,也解决了因指针大小不一样而导致偏移量不一样的问题。
1 2 3 4 5 6 var envAddr = Java .vm .tryGetEnv ().handle .readPointer ();var NewStringUTF = envAddr.add (167 * Process .pointerSize );var NewStringUTFAddr = envAddr.add (167 * Process .pointerSize ).readPointer ();console .log (hexdump (NewStringUTF ));console .log (hexdump (NewStringUTFAddr ));console .log (Instruction .parse (NewStringUTFAddr ).toString ());
当得到了函数地址后,可以通过Frida的API,Instruction.parse(NewStringUTFAddr).toString()方法,可以将指令转化为汇编代码。
Hook代码
1 2 3 4 5 6 7 8 9 10 11 12 function hook_jni2 ( ) { var envAddr = Java .vm .tryGetEnv ().handle .readPointer (); var NewStringUTFAddr = envAddr.add (167 * Process .pointerSize ).readPointer (); Interceptor .attach (NewStringUTFAddr , { onEnter : function (args ) { console .log ("FindClass args: " , args[1 ].readCString ()); }, onLeave : function (retval ) { console .log ("FindClass retval: " , retval); } }); } hook_jni2 ();
利用Frida API主动调用JNI函数 前文提到过,通过Java.vm.tryGetEnv()就可以得到Frida包装过的JNIEnv对象,然后就可以接着通过Frida封装的API来调用JNI函数。
这里需要注意,JNI的函数命名采用大驼峰命名法,Frida封装的采用小驼峰命名法,并且UTF只有首字母大写。
举两个例子:
JNI-NewStringUTF;Frida-newStringUtf;
JNI-GetStringUTFChars;Frida-getStringUtfChars
其余的到下面的链接看
https://github.com/frida/frida-java-bridge
另外两者的参数也不同,JNI的参数第一个都是JNIEnv*,frida不需要传这个参数
so层文件打印函数栈 在Frida中可以通过Thread.backtrace来获取函数栈,看看源码
1 2 3 4 5 6 7 delcare namespace Thread { function backtrace (context?:CpuContext,backtracer?:Backtracer ):NativePointer []; function sleep (delay:number ):void ; }
backtrace用于获取函数栈,掺入两个可省略的参数,返回NativePointer类型的地址数组,但两个参数不推荐省略。
sleep接受一个数值用于挂起当前线程,并在多少秒后恢复
backtrace参数需要传入BackTracer类型
1 2 3 4 declare class BackTracer { static ACCURATE : Baktracer ; static FUZZY : Backtracer ; }
实际情况可以两个都试一下。
DebugSymbol.fromAddress。该方法用于获取对应地址的调试信息
其源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 declare class DebugSymbol { address : NativePointer ; name : string | null ; moduleName : string | null ; fileName : string | null ; lineNumber : number | null ; static fromAddress (address : NativePointerValue ): DebugSymbol ; static fromName (name : string):DebugSymbol ; static getFunctionByName (name : string): NativePointer ; static findFunctionsNamed (name : string): NativePointer []; static findFunctionsMatching (glod :string): NativePointer []; static load (path :string):void ; toString ():string; }
来个简单测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 var debsym = DebugSymbol .fromName ("strcat" );console .log ("address: " , debsym.address );console .log ("name: " , debsym.name );console .log ("moduleName: " , debsym.moduleName );console .log ("fileName: " , debsym.fileName );console .log ("lineNumber: " , debsym.lineNumber );console .log ("toString: " , debsym.toString ());console .log ("getFunctionByName: " , DebugSymbol .getFunctionByName ("strcat" ));console .log ("findFunctionsNamed: " , DebugSymbol .findFunctionsNamed ("JNI_OnLoad" ));console .log ("findFunctionsMatching: " , DebugSymbol .findFunctionsMatching ("JNI_OnLoad" ));
so层主动调用任意函数 首先介绍一个Frida的API
1 new NativeFunction (address,returnType,argTypes[,abi])
该API需要传入函数地址,返回值类型,参数类型数组和可省略的abi数组。
returnTypes和argTypes支持很多种类型: void,pointer,int,uint,(u)long,(u)char,float,double,int8,uint8,(u)int16,(u)int32,(u)int64,bool,size_t,ssize_t
其实传给new NativeFunction的返回值和参数类型不用非常准确也是可以调用函数的
1 2 3 4 5 6 7 8 9 10 11 12 Java .perform (function ( ) { var soAddr = Module .findBaseAddress ("libxiaojianbang.so" ); var funAddr = soAddr.add (0x16BC ); var jstr2cstr = new NativeFunction (funAddr, 'pointer' , ['pointer' ,'pointer' ]); var env = Java .vm .tryGetEnv (); var jstring = env.newStringUtf ("xiaojianbang" ); var retval = jstr2cstr (env.handle , jstring); console .log (retval.readCString ()); });
[,abi]的个数为除了JNI特有的参数外(如JNIEnv*,jclass/jobject),函数本身有的参数加一个返回值
通过NativeFunction主动调用JNI函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var symbols = Process .getModuleByName ("libart.so" ).enumerateSymbols ();var NewStringUTFAddr = null ;var GetStringUTFCharsAddr = null ;for (var i = 0 ; i < symbols.length ; i++) { var symbol = symbols[i]; if (symbol.name .indexOf ("CheckJNI" ) == -1 && symbol.name .indexOf ("NewStringUTF" ) != -1 ){ NewStringUTFAddr = symbol.address ; }else if (symbol.name .indexOf ("CheckJNI" ) == -1 && symbol.name .indexOf ("GetStringUTFChars" ) != -1 ){ GetStringUTFCharsAddr = symbol.address ; } } var NewStringUTF = new NativeFunction (NewStringUTFAddr , 'pointer' , ['pointer' , 'pointer' ]);var GetStringUTFChars = new NativeFunction (GetStringUTFCharsAddr , 'pointer' , ['pointer' , 'pointer' , 'pointer' ]);var jstring = NewStringUTF (Java .vm .tryGetEnv ().handle , Memory .allocUtf8String ("xiaojianbang" ));console .log (jstring);var cstr = GetStringUTFChars (Java .vm .tryGetEnv (), jstring, ptr (0 ));console .log (cstr.readCString ());
查看上面两个函数NewStringUTF;GetStringUTFChars在jni中的声明
1 2 3 4 5 struct JNINativeInterface { ... jstring (*NewStringUTF )(JNIEnv *,const char*); const char* (*getStringUTFChars)(JNIEnv *, jstring,jboolean*); };
在C语言中,jboolean可以用一个字节的数值代替,故相关代码也可改为以下形式
1 2 3 var cstr = GetStringUTFChars (Java .vm .tryGetEnv (), jstring, Memory .alloc (1 ).writeS8 (1 ));console .log (cstr.readCString ());
而且在使用new NativeFunction声明函数指针时,某些参数与jni.h中不一致也是可以调用的,如GetStringUTFChars,可以只传两个参数
1 2 3 4 5 var GetStringUTFChars = new NativeFunction (GetStringUTFCharsAddr , 'pointer' , ['pointer' , 'pointer' ]);var cstr = GetStringUTFChars (Java .vm .tryGetEnv (), jstring);console .log (cstr.readCString ());
JNI函数注册的快速定位
JNI静态注册可以Hook dlsym 静态注册的方式在一开始并没有绑定so层的函数,当Java的native函数首次被调用,系统会按规则构建出对应的函数名,通过dlsym去每一个so文件中寻找符号,找到后进行绑定。
JNI函数动态注册可以Hook RegisterNatives,这个函数就是用来动态注册的,不必多说
Hook其他关键的JNI函数,如NewStringUTF,再打印函数栈。当然也可以试试jnitrace
Hook dlsym dlsym的声明
1 2 void * dlsym (void * handle,const char* symbol);
handle时使用dlopen函数之后返回的句柄,symbol是要求获取函数的名称,返回值是void* 指向函数的地址。具体看
dlsym()
1 2 3 4 5 6 7 8 9 10 11 12 13 var dlsymAddr = Module .findExportByName ("libdl.so" , "dlsym" );Interceptor .attach (dlsymAddr, { onEnter : function (args ) { this .args1 = args[1 ]; }, onLeave : function (retval ) { var module = Process .findModuleByAddress (retval); if (module == null ) return ; console .log (this .args1 .readCString (), module .name , retval, retval.sub (module .base )); } });
经过测试发现,上面那两条信息只能输出一次,就是在那两个按钮在被初次点击的时候。因为静态注册的native函数首次被调用才会经过dlsym函数,之后就不再触发。
而在jni动态注册中,在系统加载完so文件后,才去获取JNI_Onload函数地址,使用的也是dlsym
Hook RegisterNatives获取函数地址 RegisterNatives定义在libart.so中,可以通过枚举符号来获取地址。
1 2 3 4 5 6 7 8 9 var RegisterNativesAddr = null ;var _symbols = Process .findModuleByName ("libart.so" ).enumerateSymbols ();for (var i = 0 ; i < _symbols.length ; i++) { var _symbol = _symbols[i]; if (_symbol.name .indexOf ("CheckJNI" ) == -1 && _symbol.name .indexOf ("RegisterNatives" ) != -1 ) { RegisterNativesAddr = _symbols[i].address ; } } console .log (RegisterNativesAddr );
RegisterNatives在jni.h,我觉得已经不用再看了,就是一个结构体,其成员:第0个是在函数java层的名字,第二个是签名,第三个是函数在cpp文件中的指针一般为(void )开头,参数的话有四个JNIEnv * ,jcalss,JNINativeMethod ,jint
Hook代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 var RegisterNativesAddr = null ;var _symbols = Process .findModuleByName ("libart.so" ).enumerateSymbols ();for (var i = 0 ; i < _symbols.length ; i++) { var _symbol = _symbols[i]; if (_symbol.name .indexOf ("CheckJNI" ) == -1 && _symbol.name .indexOf ("RegisterNatives" ) != -1 ) { RegisterNativesAddr = _symbols[i].address ; } } Interceptor .attach (RegisterNativesAddr , { onEnter : function (args ) { var env = Java .vm .tryGetEnv (); var className = env.getClassName (args[1 ]); var methodCount = args[3 ].toInt32 (); for (let i = 0 ; i < methodCount; i++) { var methodName = args[2 ].add (Process .pointerSize * 3 * i) .readPointer ().readCString (); var signature = args[2 ].add (Process .pointerSize * 3 * i) .add (Process .pointerSize ).readPointer ().readCString (); var fnPtr = args[2 ].add (Process .pointerSize * 3 * i) .add (Process .pointerSize * 2 ).readPointer (); var module = Process .findModuleByAddress (fnPtr); console .log (className, methodName, signature, fnPtr, module .name , fnPtr.sub (module .base )); } }, onLeave : function (retval ) { } });
这里JNINativeMethod是一个结构体数组,每个成员占3个指针长度,在不知道有多少个成员的情况下,使用循环进行遍历,没循环一次,再偏移3个指针长度。
jnitrace使用 1 jnitrace -m attach - xxxx.so com.xxxxx.xxxxx //包名以frida-ps -U显示的为优先
-l xxx.so用于指定要跟踪的库,此参数可多次使用,或用*来跟踪所有库
e.g:-l libnativ-lib.so -l libanother-lib.so 或-l *
貌似遇到有壳的就不弹信息了。