前言 在动笔之前我其实还没想好该怎么写。比如这篇文章的内容是什么,我需要写到多详细,以及我所写作的目标是什么,读者读完后能收获什么,我全都没想好,在这么一种混沌的状态下,我开始动笔。
另外两个关于原理的文章:
Frida-gum 源代码速通笔记: https://bbs.kanxue.com/thread-278423.htm Frida-Core 源代码速通笔记: https://bbs.kanxue.com/thread-278533.htm
本文主要还是关于使用,如果您对原理不感兴趣,只阅读本文即可。我希望它帮我实现的目的是,在我需要某个功能的时候能够直接通过本文的一些案例来完成。若未来有新的需求,希望能及时更新。
Usage Cheat Sheet 记录那些使用上的条目。
启动模式 frida
自带两种启动模式,分别是 attach
和 spawn
。前者是启动应用以后再接入 frida
,后者则是先启动 frida
再启动应用。二者各有不同的好处,比方说程序在启动时检查 frida
的话,则后来附加进去的 frida
是不会被发现的;而如果想要在程序启动时就钩取一些函数,则 spawn
模式能够在最早的时机生效。
通过 python 运行时的模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import timeimport frida device = frida.get_usb_device() pid = device.spawn(["com.demo.demo02" ]) device.resume(pid) time.sleep(1 ) session = device.attach(pid)with open ("s1.js" ) as f: script = session.create_script(f.read()) script.load() raw_input()
注意这里的 device.resume
函数,通过 spawn
启动的程序会在启动后立即暂停,而该函数则会恢复程序的执行。
如果我们希望以 attach 的方式进行连接,那么在脚本调用 device.spawn
函数后立即使用 device.resume
即可实现相同的操作;而如果我们希望以 spawn 模式进行工作,那么在 device.spawn
函数后则需要先完成 script.load
,然后再调用 device.resume
。
frida
在 Windows 平台下也能用来钩一些 PE 文件,装好环境以后能直接这样用
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 import subprocessimport string string.printablefrom colorama import init init(autoreset=True )import frida, sys def on_message (message, data ): global new_number print (message) if message['type' ] == 'send' : print ("[*] {0}" .format (message['payload' ])) new_number = message['payload' ] elif message['type' ] == "error" : print (message["description" ]) print (message["stack" ]) print (message["fileName" ],"line:" ,message["lineNumber" ],"colum:" ,message["columnNumber" ]) else : print (message) jscode = open ("test.js" ,"rb" ).read().decode() process = subprocess.Popen("demo.exe" , stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,bufsize=0 ) session = frida.attach("demo.exe" ) script = session.create_script(jscode) script.on('message' , on_message)print ('[*] Start attach' ) script.load() process.stdin.write("data" ) output, error = process.communicate()print (output) process.terminate()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var number=0 ;function main ( ) { var base = Module .findBaseAddress ("demo.exe" ) console .log (base) if (base){ Interceptor .attach (base.add (0x11F4C ), { onEnter : function (args ) { if (res==1 ) { number+=1 ; } else { send (number); } } }); } }setImmediate (main);
Command Line 利用 frida-trace
跟踪两个函数,同时以 spawn 模式启动该程序:
1 frida-trace -U -i open -i strcmp -f com.android.chrome
跟踪所有 libcommonCrypto
下的函数,这里是通配符:
1 frida-trace -U -I "libcommonCrypto*" -f com.toyopagroup.picaboo
在 IOS 平台下用这样跟踪 OBJC 函数:
1 frida-trace -U -m "-[NSView drawRect:]" -f target
跟踪类BitmapFactory中方法名中包含 native 的所有Java方法:
1 frida-trace -U -j '*BitmapFactory*!*native*' com.android.chrome
提一句:frida-trace 的原理实际上是会根据参数在当前目录下生成对应的 JavaScript 脚本 ,然后再用 frida 加载这份脚本实现跟踪。所以我们实际上可以先用这个方法把目标钩出来,然后魔改生成的脚本就能比较方便的实现自定义功能了。
如果需要启用内置的 V8 引擎 ,也能通过这个参数启用:
1 frida -U -f com.apple.calculator --runtime=v8 -l agent.js
但请注意,V8 引擎并不总是有效的,在某些平台的某些版本下,启用后会导致无法正常 hook
如果我们希望在命令行模式下以启动命令的方式,却使用类似 attach 的办法,通过添加 --no-pause
参数可以避免程序在启动后被中断:
1 frida -U --no-pause -l hookNative.js -f com.example.app
Remote Frida Server 在一些使用网络进行通信的场景下,利用 frida-server 可以创建远程的调试环境:
1 frida-trace -H 192.168.1.3 -i "open*"
Code Cheat Sheet 记录那些代码的条目。
Script Function 我们有这些函数可供使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import inspectimport fridafor mb in inspect.getmembers(frida, inspect.isfunction): print (mb) ('attach' , <function attach at 0x000002A27AAC9318 >) ('enumerate_devices' , <function enumerate_devices at 0x000002A27AAFAF78 >) ('get_device' , <function get_device at 0x000002A27AAF64C8 >) ('get_device_manager' , <function get_device_manager at 0x000002A27AA9D4C8 >) ('get_device_matching' , <function get_device_matching at 0x000002A27AAF63A8 >) ('get_local_device' , <function get_local_device at 0x000002A27AAE8F78 >) ('get_remote_device' , <function get_remote_device at 0x000002A27AAE81F8 >) ('get_usb_device' , <function get_usb_device at 0x000002A27AAF6558 >) ('inject_library_blob' , <function inject_library_blob at 0x000002A27AA9A558 >) ('inject_library_file' , <function inject_library_file at 0x000002A27AAD8EE8 >) ('kill' , <function kill at 0x000002A27AAAE558 >) ('query_system_parameters' , <function query_system_parameters at 0x000002A27A894048 >) ('resume' , <function resume at 0x000002A27AAAE048 >) ('shutdown' , <function shutdown at 0x000002A27AAE2168 >) ('spawn' , <function spawn at 0x000002A27AAA1318 >)
在下面的代码中可能会涉及到这些函数的使用。
Python Template 一个简单的 Frida-Python 交互模板,通过 spawn 模式启动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import frida, sys, timedef on_message (message, data ): if message['type' ] == 'send' : print ("[*] {0}" .format (message['payload' ])) else : print (message) js = """js code here""" device = frida.get_usb_device() pid = device.spawn(["com.example.app" ]) session = device.attach(pid) script = session.create_script(js) script.on('message' , on_message) script.load() device.resume(pid) sys.stdin.read()
或是通过附加的方式接入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import frida, sysdef on_message (message, data ): if message['type' ] == 'send' : print ("[*] {0}" .format (message['payload' ])) else : print (message) js = """js code here""" process = frida.get_usb_device().attach('com.example.app' ) script = process.create_script(js) script.on('message' , on_message) script.load() sys.stdin.read()
JavaScript Template 记录那些常用的 JavaScript 脚本,希望我们不再需要反复的手写了。
替换函数实现 通过 Java.use
找到特定的类,然后使用 class.function_name.implementation
能够获取到其实现,如果修改改变量则会替换该函数;而在其中调用 this
可以访问原本的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 console .log ("Script loaded successfully " );Java .perform (function x ( ) { console .log ("Inside java perform function" ); var my_class = Java .use ("com.demo.MainActivity" ); console .log ("Java.Use.Successfully!" ); my_class.fun .implementation = function (x,y ){ console .log ( "original call: fun(" + x + ", " + y + ")" ); var ret_value = this .fun (2 , 5 ); return ret_value; } });
获取类及其下的方法 或许可以通过这样的方法来获得一个类和它对应的方法,在这个用例里我们打印了 MainActivity
下的所有方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const main = Java .use ("com.example.app.MainActivity" );function inspectClass (obj ) { const obj_class = Java .cast (obj.getClass (), Class ); const fields = obj_class.getDeclaredFields (); const methods = obj_class.getMethods (); console .log ("Inspect " + obj.getClass ().toString ()); console .log ("\tFields:" ); for (var i in fields) console .log ("\t" + fields[i].toString ()); console .log ("\tMethods:" ); for (var i in methods) console .log ("\t" + methods[i].toString ()); }inspectClass (main)
获取类及其下的 native 方法 在这篇问答中提到:Find manually registered (obfuscated) native function address
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 var RevealNativeMethods = function ( ) { var pSize = Process .pointerSize ; var env = Java .vm .getEnv (); var RegisterNatives = 215 , FindClassIndex = 6 ; var jclassAddress2NameMap = {}; function getNativeAddress (idx ) { return env.handle .readPointer ().add (idx * pSize).readPointer (); } Interceptor .attach (getNativeAddress (FindClassIndex ), { onEnter : function (args ) { jclassAddress2NameMap[args[0 ]] = args[1 ].readCString (); } }); Interceptor .attach (getNativeAddress (RegisterNatives ), { onEnter : function (args ) { for (var i = 0 , nMethods = parseInt (args[3 ]); i < nMethods; i++) { var structSize = pSize * 3 ; var methodsPtr = ptr (args[2 ]); var signature = methodsPtr.add (i * structSize + pSize).readPointer (); var fnPtr = methodsPtr.add (i * structSize + (pSize * 2 )).readPointer (); var jClass = jclassAddress2NameMap[args[0 ]].split ('/' ); console .log ('\x1b[3' + '6;01' + 'm' , JSON .stringify ({ module : DebugSymbol .fromAddress (fnPtr)['moduleName' ], package : jClass.slice (0 , -1 ).join ('.' ), class : jClass[jClass.length - 1 ], method : methodsPtr.readPointer ().readCString (), signature : signature.readCString (), address : fnPtr }), '\x1b[39;49;00m' ); } } }); }Java .perform (RevealNativeMethods );
对特定动态库进行 hook 1 2 3 4 5 6 7 8 9 10 11 var sign2 = Module .findExportByName ("libhello-jni.so" , "Java_com_example_hellojni_HelloJni_sign2" ); console .log (sign2); Interceptor .attach (sign2, { onEnter : function (args ) { console .log ("sign2 str1:" , ptr (Java .vm .tryGetEnv ().getStringUtfChars (args[2 ])).readCString ()); console .log ("sign2 str2:" , ptr (Java .vm .tryGetEnv ().getStringUtfChars (args[3 ])).readCString ()); }, onLeave : function (retval ) { console .log ("sign2 retval:" , ptr (Java .vm .tryGetEnv ().getStringUtfChars (retval)).readCString ()); } });
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Module .enumerateExports ("mylib.so" , { onMatch : function (e ) { if (e.type == 'function' ) { console .log ("name of function = " + e.name ); if (e.name == "Java_example_decrypt" ) { console .log ("Function Decrypt recognized by name" ); Interceptor .attach (e.address , { onEnter : function (args ) { console .log ("Interceptor attached onEnter..." ); }, onLeave : function (retval ) { console .log ("Interceptor attached onLeave..." ); } }); } } }, onComplete : function ( ) {} });
请注意,这个方法并不总是有效,尤其是当我们需要以 spawn 的方式启动应用时,这个脚本会由于目标还尚未被加载导致无效。
修改 native 函数返回值 1 2 3 4 5 6 7 8 9 10 11 Interceptor .attach (Module .getExportByName ('libnative-lib.so' , 'Jniint' ), { onEnter : function (args ) { this .first = args[0 ].toInt32 (); console .log ("on enter with: " + this .first ) }, onLeave : function (retval ) { const dstAddr = Java .vm .getEnv ().newIntArray (1117878 ); console .log ("dstAddr is : " + dstAddr.toInt32 ()) retval.replace (dstAddr); } });
如果 getExportByName
的第一个参数为 null,那么 frida 将会查找所有动态库
dlopen hook dlopen
往往被用于加载动态库,那么如果对这个函数进行 hook,那么就能解决前文所说的 “在加载动态库后再对其进行 hook” 的操作了。
主动加载:
1 2 3 4 5 6 7 const dlopen = new NativeFunction (Module .findExportByName (null , 'dlopen' ), 'pointer' , ['pointer' , 'int' ]);const dlerror = new NativeFunction (Module .findExportByName (null , 'dlerror' ), 'pointer' , []);const path = Memory .allocUtf8String ("/data/local/tmp/libdummy.so" );var ret = dlopen (path, 2 );console .log ("ret = " + ret);var error = dlerror ();console .log ("error = " + Memory .readUtf8String (error));
被动加载:
1 2 3 4 5 6 7 8 9 var dlopen = new NativeFunction (Module .findExportByName (null , 'dlopen' ), 'pointer' , ['pointer' , 'int' ]);Interceptor .replace (dlopen, new NativeCallback (function (path, mode ) { console .log ("dlopen(" + "path=\"" + Memory .readUtf8String (path) + "\"" + ", mode=" + mode + ")" ); var name = Memory .readUtf8String (path); if (name !== null ) { console .log ("[*] dlopen " + name); } return dlopen (path, mode); }, 'pointer' , ['pointer' , 'int' ]));
更常用的方案:
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 function inline_hook ( ) { var base_hello_jni = Module .findBaseAddress ("libxxxx.so" ); console .log ("base_hello_jni:" , base_hello_jni); if (base_hello_jni) { console .log (base_hello_jni); var addr_07320 = base_hello_jni.add (0x07320 ); Interceptor .attach (addr_07320, { onEnter : function (args ) { console .log ("addr_07320 x13:" , this .context .x13 ); }, onLeave : function (retval ) { } }); } }function hook_dlopen ( ) { var dlopen = Module .findExportByName (null , "dlopen" ); Interceptor .attach (dlopen, { onEnter : function (args ) { this .call_hook = false ; var so_name = ptr (args[0 ]).readCString (); if (so_name.indexOf ("libxxxx.so" ) >= 0 ) { console .log ("dlopen:" , ptr (args[0 ]).readCString ()); this .call_hook = true ; } }, onLeave : function (retval ) { if (this .call_hook ) { inline_hook (); } } }); var android_dlopen_ext = Module .findExportByName (null , "android_dlopen_ext" ); Interceptor .attach (android_dlopen_ext, { onEnter : function (args ) { this .call_hook = false ; var so_name = ptr (args[0 ]).readCString (); if (so_name.indexOf ("libhxxxx.so" ) >= 0 ) { console .log ("android_dlopen_ext:" , ptr (args[0 ]).readCString ()); this .call_hook = true ; } }, onLeave : function (retval ) { if (this .call_hook ) { inline_hook (); } } }); }
通过这个方式能保证在对动态库进行钩取时候对其是否加载进行检测。
在限定 Android
平台时,还可以直接对 loadLibrary
进行 hook:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Java .perform (function ( ) { const System = Java .use ('java.lang.System' ); const Runtime = Java .use ('java.lang.Runtime' ); const VMStack = Java .use ('dalvik.system.VMStack' ); System .loadLibrary .implementation = function (library ) { try { console .log ('System.loadLibrary("' + library + '")' ); Runtime .getRuntime ().loadLibrary0 (VMStack .getCallingClassLoader (), library); } catch (ex) { console .log (ex); } }; System .load .implementation = function (library ) { try { console .log ('System.load("' + library + '")' ); Runtime .getRuntime ().nativeLoad (library, VMStack .getCallingClassLoader ()); } catch (ex) { console .log (ex); } }; });
打印调用栈 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const membase = Module .findBaseAddress ('libtest.so' );const funcs = [ '0x21B248' , '0x21D0C8' , '0x234730' , '0x23F718' , '0x259E68' ];for (var i in funcs) { var funcPtr = membase.add (funcs[i]); var handler = (function ( ) { var name = funcs[i]; return function (args ) { console .log (name + ': ' ); var trace = Thread .backtrace (this .context , Backtracer .ACCURATE ).map (DebugSymbol .fromAddress ); for (var j in trace) console .log (trace[j]); }; })(); Interceptor .attach (funcPtr, {onEnter : handler}); }
用 Stalker 跟踪函数接下来的调用 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 const funcs = [ '0x870FF0' , '0x871BA0' ];const STALKED = 12345 ;const STARTING_ADDRESS = "0x102FE0" ;const ENDING_ADDRESS = "0x89BE04" ;const base = Module .findBaseAddress ('libtest.so' );var threads = [];for (var i in funcs) { console .log ('Hooking funcs[' + i + '] ' + funcs[i]); Interceptor .attach (base.add (funcs[i]), { onEnter : function (args ) { var tid = Process .getCurrentThreadId (); if (threads[tid] == STALKED ) return ; Stalker .follow (tid, { events : { call : true , ret : false , exec : false }, onCallSummary : function (summary ) { var log = [] for (i in summary) { var addr = ptr (i).sub (base)); if (addr.compare (ptr (STARTING_ADDRESS )) >= 0 && addr.compare (ptr (ENDING_ADDRESS )) <= 0 ) log.push (addr); } console .log (JSON .stringify (log)); } }); threads[tid] = STALKED ; }, onLeave : function (retval ) { var tid = Process .getCurrentThreadId (); if (threads[tid] == STALKED ) return ; Stalker .unfollow (tid); Stalker .garbageCollect (); } }); }
events
中能决定跟踪的颗粒度,包含跟踪所有 call
,ret
和所有指令三个等级
Hook一个Obj-C方法 1 2 3 4 5 6 7 const sendMessage = ObjC .classes .SecureStorage ["- readFile:" ];Interceptor .attach (sendMessage.implementation , { onLeave : function (retval ) { var message = ObjC .Object (retval); console .log ("- [SecureStorage readFile:] -->\n\"" + message.toString () + "\"" ); } });
JavaScript Function string2byte 1 2 3 4 5 6 7 8 9 10 11 12 13 14 function string2Bytes (str ) { var ch, st, re = []; for (var i = 0 ; i < str.length ; i++ ) { ch = str.charCodeAt (i); st = []; do { st.push ( ch & 0xFF ); ch = ch >> 8 ; } while ( ch ); re = re.concat ( st.reverse () ); } return re; }
hex2bytes 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export const hex2bytes =(hex )=>{ let pos=0 ,len=hex.length ; if (len%2 !=0 ){ return null } len/2 ; let bytes=new Array (); for (let i=0 ;i<len;i++){ let s=hex.substr (pos,2 ); let v=parseInt (s,16 ); bytes.push (v); pos+=2 ; } return bytes }
bytes2hex 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 export const bytes2hex =(bytes )=>{ let hex="" ,len=bytes.length ; for (let i=0 ;i<len;i++){ let tmp,num=bytes[i]; if (num<0 ){ tmp=(255 +num+1 ).toString (16 ); }else { tmp=num.toString (16 ); } if (tmp.length ==1 ){ return "0" +tmp; } hex+=tmp; } return hex }
Other frida-snippets: 一些可用的脚本或工具