Frida Internal - Part 1: 架構(gòu)、Gum 與 V8
frida 是一個非常優(yōu)秀的開源項目,因為項目活躍,代碼整潔,接口清晰,加上用靈活的腳本語言(JS)來實現(xiàn)指令級代碼追蹤的能力,為廣大的安全研究人員所喜愛。雖然使用人群廣泛,但對其內(nèi)部實現(xiàn)的介紹卻相對較少,因此筆者就越俎代庖,替大胡子寫寫 frida 內(nèi)部實現(xiàn)介紹,同時也作為自己的閱讀理解記錄。
系列文章傳送門(占坑):
??Frida Internal - Part 1: 架構(gòu)、Gum 與 V8 (本文)[1]
??Frida Internal - Part 2: frida-core
??Frida Internal - Part 3: frida-java-bridge 與 ART hook
??Frida Internal - Part 4: 檢測 frida 的 100 種方法
項目總覽
在?frida[2]?的主倉庫中,我們一般是直接在其 release 頁面下載 CI 的成品使用,其中可以看到有許多可供下載的組件,比如:
??frida-server
??frida-gadget
??frida-inject
??frida-core/gum/gumjs-devkit
??frida-clr
??frida-portal
??frida-tools
??...
按照封裝層級來劃分可以分為 4 級,分別是:
1. CPU 指令集級別的 inline-hook 框架: frida-gum;
2. 使用 JavaScript 引擎對 gum 進行封裝實現(xiàn)腳本拓展的能力: gum-js;
3. 運行時進程注入、腳本加載、RPC 通信管理等功能: frida-core;
4. 針對特殊運行環(huán)境的 js 模塊及其接口,如 frida-java-bridge、frida-objc-bridge 等;
還有一些其他輔助組件如 clr、portal 和針對不同語言的 binding 等功能相對花哨,就不額外進行討論了。
由于筆者的關(guān)注點主要在系統(tǒng)層的實現(xiàn)上,因此對于 frida-gum 和 gum-js 的實現(xiàn)細節(jié)不會太過深入,就讓二老委屈點擠在本文中一并進行介紹了。后續(xù)針對 frida-core 和 frida-java-bridge 都會有單獨的文章進行分析。
frida-gum
說起?frida-gum[3]?大家都知道它提供了 inline-hook 的核心實現(xiàn),但實際上它還包含了許多其他的模塊,比如用于代碼跟蹤 Stalker、用于內(nèi)存訪問監(jiān)控的 MemoryAccessMonitor,以及符號查找、棧回溯實現(xiàn)、內(nèi)存掃描、動態(tài)代碼生成和重定位等功能,可以說是上層豐富功能的關(guān)鍵基礎(chǔ)設(shè)施。
Interceptor
Interceptor[4]?是 inline-hook 的封裝,從接口上看,Interceptor 的使用方法大致如下:
GumInterceptor?*?interceptor;
GumInvocationListener?*?listener;
gum_init?();
interceptor?=?gum_interceptor_obtain?();
listener?=?g_object_new?(EXAMPLE_TYPE_LISTENER,?NULL);
//?開始?hook?`open`?函數(shù)
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));
//?結(jié)束?hook
gum_interceptor_detach_listener?(interceptor,?listener);
g_object_unref?(listener);
g_object_unref?(interceptor);其中?listener?是一個?GumInvocationListener *?類型的接口,用戶可以自行實現(xiàn)?on_enter?和?on_leave?來控制注入的邏輯:
typedef?void?(*?GumInvocationCallback)?(GumInvocationContext?*?context,?gpointer?user_data);
struct?_GumInvocationListenerInterface?{
??GTypeInterface?parent;
??void?(*?on_enter)?(GumInvocationListener?*?self,
??????GumInvocationContext?*?context);
??void?(*?on_leave)?(GumInvocationListener?*?self,
??????GumInvocationContext?*?context);
};一般直接從 release 中下載?frida-gum-devkit?去靜態(tài)編譯使用,內(nèi)部包含了一個靜態(tài)庫、頭文件和示例程序,并且頭文件有豐富的注釋說明:
$?tar?-xvf?frida-gum-devkit-15.1.17-android-arm.tar.xz
x?frida-gum.h
x?libfrida-gum.a
x?frida-gum-example.cfrdia-gum 使用 C 語言來編寫,但主要依賴于?glib[5]?去實現(xiàn)面向?qū)ο缶幊蹋@是由 GNOME 開發(fā)并從 GTK 中分離出來的一個基礎(chǔ)庫。使用純 C 編程的話這是個非常好用的工具庫,其提供了許多有用的數(shù)據(jù)類型、宏定義、類型轉(zhuǎn)換、字符串/文件處理以及線程的抽象支持。在 Linux 內(nèi)核中也能看到很多 glib 封裝設(shè)計的思想在,因此若是有 C 開發(fā)需求比如嵌入式場景,也可以考慮使用 glib 去進行輔助。
Inline hook 的原理這里就不展開了,但是值得一提的是在構(gòu)造目標函數(shù)的跳板函數(shù)時需要根據(jù)用戶指定的地址去動態(tài)生成跳板代碼,因此使用了 GumWriter 來實現(xiàn);同時因為要備份原函數(shù)還需要使用 GumRelocator 去動態(tài)修復(fù)(重定位)地址相關(guān)的代碼。frida-gum 實現(xiàn)了 5 種指令集的代碼生成和重定向功能(基于 capstone),分別是 arm、arm64、x86、x64 和 mips。以 arm64 為例,其實現(xiàn)在?gum/backend-arm64/guminterceptor-arm64.c?的?_gum_interceptor_backend_create_trampoline?中。
拓展閱讀:
??frida-example[6]
??Profiling C++ code with Frida[7]
??FRIDA-GUM源碼解讀[8]
Stalker
Stalker[9]?又稱為尾行癡漢(?),可以實現(xiàn)指定線程中所有函數(shù)、所有基本塊、甚至所有指令的跟蹤。
一般而言調(diào)試器實現(xiàn)函數(shù)或者指令跟蹤是通過斷點,但是斷點有幾個問題。一是斷點容易被反調(diào)試檢測到,軟件斷點自不必說,會在原指令中插入斷點指令,如果函數(shù)本身有完整性校驗的話會檢測出異常,而硬件斷點本身也很容易被檢測或者破壞掉;斷點的另一個問題是性能,代碼觸發(fā)斷點后會先中斷到內(nèi)核態(tài),然后再返回到用戶態(tài)(調(diào)試器)執(zhí)行跟蹤回調(diào),處理完后再返回內(nèi)核態(tài),然后再回到用戶態(tài)繼續(xù)執(zhí)行,這來來回回的黃花菜都涼了。
因此,Stalker 不是使用斷點,而是基于動態(tài)修改代碼的方式實現(xiàn)。其基本思想很簡單,在線程即將執(zhí)行下一條指令前,先將目標指令拷貝一份到新建的內(nèi)存中,然后在新的內(nèi)存中對代碼進行插樁,如下圖所示:

