Frida Internal - Part 2: 核心组件 frida-core
前文已经介绍了 frida 中的核心组件frida-gum以及对应的 js 接口gum-js,但仅有这些基础功能并不能让 frida 成为如此受欢迎的 Instrumentation (hook) 框架。为了实现一个完善框架或者说工具,需要实现许多系统层的功能。比如进程注入、进程间通信、会话管理、脚本生命周期管理等功能,屏蔽部分底层的实现细节并给最终用户提供开箱即用的操作接口。而这一切的实现都在frida-core之中,正如名字所言,这其中包含了 frida 相关的大部分关键模块和组件,比如 frida-server、frida-gadget、frida-agent、frida-helper、frida-inject 以及之间的互相通信底座。本文主要节选其中关键的部分进行分析和介绍。
前情提要:
•Frida Internal - Part 1: 架构、Gum 与 V8
Vala如果曾经浏览过 frida-core 的代码,会发现其大部分源文件都是.vala后缀的。这其实是 GNOME 中使用的一个高级语言,和传统高级语言不同的是 vala 代码会被编译器先编译成 C 代码,然后再编译成二进制文件,因此也可以认为 vala 语言是 C 的一个语法糖拓展。
Vala[1]使用 glib 的GObject类型系统来构造类和接口以实现面向对象,其语法有点类似于C#,支持许多现代语言的高级特性,包括但不限于接口、属性、内存管理、异常、lambda、信号等等。Vala 既可以通过 API 文件访问已有的 C 库文件,也可以从 C 中很容易调用 Vala 的方法。
一个简单的 Vala 代码示例如下:
//test.valaintmain(string[]args){stdout.printf("nargs=%d\n",args.length);foreach(stringarginargs){stdout.printf("%s\n",arg);}return0;}
编译运行也非常简单:
$valactest.vala$./testhelloworldnargs=3./testhelloworld$rabin2-ltest[Linkedlibraries]libglib-2.0.so.0libc.so.62libraries
Vala 程序也可以直接编译为 C 代码,使用valac -C test.vala即可生成对应的test.c文件。更多的示例程序可以查看Vala 的官方文档[2]。
接口在上文中我们介绍了 frida-gum 和 gum-js,这二者都有对应的 release devkit 可以下载,frida-core 也一样。我们在 frida-core-devkit 中可以获取到编译好的静态库、头文件以及简单的示例程序,下面就以接口为着手点进行分析。
以 Android 平台为例,最常用的方式是先将frida-server推送到设备端启动,然后在本地使用frida-tools去加载 JS 脚本并执行 hook 操作。frida-tools是基于 Python 的 binding 编写的,本质上还是调用了frida-core,连接设备并加载脚本的过程如下所示:
//获取设备句柄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);//连接设备上的指定进程FridaSession*session=frida_device_attach_sync(local_device,target_pid,FRIDA_REALM_NATIVE,NULL,&error);//创建脚本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{//...publicclassDevice:Object{//...publicSessionattach_sync(uintpid,SessionOptions?options=null,Cancellable?cancellable=null)throwsError,IOError{vartask=create();task.pid=pid;task.options=options;returntask.execute(cancellable);}}}
因此 vala 编译后的 C 代码符号也是可以比较容易对应到实际代码的,这对于开发和调试有很大的帮助。
frida-server虽然连接、加载脚本的逻辑是在 PC 端编写和执行的,但实际操作还是在设备端,即frida-server所运行的系统中。
该进程的主函数定义在server/server.vala中:
namespaceFrida.Server{privatestaticintmain(string[]args){Environment.init();//...returnrun_application(endpoint_params,options,on_ready);}privatestaticintrun_application(EndpointParametersendpoint_params,ControlServiceOptionsoptions,ReadyHandleron_ready){//...TemporaryDirectory.always_use((directory!=null)?directory:DEFAULT_DIRECTORY);application=newApplication(newControlService(endpoint_params,options));//...returnapplication.run();}//...}
Application实际上是一层简单的封装,其run方法会间接调用到servcie.start,即构造函数中传入的ControlService。该类定义在src/control-service.vala中,包含 3 个主要的属性:
•HostSession host_session
•EndpointParameters endpoint_params
•ControlServiceOptions options
其中最重要的是HostSession,该属性是一个接口类型,基于面向对象的多态方式实现了不同平台下的实现,如下所示:
publicControlService(EndpointParametersendpoint_params,ControlServiceOptions?options=null){HostSessionhost_session;#ifWINDOWSvartempdir=newTemporaryDirectory();host_session=newWindowsHostSession(newWindowsHelperProcess(tempdir),tempdir);#endif#ifDARWINhost_session=newDarwinHostSession(newDarwinHelperBackend(),newTemporaryDirectory(),opts.report_crashes);#endif#ifLINUXvartempdir=newTemporaryDirectory();host_session=newLinuxHostSession(newLinuxHelperProcess(tempdir),tempdir,opts.report_crashes);#endif#ifFREEBSDhost_session=newFreebsdHostSession();#endif#ifQNXhost_session=newQnxHostSession();#endif
HostSession的接口定义在lib/base/session.vala中,接口如下所示:
publicinterfaceHostSession:Object{publicabstractasyncvoidping(uintinterval_seconds,Cancellable?cancellable)throwsGLib.Error;publicabstractasyncHashTablequery_system_parameters(Cancellable?cancellable)throwsGLib.Error;publicabstractasyncHostApplicationInfoget_frontmost_application(HashTable options,Cancellable?cancellable)throwsGLib.Error;publicabstractasyncHostApplicationInfo[]enumerate_applications(HashTable options,Cancellable?cancellable)throwsGLib.Error;publicabstractasyncHostProcessInfo[]enumerate_processes(HashTable options,Cancellable?cancellable)throwsGLib.Error;publicabstractasyncvoidenable_spawn_gating(Cancellable?cancellable)throwsGLib.Error;publicabstractasyncvoiddisable_spawn_gating(Cancellable?cancellable)throwsGLib.Error;publicabstractasyncHostSpawnInfo[]enumerate_pending_spawn(Cancellable?cancellable)throwsGLib.Error;publicabstractasyncHostChildInfo[]enumerate_pending_children(Cancellable?cancellable)throwsGLib.Error;publicabstractasyncuintspawn(stringprogram,HostSpawnOptionsoptions,Cancellable?cancellable)throwsGLib.Error;publicabstractasyncvoidinput(uintpid,uint8[]data,Cancellable?cancellable)throwsGLib.Error;publicabstractasyncvoidresume(uintpid,Cancellable?cancellable)throwsGLib.Error;publicabstractasyncvoidkill(uintpid,Cancellable?cancellable)throwsGLib.Error;publicabstractasyncAgentSessionIdattach(uintpid,HashTable options,Cancellable?cancellable)throwsGLib.Error;publicabstractasyncvoidreattach(AgentSessionIdid,Cancellable?cancellable)throwsGLib.Error;publicabstractasyncInjectorPayloadIdinject_library_file(uintpid,stringpath,stringentrypoint,stringdata,Cancellable?cancellable)throwsGLib.Error;publicabstractasyncInjectorPayloadIdinject_library_blob(uintpid,uint8[]blob,stringentrypoint,stringdata,Cancellable?cancellable)throwsGLib.Error;publicsignalvoidspawn_added(HostSpawnInfoinfo);publicsignalvoidspawn_removed(HostSpawnInfoinfo);publicsignalvoidchild_added(HostChildInfoinfo);publicsignalvoidchild_removed(HostChildInfoinfo);publicsignalvoidprocess_crashed(CrashInfocrash);publicsignalvoidoutput(uintpid,intfd,uint8[]data);publicsignalvoidagent_session_detached(AgentSessionIdid,SessionDetachReasonreason,CrashInfocrash);publicsignalvoiduninjected(InjectorPayloadIdid);}
其中包括了应用枚举、进程查找、进程注入、进程启动以及各类信号回调的接口原型,基于这些接口实现对目标进程的劫持和动态修改。由于我们重点是分析 Android 系统上的实现,因此后文主要分析LinuxHostSession,对于其他平台的分析也是类似的。
LinuxHostSession的代码在src/linux/linux-host-session.vala中,其中定义了几个重要的属性,分别是:
•injector: 类型为 Linjector
•agent: 类型为 AgentDescriptor
•system_server_agent: 类型为 SystemServerAgent
•robo_launcher: 类型为 RoboLauncher
它们都在构造函数中进行初始化,分别实现了进程注入、server 与注入代码之间的进程间通信以及远程调用行为,下文会分别进行简要介绍。
进程注入从接口名称来看,进程注入的实现大概率是inject_library_file,表示注入一个动态库到目标进程中。所注入的动态库称为 agent。
回到注入的实现上,frida-server 是怎么实现进程注入的呢?根据已有经验,由于我们以 root 权限启动,在 Linux 中一般有多种实现进程注入的方式,比如 ptrace、/proc/pid/mem,DLL/SO 劫持、LD_PERLOAD 等,因此实际实现还要回到代码。
Android 中实现进程注入的方法如下:
protectedoverrideasyncFutureperform_attach_to(uintpid,HashTable options,Cancellable?cancellable,outObject?transport)throwsError,IOError{PipeTransport.set_temp_directory(tempdir.path);vart=newPipeTransport();varstream_request=Pipe.open(t.local_address,cancellable);uintid;stringentrypoint="frida_agent_main";stringagent_parameters=make_agent_parameters(t.remote_address,options);varlinjector=injectorasLinjector;#ifHAVE_EMBEDDED_ASSETSid=yieldlinjector.inject_library_resource(pid,agent,entrypoint,agent_parameters,cancellable);#elseid=yieldlinjector.inject_library_file(pid,Config.FRIDA_AGENT_PATH,entrypoint,agent_parameters,cancellable);#endifinjectee_by_pid[pid]=id;transport=t;returnstream_request;}
确实是使用了inject_library_xxx的方式,HAVE_EMBEDDED_ASSETS为真时使用的是从自身代码中释放出来的 agent 库,位置在:
agent=newAgentDescriptor(PathTemplate("frida-agent-.so"),newBytes.static(blob32.data),newBytes.static(blob64.data),newAgentResource[]{newAgentResource("frida-agent-arm.so",newBytes.static(emulated_arm.data),tempdir),newAgentResource("frida-agent-arm64.so",newBytes.static(emulated_arm64.data),tempdir),},AgentMode.INSTANCED,tempdir);
这也是 Android 平台中 frida-server 的默认实现。
Linjector定义在src/linux/linjector.vala,即 Linux 平台上通用的进程注入实现,调用链路为:
•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 实现,代码在src/linux/frida-helper-backend-glue.c:
void_frida_linux_helper_backend_do_inject(FridaLinuxHelperBackend*self,guintpid,constgchar*path,constgchar*entrypoint,constgchar*data,constgchar*temp_path,guintid,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");#ifdefined(HAVE_GLIBC)//...#elifdefined(HAVE_UCLIBC)//...#elifdefined(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);#endifinstance=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 实现:
staticgbooleanfrida_inject_instance_attach(FridaInjectInstance*self,FridaRegs*saved_regs,GError**error){constpid_tpid=self->pid;if(can_seize)ret=ptrace(PTRACE_SEIZE,pid,NULL,PTRACE_O_TRACEEXEC);elseret=ptrace(PTRACE_ATTACH,pid,NULL,NULL);//...}
因此我们可以得出结论,frida 的进程注入是通过 ptrace 实现的,注入之后会在远端进程分配一段内存将 agent 拷贝过去并在目标进程中执行代码,执行完成后会 detach 目标进程。
这也是为什么在 frida 先连接上目标进程后还可以用 gdb 等调试器连接,而先 gdb 连接进程后 frida 就无法再次连上的原因。
frida-agent注入到目标进程并启动后会启动一个新进程与 host 进行通信,从而 host 可以给目标进行发送命令,比如执行代码,激活/关闭 hook,同时也能接收到目标进程的执行返回以及异步事件信息等。
frida-agent在上节中调用inject_library指定了注入动态库后执行的的函数符号为frida_agent_main,该函数也是由 vala 生成而来,源文件定义在lib/agent/agent.vala中:
namespaceFrida.Agent{publicvoidmain(stringagent_parameters,refFrida.UnloadPolicyunload_policy,void*injector_state){if(Runner.shared_instance==null)Runner.create_and_run(agent_parameters,refunload_policy,injector_state);elseRunner.resume_after_transition(refunload_policy,injector_state);}//...
关键的调用路径如下:
•Runner.create_and_run
•new Runner (...)
•shared_instance.run
•Runner.run
•Runner.start
其中Runner.run函数在调用完start后会进入main_loop循环,直至进程退出或者收到 server 的解除命令。
Runner.start的作用是准备 Interceptor 以及 GumJS 的 ScriptBackend,并连接到启动时指定的transport_uri建立通信隧道。
system_server另外一个我觉得比较有意思的是 frida-server 中除了注入到目标进程的 agent,还有一个 agent,即system_server_agent。
用过frida-tools的朋友应该都知道其中有个frida-ps命令,可以查看设备中的应用名称、包名甚至是 Icon,也可以查看当前运行在窗口顶部的应用。之前一直以为是通过pm list packages和dumpsys等命令实现的,看过代码之后才发现原来 frida-server 还对system_server进程进行了注入,并且所使用的 agent 就是system_server_agent。
如果不了解system_server,可以先回顾一下笔者之前的文章 "Android 用户态启动流程分析",其中提到在 Android 系统启动时,zygote 启动的第一个进程就是system_server,这也是系统中第一个启动的 Java 进程,其中包含了ActivityManagerService、PackageManagerService等系统服务。frida-server 对其进行注入,一方面是为了获取系统中的应用信息,另一方面也是为了可以实现spawn方式启动应用的功能。
以获取当前窗口中展示在最上层的应用功能为例,接口为get_frontmost_application,最终的实现在SystemServerAgent:
publicasyncHostApplicationInfoget_frontmost_application(FrontmostQueryOptionsoptions,Cancellable?cancellable)throwsError,IOError{varscope=options.scope;varscope_node=newJson.Node.alloc().init_string(scope.to_nick());Json.Noderesult=yieldcall("getFrontmostApplication",newJson.Node[]{scope_node},cancellable);//...}
这实际上是调用了src/linux/agent/system-server.js中导出的方法:
rpc.exports={getFrontmostApplication(scope){returnperformOnJavaVM(()=>{constpkgName=getFrontmostPackageName();if(pkgName===null)returnnull;constappInfo=packageManager.getApplicationInfo(pkgName,0);constappLabel=loadAppLabel.call(appInfo,packageManager).toString();constpid=computeAppPids(getAppProcesses()).get(pkgName)??0;constparameters=(scope!=="minimal")?fetchAppParameters(pkgName,appInfo,scope):null;return[pkgName,appLabel,pid,parameters];});}//...}
这部分 JS 代码也是内置到 frida-server 中的,基于 gum-js 实现了对system_server的 Java 代码调用和劫持功能。
frida-gadget除了frida-server,另外一个比较常用的模块就是frida-gadget[3]了。攻击者可以通过重打包修改动态库的依赖或者修改 smali 代码去实现向三方应用注入 gadget,当然也可以通过任意其他加载动态库的方式。
gadget 本身是一个动态库,在加载到目标进程中后会马上触发 ctor 执行指定代码,默认情况下是挂起当前进程并监听在 27042 端口等待 Host 的连接并恢复运行。其文件路径为lib/gadget/gadget.vala,启动入口为Frida.Gadget.load:
publicvoidload(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-catchvarinteraction=config.interaction;if(interactionisScriptInteraction){controller=newScriptRunner(config,location);}elseif(interactionisScriptDirectoryInteraction){controller=newScriptDirectoryRunner(config,location);}elseif(interactionisListenInteraction){controller=newControlServer(config,location);}elseif(interactionisConnectInteraction){controller=newClusterClient(config,location);}else{//throw"Invalidinteractionspecified"}interceptor.end_transaction();if(!wait_for_resume_needed)resume();//..start(request);}
正如文档中所说,Gadget 启动时会去指定路径搜索配置文件,并根据其设置进入不同的运行模式,默认的配置文件如下所示:
{"interaction":{"type":"listen","address":"127.0.0.1","port":27042,"on_port_conflict":"fail","on_load":"wait"}}
即使用listen模式,监听在 27042 端口并等待连接。除了 listen 以外,还支持以下几种模式:
•connect: Gadget 启动后主动连接到指定地址;
•script: 启动后直接加载指定的 JavaScript 文件;
•script-directory: 启动后加载指定目录下的所有 JavaScript 文件;
值得一提的是,script-directory模式可以用来实现系统级别的插件功能,比如在指定目录中编写一个twitter.js,用于实现针对 Twitter 的 Hook 功能,比如自定义过滤、自动点赞等等;另外写一个twitter.config来过滤应用:
{"filter":{"executables":["Twitter"],"bundles":["com.twitter.twitter-mac"],"objc_classes":["Twitter"]}}
这样就只有满足指定条件的进程才会加载twitter.js脚本,从而实现特定应用的持久化 Hook 功能。熟悉 Xposed 或者 Tweak 开发的应该都知道这意味着什么。
IPC 通信在 frida-core 中有许多需要进程间通信的行为,比如 frida-server 需要与注入到目标进程中的 agent 进行通信,通知目标进程开启或者关闭 Interceptor;agent 同样也需要与 host 进行通信,在 gum-js 中将console.log或者send的消息发送给 host,或者接收一些异步的应用退出和异常事件等。而其中这些进程间的交互都是通过D-Bus[4]去实现的。
D-Bus是一种基于消息的进程间通信机制,全称为 Desktop Bus,最初从 FreeDesktop 中的模块独立出来。其主要作用是提供在 Linux 操作系统桌面环境中的组件通信,比如 GNOME 或 KDE。D-Bus 使用 C 语言开发,提供了 GLib、Qt、Python 等编程接口,在 frida-core 中主要使用其 Vala 接口进行集成。
下面从实际代码看一个 D-Bus Vala 接口实现的 C/S 通信示例。首先是服务端,定义一个DemoServer, 内部定义了四个方法:
[DBus(name="org.example.Demo")]publicclassDemoServer:Object{privateintcounter;publicintping(stringmsg){stdout.printf("%s\n",msg);returncounter++;}publicintping_with_signal(stringmsg){stdout.printf("%s\n",msg);pong(counter,msg);returncounter++;}/*IncludinganyparameteroftypeGLib.BusNamewon"tbeaddedtotheinterfaceandwillreturnthedbussendername(whoiscallingthemethod)*/publicintping_with_sender(stringmsg,GLib.BusNamesender){stdout.printf("%s,from:%s\n",msg,sender);returncounter++;}publicvoidping_error()throwsError{thrownewDemoError.SOME_ERROR("Therewasanerror!");}publicsignalvoidpong(intcount,stringmsg);}[DBus(name="org.example.DemoError")]publicerrordomainDemoError{SOME_ERROR}voidon_bus_aquired(DBusConnectionconn){try{conn.register_object("/org/example/demo",newDemoServer());}catch(IOErrore){stderr.printf("Couldnotregisterservice\n");}}voidmain(){Bus.own_name(BusType.SESSION,"org.example.Demo",BusNameOwnerFlags.NONE,on_bus_aquired,()=>{},()=>stderr.printf("Couldnotaquirename\n"));newMainLoop().run();}
其中使用Bus.own_name去连接总线org.example.Demo,连接成功后注册了名为/org/example/demo的服务。客户端可以通过连接同样的总线和服务去获取到对应的远程对象接口,从而在本地实现远程调用。客户端示例如下:
[DBus(name="org.example.Demo")]interfaceDemo:Object{publicabstractintping(stringmsg)throwsIOError;publicabstractintping_with_sender(stringmsg)throwsIOError;publicabstractintping_with_signal(stringmsg)throwsIOError;publicsignalvoidpong(intcount,stringmsg);}voidmain(){/*Neededonlyifyourclientislisteningtosignals;youcanomititotherwise*/varloop=newMainLoop();/*Important:keepdemovariableoutoftry/catchscopenotlosesignals!*/Demodemo=null;try{demo=Bus.get_proxy_sync(BusType.SESSION,"org.example.Demo","/org/example/demo");/*Connectingtosignalpong!*/demo.pong.connect((c,m)=>{stdout.printf("Gotpong%dformsg"%s"\n",c,m);loop.quit();});intreply=demo.ping("HellofromVala");stdout.printf("%d\n",reply);reply=demo.ping_with_sender("HellofromValawithsender");stdout.printf("%d\n",reply);reply=demo.ping_with_signal("HellofromValawithsignal");stdout.printf("%d\n",reply);}catch(IOErrore){stderr.printf("%s\n",e.message);}loop.run();}
客户端中需要定义与远程对象一致的interface,并通过get_proxy请求获得远程对象的本地代理,从而实现透明的远程调用。注意的是编译需要添加对应的库:
$valac--pkggio-2.0demo-server.vala$valac--pkggio-2.0demo-client.vala
从效果上看有点类似于 Android 的 binder 通信机制。但是 D-Bus 是一种更加上层的封装,在不同操作系统上可以使用不同的底层实现。在 Linxu 操作系统中通常是基于 UNIX socket 实现的多进程通信,当然也可以修改配置去通过 TCP socket 实现。
frida 执行进程注入时同时也实现了 IPC 相关的初始化,在src/host-session-service.vala中可以看到关键代码如下:
privateasyncAgentEntryestablish(uintpid,HashTableoptions,Cancellable?cancellable)throwsError,IOError{//...DBusConnectionconnection;varstream_request=yieldperform_attach_to(pid,options,io_cancellable,outtransport);IOStreamstream=yieldstream_request.wait_async(io_cancellable);connection=yieldnewDBusConnection(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=yieldconnection.get_proxy(null,ObjectPath.AGENT_SESSION_PROVIDER,DO_NOT_LOAD_PROPERTIES,io_cancellable);}
这里使用了DBusConnection来连接总线,效果和own_name是类似的。其中总线的名称为ServerGuid.HOST_SESSION_SERVICE,所注册的 RPC 服务名称为ObjectPath.AGENT_CONTROLLER。另外这里还通过总线去获取了一个远程对象ObjectPath.AGENT_SESSION_PROVIDER,用以实现透明的远程调用。这些服务的名称都定义在lib/base/session.vala文件中:
namespaceFrida{//...namespaceServerGuid{publicconststringHOST_SESSION_SERVICE="6769746875622e636f6d2f6672696461";}namespaceObjectPath{publicconststringHOST_SESSION="/re/frida/HostSession";publicconststringAGENT_SESSION_PROVIDER="/re/frida/AgentSessionProvider";publicconststringAGENT_SESSION="/re/frida/AgentSession";publicconststringAGENT_CONTROLLER="/re/frida/AgentController";publicconststringAGENT_MESSAGE_SINK="/re/frida/AgentMessageSink";publicconststringCHILD_SESSION="/re/frida/ChildSession";publicconststringTRANSPORT_BROKER="/re/frida/TransportBroker";publicconststringPORTAL_SESSION="/re/frida/PortalSession";publicconststringBUS_SESSION="/re/frida/BusSession";publicconststringAUTHENTICATION_SERVICE="/re/frida/AuthenticationService";}}
通过搜索[DBus (name = "xxx")]可以看到 frida-core 中定义的所有 IPC 服务,感兴趣的可以有针对性地去进行深入分析。
拓展阅读:
•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 的那些事儿中曾经介绍过 Android 系统的安全访问控制体系,不是简单的uid=0就可以为所欲为的。frida 这种又注入进程又各种进程间通信的,显然违反了 SELinux 的默认规则,那么它是如何实现的呢?其实说起来也很简单,就是对当前系统的 SELinux 规则进行了 patch,本节就来分析下其具体是如何做的。
patch 的代码主要集中在lib/selinux/patch.c文件中,代码就不贴了,主要流程可以简单描述如下:
1.从/sys/fs/selinux/policy文件中加载当前系统的 SELinux policy;
2.在当前 sepolicy 中增加新的 SELinux 文件类型frida_file;
3. 依次插入新的 SELinux 规则(见后文);
4.通过写入/sys/fs/selinux/load文件保存修改后的 sepolicy;
5. (可选) 如果上述保存操作失败,就先临时将 SELinux 设置为 permissive 然后再保存一次,保存成功后重新恢复为 enforcing;
第 3 步中插入的 SELinux 规则如下:
typedefstruct_FridaSELinuxRuleFridaSELinuxRule;struct_FridaSELinuxRule{constgchar*sources[4];constgchar*target;constgchar*klass;constgchar*permissions[16];};staticconstFridaSELinuxRulefrida_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 的动态修改后,frida 就可以正常的在 enforcing 的环境下运行了。
后记frida-core在gum-js的基础上实现了操作系统层面的主要功能,比如进程注入、进程间通信、系统信息采集、系统权限管理等功能。正是因为这些核心组件的加入,使得 frida 成为了一个上手简单却又功能强大的动态分析框架。另外由于这些核心组件大部分使用 Vala 语言编写,在保障内存安全性的同时也提供了丰富的跨平台能力,支持 Android、iOS、Linux、MacOS、Windows 这些主流操作系统的不同架构。虽然本文仅针对 Android/Linux 操作系统的主要组件进行了介绍,但相信从中也能看出 frida-core 项目本身的代码风格架构设计,对于其他组件或者其他操作系统上的实现也是大同小异的,后续遇到具体问题的时候再深入分析对应组件即可。
引用链接[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
相关阅读
-
世界热推荐:今晚7:00直播丨下一个突破...
今晚19:00,Cocos视频号直播马上点击【预约】啦↓↓↓在运营了三年... -
NFT周刊|Magic Eden宣布支持Polygon网...
Block-986在NFT这样的市场,每周都会有相当多项目起起伏伏。在过去... -
环球今亮点!头条观察 | DeFi的兴衰与...
在比特币得到机构关注之后,许多财务专家预测世界将因为加密货币的... -
重新审视合作,体育Crypto的可靠关系才能双赢
Block-987即使在体育Crypto领域,人们的目光仍然集中在FTX上。随着... -
简讯:前端单元测试,更进一步
前端测试@2022如果从2014年Jest的第一个版本发布开始计算,前端开发... -
焦点热讯:刘强东这波操作秀
近日,刘强东发布京东全员信,信中提到:自2023年1月1日起,逐步为...