Frida so层进阶应用 示例应用的so文件加载时机是在一些按钮被点击后才加载,注意
内存读写 内存页是分权限的,权限通常用rwx来表示,r-可读,w-可写,x-可执行
报错acess violation即为非法访问
先来看看Memory的protect方法的源码
1 2 3 4 5 type PageProtection = string; declare namespace Memory { function protect (address: NativePointerValue, size: number | UInt64, protection: PageProtection ): boolean;...... }
对指定内存修改权限
1 2 var soAddr = Module .findBaseAddress ("libxiaojianbang.so" );Memory .protect (soAddr.add (0x3DED ), 16 , 'rwx' );
读取指定地址的字符串
1 2 var soAddr = Module .findBaseAddress ("libxiaojianbang.so" );console .log (soAddr.add (0x3DED ).readCString ());
导出指定地址的内存数据
1 2 var soAddr = Module .findBaseAddress ("libxiaojianbang.so" );console .log (hexdump (soAddr.add (0x3DED )));
读取指定地址的内存数据
1 2 var soAddr = Module .findBaseAddress ("libxiaojianbang.so" );console .log (soAddr.add (0x3DED ).readByteArray (16 ));
写入数据到指定内存地址
在计算出指定的内存地址后,可以使用NativePointer的writeByteArray方法,从该地址往后写入数据,源码声明如下
1 2 3 declare class NativePointer { writeByteArray (value : ArrayBuffer | number[]): NativePointer ; }
writeByteArray接受一个参数,ArrayBuffer或者是数值数组(想要写入的字节数组)。先来几个转换进制的工具函数。这些就单独放吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function stringToBytes (str ){ return hexToBytes (stringToHex (str)); } function stringToHex (str ) { return str.split ("" ).map (function (c ) { return ("0" + c.charCodeAt (0 ).toString (16 )).slice (-2 ); }).join ("" ); } function hexToBytes (hex ) { for (var bytes = [], c = 0 ; c < hex.length ; c += 2 ) bytes.push (parseInt (hex.substr (c, 2 ), 16 )); return bytes; } function hexToString (hexStr ) { var hex = hexStr.toString (); var str = '' ; for (var i = 0 ; i < hex.length ; i += 2 ) str += String .fromCharCode (parseInt (hex.substr (i, 2 ), 16 )); return str; }
写入字符串(跟上面的一起食用)
1 2 3 4 var soAddr = Module .findBaseAddress ("libxiaojianbang.so" );var tmpAddr = soAddr.add (0x3DED );Memory .protect (tmpAddr, 16 , 'rwx' );console .log (hexdump ( tmpAddr.writeByteArray (stringToBytes ("xiaojianbang\0" )) ));
分配内存
Memory方法的alloc的源码声明如下
alloc会在Frida私有堆上分配指定大小的内存,返回NativePointer类型的首地址,接着用NativePointer类的writeByteArray方法写入内存即可
1 2 3 4 declare namespace Memory { function alloc(size: number | UInt64, options?: MemoryAllocOptions): NativePointer; function allocUtf8String(str: string): NativePointer; }
e.g.:
1 2 3 var addr = Memory .alloc (8 );addr.writeByteArray (hexToBytes ("eeeeeeeeeeeeeeee" )); console .log (addr.readByteArray (8 ));
Frida修改so函数代码 首先是Instruction的parse方法,可以将16进制转化为汇编代码,该方法参数仅有一个,即NativePointer类型的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var funcAddr = Module .findBaseAddress ("libxiaojianbang.so" ).add (0x1ACC );var asm = Instruction .parse (funcAddr);console .log (asm);for (var i = 0 ; i < 3 ; i++) { asm = Instruction .parse (asm.next ); console .log (asm); }
然后
opcode与arm汇编的转换网址
https://armconverter.com/
修改代码需要写入的是opcode,并且需要用到上面的向指定内存中地址写入数据的方式。
1 2 3 4 var soAddr = Module .findBaseAddress ("libxiaojianbang.so" );soAddr.add (0x1AF4 ).writeByteArray (hexToBytes ("0001094B" )); console .log (Instruction .parse (soAddr.add (0x1AF4 )));
另一种方式是利用frida的api-ArmWriter
1 2 3 4 var soAddr = Module .findBaseAddress ("libxiaojianbang.so" );new Arm64Writer (soAddr.add (0x1AEC )).putNop ();console .log (Instruction .parse (soAddr.add (0x1AEC )).toString ());
再或者说是patchCode,它的第0个参数是修改的其实地址,第一个参数是要修改的字节数,第二个参数是回调函数,当函数执行到起始地址是,会调用回调函数
1 2 3 4 5 6 7 8 var codeAddr = Module .findBaseAddress ("libxiaojianbang.so" ).add (0x1AF4 );Memory .patchCode (codeAddr, 4 , function (code ) { var writer = new Arm64Writer (code, { pc : codeAddr }); writer.putBytes (hexToBytes ("0001094B" )); writer.flush (); }); console .log (Instruction .parse (codeAddr));
Frida在进行so层hook是,需要修改函数的前16个字节,这也是函数被Hook的检测点之一
示例如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function hook_func ( ) { var soAddr = Module .findBaseAddress ("libxiaojianbang.so" ); var MD5Final = soAddr.add (0x3A78 ); console .log (hexdump (MD5Final .readByteArray (20 ))); Interceptor .attach (MD5Final , { onEnter : function (args ) { console .log (hexdump (MD5Final .readByteArray (20 ))); }, onLeave : function (retval ) { } }); } hook_func ();
Frida从内存中导出so函数 通过传入模块名,找到对应的Module,得到模块基址,模块大小等必要信息,然后保存生成路径,可能由于目标APP的权限影响,最好把路径设在APP本身的私有目录,防止保存失败。然后用Memory.protect修改内存权限,再使用NativePointer的readByteArray方法读取整个so文件对应的内存数据。最后使用Frida的API将文件写出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function dump_so (so_name ) { Java .perform (function ( ) { var module = Process .getModuleByName (so_name); console .log ("[name]:" , module .name ); console .log ("[base]:" , module .base ); console .log ("[size]:" , module .size ); console .log ("[path]:" , module .path ); var currentApplication = Java .use ("android.app.ActivityThread" ).currentApplication (); var dir = currentApplication.getApplicationContext ().getFilesDir ().getPath (); var path = dir + "/" + module .name + "_" + module .base + "_" + module .size + ".so" ; var file = new File (path, "wb" ); if (file) { Memory .protect (module .base , module .size , 'rwx' ); var buffer = module .base .readByteArray (module .size ); file.write (buffer); file.flush (); file.close (); console .log ("[dump]:" , path); } }); } dump_so ("libxiaojianbang.so" );
Frida dump dex 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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 function get_self_process_name ( ) { var openPtr = Module .getExportByName ('libc.so' , 'open' ); var open = new NativeFunction (openPtr, 'int' , ['pointer' , 'int' ]); var readPtr = Module .getExportByName ("libc.so" , "read" ); var read = new NativeFunction (readPtr, "int" , ["int" , "pointer" , "int" ]); var closePtr = Module .getExportByName ('libc.so' , 'close' ); var close = new NativeFunction (closePtr, 'int' , ['int' ]); var path = Memory .allocUtf8String ("/proc/self/cmdline" ); var fd = open (path, 0 ); if (fd != -1 ) { var buffer = Memory .alloc (0x1000 ); var result = read (fd, buffer, 0x1000 ); close (fd); result = ptr (buffer).readCString (); return result; } return "-1" ; } function dump_dex ( ) { var libart = Process .findModuleByName ("libart.so" ); var addr_DefineClass = null ; var symbols = libart.enumerateSymbols (); for (var index = 0 ; index < symbols.length ; index++) { var symbol = symbols[index]; var symbol_name = symbol.name ; if (symbol_name.indexOf ("ClassLinker" ) >= 0 && symbol_name.indexOf ("DefineClass" ) >= 0 && symbol_name.indexOf ("Thread" ) >= 0 && symbol_name.indexOf ("DexFile" ) >= 0 ) { console .log (symbol_name, symbol.address ); addr_DefineClass = symbol.address ; } } var dex_maps = {}; console .log ("[DefineClass:]" , addr_DefineClass); if (addr_DefineClass) { Interceptor .attach (addr_DefineClass, { onEnter : function (args ) { var dex_file = args[5 ]; var base = ptr (dex_file).add (Process .pointerSize ).readPointer (); var size = ptr (dex_file).add (Process .pointerSize + Process .pointerSize ).readUInt (); if (dex_maps[base] == undefined ) { dex_maps[base] = size; var magic = ptr (base).readCString (); if (magic.indexOf ("dex" ) == 0 ) { var process_name = get_self_process_name (); if (process_name != "-1" ) { var dex_path = "/data/data/" + process_name + "/files/" + base.toString (16 ) + "_" + size.toString (16 ) + ".dex" ; console .log ("[find dex]:" , dex_path); var fd = new File (dex_path, "wb" ); if (fd && fd != null ) { var dex_buffer = ptr (base).readByteArray (size); fd.write (dex_buffer); fd.flush (); fd.close (); console .log ("[dump dex]:" , dex_path); } } } } }, onLeave : function (retval ) { } }); } } var is_hook_libart = false ;function hook_dlopen ( ) { Interceptor .attach (Module .findExportByName (null , "dlopen" ), { onEnter : function (args ) { var pathptr = args[0 ]; if (pathptr !== undefined && pathptr != null ) { var path = ptr (pathptr).readCString (); if (path.indexOf ("libart.so" ) >= 0 ) { this .can_hook_libart = true ; console .log ("[dlopen:]" , path); } } }, onLeave : function (retval ) { if (this .can_hook_libart && !is_hook_libart) { dump_dex (); is_hook_libart = true ; } } }) Interceptor .attach (Module .findExportByName (null , "android_dlopen_ext" ), { onEnter : function (args ) { var pathptr = args[0 ]; if (pathptr !== undefined && pathptr != null ) { var path = ptr (pathptr).readCString (); if (path.indexOf ("libart.so" ) >= 0 ) { this .can_hook_libart = true ; console .log ("[android_dlopen_ext:]" , path); } } }, onLeave : function (retval ) { if (this .can_hook_libart && !is_hook_libart) { dump_dex (); is_hook_libart = true ; } } }); } setImmediate (hook_dlopen);
加密的字符串解密 这个emmm
直接打印内存中的字符串,以某一偏移地址为例
1 2 3 4 5 var soAddr = Module .findBaseAddress ("liblogin_encrypt.so" );console .log (soAddr.add (0xD060 ).readCString ());
使用jnitrace
这种的话感觉不太灵光,参照前面写的
从内存中导出so。需要注意的是,导出的so是需要经过修复的,这种方法脱壳也经常用。具体方式参考上面的。
构造二级指针 这块主要用到new NativeFunction声明函数指针
xiugaiStr函数在so文件的偏移地址为0x1B00,主动调用它,需要先得到它的地址,然后用new NativeFunction声明函数指针;再接着构建实参,使用Memory的allocUtf8String方法构建一个字符串,也就是char* 类型,将返回的地址保存在strAddr中,再将strAddr存入到指针变量中,就完成了二级指针的构建。最后调用函数直接传入二级指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 var soAddr = Module .findBaseAddress ("liblogin_encrypt.so" );console .log (soAddr.add (0xD060 ).readCString ());var xiugaiStrAddr = Module .findBaseAddress ("libxiaojianbang.so" ).add (0x1B00 );var xiugaiStr = new NativeFunction (xiugaiStrAddr, 'int64' , ['pointer' ]);var strAddr = Memory .allocUtf8String ("dajianbang" );console .log (hexdump (strAddr));var finalAddr = Memory .alloc (8 ).writePointer (strAddr);console .log (hexdump (finalAddr));xiugaiStr (finalAddr);console .log (hexdump (strAddr));
读写文件 写出文件的方法:使用fopen打开文件,fputs写入数据,fclose关闭文件。
主动调用so函数还是得先拿到地址,上代码,没什么好说的,就是注意f那三个函数的源码声明,看看参数类型,返回值类型啥的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var fopenAddr = Module .findExportByName ("libc.so" , "fopen" );var fputsAddr = Module .findExportByName ("libc.so" , "fputs" );var fcloseAddr = Module .findExportByName ("libc.so" , "fclose" );var fopen = new NativeFunction (fopenAddr, "pointer" , ["pointer" , "pointer" ]);var fputs = new NativeFunction (fputsAddr, "int" , ["pointer" , "pointer" ]);var fclose = new NativeFunction (fcloseAddr, "int" , ["pointer" ]);var fileName = Memory .allocUtf8String ("/data/data/com.xiaojianbang.app/xiaojianbang.txt" );var openMode = Memory .allocUtf8String ("w" );var buffer = Memory .allocUtf8String ("111111111" );var file = fopen (filename, open_mode);fputs (buffer, file);fclose (file);
fopen和fputs接受的都是指针类型的参数,不能将JS的字符串直接传递过去,需要使用Memory的allocUtf8String在内存中构建相应字符串后,再传递地址。
读取文件:使用libc.so的fgets。几乎一样,就不说了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var fopenAddr = Module .findExportByName ("libc.so" , "fopen" );var fgetsAddr = Module .findExportByName ("libc.so" , "fgets" );var fcloseAddr = Module .findExportByName ("libc.so" , "fclose" );var fopen = new NativeFunction (fopenAddr, "pointer" , ["pointer" , "pointer" ]);var fgets = new NativeFunction (fgetsAddr, "pointer" , ["pointer" , "int" , "pointer" ]);var fclose = new NativeFunction (fcloseAddr, "int" , ["pointer" ]);var fileName = Memory .allocUtf8String ("/data/data/com.xiaojianbang.app/xiaojianbang.txt" );var openMode = Memory .allocUtf8String ("r" );var buffer = Memory .alloc (60 );var file = fopen (fileName, openMode);var data = fgets (buffer, 60 , file);console .log (data.readCString ());fclose (file);
Frida其他常用API介绍 NativePointer类的常用方法 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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 declare class NativePointer { constructor (v: string | number | UInt64 | Int64 | NativePointerValue ); isNull (): boolean; add (v : NativePointerValue | UInt64 | Int64 | number | string): NativePointer ; sub (v : NativePointerValue | UInt64 | Int64 | number | string): NativePointer ; ...... equals (v : NativePointerValue | UInt64 | Int64 | number | string): boolean; compare (v : NativePointerValue | UInt64 | Int64 | number | string): number; toInt32 (): number; toUInt32 (): number; toString (radix?: number): string; toJSON (): string; readPointer (): NativePointer ; readS8 (): number; readU8 (): number; ...... readByteArray (length : number): ArrayBuffer | null ; readCString (size?: number): string | null ; readUtf8String (size?: number): string | null ; readUtf16String (length?: number): string | null ; readAnsiString (size?: number): string | null ; writePointer (value : NativePointerValue ): NativePointer ; writeS8 (value : number | Int64 ): NativePointer ; writeU8 (value : number | UInt64 ): NativePointer ; ...... writeByteArray (value : ArrayBuffer | number[]): NativePointer ; writeUtf8String (value : string): NativePointer ; writeUtf16String (value : string): NativePointer ; writeAnsiString (value : string): NativePointer ; }
Memory的常用方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 declare namespace Memory { function scan (address: NativePointerValue, size: number | UInt64, pattern: string, callbacks: MemoryScanCallbacks ): void ; function scanSync (address: NativePointerValue, size: number | UInt64, pattern: string ): MemoryScanMatch []; function alloc (size: number | UInt64, options?: MemoryAllocOptions ): NativePointer ; function allocUtf8String (str: string ): NativePointer ; function allocUtf16String (str: string ): NativePointer ; function allocAnsiString (str: string ): NativePointer ; function copy (dst: NativePointerValue, src: NativePointerValue, n: number | UInt64 ): void ; function dup (address: NativePointerValue, size: number | UInt64 ): NativePointer ; function protect (address: NativePointerValue, size: number | UInt64, protection: PageProtection ): boolean; function patchCode (address: NativePointerValue, size: number | UInt64, apply: MemoryPatchApplyCallback ): void ; }
替换函数 介绍一个新的FridaAPi:Interceptor.replace方法
其源码声明如下
1 2 3 4 declare namespace Interceptor { function replace (target: NativePointerValue, replacement: NativePointerValue, data?: NativePointerValue ): void ;}
第0个参数需要传入NativePointer类型的目标地址,第一个参数掺入用于替换的函数(通常由new NativeCallback()构建),也得是NativePointer型的
NativeCallback源码声明如下
1 2 3 4 declare class NativeCallback extends NativePointer { constructor (func: NativeCallbackImplementation, retType: NativeType, argTypes: NativeType[], abi?: NativeABI );}
NativeCallback在实例化时,需要传入函数的实现,返回值类型,参数类型数组。该类继承了NativePointer,所以可传给replace函数的replacement参数。
简单案例如下
1 2 3 4 5 6 7 8 9 function hook_func ( ) { var md5Func = Module .findBaseAddress ("libxiaojianbang.so" ).add (0x1F2C ); Interceptor .replace (md5Func, new NativeCallback (function ( ) { }, "void" , [ ])); } hook_func ();
值得注意的是,如果替换成空函数,NativeCallback的参数类型数组可以随便写或者为空数组。但是返回值类型最好和原函数返回值类型一致,而且要合理,别整太抽象。如果新韩淑影响了原函数的代码逻辑,APP是有可能崩溃的
稍作修改如下
1 2 3 4 5 6 7 8 function hook_func ( ) { var md5Func = Module .findBaseAddress ("libxiaojianbang.so" ).add (0x1F2C ); Interceptor .replace (md5Func, new NativeCallback (function ( ) { return 100 ; }, "int" , [])); } hook_func ();**
替换时打印原函数实参(需要将参数类型写对)
1 2 3 4 5 6 7 8 9 10 11 12 13 function hook_func ( ) { var md5Func = Module .findBaseAddress ("libxiaojianbang.so" ).add (0x1F2C ); Interceptor .replace (md5Func, new NativeCallback (function (env, jclass, data ) { console .log (env); console .log (jclass); console .log (Java .vm .tryGetEnv ().getStringUtfChars (data).readCString ()); return Java .vm .tryGetEnv ().newStringUtf ("this is return value" ); }, "pointer" , ["pointer" , "int" , "pointer" ])); } hook_func ();
这个才是正确的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function hook_func ( ) { var md5Addr = Module .findBaseAddress ("libxiaojianbang.so" ).add (0x1F2C ); var md5Func = new NativeFunction (md5Addr, "pointer" , ["pointer" , "pointer" , "pointer" ]); Interceptor .replace (md5Addr, new NativeCallback (function (env, jclass, data ) { var fridaEnv = Java .vm .tryGetEnv (); console .log (env, jclass, fridaEnv.getStringUtfChars (data).readCString ()); var retval = md5Func (env, jclass, data); console .log (fridaEnv.getStringUtfChars (retval).readCString ()); return retval; }, "pointer" , ["pointer" , "pointer" , "pointer" ])); } hook_func ();
Frida的一些进阶Hook Hook系统函数dlopen dlopen()
Hook so层,时机很重要,比如想要在so函数加载完成后,而so中的某个函数还没有被调用之前(或者在JNI_Onload被调用之前-因为有可能该函数被放在JNI_Onload的第一行)对该函数进行Hook。这时候,dlopen就派上用场了。系统加载的so文件和自己加载的so文件,一般都要通过这个函数(最好以spawn方式注入)
dlopen的第0个参数是被加载的so文件路径,第一个参数用于指定加载模式。Hook代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function hook_func ( ) { var myInit = Module .findBaseAddress ("libxiaojianbang.so" ).add (0x1DE8 ); Interceptor .replace (myInit, new NativeCallback (function ( ) { console .log ("replace myInit success" ); }, "void" , [ ])); } function hook_dlopen ( ) { var android_dlopen_ext = Module .findExportByName ("libdl.so" , "android_dlopen_ext" ); Interceptor .attach (android_dlopen_ext, { onEnter : function (args ) { var soPath = args[0 ].readCString (); if (soPath.indexOf ("libxiaojianbang.so" ) != -1 ) this .hook = true ; }, onLeave : function (retval ) { if (this .hook ) hook_func (); } }); } hook_dlopen ();
android_dlopen_ext用于加载so文件,__filename 和 __flags 参数与 dlopen(3) 相同,通过 __info 的 flags 成员提供特定于 Android 的flag。
源码声明如下
1 2 3 4 5 6 7 void * android_dlopen_ext ( const char *__filename, int __flags, const android_dlextinfo *__info )
低android版本中,dlopen用于加载so文件,这里改一下啊,两个都Hook
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function hook_func ( ) { var myInit = Module .findBaseAddress ("libxiaojianbang.so" ).add (0x1DE8 ); Interceptor .replace (myInit, new NativeCallback (function ( ) { console .log ("replace myInit success" ); }, "void" , [ ])); } function hook_dlopen (addr, soName, callback ) { Interceptor .attach (addr, { onEnter : function (args ){ var name = args[0 ].readCString (); if (name.indexOf (soName) != -1 ) this .hook = true ; }, onLeave : function (retval ){ if (this .hook ) callback (); } }); } var dlopen = Module .findExportByName ("libdl.so" , "dlopen" );var android_dlopen_ext = Module .findExportByName ("libdl.so" , "android_dlopen_ext" );hook_dlopen (dlopen, "libxiaojianbang.so" , hook_func);hook_dlopen (android_dlopen_ext, "libxiaojianbang.so" , hook_func);
Hook JNI_Onload 在so文件被加载后,系统会自动调用JNI_Onload,但也是在dlopen(或android_dlopen_ext)之后。所以Hook方法与普通函数一致,直接Hook dlopen即可
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 function hook_JNIOnload ( ) { var JNI_OnLoad = Module .findExportByName ("libxiaojianbang.so" , "JNI_OnLoad" ); Interceptor .attach (JNI_OnLoad, { onEnter : function (args ){ console .log (args[0 ]); }, onLeave : function (retval ){ console .log (retval); } }); } function hook_dlopen (addr, soName, callback ) { Interceptor .attach (addr, { onEnter : function (args ){ var name = args[0 ].readCString (); if (name.indexOf (soName) != -1 ) this .hook = true ; }, onLeave : function (retval ){ if (this .hook ) callback (); } }); } var dlopen = Module .findExportByName ("libdl.so" , "dlopen" );var android_dlopen_ext = Module .findExportByName ("libdl.so" , "android_dlopen_ext" );hook_dlopen (dlopen, "libxiaojianbang.so" , hook_JNIOnload);hook_dlopen (android_dlopen_ext, "libxiaojianbang.so" , hook_JNIOnload);
Hook initarray 在so文件加载过程中,系统会自动调用so文件中的init,init_array,和JNI_Onload函数。前两个实在dlopen函数执行过程中调用的。JNI_Onload是在dlopen函数执行之后调用的。
此时就有一个问题。如果将Hook initarray的代码放在dlopen的onEnter里面,so文件还没有加载,自然无法Hook initarray,onLeave就不用想了。故,需要在dlopen函数内部找一个时机。so已经加载,但init,init_array未被调用。linker (这玩意儿有32位和64位的区别 ) 的call_constructors满足这些需求。该函数用于调用so文件的init和init_array函数。且all_constructors在linker的符号表中,不需要额外计算偏移地址。但还是要Hook dlopen来监控so文件的加载
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 function hook_dlopen (addr, soName, callback ) { Interceptor .attach (addr, { onEnter : function (args ) { var soPath = args[0 ].readCString (); if (soPath.indexOf (soName) != -1 ) callback (); }, onLeave : function (retval ) { } }); } var dlopen = Module .findExportByName ("libdl.so" , "dlopen" );var android_dlopen_ext = Module .findExportByName ("libdl.so" , "android_dlopen_ext" );hook_dlopen (dlopen, "libxiaojianbang.so" , hook_call_constructors);hook_dlopen (android_dlopen_ext, "libxiaojianbang.so" , hook_call_constructors);function hook_call_constructors ( ) { var _symbols = Process .getModuleByName ("linker64" ).enumerateSymbols (); var call_constructors_addr = null ; for (let i = 0 ; i < _symbols.length ; i++) { var _symbol = _symbols[i]; if (_symbol.name .indexOf ("call_constructors" ) != -1 ){ call_constructors_addr = _symbol.address ; } } Interceptor .attach (call_constructors_addr, { onEnter : function (args ) { hook_initarray (); }, onLeave : function (retval ) { } }); } function hook_initarray ( ){ var xiaojianbangAddr = Module .findBaseAddress ("libxiaojianbang.so" ); var func1_addr = xiaojianbangAddr.add (0x1D14 ); var func2_addr = xiaojianbangAddr.add (0x1D3C ); var func3_addr = xiaojianbangAddr.add (0x1CEC ); Interceptor .replace (func1_addr, new NativeCallback (function ( ) { console .log ("func1 is replaced!!!" ); }, 'void' , [])); Interceptor .replace (func2_addr, new NativeCallback (function ( ) { console .log ("func2 is replaced!!!" ); }, 'void' , [])); Interceptor .replace (func3_addr, new NativeCallback (function ( ) { console .log ("func3 is replaced!!!" ); }, 'void' , [])); Interceptor .detachAll (); }
Hook pthread_create 一些用于检测的函数通常需要实时运行,就有可能用到pthrea_create开启一个子线程。通过Hook这个函数,可以查看App应用程序为哪些函数开启了线程,就可以有针对性的去分析这些函数的代码逻辑是否与检测相关
其源码声明如下
1 int pthread_create (pthread * tidp,const pthread_attr_t * attr,void * (* start_rtn)(void *),void * arg);
第0个参数为指向线程标识符的指针,第一个参数用来设置线程属性,第二个参数是线程运行函数的起始地址,最后一个参数是传递给线程运行函数的参数
实现代码如下
1 2 3 4 5 6 7 8 9 10 11 12 var pthread_create_addr = Module .findExportByName ("libc.so" , "pthread_create" );Interceptor .attach (pthread_create_addr,{ onEnter :function (args ){ console .log (args[0 ], args[1 ], args[2 ], args[3 ]); var Module = Process .findModuleByAddress (args[2 ]); if (Module != null ) console .log (Module .name , args[2 ].sub (Module .base )); },onLeave :function (retval ){ } });
监控内存读写 使用Process.setExceptionHandler(callback)可以设置异常处理回调函数。当异常触发时,会调用该函数。在该回调函数中,可通过修改寄存器和内存来让程序从异常中恢复。如果处理了异常,函数最后需要返回true,Frida会立即恢复线程。
直接上代码
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 function hook_dlopen (addr, soName, callback ) { Interceptor .attach (addr, { onEnter : function (args ) { var soPath = args[0 ].readCString (); if (soPath.indexOf (soName) != -1 ) this .hook = true ; }, onLeave : function (retval ) { if (this .hook ) {callback ()} } }); } var dlopen = Module .findExportByName ("libdl.so" , "dlopen" );var android_dlopen_ext = Module .findExportByName ("libdl.so" , "android_dlopen_ext" );hook_dlopen (dlopen, "libxiaojianbang.so" , set_read_write_break);hook_dlopen (android_dlopen_ext, "libxiaojianbang.so" , set_read_write_break);function set_read_write_break ( ){ Process .setExceptionHandler (function (details ) { console .log (JSON .stringify (details, null , 2 )); Memory .protect (details.memory .address , Process .pointerSize , 'rwx' ); return true ; }); var addr = Module .findBaseAddress ("libxiaojianbang.so" ).add (0x3DED ); Memory .protect (addr, 8 , '---' ); }
上述代码先Hook乐dlopen函数,然后在onLeave函数中执行set_read_write_break,该函数中,首先设置异常回调,接着把需要监控的内存区域权限设置为”—-”,当异常发生时,会调用异常回调函数,该回调函数接受一个参数,details。该参数是一个对象,记录了发生异常的消息描述message,异常的类型-type,发生异常的地址,发生异常时访问的内存地址以及发生异常时的寄存器信息。还可以在回调函数中打印函数栈,获取地址对应的符号信息等
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 29 30 31 32 33 34 35 36 37 38 39 40 41 function hook_dlopen (addr, soName, callback ) { Interceptor .attach (addr, { onEnter : function (args ) { var soPath = args[0 ].readCString (); if (soPath.indexOf (soName) != -1 ) this .hook = true ; }, onLeave : function (retval ) { if (this .hook ) {callback ()} } }); } var dlopen = Module .findExportByName ("libdl.so" , "dlopen" );var android_dlopen_ext = Module .findExportByName ("libdl.so" , "android_dlopen_ext" );hook_dlopen (dlopen, "libxiaojianbang.so" , set_read_write_break);hook_dlopen (android_dlopen_ext, "libxiaojianbang.so" , set_read_write_break);function set_read_write_break ( ){ Process .setExceptionHandler (function (details ) { console .log (JSON .stringify (details, null , 2 )); console .log ("lr" , DebugSymbol .fromAddress (details.context .lr )); console .log ("pc" , DebugSymbol .fromAddress (details.context .pc )); Memory .protect (details.memory .address , Process .pointerSize , 'rwx' ); console .log (Thread .backtrace (details.context , Backtracer .ACCURATE ).map (DebugSymbol .fromAddress ).join ('\n' ) + '\n' ); return true ; }); var addr = Module .findBaseAddress ("libxiaojianbang.so" ).add (0x3DED ); Memory .protect (addr, 8 , '---' ); }
但是由于Memory.protect修改的时内存页的权限,并不只是修改给定字节数的权限,所以会存在误差,导致监控内存读写的位置不是很精确。监控内存读写最推荐使用的是使用unidbg,这是一个基于unicorn开发的,在PC端模拟执行so文件的框架。
先插个眼
unidbg
函数追踪工具frida-trace IDA插件:trace_natives
https://github.com/Pr0214/trace_natives
用该插件获取so文件代码段中的所有函数的偏移地址后,再配合frida-trace就可以打印函数内部调用流程。
通过修改traceNatives.py中的代码,可以自行调整汇编指令多余几行的函数地址记录在txt文件中
trace_natives用来生成frida-trace运行时所需的参数,而真正的Hook由frida-trace来完成
并且,frida-trace还支持正则表达式模糊匹配来批量Hook java方法,Hook所有静态注册的jni函数等。代码如下
1 2 frida-trace -UF -j '*!*certificate*/isu' frida-trace -UF -i "Java_*"
Frida API的简单封装 eeeee,这个感觉没啥好特别注意的。直接上示例代码吧
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 var soAddr = Module .findBaseAddress ("libxiaojianbang.so" );hookAddr (soAddr.add (0x1ACC ), 5 ); hookAddr (soAddr.add (0x22A0 ), 3 ); function hookAddr (funcAddr, paramsNum ){ var module = Process .findModuleByAddress (funcAddr); Interceptor .attach (funcAddr, { onEnter : function (args ){ this .logs = []; this .params = []; this .logs .push ("call " + module .name + "!" + ptr (funcAddr).sub (module .base ) + "\n" ); for (let i = 0 ; i < paramsNum; i++){ this .params .push (args[i]); this .logs .push ("this.args" + i + " onEnter: " + printAddr (args[i])); } }, onLeave : function (retval ){ for (let i = 0 ; i < paramsNum; i++){ this .logs .push ("this.args" + i + " onLeave: " + printAddr (this .params [i])); } this .logs .push ("retval onLeave: " + printAddr (retval) + "\n" ); console .log (this .logs ); } }); } function printAddr (addr ){ var module = Process .findRangeByAddress (addr); if (module != null ) return hexdump (addr) + "\n" ; return ptr (addr) + "\n" ; }
代码跟踪引擎stalker Stalker是Frida的代码跟踪引擎,允许跟踪线程,捕获每个调用,每个块,甚至每个执行的指令。它支持AArch64架构以及Intel64和IA-32
示例如下
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 md5Addr = Module .getExportByName ("libxiaojianbang.so" , "Java_com_xiaojianbang_ndk_NativeHelper_md5" );Interceptor .attach (md5Addr, { onEnter : function ( ) { this .tid = Process .getCurrentThreadId (); Stalker .follow (this .tid , { events : { call : true , }, onReceive (events ) { var _events = Stalker .parse (events); for (var i = 0 ; i < _events.length ; i++) { console .log (_events[i]); } }, }); }, onLeave : function ( ) { Stalker .unfollow (this .tid ); } });
Stalker.follow源码声明如下
1 2 3 4 5 6 declare namespace Stalker { function follow (threadId?: ThreadId, options?: StalkerOptions ): void ; function unfollow (threadId?: ThreadId ): void ; function parse (events: ArrayBuffer , options?: StalkerParseOptions ): StalkerEventFull [] | StalkerEventBare []; ...... }
第0个参数是threaID,用于指定跟踪的线程(默认是当前线程)。用Process.getCurrentThreadId()来获取当前线程id,赋值给this.id,并传递给Stalker.unfollow,用于解除跟踪,函数第一个参数是options,类型为StalkerOptions,用于自定义跟踪选项。跟踪选项源码声明如下
1 2 3 4 5 6 7 8 9 10 11 12 interface StalkerOptions { events?: { call?: boolean; ret?: boolean; exec?: boolean; block?: boolean; compile?: boolean; }; onReceive?: (events: ArrayBuffer ) => void ; onCallSummary?: (summary: StalkerCallSummary ) => void ; ...... }
events用于指定生成哪些事件,传递给onReceive和onCallSummary。比如当events里面的call为true,Stalker在跟踪代码时,遇到函数调用,就会记录一些信息传递给onReceive和onCallSummary。events还支持在执行ret指令,所有指令exec,基本块block时生成事件。
events参数可以用Stalker.parse来解析。返回结果为StalkerEventFull数组或者StalkerEventBare数组。故使用循环即可遍历这些数组。并且Stalker.parse可以指定选项,StalkerParseOptions,源码声明如下
1 2 3 4 5 6 7 8 9 10 11 12 interface StalkerParseOptions { annotate?: boolean; stringify?: boolean; } type StalkerEventFull = StalkerCallEventFull | StalkerRetEventFull | StalkerExecEventFull | StalkerBlockEventFull | StalkerCompileEventFull ; type StalkerEventBare = StalkerCallEventBare | StalkerRetEventBare | StalkerExecEventBare | StalkerBlockEventBare | StalkerCompileEventBare ; type StalkerCallEventFull = [ "call" , NativePointer | string, NativePointer | string, number ]; type StalkerCallEventBare = [ NativePointer | string, NativePointer | string, number ]; ......
在示例代码的输出中 call,0x7590e77054,0x7590d63f60,0。对应上面来看。
从左到右分别表示事件类型为call,发生call时的地址,call所调用的函数地址。将示例代码稍作修改如下
onReceive事件通常用于查看函数流程,onCallSummary事件用于查看对应地址被调用次数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 onReceive (events ) { var _events = Stalker .parse (events); for (var i = 0 ; i < _events.length ; i++) { var addr1 = _events[i][1 ]; var module1 = Process .findModuleByAddress (addr1); if (module1 && module1.name == "libxiaojianbang.so" ) { var addr2 = _events[i][2 ]; var module2 = Process .findModuleByAddress (addr2); console .log (module1.name , addr1.sub (module1.base ), module2.name , addr2.sub (module2.base )); } } }
注意Stalker.follow会跟踪所有so文件的调用,包括一些系统so文件,所以容易崩,做好过滤和注释掉一些不必要的。
再来介绍一下onCallSummary的使用,该回调函数接受一个类型为StalkerCallSummary的参数,源码声明如下
1 2 3 interface StalkerCallSummary { [target : string]: number; }
summary是一个对象,属性名时被调用的函数地址,属性值是被调用的次数
看看它的写法
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 29 30 31 var md5Addr = Module .getExportByName ("libxiaojianbang.so" , "Java_com_xiaojianbang_ndk_NativeHelper_md5" );Interceptor .attach (md5Addr, { onEnter : function ( ) { this .tid = Process .getCurrentThreadId (); Stalker .follow (this .tid , { events : { call : true , }, onCallSummary (summary ) { for (const addr in summary) { var module = Process .findModuleByAddress (addr); if (module && module .name == "libxiaojianbang.so" ) { const num = summary[addr]; console .log (module .name , ptr (addr).sub (module .base ), num); } } }, }); }, onLeave : function ( ) { Stalker .unfollow (this .tid ); } });
找到目标so的信息,并将那些地址全部Hook,打印参数和返回值,以此来缩小关键函数范围,甚至对于封装的比较好的加密算法,可以直接得到密钥等关键信息。
但是需要注意Stalker记录的call函数地址,包含plt表中的地址,这些地址无法被Hook,需要剔除。