Frida Internal - Part 2: 核心組件 frida-core
前文已經(jīng)介紹了 frida 中的核心組件?frida-gum?以及對應(yīng)的 js 接口?gum-js,但僅有這些基礎(chǔ)功能并不能讓 frida 成為如此受歡迎的 Instrumentation (hook) 框架。為了實現(xiàn)一個完善框架或者說工具,需要實現(xiàn)許多系統(tǒng)層的功能。比如進程注入、進程間通信、會話管理、腳本生命周期管理等功能,屏蔽部分底層的實現(xiàn)細節(jié)并給最終用戶提供開箱即用的操作接口。而這一切的實現(xiàn)都在?frida-core?之中,正如名字所言,這其中包含了 frida 相關(guān)的大部分關(guān)鍵模塊和組件,比如 frida-server、frida-gadget、frida-agent、frida-helper、frida-inject 以及之間的互相通信底座。本文主要節(jié)選其中關(guān)鍵的部分進行分析和介紹。
前情提要:
Vala
如果曾經(jīng)瀏覽過 frida-core 的代碼,會發(fā)現(xiàn)其大部分源文件都是?.vala?后綴的。這其實是 GNOME 中使用的一個高級語言,和傳統(tǒng)高級語言不同的是 vala 代碼會被編譯器先編譯成 C 代碼,然后再編譯成二進制文件,因此也可以認為 vala 語言是 C 的一個語法糖拓展。
Vala[1]?使用 glib 的?GObject?類型系統(tǒng)來構(gòu)造類和接口以實現(xiàn)面向?qū)ο螅湔Z法有點類似于?C#,支持許多現(xiàn)代語言的高級特性,包括但不限于接口、屬性、內(nèi)存管理、異常、lambda、信號等等。Vala 既可以通過 API 文件訪問已有的 C 庫文件,也可以從 C 中很容易調(diào)用 Vala 的方法。
一個簡單的 Vala 代碼示例如下:
//?test.vala
int?main?(string[]?args)?{
????stdout.printf?("nargs=%d\n",?args.length);
????foreach?(string?arg?in?args)?{
????????stdout.printf?("%s\n",?arg);
????}
????return?0;
}編譯運行也非常簡單:
$?valac?test.vala
$?./test?hello?world
nargs=3
./test
hello
world
$?rabin2?-l?test
[Linked?libraries]
libglib-2.0.so.0
libc.so.6
2?librariesVala 程序也可以直接編譯為 C 代碼,使用?valac -C test.vala?即可生成對應(yīng)的?test.c?文件。更多的示例程序可以查看?Vala 的官方文檔[2]。
接口
在上文中我們介紹了 frida-gum 和 gum-js,這二者都有對應(yīng)的 release devkit 可以下載,frida-core 也一樣。我們在 frida-core-devkit 中可以獲取到編譯好的靜態(tài)庫、頭文件以及簡單的示例程序,下面就以接口為著手點進行分析。
以 Android 平臺為例,最常用的方式是先將?frida-server?推送到設(shè)備端啟動,然后在本地使用?frida-tools?去加載 JS 腳本并執(zhí)行 hook 操作。frida-tools?是基于 Python 的 binding 編寫的,本質(zhì)上還是調(diào)用了?frida-core,連接設(shè)備并加載腳本的過程如下所示:
//?獲取設(shè)備句柄
FridaManager?*manager?=?frida_device_manager_new?();
FridaDeviceList?*devices?=?frida_device_manager_enumerate_devices_sync?(manager,?NULL,?&error);
FridaDevice?*?local_device?=?frida_device_list_get?(devices,?i);
//?連接設(shè)備上的指定進程
FridaSession?*session?=?frida_device_attach_sync?(local_device,?target_pid,?FRIDA_REALM_NATIVE,?NULL,?&error);
//?創(chuàng)建腳本
options?=?frida_script_options_new?();
frida_script_options_set_name?(options,?"example");
frida_script_options_set_runtime?(options,?FRIDA_SCRIPT_RUNTIME_QJS);
script?=?frida_session_create_script_sync?(session,
?????????"console.log('hello');",
?????????options,?NULL,?&error);
//?加載腳本
g_signal_connect?(script,?"message",?G_CALLBACK?(on_message),?NULL);
frida_script_load_sync?(script,?NULL,?&error);雖然這里用的是 C 的接口,但實際上代碼是在?vala?中以類方法的方式定義的,以?frida_device_attach_sync?這個方法為例,其定義在?src/frida.vala?中:
namespace?{
????//?...
????public?class?Device?:?Object?{
????????//?...
????????public?Session?attach_sync?(uint?pid,?SessionOptions??options?=?null,
????????????????Cancellable??cancellable?=?null)?throws?Error,?IOError?{
????????????var?task?=?create<AttachTask>?();
????????????task.pid?=?pid;
????????????task.options?=?options;
????????????return?task.execute?(cancellable);
????????}
????}
}因此 vala 編譯后的 C 代碼符號也是可以比較容易對應(yīng)到實際代碼的,這對于開發(fā)和調(diào)試有很大的幫助。
frida-server
雖然連接、加載腳本的邏輯是在 PC 端編寫和執(zhí)行的,但實際操作還是在設(shè)備端,即?frida-server?所運行的系統(tǒng)中。
該進程的主函數(shù)定義在?server/server.vala?中:
namespace?Frida.Server?{
private?static?int?main?(string[]?args)?{
????Environment.init?();
????//?...
????return?run_application?(endpoint_params,?options,?on_ready);
}
private?static?int?run_application?(EndpointParameters?endpoint_params,?ControlServiceOptions?options,?ReadyHandler?on_ready)?{
????//?...
????TemporaryDirectory.always_use?((directory?!=?null)???directory?:?DEFAULT_DIRECTORY);
????application?=?new?Application?(new?ControlService?(endpoint_params,?options));
????//?...
????return?application.run?();
}
//?...
}Application?實際上是一層簡單的封裝,其?run?方法會間接調(diào)用到?servcie.start,即構(gòu)造函數(shù)中傳入的?ControlService。該類定義在?src/control-service.vala?中,包含 3 個主要的屬性:
??HostSession host_session
??EndpointParameters endpoint_params
??ControlServiceOptions options
其中最重要的是?HostSession,該屬性是一個接口類型,基于面向?qū)ο蟮亩鄳B(tài)方式實現(xiàn)了不同平臺下的實現(xiàn),如下所示:
public?ControlService?(EndpointParameters?endpoint_params,?ControlServiceOptions??options?=?null)?{
????HostSession?host_session;
#if?WINDOWS
????var?tempdir?=?new?TemporaryDirectory?();
????host_session?=?new?WindowsHostSession?(new?WindowsHelperProcess?(tempdir),?tempdir);
#endif
#if?DARWIN
????host_session?=?new?DarwinHostSession?(new?DarwinHelperBackend?(),?new?TemporaryDirectory?(),
????????opts.report_crashes);
#endif
#if?LINUX
????var?tempdir?=?new?TemporaryDirectory?();
????host_session?=?new?LinuxHostSession?(new?LinuxHelperProcess?(tempdir),?tempdir,?opts.report_crashes);
#endif
#if?FREEBSD
????host_session?=?new?FreebsdHostSession?();
#endif
#if?QNX
????host_session?=?new?QnxHostSession?();
#endifHostSession?的接口定義在?lib/base/session.vala?中,接口如下所示:
public?interface?HostSession?:?Object?{
public?abstract?async?void?ping?(uint?interval_seconds,?Cancellable??cancellable)?throws?GLib.Error;
public?abstract?async?HashTableVariant>?query_system_parameters?(Cancellable??cancellable)?throws?GLib.Error;
public?abstract?async?HostApplicationInfo?get_frontmost_application?(HashTableVariant>?options,
????Cancellable??cancellable)?throws?GLib.Error;
public?abstract?async?HostApplicationInfo[]?enumerate_applications?(HashTableVariant>?options,
????Cancellable??cancellable)?throws?GLib.Error;
public?abstract?async?HostProcessInfo[]?enumerate_processes?(HashTableVariant>?options,
????Cancellable??cancellable)?throws?GLib.Error;
public?abstract?async?void?enable_spawn_gating?(Cancellable??cancellable)?throws?GLib.Error;
public?abstract?async?void?disable_spawn_gating?(Cancellable??cancellable)?throws?GLib.Error;
public?abstract?async?HostSpawnInfo[]?enumerate_pending_spawn?(Cancellable??cancellable)?throws?GLib.Error;
public?abstract?async?HostChildInfo[]?enumerate_pending_children?(Cancellable??cancellable)?throws?GLib.Error;
public?abstract?async?uint?spawn?(string?program,?HostSpawnOptions?options,?Cancellable??cancellable)?throws?GLib.Error;
public?abstract?async?void?input?(uint?pid,?uint8[]?data,?Cancellable??cancellable)?throws?GLib.Error;
public?abstract?async?void?resume?(uint?pid,?Cancellable??cancellable)?throws?GLib.Error;
public?abstract?async?void?kill?(uint?pid,?Cancellable??cancellable)?throws?GLib.Error;
public?abstract?async?AgentSessionId?attach?(uint?pid,?HashTableVariant>?options,
????Cancellable??cancellable)?throws?GLib.Error;
public?abstract?async?void?reattach?(AgentSessionId?id,?Cancellable??cancellable)?throws?GLib.Error;
public?abstract?async?InjectorPayloadId?inject_library_file?(uint?pid,?string?path,?string?entrypoint,?string?data,
????Cancellable??cancellable)?throws?GLib.Error;
public?abstract?async?InjectorPayloadId?inject_library_blob?(uint?pid,?uint8[]?blob,?string?entrypoint,?string?data,
????Cancellable??cancellable)?throws?GLib.Error;
public?signal?void?spawn_added?(HostSpawnInfo?info);
public?signal?void?spawn_removed?(HostSpawnInfo?info);
public?signal?void?child_added?(HostChildInfo?info);
public?signal?void?child_removed?(HostChildInfo?info);
public?signal?void?process_crashed?(CrashInfo?crash);
public?signal?void?output?(uint?pid,?int?fd,?uint8[]?data);
public?signal?void?agent_session_detached?(AgentSessionId?id,?SessionDetachReason?reason,?CrashInfo?crash);
public?signal?void?uninjected?(InjectorPayloadId?id);
} 其中包括了應(yīng)用枚舉、進程查找、進程注入、進程啟動以及各類信號回調(diào)的接口原型,基于這些接口實現(xiàn)對目標進程的劫持和動態(tài)修改。由于我們重點是分析 Android 系統(tǒng)上的實現(xiàn),因此后文主要分析?LinuxHostSession,對于其他平臺的分析也是類似的。
LinuxHostSession?的代碼在?src/linux/linux-host-session.vala?中,其中定義了幾個重要的屬性,分別是:
??injector: 類型為 Linjector
??agent: 類型為 AgentDescriptor
??system_server_agent: 類型為 SystemServerAgent
??robo_launcher: 類型為 RoboLauncher
它們都在構(gòu)造函數(shù)中進行初始化,分別實現(xiàn)了進程注入、server 與注入代碼之間的進程間通信以及遠程調(diào)用行為,下文會分別進行簡要介紹。
進程注入
從接口名稱來看,進程注入的實現(xiàn)大概率是?inject_library_file,表示注入一個動態(tài)庫到目標進程中。所注入的動態(tài)庫稱為 agent。
回到注入的實現(xiàn)上,frida-server 是怎么實現(xiàn)進程注入的呢?根據(jù)已有經(jīng)驗,由于我們以 root 權(quán)限啟動,在 Linux 中一般有多種實現(xiàn)進程注入的方式,比如 ptrace、/proc/pid/mem,DLL/SO 劫持、LD_PERLOAD 等,因此實際實現(xiàn)還要回到代碼。
Android 中實現(xiàn)進程注入的方法如下:
protected?override?async?Future<IOStream>?perform_attach_to?(uint?pid,?HashTableVariant>?options,
????????Cancellable??cancellable,?out?Object??transport)?throws?Error,?IOError?{
????PipeTransport.set_temp_directory?(tempdir.path);
????var?t?=?new?PipeTransport?();
????var?stream_request?=?Pipe.open?(t.local_address,?cancellable);
????uint?id;
????string?entrypoint?=?"frida_agent_main";
????string?agent_parameters?=?make_agent_parameters?(t.remote_address,?options);
????var?linjector?=?injector?as?Linjector;
#if?HAVE_EMBEDDED_ASSETS
????id?=?yield?linjector.inject_library_resource?(pid,?agent,?entrypoint,?agent_parameters,?cancellable);
#else
????id?=?yield?linjector.inject_library_file?(pid,?Config.FRIDA_AGENT_PATH,?entrypoint,?agent_parameters,?cancellable);
#endif
????injectee_by_pid[pid]?=?id;
????transport?=?t;
????return?stream_request;
} 確實是使用了?inject_library_xxx?的方式,HAVE_EMBEDDED_ASSETS?為真時使用的是從自身代碼中釋放出來的 agent 庫,位置在:
agent?=?new?AgentDescriptor?(PathTemplate?("frida-agent-.so" ),
????new?Bytes.static?(blob32.data),
????new?Bytes.static?(blob64.data),
????new?AgentResource[]?{
????????new?AgentResource?("frida-agent-arm.so",?new?Bytes.static?(emulated_arm.data),?tempdir),
????????new?AgentResource?("frida-agent-arm64.so",?new?Bytes.static?(emulated_arm64.data),?tempdir),
????},
????AgentMode.INSTANCED,
????tempdir);這也是 Android 平臺中 frida-server 的默認實現(xiàn)。
Linjector?定義在?src/linux/linjector.vala?,即 Linux 平臺上通用的進程注入實現(xiàn),調(diào)用鏈路為:
??inject_library_resource
??inject_library_file_with_template
??helper.inject_library_file
??helper.
_do_inject?(src/linux/frida-helper-backend.vala)
_do_inject?不使用 vala 而是直接使用 C 實現(xiàn),代碼在?src/linux/frida-helper-backend-glue.c?:
void
_frida_linux_helper_backend_do_inject?(FridaLinuxHelperBackend?*?self,?guint?pid,?const?gchar?*?path,?const?gchar?*?entrypoint,?const?gchar?*?data,?const?gchar?*?temp_path,?guint?id,?GError?**?error)
{
??params.open_impl?=?frida_resolve_libc_function?(pid,?"open");
??params.close_impl?=?frida_resolve_libc_function?(pid,?"close");
??params.write_impl?=?frida_resolve_libc_function?(pid,?"write");
??params.syscall_impl?=?frida_resolve_libc_function?(pid,?"syscall");
#if?defined?(HAVE_GLIBC)
//?...
#elif?defined?(HAVE_UCLIBC)
//?...
#elif?defined?(HAVE_ANDROID)
??params.dlopen_impl?=?frida_resolve_android_dlopen?(pid);
??params.dlopen_flags?=?RTLD_LAZY;
??params.dlclose_impl?=?frida_resolve_linker_address?(pid,?dlclose);
??params.dlsym_impl?=?frida_resolve_linker_address?(pid,?dlsym);
#endif
??instance?=?frida_inject_instance_new?(self,?id,?pid,?temp_path);
??frida_inject_instance_attach?(instance,?&saved_regs,?error);
??//?...
??frida_inject_instance_start_remote_thread?(instance,?&exited,?error);
??frida_inject_instance_detach?(instance,?&saved_regs,?NULL);
}frida_inject_instance_attach?中定義了真正的 attach 實現(xiàn):
static?gboolean
frida_inject_instance_attach?(FridaInjectInstance?*?self,?FridaRegs?*?saved_regs,?GError?**?error)
{
??const?pid_t?pid?=?self->pid;
??if?(can_seize)
????ret?=?ptrace?(PTRACE_SEIZE,?pid,?NULL,?PTRACE_O_TRACEEXEC);
??else
????ret?=?ptrace?(PTRACE_ATTACH,?pid,?NULL,?NULL);
??//?...
}因此我們可以得出結(jié)論,frida 的進程注入是通過 ptrace 實現(xiàn)的,注入之后會在遠端進程分配一段內(nèi)存將 agent 拷貝過去并在目標進程中執(zhí)行代碼,執(zhí)行完成后會 detach 目標進程。
這也是為什么在 frida 先連接上目標進程后還可以用 gdb 等調(diào)試器連接,而先 gdb 連接進程后 frida 就無法再次連上的原因。
frida-agent?注入到目標進程并啟動后會啟動一個新進程與 host 進行通信,從而 host 可以給目標進行發(fā)送命令,比如執(zhí)行代碼,激活/關(guān)閉 hook,同時也能接收到目標進程的執(zhí)行返回以及異步事件信息等。
frida-agent
在上節(jié)中調(diào)用?inject_library?指定了注入動態(tài)庫后執(zhí)行的的函數(shù)符號為?frida_agent_main,該函數(shù)也是由 vala 生成而來,源文件定義在?lib/agent/agent.vala?中:
namespace?Frida.Agent?{
????public?void?main?(string?agent_parameters,?ref?Frida.UnloadPolicy?unload_policy,?void?*?injector_state)?{
????????if?(Runner.shared_instance?==?null)
????????????Runner.create_and_run?(agent_parameters,?ref?unload_policy,?injector_state);
????????else
????????????Runner.resume_after_transition?(ref?unload_policy,?injector_state);
????}
????//?...關(guān)鍵的調(diào)用路徑如下:
??Runner.create_and_run
??new Runner (...)
??shared_instance.run
??Runner.run
??Runner.start
其中?Runner.run?函數(shù)在調(diào)用完?start?后會進入?main_loop?循環(huán),直至進程退出或者收到 server 的解除命令。
Runner.start?的作用是準備 Interceptor 以及 GumJS 的 ScriptBackend,并連接到啟動時指定的?transport_uri?建立通信隧道。
system_server
另外一個我覺得比較有意思的是 frida-server 中除了注入到目標進程的 agent,還有一個 agent,即?system_server_agent。
用過?frida-tools?的朋友應(yīng)該都知道其中有個?frida-ps?命令,可以查看設(shè)備中的應(yīng)用名稱、包名甚至是 Icon,也可以查看當前運行在窗口頂部的應(yīng)用。之前一直以為是通過?pm list packages?和?dumpsys?等命令實現(xiàn)的,看過代碼之后才發(fā)現(xiàn)原來 frida-server 還對?system_server?進程進行了注入,并且所使用的 agent 就是?system_server_agent。
如果不了解?system_server,可以先回顧一下筆者之前的文章 "Android 用戶態(tài)啟動流程分析",其中提到在 Android 系統(tǒng)啟動時,zygote 啟動的第一個進程就是?system_server,這也是系統(tǒng)中第一個啟動的 Java 進程,其中包含了?ActivityManagerService、?PackageManagerService?等系統(tǒng)服務(wù)。frida-server 對其進行注入,一方面是為了獲取系統(tǒng)中的應(yīng)用信息,另一方面也是為了可以實現(xiàn)?spawn?方式啟動應(yīng)用的功能。
以獲取當前窗口中展示在最上層的應(yīng)用功能為例,接口為?get_frontmost_application,最終的實現(xiàn)在?SystemServerAgent:
public?async?HostApplicationInfo?get_frontmost_application?(FrontmostQueryOptions?options,
????????Cancellable??cancellable)?throws?Error,?IOError?{
????var?scope?=?options.scope;
????var?scope_node?=?new?Json.Node.alloc?().init_string?(scope.to_nick?());
????Json.Node?result?=?yield?call?("getFrontmostApplication",?new?Json.Node[]?{?scope_node?},?cancellable);
????//?...
}這實際上是調(diào)用了?src/linux/agent/system-server.js?中導(dǎo)出的方法:
rpc.exports?=?{
??getFrontmostApplication(scope)?{
????return?performOnJavaVM(()?=>?{
??????const?pkgName?=?getFrontmostPackageName();
??????if?(pkgName?===?null)
????????return?null;
??????const?appInfo?=?packageManager.getApplicationInfo(pkgName,?0);
??????const?appLabel?=?loadAppLabel.call(appInfo,?packageManager).toString();
??????const?pid?=?computeAppPids(getAppProcesses()).get(pkgName)????0;
??????const?parameters?=?(scope?!==?'minimal')???fetchAppParameters(pkgName,?appInfo,?scope)?:?null;
??????return?[pkgName,?appLabel,?pid,?parameters];
????});
??}
??//?...
}這部分 JS 代碼也是內(nèi)置到 frida-server 中的,基于 gum-js 實現(xiàn)了對?system_server?的 Java 代碼調(diào)用和劫持功能。
frida-gadget
除了?frida-server,另外一個比較常用的模塊就是?frida-gadget[3]?了。攻擊者可以通過重打包修改動態(tài)庫的依賴或者修改 smali 代碼去實現(xiàn)向三方應(yīng)用注入 gadget,當然也可以通過任意其他加載動態(tài)庫的方式。
gadget 本身是一個動態(tài)庫,在加載到目標進程中后會馬上觸發(fā) ctor 執(zhí)行指定代碼,默認情況下是掛起當前進程并監(jiān)聽在 27042 端口等待 Host 的連接并恢復(fù)運行。其文件路徑為?lib/gadget/gadget.vala?,啟動入口為?Frida.Gadget.load:
public?void?load?(Gum.MemoryRange??mapped_range,?string??config_data,?int?*?result)?{
????location?=?detect_location?(mapped_range);
????config?=?(config_data?!=?null)
??????????????????parse_config?(config_data)
????????????????:?load_config?(location);
????Gum.Cloak.add_range?(location.range);
????interceptor?=?Gum.Interceptor.obtain?();
????interceptor.begin_transaction?();
????exceptor?=?Gum.Exceptor.obtain?();
????//?try-catch
????var?interaction?=?config.interaction;
????if?(interaction?is?ScriptInteraction)?{
????????controller?=?new?ScriptRunner?(config,?location);
????}?else?if?(interaction?is?ScriptDirectoryInteraction)?{
????????controller?=?new?ScriptDirectoryRunner?(config,?location);
????}?else?if?(interaction?is?ListenInteraction)?{
????????controller?=?new?ControlServer?(config,?location);
????}?else?if?(interaction?is?ConnectInteraction)?{
????????controller?=?new?ClusterClient?(config,?location);
????}?else?{
????????//?throw?"Invalid?interaction?specified"
????}
????interceptor.end_transaction?();
????if?(!wait_for_resume_needed)
????????resume?();
????//?..
????start?(request);
}正如文檔中所說,Gadget 啟動時會去指定路徑搜索配置文件,并根據(jù)其設(shè)置進入不同的運行模式,默認的配置文件如下所示:
{
??"interaction":?{
????"type":?"listen",
????"address":?"127.0.0.1",
????"port":?27042,
????"on_port_conflict":?"fail",
????"on_load":?"wait"
??}
}即使用?listen?模式,監(jiān)聽在 27042 端口并等待連接。除了 listen 以外,還支持以下幾種模式:
??connect: Gadget 啟動后主動連接到指定地址;
??script: 啟動后直接加載指定的 JavaScript 文件;
??script-directory: 啟動后加載指定目錄下的所有 JavaScript 文件;
值得一提的是,script-directory?模式可以用來實現(xiàn)系統(tǒng)級別的插件功能,比如在指定目錄中編寫一個?twitter.js,用于實現(xiàn)針對 Twitter 的 Hook 功能,比如自定義過濾、自動點贊等等;另外寫一個?twitter.config?來過濾應(yīng)用:
{
??"filter":?{
????"executables":?["Twitter"],
????"bundles":?["com.twitter.twitter-mac"],
????"objc_classes":?["Twitter"]
??}
}這樣就只有滿足指定條件的進程才會加載?twitter.js?腳本,從而實現(xiàn)特定應(yīng)用的持久化 Hook 功能。熟悉 Xposed 或者 Tweak 開發(fā)的應(yīng)該都知道這意味著什么。
IPC 通信
在 frida-core 中有許多需要進程間通信的行為,比如 frida-server 需要與注入到目標進程中的 agent 進行通信,通知目標進程開啟或者關(guān)閉 Interceptor;agent 同樣也需要與 host 進行通信,在 gum-js 中將?console.log?或者?send?的消息發(fā)送給 host,或者接收一些異步的應(yīng)用退出和異常事件等。而其中這些進程間的交互都是通過?D-Bus[4]?去實現(xiàn)的。
D-Bus?是一種基于消息的進程間通信機制,全稱為 Desktop Bus,最初從 FreeDesktop 中的模塊獨立出來。其主要作用是提供在 Linux 操作系統(tǒng)桌面環(huán)境中的組件通信,比如 GNOME 或 KDE。D-Bus 使用 C 語言開發(fā),提供了 GLib、Qt、Python 等編程接口,在 frida-core 中主要使用其 Vala 接口進行集成。
下面從實際代碼看一個 D-Bus Vala 接口實現(xiàn)的 C/S 通信示例。首先是服務(wù)端,定義一個?DemoServer, 內(nèi)部定義了四個方法:
[DBus?(name?=?"org.example.Demo")]
public?class?DemoServer?:?Object?{
????private?int?counter;
????public?int?ping?(string?msg)?{
????????stdout.printf?("%s\n",?msg);
????????return?counter++;
????}
????public?int?ping_with_signal?(string?msg)?{
????????stdout.printf?("%s\n",?msg);
????????pong(counter,?msg);
????????return?counter++;
????}
????/*?Including?any?parameter?of?type?GLib.BusName?won't?be?added?to?the
???????interface?and?will?return?the?dbus?sender?name?(who?is?calling?the?method)?*/
????public?int?ping_with_sender?(string?msg,?GLib.BusName?sender)?{
????????stdout.printf?("%s,?from:?%s\n",?msg,?sender);
????????return?counter++;
????}
????public?void?ping_error?()?throws?Error?{
????????throw?new?DemoError.SOME_ERROR?("There?was?an?error!");
????}
????public?signal?void?pong?(int?count,?string?msg);
}
[DBus?(name?=?"org.example.DemoError")]
public?errordomain?DemoError
{
????SOME_ERROR
}
void?on_bus_aquired?(DBusConnection?conn)?{
????try?{
????????conn.register_object?("/org/example/demo",?new?DemoServer?());
????}?catch?(IOError?e)?{
????????stderr.printf?("Could?not?register?service\n");
????}
}
void?main?()?{
????Bus.own_name?(BusType.SESSION,?"org.example.Demo",?BusNameOwnerFlags.NONE,
??????????????????on_bus_aquired,
??????????????????()?=>?{},
??????????????????()?=>?stderr.printf?("Could?not?aquire?name\n"));
????new?MainLoop?().run?();
}其中使用?Bus.own_name?去連接總線?org.example.Demo,連接成功后注冊了名為?/org/example/demo?的服務(wù)。客戶端可以通過連接同樣的總線和服務(wù)去獲取到對應(yīng)的遠程對象接口,從而在本地實現(xiàn)遠程調(diào)用。客戶端示例如下:
[DBus?(name?=?"org.example.Demo")]
interface?Demo?:?Object?{
????public?abstract?int?ping?(string?msg)?throws?IOError;
????public?abstract?int?ping_with_sender?(string?msg)?throws?IOError;
????public?abstract?int?ping_with_signal?(string?msg)?throws?IOError;
????public?signal?void?pong?(int?count,?string?msg);
}
void?main?()?{
????/*?Needed?only?if?your?client?is?listening?to?signals;?you?can?omit?it?otherwise?*/
????var?loop?=?new?MainLoop();
????/*?Important:?keep?demo?variable?out?of?try/catch?scope?not?lose?signals!?*/
????Demo?demo?=?null;
????try?{
????????demo?=?Bus.get_proxy_sync?(BusType.SESSION,?"org.example.Demo",
????????????????????????????????????????????????????"/org/example/demo");
????????/*?Connecting?to?signal?pong!?*/
????????demo.pong.connect((c,?m)?=>?{
????????????stdout.printf?("Got?pong?%d?for?msg?'%s'\n",?c,?m);
????????????loop.quit?();
????????});
????????int?reply?=?demo.ping?("Hello?from?Vala");
????????stdout.printf?("%d\n",?reply);
????????reply?=?demo.ping_with_sender?("Hello?from?Vala?with?sender");
????????stdout.printf?("%d\n",?reply);
????????reply?=?demo.ping_with_signal?("Hello?from?Vala?with?signal");
????????stdout.printf?("%d\n",?reply);
????}?catch?(IOError?e)?{
????????stderr.printf?("%s\n",?e.message);
????}
????loop.run();
}客戶端中需要定義與遠程對象一致的?interface,并通過?get_proxy?請求獲得遠程對象的本地代理,從而實現(xiàn)透明的遠程調(diào)用。注意的是編譯需要添加對應(yīng)的庫:
$?valac?--pkg?gio-2.0?demo-server.vala
$?valac?--pkg?gio-2.0?demo-client.vala從效果上看有點類似于 Android 的 binder 通信機制。但是 D-Bus 是一種更加上層的封裝,在不同操作系統(tǒng)上可以使用不同的底層實現(xiàn)。在 Linxu 操作系統(tǒng)中通常是基于 UNIX socket 實現(xiàn)的多進程通信,當然也可以修改配置去通過 TCP socket 實現(xiàn)。
frida 執(zhí)行進程注入時同時也實現(xiàn)了 IPC 相關(guān)的初始化,在?src/host-session-service.vala?中可以看到關(guān)鍵代碼如下:
private?async?AgentEntry?establish?(uint?pid,?HashTableVariant>?options,
????????Cancellable??cancellable)?throws?Error,?IOError?{
????//?...
????DBusConnection?connection;
????var?stream_request?=?yield?perform_attach_to?(pid,?options,?io_cancellable,?out?transport);
????IOStream?stream?=?yield?stream_request.wait_async?(io_cancellable);
????connection?=?yield?new?DBusConnection?(
????????stream,
????????ServerGuid.HOST_SESSION_SERVICE,
????????AUTHENTICATION_SERVER?|?AUTHENTICATION_ALLOW_ANONYMOUS?|?DELAY_MESSAGE_PROCESSING,
????????null,?io_cancellable);
????controller_registration_id?=?connection.register_object?(
????????ObjectPath.AGENT_CONTROLLER,
????????(AgentController)?this);
????connection.start_message_processing?();
????provider?=?yield?connection.get_proxy?(null,?ObjectPath.AGENT_SESSION_PROVIDER,
????????DO_NOT_LOAD_PROPERTIES,?io_cancellable);
} 這里使用了?DBusConnection?來連接總線,效果和?own_name?是類似的。其中總線的名稱為?ServerGuid.HOST_SESSION_SERVICE,所注冊的 RPC 服務(wù)名稱為?ObjectPath.AGENT_CONTROLLER。另外這里還通過總線去獲取了一個遠程對象?ObjectPath.AGENT_SESSION_PROVIDER,用以實現(xiàn)透明的遠程調(diào)用。這些服務(wù)的名稱都定義在?lib/base/session.vala?文件中:
namespace?Frida?{
????//?...
????namespace?ServerGuid?{
????????public?const?string?HOST_SESSION_SERVICE?=?"6769746875622e636f6d2f6672696461";
????}
????namespace?ObjectPath?{
????????public?const?string?HOST_SESSION?=?"/re/frida/HostSession";
????????public?const?string?AGENT_SESSION_PROVIDER?=?"/re/frida/AgentSessionProvider";
????????public?const?string?AGENT_SESSION?=?"/re/frida/AgentSession";
????????public?const?string?AGENT_CONTROLLER?=?"/re/frida/AgentController";
????????public?const?string?AGENT_MESSAGE_SINK?=?"/re/frida/AgentMessageSink";
????????public?const?string?CHILD_SESSION?=?"/re/frida/ChildSession";
????????public?const?string?TRANSPORT_BROKER?=?"/re/frida/TransportBroker";
????????public?const?string?PORTAL_SESSION?=?"/re/frida/PortalSession";
????????public?const?string?BUS_SESSION?=?"/re/frida/BusSession";
????????public?const?string?AUTHENTICATION_SERVICE?=?"/re/frida/AuthenticationService";
????}
}通過搜索?[DBus (name = "xxx")]?可以看到 frida-core 中定義的所有 IPC 服務(wù),感興趣的可以有針對性地去進行深入分析。
拓展閱讀:
??https://dbus.freedesktop.org/doc/dbus-tutorial.html
??https://wiki.gnome.org/Projects/Vala/DBusServerSample
??https://www.reddit.com/r/commandline/comments/13o581/dbus_vs_unix_sockets/
SELinux
在?Android/Linux Root 的那些事兒?中曾經(jīng)介紹過 Android 系統(tǒng)的安全訪問控制體系,不是簡單的?uid=0?就可以為所欲為的。frida 這種又注入進程又各種進程間通信的,顯然違反了 SELinux 的默認規(guī)則,那么它是如何實現(xiàn)的呢?其實說起來也很簡單,就是對當前系統(tǒng)的 SELinux 規(guī)則進行了 patch,本節(jié)就來分析下其具體是如何做的。
patch 的代碼主要集中在?lib/selinux/patch.c?文件中,代碼就不貼了,主要流程可以簡單描述如下:
1.?從?/sys/fs/selinux/policy?文件中加載當前系統(tǒng)的 SELinux policy;
2.?在當前 sepolicy 中增加新的 SELinux 文件類型?
frida_file;3. 依次插入新的 SELinux 規(guī)則(見后文);
4.?通過寫入?/sys/fs/selinux/load?文件保存修改后的 sepolicy;
5. (可選) 如果上述保存操作失敗,就先臨時將 SELinux 設(shè)置為 permissive 然后再保存一次,保存成功后重新恢復(fù)為 enforcing;
第 3 步中插入的 SELinux 規(guī)則如下:
typedef?struct?_FridaSELinuxRule?FridaSELinuxRule;
struct?_FridaSELinuxRule
{
??const?gchar?*?sources[4];
??const?gchar?*?target;
??const?gchar?*?klass;
??const?gchar?*?permissions[16];
};
static?const?FridaSELinuxRule?frida_selinux_rules[]?=
{
??{?{?"domain",?NULL?},?"domain",?"process",?{?"execmem",?NULL?}?},
??{?{?"domain",?NULL?},?"frida_file",?"dir",?{?"search",?NULL?}?},
??{?{?"domain",?NULL?},?"frida_file",?"fifo_file",?{?"open",?"write",?NULL?}?},
??{?{?"domain",?NULL?},?"frida_file",?"file",?{?"open",?"read",?"getattr",?"execute",?"?map",?NULL?}?},
??{?{?"domain",?NULL?},?"frida_file",?"sock_file",?{?"write",?NULL?}?},
??{?{?"domain",?NULL?},?"shell_data_file",?"dir",?{?"search",?NULL?}?},
??{?{?"domain",?NULL?},?"zygote_exec",?"file",?{?"execute",?NULL?}?},
??{?{?"domain",?NULL?},?"$self",?"process",?{?"sigchld",?NULL?}?},
??{?{?"domain",?NULL?},?"$self",?"fd",?{?"use",?NULL?}?},
??{?{?"domain",?NULL?},?"$self",?"unix_stream_socket",?{?"connectto",?"read",?"write",?"getattr",?"getopt",?NULL?}?},
??{?{?"domain",?NULL?},?"$self",?"tcp_socket",?{?"read",?"write",?"getattr",?"getopt",?NULL?}?},
??{?{?"zygote",?NULL?},?"zygote",?"capability",?{?"sys_ptrace",?NULL?}?},
??{?{?"?app_zygote",?NULL?},?"zygote_exec",?"file",?{?"read",?NULL?}?},
};其中?$self?表示當前進程的 context,即 getcon 獲取的值。在完成對 sepolicy 的動態(tài)修改后,frida 就可以正常的在 enforcing 的環(huán)境下運行了。
后記
frida-core?在?gum-js?的基礎(chǔ)上實現(xiàn)了操作系統(tǒng)層面的主要功能,比如進程注入、進程間通信、系統(tǒng)信息采集、系統(tǒng)權(quán)限管理等功能。正是因為這些核心組件的加入,使得 frida 成為了一個上手簡單卻又功能強大的動態(tài)分析框架。另外由于這些核心組件大部分使用 Vala 語言編寫,在保障內(nèi)存安全性的同時也提供了豐富的跨平臺能力,支持 Android、iOS、Linux、MacOS、Windows 這些主流操作系統(tǒng)的不同架構(gòu)。雖然本文僅針對 Android/Linux 操作系統(tǒng)的主要組件進行了介紹,但相信從中也能看出 frida-core 項目本身的代碼風(fēng)格架構(gòu)設(shè)計,對于其他組件或者其他操作系統(tǒng)上的實現(xiàn)也是大同小異的,后續(xù)遇到具體問題的時候再深入分析對應(yīng)組件即可。
引用鏈接
[1]?Vala:?https://wiki.gnome.org/Projects/Vala/[2]?Vala Sample:?https://wiki.gnome.org/Projects/Vala/Documentation#Sample_Code[3]?frida-gadget:?https://frida.re/docs/gadget/[4]?D-Bus:?https://en.wikipedia.org/wiki/D-Bus