這其中使用到了代碼動態(tài)重編譯的方法,好處是原本的代碼沒有被修改,因此即便代碼有完整性校驗也不影響,另外由于執(zhí)行過程都在用戶態(tài),省去了多次中斷內(nèi)核切換,性能損耗也達到了可以接受的水平。由于代碼的位置發(fā)生了改變,如前文 Interceptor 一樣,同樣要對代碼進行重定位的修復(fù)。
Stalker 中可以以任意單位基本進行跟蹤,越細粒度的跟蹤性能損耗越高。比如在流量分析時可以只針對 SSL_read、SSL_write 進行跟蹤獲取流量明文;又比如在 Fuzzing 中經(jīng)常以基本塊為單位進行覆蓋率反饋,此時可以以塊為單位進行跟蹤,只對 blx、br、jmp、call 等跳轉(zhuǎn)指令進行插樁;或者針對某些自定義的加密算法,可以以指令級別進行跟蹤,通過動態(tài)運行的路徑記錄來輔助逆向分析還原加密流程,等等等等。
此外 Stalker 中還針對動態(tài)編譯進行了大量的性能優(yōu)化,進一步減少運行時的額外開銷。由于動態(tài)重編譯與系統(tǒng)架構(gòu)關(guān)系較大,代碼中需要對當前平臺的指令集進行準確的歸類和處理,因此當前 Stalker 只支持常用的 ARM64、X86 和 IA32 架構(gòu),而且對于動態(tài)自修改的代碼支持也不完善,但即便如此也足以滿足大部分日常的需求了。
拓展閱讀:
??Anatomy of a code tracer[10]
??frida docs - stalker[11]
內(nèi)存監(jiān)控
frida-gum 中另外一個很有意思的模塊是?MemoryAccessMonitor,可以實現(xiàn)對指定內(nèi)存區(qū)間的訪問監(jiān)控,在目標內(nèi)存區(qū)間發(fā)生讀寫行為時可以觸發(fā)用戶指定的回調(diào)函數(shù)。
通過閱讀源碼發(fā)現(xiàn)這個功能的實現(xiàn)方法非常簡潔,本質(zhì)上是將目標內(nèi)存頁設(shè)置為不可讀寫,這樣在發(fā)生讀寫行為時會觸發(fā)事先注冊好的中斷處理函數(shù),其中會調(diào)用到用戶使用?gum_memory_access_monitor_new?注冊的回調(diào)方法中。
gboolean
gum_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);
??//?...
}事實上目前市面上也有一些加殼的應(yīng)用使用這種方法來進行加固,在?art::DexFile?的某些地址區(qū)間中加上內(nèi)存監(jiān)控的功能,一旦發(fā)現(xiàn)讀取行為就崩潰退出以實現(xiàn)代碼保護的目的。至于效果嘛,就見仁見智了?!?/p>
其他
作為一個 inline-hook 框架,自然還需要有一定的內(nèi)省能力,比如搜索當前虛擬內(nèi)存中已加載的動態(tài)庫信息,在動態(tài)庫中查找符號地址,在內(nèi)存中搜索數(shù)據(jù)/代碼等功能,這些 frida-gum 中都有實現(xiàn),并且支持 Linux、Darwin、FreeBSD、QNX、Windows 等操作系統(tǒng)環(huán)境。在不同平臺中往往有不同的實現(xiàn)方法,比如搜索符號在 Android 中就是通過 linker 的一些內(nèi)部函數(shù)去實現(xiàn)模塊查找并通過解析 ELF 的方式去定位符號。對其中哪些平臺的實現(xiàn)感興趣的,去查看對應(yīng) backend 的代碼即可。
gum-js
frida-gum?雖然功能強大,但由于使用了 C 語言的接口,調(diào)用起來各種宏和 typedef 頗為不便,因此就有了以此為基礎(chǔ)的上層封裝(binding)。目前倉庫中只有兩種語言的封裝,分別是 C++(gumpp) 和 JavaScript(gumjs),后者也是 frida-core 和大多數(shù)人日常使用的接口底座。
大多數(shù)人一開始接觸 frida 應(yīng)該也和筆者一樣很奇怪為什么 frida 使用 JavaScript 作為編寫 hook 的語言,為什么還特地集成了一個 JS 腳本引擎,具體是如何實現(xiàn)的,……
其實這個問題可以簡化成: JS 代碼如何與 native 代碼進行交互?我們以 v8 為例,先看如何從 native 代碼中去解析 JS 腳本。一般來說,v8 解析腳本的流程可以概況如下:
int?main(int?argc,?char*?argv[])?{
??//?1.?初始化?V8.
??v8::V8::InitializeICUDefaultLocation(argv[0]);
??v8::V8::InitializeExternalStartupData(argv[0]);
??std::unique_ptr?platform?=?v8::platform::NewDefaultPlatform();
??v8::V8::InitializePlatform(platform.get());
??v8::V8::Initialize();
??
??//?2.?新建?Isolate?并將其設(shè)為當前默認.
??v8::Isolate::CreateParams?create_params;
??create_params.array_buffer_allocator?=?v8::ArrayBuffer::Allocator::NewDefaultAllocator();
??v8::Isolate*?isolate?=?v8::Isolate::New(create_params);
??{
????v8::Isolate::Scope?isolate_scope(isolate);
????v8::HandleScope?handle_scope(isolate);
????//?3.?創(chuàng)建上下文
????v8::Local?context?=?v8::Context::New(isolate);
????//?4.?進入?JS?腳本編譯和執(zhí)行的上下文
????v8::Context::Scope?context_scope(context);
????//?5.?創(chuàng)建腳本
????v8::Local?source?=
????????v8::String::NewFromUtf8(isolate,?"'Hello'?+?',?World!'",
????????????????????????????????v8::NewStringType::kNormal)
????????????.ToLocalChecked();
????//?6.?編譯腳本
????v8::Local?script?=
????????v8::Script::Compile(context,?source).ToLocalChecked();
????//?7.?執(zhí)行腳本并獲取結(jié)果
????v8::Local?result?=?script->Run(context).ToLocalChecked();
????//?8.?將結(jié)果轉(zhuǎn)換為字符串并打印輸出
????v8::String::Utf8Value?utf8(isolate,?result);
????printf("%s\n",?*utf8);
??}
??//?n.?關(guān)閉?V8,釋放相關(guān)資源
??isolate->Dispose();
??v8::V8::Dispose();
??v8::V8::ShutdownPlatform();
??delete?create_params.array_buffer_allocator; 如果我們不僅僅是從 JS 中獲取執(zhí)行結(jié)果,而是需要向 JS 動態(tài)傳遞參數(shù)呢?比如在 frida 中?Interceptor.attach?的參數(shù)之一實際上就是目標函數(shù)(指令)的 native 地址值,我們需要在 JS 中將這個值進行處理并傳遞到?frida-gum?的?gum_interceptor_attach_listener?函數(shù)中。
這種情況下需要先在 JS 腳本中定義一個函數(shù),姑且稱之為?Attach,前面的步驟都一樣,先創(chuàng)建腳本并編譯執(zhí)行,執(zhí)行之后可以從當前的 Isolate 中獲取到目標函數(shù)對象,進而轉(zhuǎn)換為可以調(diào)用的 Function 類型從而進行調(diào)用,如下所示:
//?1-7?步類似,執(zhí)行腳本
//?定義函數(shù)名稱
Local?attach_name?=
??????String::NewFromUtf8Literal(GetIsolate(),?"Attach");
//?判斷對象是否存在,以及類型是否是函數(shù)
Local?attach_val;
if?(!context->Global()->Get(context,?attach_name).ToLocal(&attach_val)?||?!attach_val->IsFunction())?{
????return?false;
}
//?如果是,則轉(zhuǎn)換為函數(shù)類型
Local?attach_func?=?attach_val.As();
//?將調(diào)用參數(shù)封裝為?JS?對象
Local 有了這個理論基礎(chǔ),后面的實現(xiàn)就是工程化編碼的問題了。早期 gum-js 默認使用?Duktape[12]?作為腳本引擎進行集成,后來也增加了對?QuickJS[13]?和?V8[14]?的支持,實際上 frida 對于不同的腳本引擎也做了一層封裝,可以對不同引擎的接口實現(xiàn)透明的切換。
gum-js 同樣提供了類似的 devkit,里面包含對應(yīng)的接口介紹和示例程序,可自行下載食用。
代碼調(diào)試
軟件有 Bug 其實是在所難免的事情,尤其是對于 frdia 這種偏底層而又復(fù)雜的項目而言,因此在出現(xiàn)未知錯誤時需要能夠通過調(diào)試器或者日志去進行定位。另外從學(xué)習(xí)的角度來說,也希望可以通過實時調(diào)試去加深對于 frida 中一些操作比如 trampoline/shellcode 生成的理解。
首先是調(diào)試。在 frida 主倉庫的?config.mk?中去掉?--strip?以保留符號,或者加上 make 參數(shù)?FRIDA_ASAN=true?加上 ASAN 信息輔助定位內(nèi)存問題。在 hook 代碼中可以加入以下代碼來等待調(diào)試器的掛載:
while?(!gum_process_is_debugger_attached?())
{
??g_printerr?("Waiting?for?debugger?in?PID?%u...\n",?getpid?());
??g_usleep?(G_USEC_PER_SEC);
}或者直接在 JS 代碼中實現(xiàn):
while?(!Process.isDebuggerAttached())?{
??console.log('Waiting?for?debugger?in?PID:',?Process.id);
??Thread.sleep(1);
}除了使用調(diào)試器進行調(diào)試,也可以使用 print 大法在代碼的關(guān)鍵位置插入打印日志來幫助理解代碼的執(zhí)行流程,常用的打印函數(shù)是:
//?輸出到系統(tǒng)日志中
g_info?("Test?%d\n",?__line__);
//?輸出到?stderr?
g_printerr?("Test?%d\n",?__line__);作者還專門寫過一個 gist 來介紹使用日志功能來輔助分析的 Tricks[15],主要是通過 patch 實現(xiàn)向?/data/local/tmp?中寫入指定的日志文件。
后記
本文介紹了 frida 的主要工程結(jié)構(gòu)以及 frida-gum 和 gumjs 這兩大基礎(chǔ)設(shè)施的基本原理。當然文章所提及的也只是冰山一角,感興趣的人自然會去閱讀對應(yīng)模塊的具體實現(xiàn)。希望這個系列能起到個拋磚引玉的作用,讓后來者不至于搜索 frida internal 或者 frida 源碼解析時再經(jīng)歷一遍鄙人的痛苦 :)
參考資料
??frida.re/Hacking[16]
??Ole André Vadla Ravn?s - Frida: The engineering behind the reverse-engineering (Video)[17]
引用鏈接
[1]?Frida Internal - Part 1: 架構(gòu)、Gum 與 V8 (本文):?https://evilpan.com/2022/04/05/frida-internal/[2]?frida:?https://github.com/frida/frida[3]?frida-gum:?https://github.com/frida/frida-gum[4]?Interceptor:?https://github.com/frida/frida-gum/blob/main/gum/guminterceptor.h[5]?glib:?https://wiki.gnome.org/Projects/GLib[6]?frida-example:?https://gist.github.com/oleavr/3edc47c9f69eb048de9d70ed45998f9c[7]?Profiling C++ code with Frida:?https://lief-project.github.io/blog/2021-03-10-profiling-cpp-code-with-frida/[8]?FRIDA-GUM源碼解讀:?http://jmpews.github.io/2017/06/27/pwn/frida-gum%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB/[9]?Stalker:?https://frida.re/docs/stalker[10]?Anatomy of a code tracer:?https://medium.com/@oleavr/anatomy-of-a-code-tracer-b081aadb0df8[11]?frida docs - stalker:?https://frida.re/docs/stalker[12]?Duktape:?https://duktape.org/[13]?QuickJS:?https://bellard.org/quickjs/[14]?V8:?https://v8.dev/docs/[15]?使用日志功能來輔助分析的 Tricks:?https://gist.github.com/oleavr/00d71868d88d597ee322a5392db17af6[16]?frida.re/Hacking:?https://frida.re/docs/hacking/[17]?Ole André Vadla Ravn?s - Frida: The engineering behind the reverse-engineering (Video):?https://www.youtube.com/watch?v=uc1mbN9EJKQ&ab_channel=OSDCNordic
