Gadget
Gadget 是 Frida 提供的一个共享库。当没办法使用注入模式时,可以将该库加载到需要调试的程序中。
通常有几个办法可以把Gadget嵌入到目标程序中,比如:
- 修改程序源码
- 给目标程序或目标程序的某个库打补丁。比如使用 insert_dylib 类似的工具
- 借助动态链接特性,如 LD_PRELOAD 或 DYLD_INSERT_LIBRARIES
动态链接器在执行Gadget的构造器后就会立刻开始工作。
Gadget 可以根据你的需求提供四种不同的交互模式。其中默认交互模式为 监听 模式. 你可以在配置文件中修改默认模式。 这个配置文件命名应该和Gadget二进制文件一模一样,除了后缀名是.config。 举个例子,如果你的Gadget名字为FridaGadget.dylib,那么对应的配置文件名就应该是FridaGadget.config。
注意,Gadget二进制文件的名字可以随意设定,这样可以规避一些反Frida引擎的检测。
使用Xcode向iOS应用中添加.config的时候需要注意,你可能会倾向于将FridaGadget.dylib放到一个称为"Frameworks"的子目录下, 然后将".config"文件放到父目录中,也就是和app以及其他资源文件同级的目录。处于这个原因,Gadget也会在其父目录名字为"Framework"时,在父目录里寻找.config文件。
在Android上,对一个非debug模式的应用,包管理器只会在满足以下条件之一的情况下拷贝/lib
文件:
- 名字前缀为
lib
- 名字后缀为
.so
- 名字为
gdbserver
因此Frida会很智能的检测如下的配置文件名称:
lib
└── arm64-v8a
├── libgadget.config.so
├── libgadget.so
更多详情,查阅这篇 文章.
配置文件应该是使用UTF-8编码的JSON文本文件。支持四种根键:
-
interaction
: 指明交互模式的对象。默认为 监听 交互. -
teardown
: 字符串minimal
或full
, 指明该库卸载时,需要清理的干净程度 默认值为minimal
, 也就是不关闭内部线程,也不释放已分配的内存和系统资源。 这么做没啥问题,因为Gadget的生命周期是和程序绑定的。如果你想在某个时间点完全卸载该库,你可以设置为full
-
runtime
: 字符串default
,qjs
, 或v8
。默认使用的JS运行时。 -
code_signing
: 字符串optional
或required
, 设置为required
,将允许Gadget在已越狱iOS设备上不实用调试器附加的情况下运行。 默认值为optional
, 这时Gadget会假定拥有修改内存中已存在代码的权限和运行未签名代码的权限,而且做这两件事不会被内核杀死。将该值设置为required
也意味着拦截API是不可行的。因此,在已越狱的iOS设备上,使用拦截API的唯一方式是在Gadget加载完毕前附加上调试器。需要注意的是,启动应用时附加调试器就够了, 没有必要一直附加,因为代码签名状态是黏性的,设置后就不会再变了。
支持的交互模式
被动监听模式
默认的交互模式。 Gadget 会将 frida-server 接口暴露出来, 默认在 localhost:27042 监听。唯一的区别是,正在运行的进程列表和已安装app列表里只包函一个程序。程序名永远为Gadget,以及app的标识符永远为 re.frida.Gadget。
为了实现早期调试,我们会让Gadget的构造器阻塞,直到你调用attach()附加到进程,或者在使用spawn() -> attach()-> …apply instrumentation…过程中 调用resume()。 这也就意味着原本就存在工具(比如 frida-trace)可以仍按照器原本的方式运作。
如果你不想阻塞,想让程序立刻启动起来,或者你更想在不同的接口上监听,你可以在配置文件里自定义。
默认的配置文件:
支持的配置键有:
-
address
: 字符串,指明监听的接口。 支持 IPv4 和 IPv6。 默认值为127.0.0.1
. 设置为0.0.0.0
表明在所有IPv4上监听。::
为在所有IPv6上监听。 -
port
: 数字,指明默认监听的端口号. 默认值为27042
. -
certificate
: 设置该键用于支持TLS。 其值应当为PEM编码的公钥和私钥 可以是包函多行PEM数据的字符串,或者是指明文件位置的路径。服务器接受所有客户端侧的任何证书。 -
token
: 设置该键用于打开身份认证。 其值应为客户端访问时应该提供的密码。 -
on_port_conflict
: 字符串fail
或pick-next
, 指明监听端口冲突时的行为。 默认fail
, 也就是端口冲突时,Gadget会启动失败。 设置为pick-next
时,Gadget 会在端口冲突时逐个检测下个端口,直到找到一个可用端口。 -
on_load
: 字符串resume
或wait
, 指明Gadget加载完毕后的行为。 默认值为wait
, 也就是等待你连接并通知它恢复运行 设置为resume
时,程序会立刻启动,当你想晚一点附加进程的时候,你可以使用这个选项。 -
origin
: 设置该值用来保护未认证的跨源访问,设置后将只接受匹配头的请求。 -
asset_root
: 设置该值以启用HTTP/HTTPS托管静态文件。所有目录内的文件都将暴露出来。默认情况下不托管任何文件。
主动连接模式
这是一个和 “监听模式” 对立的模式。监听模式下,Gadget会监听一个TCP接口,等待连接,而连接模式下,Gadget会主动连接到一个运行的frida-portal,然后 成为其集群的一个进程节点。 这就是所谓的cluster接口。 而Portal通常也会暴露一个control接口,该接口和frida-server使用相同的协议。这允许任何已连接的控制器,就像在本地机器上的进程一样运行enumerate_processes 和 attach()。
为了实现早期调试,我们会让Gadget的构造器阻塞,直到一个控制器发出resume()请求(前提启用了 spawn-gating,可以通过Device.enable_spawn_gating()启用)。 这意味着启动后,Gadget会阻塞,直到其连接的Portal或集群发出命令。
默认的配置文件:
支持的配置键有:/p>
-
address
: 字符串,指明要连接的主机,或暴露的集群接口。 支持 IPv4 和 IPv6。 默认值为127.0.0.1
。 -
port
: 数字,指明要连接的TCP端口。 默认为27052
。 -
certificate
: 若 Portal启用了TLS,则必须设置该值。也就是PEM编码的公钥。 和前面一样,可以是多行的PEM数据,也可以是指明公钥位置的字符串。这个公钥应该来自被信任的CA,也就是服务端证书匹配或者继承来的。 -
token
: 若 Portal 集群启用了身份认证,就必须设置该值。该值会在连接到 Portal 时提供给服务器用于身份验证。 该键的具体值取决于Portal的实现。可能是固定的密码,也可能是任何验证方式。该验证服务接口是可外部集成定制的。 -
acl
: 字符串数组,用于指明存取控制列表。该值限定了可以和发现并和该进程的控制器。比如["team-a", "team-b"]
,任何来自"team-a"或"team-b"的控制器都将获取控制权限。 只有 Portal 通过API实例化,且需要提供身份认证的时候,才需要设置该键。
高级用法Advanced users
为了更好的控制,比如定制的身份验证,节点ACLs,以及应用协议信息。你也可以自己实例化 PortalService 对象,而不是运行 frida-portal 的CLI程序。
脚本模式
有时,从本地直接加载脚本,并在程序入口前执行全面的调试非常有必要。
这里有一个简单的必要配置:
其中 explore.js 包含如下的框架:
其中 rpc.exports 不是必须的, 但在你的脚本需要知晓生命周期的时候很有用。
Gadget 会调用你的 init()
方法,然后等待其返回,这部分发生在程序进入其入口时发生。
如果你同时还想做点别的什么事,你可以返回一个Promise。比如 Socket.connect(), 然后保证不要错过早期的调用。
第一个参数, stage
, 是一个字符串,取值为 early
或 late
,
该参数在知晓Gadget是否是刚加载时十分有用,或在脚本重新加载时也很有用。
后面的主题会介绍更多。
第二个参数, parameters
, 是一个对象,该对象为配置文件中的对象。如果配置文件中没配置,则默认为空。
如果你想参数化你的脚本,这个参数将会很重要。
如果你需要显式地在脚本卸载后做一些清理工作,那么你需要暴露dispose()
方法。
通常在进程退出,Gadget卸载时,或者你的脚本发生变动,旧版本退出时调用。
你可以使用 console.log(), console.warn(), 和 console.error() 进行调试, 这些信息会打印到 stdout/stderr。
支持的配置键有:
-
path
: 字符串,指明需要需要加载的脚本路径。相对路径时则是相对于Gadget二进制文件。 在 iOS 上,Gadget 首先会查看相对于 app 文档目录的路径。这意味着你可以通过 iTunes 文件共享来上传和更新脚本。这个功能和"on_change": "reload"
一起使用时非常有用。该键没有默认值,而且必须提供。 -
parameters
: 对象,包含任意的配置数据。该对象会传递给init()
RPC 方法。默认为空。 -
on_change
: 字符串ignore
或reload
, 其中ignore
意味着脚本只会加载一次, 而reload
意味着 Gadget 会监视脚本文件,并在脚本变动时立刻重新加载脚本 默认值为ignore
, 但在开发期间,强烈建议使用reload
脚本目录模式
有时你可能会想在系统层面篡改程序和库,但是不用自己的脚本逻辑去筛选程序,而是想尽可能少的作筛选, 并且根据当前运行Gadget的程序选择不同的脚本注入。你可能甚至不需要过滤,就可以将每个脚本当作独立的插件。 在 GNU/Linux 系统里,类似的脚本甚至可以来自于包,通常这些脚本用来安装并微调已存在的应用。
这里有一个简单的必要配置:
支持的配置键有:
-
path
: 字符串,用于指明包含脚本的目录。也可以是相对Gadget二进制文件的相对目录。 该键没有默认值,必须提供。脚本必须以.js作为其后缀名,而且每个脚本都可以有一个对应的.config文件。也就是说,twitter.js会将twitter.config识别为其配置文件。 -
on_change
: 字符串,值为ignore
或rescan
, 其中ignore
意味着目录只会扫描一次,而rescan
意味着 Gadget 会监视目录,并在有变动时重新扫描。 默认值为ignore
, 但在开发期间,强烈建议使用rescan
每个脚本可选的配置文件可以包含以下键:
-
filter
: 对象,包含了若干加载目标脚本的条件。这下加载条件中只有一个会被匹配到,所以如果需要复杂的过滤逻辑,最好还是用脚本实现。支持的匹配方式如下:executables
: 字符串数组,指明可执行文件的名称bundles
: 字符串数组,指明一系列标识符objc_classes
: 字符串数组,用于指明Objectiv-C的类名
-
parameters
: 对象,包含任意的配置数据。该对象会传递给init()
RPC 方法。默认为空。 -
on_change
: 字符串,取值为ignore
或reload
,其中ignore
意味着脚本只会加载一次,而reload
意味着 Gadget 会监视脚本文件,并在脚本变动时立刻重新加载脚本。 默认值为ignore
, 不过强烈建议开发阶段使用reload
。
比如你想在Twitter的macOS应用中写一个延迟脚本,你可以编写这样一个文件 twitter.js,将其放在/usr/local/frida/scripts,内容为:
然后为了确保该脚本只会加载到特定的app中,你需要再创建一个配置文件twitter.config,其中包含:
这个例子里,配置文件说明了这样一些事:
- 可执行文件名字为
Twitter
, 或 - 其打包标识符是
com.twitter.twitter-mac
, 或 - 检测到名为
Twitter
的 Objective-C class被加载。
这个例子里,你可能只会用 bundle ID作为过滤条件,因为通常来说这个是最稳定的标识。不过如果需要的话,还是多一些检查更好。
接下来你可能想再声明一些其他键,比如 parameters
和 on_change
,
这些配置和前面
脚本 的配置一模一样。