Frida工作原理學習
一、frida介紹
frida是一款便攜的、自由的、支持全平臺的hook框架,可以通過編寫JavaScript、Python代碼來和frida_server端進行交互,還記得當年用xposed時那種寫了一大堆代碼每次修改都要重新打包安裝重啟手機、那種調試調到頭皮發麻的痛苦,百分之30的時間都是在那里安裝重啟安裝重啟。
二、frida的代碼結構
frida-core: Frida 核心庫frida-gum: inline-hook 框架bindings:frida-python: pythonfrida-node: Node.jsfrida-qml: Qmlfrida-swift: Swiftfrida-tools: CLI toolscapstone: instruction disammbler
Frida的核心是c編寫的有多種語言綁定例如 Node.js、 Python、 Swift、 .NET、 Qml。
一般我們都使用js去編寫frida腳本因為js的異常處理機制非常棒相比于其他語言更高效好用。
frida-core
frida-core的功能有進程注入、進程間通信、會話管理、腳本生命周期管理等功能,屏蔽部分底層的實現細節并給最終用戶提供開箱即用的操作接口。而這一切的實現都在 frida-core 之中。
正如名字所言,這其中包含了 frida 相關的大部分關鍵模塊和組件,比如 frida-server、frida-gadget、frida-agent、frida-helper、frida-inject 以及之間的互相通信底座。
frida-gum
frida-gum是基于inline-hook實現的他還有很多豐富的功能比如用于代碼跟蹤 Stalker、用于內存訪問監控的MemoryAccessMonitor,以及符號查找、棧回溯實現、內存掃描、動態代碼生成和重定位等。
Interceptor
Interceptor 是 inline-hook 的封裝。
GumInterceptor * interceptor;GumInvocationListener * listener;gum_init ();interceptor = gum_interceptor_obtain ();//GumInvocationListener*的接口listener = g_object_new (EXAMPLE_TYPE_LISTENER, NULL);
// 開始 hook `open` 函數gum_interceptor_begin_transaction (interceptor);gum_interceptor_attach_listener (interceptor, GSIZE_TO_POINTER (gum_module_find_export_by_name (NULL, "open")), listener, GSIZE_TO_POINTER (EXAMPLE_HOOK_OPEN));gum_interceptor_end_transaction (interceptor);
// 測試 hook 效果close (open ("/etc/hosts", O_RDONLY));
// 結束 hookgum_interceptor_detach_listener (interceptor, listener);g_object_unref (listener);g_object_unref (interceptor);
Stalker
潛行者又稱為尾行癡漢,可以實現指定線程中所有函數、所有基本塊、甚至所有指令的跟蹤但是有很大的缺點比如在32位或者thumb下問題很大,一般想使用指令跟蹤都是使用內存斷點或者unidbg模擬執行so但是有很多問題,內存斷點的反調試倒是很容易解決但是性能是一個很大的缺陷代碼觸發斷點后會先中斷到內核態,然后再返回到用戶態(調試器)執行跟蹤回調,處理完后再返回內核態,然后再回到用戶態繼續執行,這來來回回的黃花菜都涼了。
但Unidbg的使用門檻動不動就補環境,龍哥說樣本和Unidbg之間摩擦出的火花才是最迷人的。或者說人話——“Unidbg怎么又報錯了,我該怎么辦?”
Stalker的簡單使用
Interceptor.attach(addr, { onEnter: function (args) { this.args0 = args[0]; this.tid = Process.getCurrentThreadId(); //跟隨 Stalker.follow(this.tid, { events: {//事件 call: true,//呼叫 ret: false,//返回 exec: true,//執行 block: false,//塊 compile: false//編譯 }, //接收 onReceive(events){ for (const [index,value] of Stalker.parse(events)) { console.log(index,value); //findModuleByAddress {"name":"libc.so","base":"0x7d1f0af000","size":3178496,"path":"/apex/com.android.runtime/lib64/bionic/libc.so"} //console.log("tuzi",Process.findModuleByAddress(0x7d1f13adb8));
} } // onCallSummary(summay){ //console.log("onCallSummary"+JSON.stringify(summay)); // }, }); }, onLeave: function (retval) { Stalker.unfollow(this.tid); } });
Stalker也可以用來還原ollvm混淆 記錄函數的真實執行地址結合ida反匯編沒執行的代碼都nop掉可以很大程度上幫助輔助混淆算法分析當然可能不太準確但也是一種非常棒的思路。
Stalker的功能實現,在線程即將執行下一條指令前,先將目標指令拷貝一份到新建的內存中,然后在新的內存中對代碼進行插樁,如下圖所示:

這其中使用到了代碼動態重編譯的方法,好處是原本的代碼沒有被修改,因此即便代碼有完整性校驗也不影響,另外由于執行過程都在用戶態,省去了多次中斷內核切換,性能損耗也達到了可以接受的水平。由于代碼的位置發生了改變,如前文 Interceptor 一樣,同樣要對代碼進行重定位的修復。
內存監控
MemoryAccessMonitor可以實現對指定內存區間的訪問監控,在目標內存區間發生讀寫行為時可以觸發用戶指定的回調函數。
通過閱讀源碼發現這個功能的實現方法非常簡潔,本質上是將目標內存頁設置為不可讀寫,這樣在發生讀寫行為時會觸發事先注冊好的中斷處理函數,其中會調用到用戶使用 gum_memory_access_monitor_new 注冊的回調方法中。
//C 代碼gbooleangum_memory_access_monitor_enable (GumMemoryAccessMonitor * self, GError ** error){ if (self->enabled) return TRUE; // ... self->exceptor = gum_exceptor_obtain (); gum_exceptor_add (self->exceptor, gum_memory_access_monitor_on_exception, self); // ...}
//js代碼function read_write_break(){ function hook_dlopen(addr, soName, callback) { Interceptor.attach(addr, { onEnter: function (args) { var soPath = args[0].readCString(); if(soPath.indexOf(soName) != -1) hook_call_constructors(); }, onLeave: function (retval) { } }); } var dlopen = Module.findExportByName("libdl.so", "dlopen"); var android_dlopen_ext = Module.findExportByName("libdl.so", "android_dlopen_ext"); hook_dlopen(dlopen, "libaes.so", set_read_write_break); hook_dlopen(android_dlopen_ext, "libaes.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('') + ''); return true; }); var addr = Module.findBaseAddress("libaes.so").add(0x6666); Memory.protect(addr, 8, '---'); //修改內存頁的權限 /** * 比如有一個地址是0x12345678 我想看一下是那個代碼去訪問了這個地址 * 我只需要把這個內存地址置空 有函數去訪問這個地址時 就會觸發非法訪問異常 * 比較雞肋這種方法 這種方法會一次修改一個內存頁 并且觸發一次就無效了 */ }}
hook原理
1.注入進程ptracedlopen2.hook 目標函數2.1 Java HookStatic Field Hook:靜態成員hookMethod Hook:函數hook2.2 Native So HookGOT Hook:全局偏移表hookSYM Hook:符號表hookInline Hook:函數內聯hook執行自身代碼獲取敏感信息修改返回值etc.
frida注入的主要思路就是找到目標進程,使用ptrace跟蹤目標進程獲取mmap,dlpoen,dlsym等函數庫的便宜獲取mmap在目標進程申請一段內存空間將在目標進程中找到存放[frida-agent-32/64.so]的空間啟動執行各種操作由agent去實現。
補充:frida注入之后會在遠端進程分配一段內存將agent拷貝過去并在目標進程中執行代碼,執行完成后會 detach 目標進程,這也是為什么在 frida 先連接上目標進程后還可以用gdb/ida等調試器連接,而先gdb連接進程后 frida 就無法再次連上的原因(frida在注入時只會ptrace一下下注入完畢后就會結束ptrace所以ptrace占坑這種反調試使用spawn方式啟動即可)。
frida-agent 注入到目標進程并啟動后會啟動一個新進程與 host 進行通信,從而 host 可以給目標進行發送命令,比如執行代碼,激活/關閉 hook,同時也能接收到目標進程的執行返回以及異步事件信息等。
hook java層
frida 的 hook 區分了 art 模式和 dalvik 模式。
dalvik 模式
把 java 函數變成 native 函數,然后修改入口信息為自定義函數信息。

struct Method { ClassObject* clazz; /* method所屬的類 public、native等*/ u4 accessFlags; /* 訪問標記 */ u2 methodIndex; //method索引 //三個size為邊界值,對于native函數,這3個size均等于參數列表的size u2 registersSize; /* ins + locals */ u2 outsSize; u2 insSize; const char* name;//函數名稱 /* * Method prototype descriptor string (return and argument types) */ DexProto prototype; /* short-form method descriptor string */ const char* shorty; /* * The remaining items are not used for abstract or native methods. * (JNI is currently hijacking "insns" as a function pointer, set * after the first call. For internal-native this stays null.) */ /* the actual code */ const u2* insns; /* instructions, in memory-mapped .dex */ /* cached JNI argument and return-type hints */ int jniArgInfo; /* * Native method ptr; could be actual function or a JNI bridge. We * don't currently discriminate between DalvikBridgeFunc and * DalvikNativeFunc; the former takes an argument superset (i.e. two * extra args) which will be ignored. If necessary we can use * insns==NULL to detect JNI bridge vs. internal native. */ DalvikBridgeFunc nativeFunc; /* * Register map data, if available. This will point into the DEX file * if the data was computed during pre-verification, or into the * linear alloc area if not. */ const RegisterMap* registerMap;
};
………
function replaceDalvikImplementation (fn) { if (fn === null && dalvikOriginalMethod === null) { return; }//備份原來的method, if (dalvikOriginalMethod === null) { dalvikOriginalMethod = Memory.dup(methodId, DVM_METHOD_SIZE); dalvikTargetMethodId = Memory.dup(methodId, DVM_METHOD_SIZE); }
if (fn !== null) { //自定的代碼 implementation = implement(f, fn);
let argsSize = argTypes.reduce((acc, t) => (acc + t.size), 0); if (type === INSTANCE_METHOD) { argsSize++; } // 把method變成native函數 /* * make method native (with kAccNative) * insSize and registersSize are set to arguments size */ const accessFlags = (Memory.readU32(methodId.add(DVM_METHOD_OFFSET_ACCESS_FLAGS)) | kAccNative) >>> 0; const registersSize = argsSize; const outsSize = 0; const insSize = argsSize;
Memory.writeU32(methodId.add(DVM_METHOD_OFFSET_ACCESS_FLAGS), accessFlags); Memory.writeU16(methodId.add(DVM_METHOD_OFFSET_REGISTERS_SIZE), registersSize); Memory.writeU16(methodId.add(DVM_METHOD_OFFSET_OUTS_SIZE), outsSize); Memory.writeU16(methodId.add(DVM_METHOD_OFFSET_INS_SIZE), insSize); Memory.writeU32(methodId.add(DVM_METHOD_OFFSET_JNI_ARG_INFO), computeDalvikJniArgInfo(methodId)); //調用dvmUseJNIBridge為這個Method設置一個Bridge,本質上是修改結構體中的nativeFunc為自定義的implementation函數 api.dvmUseJNIBridge(methodId, implementation);
patchedMethods.add(f); } else { patchedMethods.delete(f);
Memory.copy(methodId, dalvikOriginalMethod, DVM_METHOD_SIZE); implementation = null; }}
art 模式
art模式也是需要將java 函數變成 native 函數但是不同于dalvik,art下有兩種解釋器一種匯編解釋器一種smali解釋器。
quick code 模式:執行 arm 匯編指令Interpreter 模式:由解釋器解釋執行 Dalvik 字節碼

1.如果函數已經存在quick code, 則指向這個函數對應的 quick code的起始地址,而當quick code不存在時,它的值則會代表其他的意義。
2.當一個 java 函數不存在 quick code時,它的值是函數artQuickToInterpreterBridge 的地址,用以從 quick 模式切換到 Interpreter 模式來解釋執行 java 函數代碼。
3.當一個 java native(JNI)函數不存在 quick code時,它的值是函數 art_quick_generic_jni_trampoline 的地址,用以執行沒有quick code的 jni 函數。
所以 frida 要將 java method 轉為 native method,需要將ARTMethod 結構進行如下修改:
patchMethod(methodId, { //jnicode入口entry_point_from_jni_改為自定義的代碼 'jniCode': implementation, //修改為access_flags_為native 'accessFlags': (Memory.readU32(methodId.add(artMethodOffset.accessFlags)) | kAccNative | kAccFastNative) >>> 0, //art_quick_generic_jni_trampoline函數的地址 'quickCode': api.artQuickGenericJniTrampoline, //artInterpreterToCompiledCodeBridge函數地址 'interpreterCode': api.artInterpreterToCompiledCodeBridge});