发生了太多变化。先从一项重要的新功能说起,本次发布中的大多数其他变更都由它引领:
Portal
第一部分:构想
今年早些时候,@insitusec 和我一起构思如何简化分布式插桩场景。本质上,就是交付 一个“空心”的 Frida Gadget,由后端提供应用专用的插桩逻辑。
一种实现方式是使用 Socket.connect() JavaScript API,然后定义一种应用专用的信令协议, 通过它加载代码,再将代码交给 JavaScript 运行时。
但这种做法很快就会产生大量乏味的胶水代码,而且 frida-trace 等现有工具实际上无法在 这种配置下使用。
这时,@insitusec 提出 Frida Gadget 或许可以提供一种与其 Listen 交互模式相反的 对应模式。这样一来,它不再是公开兼容 frida-server 接口的服务器,而是可以配置成连接 Portal 的客户端。
这样的 Portal 会聚合所有已连接的 Gadget,并公开一个兼容 frida-server 的接口,其中 每个 Gadget 都表现为一个进程。从外部看,它们就像是 Portal 所在机器上的进程:使用 enumerate_processes() 或 frida-ps 时,每个进程都有唯一的进程 ID,而且可以无缝地 对它们执行 attach()。
这样,现有 Frida 工具的工作方式完全不变;在 Portal 上启用 spawn-gating 后,可以指示 任何接入的 Gadget 等待,直到有人应用所需的插桩逻辑并对其调用 resume()。这与其他 场景下 spawn-gating 的工作方式相同。
第二部分:实现
实现它非常有趣,没过多久,第一个 PoC 就运行起来了。不过,弄清所有细节花了一些时间, 最终方案定型如下:
Portal 应该公开两个不同的接口:
- Gadget 可以连接的 cluster 接口,使其能够加入集群。
- 还可选择提供供控制器通信的 control 接口。例如:
frida-trace -H my.portal.com -n Twitter -i open
对用户而言,这非常简单:只需从我们的 releases 获取 frida-portal 二进制文件, 在 Gadget 能够访问的某台机器上运行它,然后将工具指向该地址——就像使用普通 frida-server 一样。
不过,这只是其中一部分,也就是简单场景下的使用方式。frida-portal CLI 程序实际上只是 底层 PortalService 的一层轻量 CLI 包装。这个 CLI 程序只有略多于 200 行代码,其中真正的逻辑很少。
也可以使用 frida-core 的语言绑定(例如 Python 或 Node.js)来实例化 PortalService。 这样可以将它配置为不提供任何控制接口,转而访问它的 device 属性。这是一个标准的 Frida Device 对象,可以对其调用 enumerate_processes()、attach() 等。当然,也可以 同时采用两种方式。
使用 API 还会带来其他功能,稍后我们会回到这些内容。
第三部分:TLS
考虑到在公共互联网上运行 frida-portal 可能非常有用,我们显然也应该支持 TLS。由于其他 功能已经将 glib-networking 作为依赖,因此从体积角度看,添加 TLS 的成本很低。
实现方面,只有客户端需要少量逻辑,服务器端也同样直接。
对于 CLI 工具,只需传入 --certificate=/path/to/pem。服务器需要一个包含公钥和私钥的
PEM 编码文件,并会接受传入客户端的任何证书。客户端同样需要 PEM 编码文件,但其中只需
包含受信任 CA 的公钥;服务器证书必须与该公钥匹配,或由其派生。
在 API 层面,归结为以下代码:
import frida
manager = frida.get_device_manager()
device = manager.add_remote_device("my.portal.com",
certificate="/path/to/pem/or/inline/pem-data")
session = device.attach("Twitter")
…第四部分:身份验证
与在公共互联网上运行 frida-portal 相伴的另一个显而易见的功能是身份验证。现在,我们的
服务器 CLI 程序和其他 CLI 工具都支持 --token=secret。
在 API 层面也很简单:
import frida
manager = frida.get_device_manager()
device = manager.add_remote_device("my.portal.com",
token="secret")
session = device.attach("Twitter")
…如果通过 API 实例化 PortalService,事情就有趣得多,因为这样可以轻松接入自定义的 身份验证后端:
import frida
def authenticate(token):
# Where `token` might be an OAuth access token
# that is used to grab user details from e.g.
# GitHub, Twitter, etc.
user = …
# Attach some application-specific state to the connection.
return {
'name': user.name,
}
cluster_params = frida.EndpointParameters(authentication=('token', "wow-such-secret"))
control_params = frida.EndpointParameters(authentication=('callback', authenticate))
service = frida.PortalService(cluster_params, control_params)EndpointParameters 构造函数还支持 address、port、certificate 等其他选项。
第五部分:离线模式
这就引出了下一个挑战:如何处理暂时性的连接问题。我确实在 PortalClient 中实现了 自动重连逻辑,Gadget 正是使用它连接 PortalService。
但即使 Gadget 能重新连接 Portal,在断线期间已加载的脚本该怎么办?如果控制器与 Portal 断开连接,又该怎么办?
现在我们已经有了同时处理这两种情况的方案。不过,它需要主动启用,因此旧行为仍是默认值。
使用方式如下:
session = device.attach("Twitter",
persist_timeout=30)现在,一旦发生连接故障,脚本会继续加载在远端,但发出的所有消息都会进入队列。在上面的 示例中,客户端有 30 秒可以重新连接;超时后脚本会被卸载,数据也会丢失。
控制器可以订阅 Session.detached 信号来处理这种情况:
def on_detached(reason, crash):
if reason == 'connection-terminated':
# Oops. Better call session.resume()
session.on('detached', on_detached)一旦 session.resume() 成功,所有缓冲的消息都会送达,一切恢复正常。
上面的示例略过了一些细节,例如当前 Python 绑定较为棘手的线程约束;完整示例请参阅 此处。(等我们将 Python 绑定从同步 API 迁移到 async/await 后,事情会简单很多。)
第六部分:延迟与瓶颈
接下来,假设 Portal 运行在美国的数据中心,Gadget 位于西班牙朋友的住处,而我正在挪威 使用 frida-trace 控制它。如果来自西班牙的脚本消息必须横跨大西洋两次,那就太糟糕了; 问题不仅在于延迟,还在于我下个月要付的 AWS 账单。因为我正在转储内存,这会产生大量流量。
这个问题稍难一些,不过有了 libnice——一个基于 GLib 构建的轻量、成熟的 ICE 实现——我们可以直接使用它。GLib 已经是技术栈的一部分:它是我们进行 C 编程的标准库, 而 Vala 代码也会编译为依赖 GLib 的 C 代码,因此非常契合。从体积角度看,这也是个好消息。
作为用户,只需传入 --p2p 并指定 STUN 服务器:
$ frida-trace \
-H my.portal.com \
--p2p \
--stun-server=my.stunserver.com \
-n Twitter \
-i open(也支持 TURN 中继。)
API 端的用法如下:
session.setup_peer_connection(stun_server="my.stunserver.com")就是这么简单!
第七部分:只有 Gadget 能参加吗?
你可能已经注意到,Gadget 一直是到目前为止反复出现的主题。我不愿意添加只适用于某一种 模式的功能,例如只支持 Injected 模式而不支持 Embedded 模式。因此我很早就 意识到,Portal 必须是一项普遍可用的功能。
假设我的朋友正在意大利家中的客厅里逆向 iPhone 上的目标,而我也想参与,他可以运行:
$ frida-join -U ReversingTarget my.portal.com cert.pem secret现在我就可以通过 Frida REPL 加入:
$ frida \
-H my.portal.com \
--certificate=cert.pem \
--token=secret \
--p2p \
--stun-server=my.stunserver.com \
-n ReversingTarget如果朋友想通过 API 加入 Portal,可以这样做:
session = frida.get_usb_device().attach("ReversingTarget")
membership = session.join_portal("my.portal.com",
certificate="/path/to/cert.pem",
token="secret")第八部分:Web
早在 Frida 诞生之前,我就一直想构建一款在线协作逆向应用。在 Frida 项目刚起步时, 我做过一个集成聊天、控制台等功能的桌面 GUI。然而,有限的业余时间成了难题,因此最终 我删掉了 GUI 代码,决定专注于 API。
如今已是 2021 年,单页应用(SPA)在许多场景下都非常有吸引力。我也注意到已经有不少 基于 Frida 构建的 SPA,这令人十分兴奋!但我自己尝试 SPA 时发现,编写中间件相当繁琐。
为了容纳前面介绍的功能,Frida 15 必须修改一些协议,因此现在似乎也是彻底打破协议、 提升主版本号的合适时机。我一直尽量避免这样做,因为我知道这对所有人都很痛苦,包括我自己。
现在,浏览器终于也能参与进来,而且不需要任何中间件:
async function start() {
const ws = wrapEventStream(new WebSocket(`ws://${location.host}/ws`));
const bus = dbus.peerBus(ws, {
authMethods: [],
});
const hostSessionObj = await bus.getProxyObject('re.frida.HostSession15',
'/re/frida/HostSession');
const hostSession = hostSessionObj.getInterface('re.frida.HostSession15');
const processes: HostProcessInfo[] = await hostSession.enumerateProcesses({});
console.log('Got processes:', processes);
const target = processes.find(([, name]) => name === 'hello2');
if (target === undefined) {
throw new Error('Target process not found');
}
const [pid] = target;
console.log('Got PID:', pid);
const sessionId = await hostSession.attach(pid, {
'persist-timeout': new Variant('u', 30)
});
…
}(完整示例位于 examples/web_client。)
这意味着 Frida 的网络协议现在基于 WebSocket,因此浏览器终于可以直接与运行中的 Portal/frida-server 通信,中间无需任何中间件或网关。
不过,我不想只做个半成品,因此确保点对点实现基于 WebRTC 数据通道。这样,即使是浏览器也能以最低延迟通信,还有助于降低 AWS 账单。
第九部分:静态资源
当我们为 Portal 构建了 Web 应用后,由于 Portal 原生使用 WebSocket,也就同时支持 HTTP,因此可以非常轻松地从同一服务器提供该 SPA:
$ ./frida-portal --asset-root=/path/to/web/app在 API 层面也很简单:
control_params = frida.EndpointParameters(asset_root="/path/to/web/app")
service = frida.PortalService(cluster_params, control_params)第十部分:协作
拥有控制器(比如 Web 应用)之后,自然的下一步就是加入协作功能,让该 SPA 的多个运行 实例能够相互通信。
既然控制器与 PortalService 之间已经存在 TCP 连接,那么让开发者也使用这条通道几乎 不增加成本。在许多场景中,额外增加一条信令通道会带来大量本可避免的复杂性。
新的 Bus API 正是在这里派上用场:
import frida
def on_message(message, data):
# TODO: Handle incoming message.
pass
manager = frida.get_device_manager()
device = manager.add_remote_device("my.portal.com")
bus = device.bus
bus.on('message', on_message)
bus.attach()
bus.post({
'type': 'rename',
'address': "0x1234",
'name': "EncryptPacket"
})
bus.post({
'type': 'chat',
'text': "Hey, check out EncryptPacket everybody"
})这里首先附加消息处理程序,以便接收来自 Portal 的消息。
然后调用 attach(),让 Portal 知道我们希望与它通信。(我们不希望它向不使用 Bus 的
控制器发送消息,例如 frida-trace。)
最后,我们通过 post() 发送两种不同类型的消息。如何处理由 PortalService 决定。
这意味着远程 PortalService 需要通过 API 实例化,因为必须处理传入消息——Portal 不会 自行将消息转发给其他控制器。
不过不用担心,实现起来很简单:
import frida
import sys
def on_message(connection_id, message, data):
# TODO: Handle incoming message.
pass
cluster_params = frida.EndpointParameters()
control_params = frida.EndpointParameters()
service = frida.PortalService(cluster_params, control_params)
service.on('message', on_message)
service.start()
sys.stdin.read()on_message() 应检查 message 并决定如何处理。
它可以选择回复发送消息的控制器:
service.post(connection_id, {
'type': 'rename-rejected',
'reason': "Not authorized"
})另一个实用做法是,每当有人对其 Bus 对象调用 attach() 时发送欢迎消息:
def on_subscribe(connection_id):
service.post(connection_id, {
'type': 'welcome',
'users': [user.nick for user in connected_users]
})
service.on('subscribe', on_subscribe)根据应用需求,你可能还需要向所有已附加到 Bus 的控制器广播消息:
service.broadcast({
'type': 'announce',
'text': "Important Service Announcement"
})也可以通过 narrowcast() 向一部分控制器发送消息:
service.narrowcast("#reversing", {
'type': 'chat',
'sender': user.nick,
'text': "Hello everyone"
})这意味着,任何带有 #reversing 标签的控制器连接都会收到该消息。标签的设置方式如下:
service.tag(connection_id, "#reversing")随后可以根据操作添加这些标签,例如控制器发送“join”消息加入频道。也可以根据身份验证 应用标签,例如只让属于某个 GitHub 组织的连接收到消息。
最后,在集群端加入 Portal 时还可以指定访问控制列表(ACL)。ACL 是字符串数组,其中
指定的标签会授予控制器发现给定进程并与之交互的权限。这意味着,对于每个应获准访问某个
节点或节点组的控制器,都需要使用 service.tag()。
基本就是这些。更完整的示例请参阅 examples/portal_server.py 和 examples/portal_client.py,它们实现了一个类似 IRC 的聊天服务。
系统参数
五月时,我与 @Hexploitable 聊过。他当时正在开发一款工具,需要根据设备运行的是 iOS 还是 Android 来选择 Device 对象。过去也有人提出过这项功能,现在似乎终于到了 解决它的时候。
虽然可以执行 device.attach(0),在系统会话中加载脚本,从而在 Frida 自身内部运行代码 (例如远程 frida-server 中),但这样做比较繁琐。如果 Device 代表受限/未 root 的设备, 这种方式也无法工作,因为那里的代码执行受到更多限制。
经过一番讨论,@Hexploitable 开始实现这项功能,并很快做出了可以工作的初稿。之后我又 加以完善,并在 Portal 功能最终落地后不久将其合并。
该 API 很简单,未来也易于扩展:
$ python3 -c 'import frida; import json; \
print(json.dumps(frida.query_system_parameters()))' \
| jq
如果附加一台受限的 iOS 设备,也可以对其进行查询:
$ python3 -c 'import frida; import json; \
device = frida.get_usb_device(); \
print(json.dumps(device.query_system_parameters()))' \
| jq
这里需要注意一个重要细节:access: 'jailed'。通过它可以判断当前是在使用我们对受限
iOS/Android 系统的支持(也就是只能访问可调试应用),还是确实在与远程 frida-server
通信——后者由 access: 'full' 表示。
Android 上的内容还没这么丰富(欢迎提交 PR!),但仍有许多实用信息:

如果 Linux 发行版符合 LSB,我们还能识别其具体发行版:

最后还有 Windows:

应用和进程参数
另一个在即兴交流后逐渐成形的好点子,源于 @pancake 告诉我:如果能知道已安装 iOS 应用的具体版本,会非常有用。
开发 Portal 功能时,我已经从许多方面打破了协议,所以现在似乎也是继续调整的好时机, 从而避免未来再次痛苦地提升主版本号。
快进到实现完成:Application 和 Process 对象不再包含 small_icon 或 large_icon
属性,取而代之的是一个 parameters 字典。
默认调用 enumerate_applications() 时,看起来仍很熟悉:

但改为 enumerate_applications(scope='metadata') 后,内容就丰富多了:

这里可以看到 iOS Twitter 应用的版本和构建号、应用 bundle 在文件系统中的位置、它所 拥有的容器、它当前是否为最前端应用、它在多久以前启动等信息。
还可以进一步使用 enumerate_applications(scope='full'),同时获取图标:

如果 query_system_parameters() 报告 access: 'jailed',debuggable: true
参数就非常有用,因为你的应用可能需要筛选应用列表,只显示能够 spawn() 和/或 attach()
的应用,或者更突出地显示可调试应用,以提供更好的用户体验。
另外值得一提的是,get_frontmost_application() 现在也支持传入 scope。
熟悉旧 API 的读者可能已经注意到,图标现在可以用压缩后的 PNG 形式传输。过去传输的 始终是未压缩的 RGBA 数据,iOS 端会执行 PNG 解码,并缩小为两种固定分辨率(16x16 和 32x32)。
这一切意味着,为了包含图标,我们会浪费大量 CPU 时间、内存和带宽,即便数据最终进入 一个根本不会使用图标的 CLI 工具。现在使用 Frida 15 时,你可能会发现列出应用和进程 快了很多。即使确实请求图标,也应该比以前更快,因为不再进行解压和缩放。
以上是应用列表。上述改进同样适用于进程列表,现在的
enumerate_applications(scope='full') 可能如下所示:

这里还可以清楚看到 Twitter 应用当前位于最前端、其父进程是 launchd(PID 1)、运行它的 用户、启动时间等信息。
你可能会疑惑为什么 applications 是数组。Android 上的示例最能说明原因:

“com.android.phone”进程实际上承载了六个不同的“应用”!
当然,我也没有忘记 Windows:

这就是“scope”选项。另外还有一个面向 UI 的选项。UI 可能希望快速获取应用/进程列表, 只有当用户与特定条目交互,或将一部分条目滚动到可见区域时,才真正需要元数据/图标。 现在我们提供了一个选项来支持这类场景。
假设只想获取两个特定应用的元数据,现在可以这样做:
ids = [
"com.atebits.Tweetie2",
"no.sparebank1.mobilbank"
]
apps = device.enumerate_applications(identifiers=ids,
scope='full')
进程列表也支持同样的功能,用法如下:
processes = device.enumerate_processes(pids=[1337, 1338],
scope='full')Portal 与应用/进程参数
介绍完应用参数和 Portal 后,还有一个重要细节值得说明:PortalService 会呈现来自 任意数量(可能位于远程)的系统中的进程,因此在其中实现 query_system_parameters() 意义不大。我们可以使用应用/进程参数来填补这一空缺。
这意味着,对于来自 PortalService 的任何 Application 和 Process,如果 scope 设为
metadata 或 full,都会提供名为 system 的参数,其中包含该特定应用/进程的系统
参数。这样,应用仍可提前判断自己是否关注某个进程。
受限 iOS 与 Android 改进
实现 Application 和 Process 参数功能非常有趣,我也尝试尽可能缩小受限(未 root)与 越狱(已 root)设备之间的差距。例如在 Android 上,未 root 代码路径过去甚至不会获取 应用标签。这是因为我们依赖通过 ADB 运行 shell 命令,而我找不到在这种情况下获取标签的 方法。
shell 命令方案非常脆弱,因为大多数工具输出详情时采用的是供人阅读而非机器处理的格式。 显然,这类输出很可能随着 Android 的演进而变化。
因此,我们现在会复制并运行一个很小的预构建 .dex;获取元数据只需向该辅助进程发出 RPC 调用。这样,我们能够为未 root 设备提供与已 root 场景相同的全部详情;后者会在 Android 端运行 frida-server。
另一点值得一提:我们不再将 Android 启动器视为最前端应用。现在这与 iOS 上的行为一致, 在那里 SpringBoard 从不会被视为最前端应用。
作为这些重大变更的一部分,我还添加了在 Android 上获取图标的代码,同时支持未 root 和 已 root 设备。因此,这项功能不再局限于 iOS、macOS 和 Windows。
受限 iOS 过去也不提供图标,但这项功能缺口现在已经补上。不过,受限和越狱 iOS 之间仍有
一项区别:受限场景中无法获得 ppid 和 user 参数,因为据我所知,目前没有任何
lockdown/DTX API 公开这些信息。除此之外,整体状况已经相当不错。
i/macOS 回溯能力大幅提升
得益于 @hot3eed 提交的一项令人兴奋的拉取请求,我们现在拥有使用 Objective-C
运行时的 i/macOS 符号化后备方案。这样一来,系统或许能够将 module!0x1234 解析为
Objective-C 方法,而不只是显示偏移地址。太棒了!
@mrmacete 还带来了另一项出色贡献:NativeCallback 现在始终公开上下文,因此可以
调用 Thread.backtrace(this.context),并预期它在所有情况下都能正常工作。
过去,只有将 NativeCallback 用作 Interceptor 替代项时才能做到这一点。因此,如果使用 ObjC.implement() 调配 Objective-C API,实际上无法从该 NativeCallback 捕获回溯。 这是一项令人振奋的改进!
Android 上未解压的原生库
在 Android 上使用 Frida 时,你可能遇到过原生库并不位于文件系统中,而是直接从应用 .apk 加载的应用。得益于 @P-Sc 的出色贡献,我们现在能够透明支持这种情况——无需 修改现有插桩代码。
操作系统支持升级
现在也支持 macOS Monterey、iOS 15 和 Android 12 的最新测试版。特别感谢 Corellium 的 @alexhude 协助调试和测试 iOS 15,也感谢 @pengzhangdev 为 frida-java-bridge 提交修复,以支持 Android 12。
联网的 iOS 设备
另一个多次被请求的功能是支持联网的 iOS 设备。如果你不想让 iPhone/iPad 整天插着线、
损耗电池,这项功能会非常实用。它的好处在于“开箱即用”——运行 frida-ls-devices 时
应该就能看到这些设备。
只有两个需要注意的陷阱。如果联网的 iOS 设备既能通过网络访问,又同时插着线,那么现在 可能会出现两个 ID 相同的不同 Device 对象。
例如:
$ frida-ls-devices
Id Type Name
------------------------- ------ -------------------------------------
local local Local System
00008027-xxxxxxxxxxxxxxxx usb iPad
socket remote Local Socket
00008027-xxxxxxxxxxxxxxxx remote iOS Device [fe80::146f:75af:d79:630c]因此,如果使用 -U 或 frida.get_usb_device(),行为会与以前一样,通过 USB 使用
设备。但如果想使用联网设备,按 ID 解析时 USB 条目会优先,因为它在设备列表中通常位于
联网设备之前。
这意味着还需要检查设备的 type。CLI 工具尚未提供用于执行此操作的开关;如果有人
感兴趣,我们非常欢迎相关拉取请求!
第二个陷阱是,frida-server 默认只监听环回接口,因此无法通过网络连接。如果手动或通过
Cydia 使用我们的 iOS .deb,就必须编辑 /Library/LaunchDaemons/re.frida.server.plist
添加 --listen 开关,再使用 launchctl 重启服务。
根据你对网络环境的信任程度,这种情况下可能还需要使用前面提到的新 TLS 和身份验证功能。
结语
此外还有许多令人兴奋的变更,请务必查看下面的变更日志。
尽情使用吧!
15.0.0 变更
- 引入 PortalService API 和守护进程。这项网络服务用于编排由 Frida 插桩的远程进程 集群,同时实现兼容 frida-server 的控制接口,以及供目标进程中的 Agent 和 Gadget 通信的集群接口。已连接的控制器可以像枚举 Portal 所在系统的本地进程一样枚举这些 进程,并可执行 attach(),也可启用 spawn-gating 来应用早期插桩。
- 添加 Session.join_portal(),方便与远程 PortalService 共享进程控制权,并与其他 节点一起加入其集群。
- 为 frida-gadget 添加“connect”交互,使其也能加入 PortalService 集群。
- 添加 PortalClient,用于实现 Session.join_portal() 和 frida-gadget 的“connect”交互。 它会连接 PortalService 并加入集群,在发生暂时性故障时自动重连。还支持指定 ACL, 即一组标签;PortalService 要求已连接控制器至少拥有其中一个标签。应用负责根据身份 验证等条件为控制器添加标签。
- 添加 Device.bus API,让连接到 PortalService 的客户端能够与其交换应用专用消息。 需要通过 API 实例化服务,才能接入消息处理程序和协议逻辑。
- 添加 Session 持久化支持。在 attach() 到进程时指定非零“persist_timeout”选项即可 启用。服务器之后检测到拥有该会话的客户端断开连接时,会允许脚本继续加载,直到达到 超时时间(秒)。期间发出的所有脚本和调试器消息都会排队;如果客户端在超时前返回, 这些消息稍后会被送达。
- 添加 TLS 支持,通过指定证书启用。服务器端使用包含公钥和私钥的 PEM,并接受客户端 提供的任何证书。客户端则使用包含受信任 CA 公钥的 PEM,服务器证书必须与其匹配或 由其派生。
- 添加身份验证支持,通过指定 token 启用。守护进程可通过 CLI 选项指定静态 token, API 则允许接入自定义身份验证后端,也就是说可以按需解释 token。
- 将网络协议迁移到 WebSocket。
- 添加协议级保活。
- 实现兼容 WebRTC Data Channel 的点对点支持,通过对 Session 调用 setup_peer_connection() 启用。这样客户端与远程进程之间可以建立直接连接,适合通过 Portal 等中介与进程通信的场景。
- 跳过 DBus 身份验证握手,并指示 GDBus 不获取属性,从而优化协议并省下一次往返。
- 移除已弃用的协议部分,例如 Session.enable_jit()。
- 添加 Device.query_system_parameters()。感谢 @Hexploitable!
- 改进应用和进程查询 API。(上文已有详细介绍。)
- 将 Crash 参数名称改为 kebab-case。
- 添加 Session.is_detached(),适用于多线程场景:当我们成功连接“detached”信号时, 该信号可能已经发出。
- 修复 Stalker 对 Linux/x86 上 SYSCALL 指令的处理。
- 修复 macOS 上的 iPad 设备名称。
- 当符号缺失但待符号化地址属于 Objective-C 方法时,大幅改进 i/macOS 上的回溯/符号化。 感谢 @hot3eed!
- 提高回溯器的准确性和灵活性:当 NativeCallback 未用作 Interceptor 替代项时,现在会 为其公开最小上下文。ObjC.implement() 就是一个例子,它用于调配 API。感谢 @mrmacete!
- 切换到 iOS 所用的映射策略,提高 Interceptor 在 macOS/arm64 上的可靠性。
- 支持通过网络连接的 iOS 设备。
- 避免 iOS 上的沙箱检查失败时遗留文件。
- 修复 Stalker 在使用 checkra1n 越狱的较新 iOS 硬件上的问题。解决方案是暂时禁用 iOS 上的 RWX 支持:即使越狱看起来提供了这项能力,它是否真正有效最终取决于硬件支持哪些 缓解措施。感谢 @stacksmashing 报告问题并帮助我们查明根因!
- 从 iOS 维护脚本中移除 bash 专用语法,提高越狱兼容性。感谢 @nyuszika7h!
- 改进 arm64 上的 Interceptor 栈帧布局,使回溯代码能够越过我们生成的代码继续遍历栈。 这也让调试器与 Interceptor 一起使用时更加方便。
- 修复 ObjC ApiResolver 死锁。问题发生在从 dyld 镜像回调中延迟解析 free() 时,而此时 调用 dlsym() 并不合适。
- 支持 Android 上未解压的原生库。感谢 @P-Sc!
- 修复 Android get_frontmost_application() 名称截断。
- 不再将 Android 启动器视为最前端应用。
- 如果进程代表 Android 应用的主 UI 进程,则使用应用标签作为进程名称。这终于与 iOS 上的行为保持一致。
- 改进受限 Android 注入器:现在使用长生命周期的 shell 会话来提速;同时确保放入 /data/local/tmp 的文件名唯一,并清理临时文件。
- 移除 Firefox OS 支持。
- 重命名弱引用 API,以免与 ES2021 冲突。WeakRef.bind() 和 WeakRef.unbind() 现分别 改为 Script.bindWeak() 和 Script.unbindWeak()。
- 将 dlmalloc 自旋锁睡眠时长降为零,大幅提升 Windows 上的内存分配性能;这意味着它只会 让出当前时间片的剩余部分。默认的 50 ms 在线程开始竞争该锁后很容易引入明显延迟。
- 延迟创建 ScriptScheduler 线程池。这意味着 GumJS 用户可以在真正需要之前避免后台线程。 如果你正在编写需要处理 fork() 的工具或 Agent,这非常有用,无需实现停止线程并在之后 重启的逻辑。(frida-agent 会这样做,但简单场景不一定需要。)
- Java:支持 Android 12 测试版。感谢 @pengzhangdev!
- Java:修复 Java.array() 对未加载数组类型的处理。如果应用没有使用该数组类型,它就 不会加载到内存中,而 Java.array() 过去会因此失败。感谢 @yotamN!
- Java:为原始类型数组添加 toString()。现在会生成由逗号分隔数组值的字符串,而不是 通用的“[Object Object]”字符串。感谢 @yotamN!
- CModule:补充 32 位 x86 缺失的 TinyCC 内建项。
- python:添加 Script.is_destroyed 属性。
- python:添加 Session.is_detached 属性。
- python:添加 Device.is_lost 属性。
- python:尝试对已销毁脚本进行 RPC 调用时抛出异常。
- node:修复与较新 Node.js 版本的兼容性;这些版本要求任意给定 Buffer 的后备存储唯一。 在无法保证同一缓冲区只会被观察一次时,我们直接制作副本。
- node:修复信号转换回调中的释放后使用问题。
- node:修复信号连接回调中的释放后使用问题。
- node:迁移到 C++17,以兼容较新的 Node.js 头文件。
- node:添加 Script.isDestroyed 属性。
- node:添加 Device.isLost 属性。
15.0.1 变更
- 确保 DarwinGrafter 合并 bind 时不会在 __LINKEDIT 中产生间隙,否则会触发
codesign的错误,导致签名后的二进制文件损坏。感谢 @mrmacete! - node:修复使用 MSVC 构建时的编译错误。
15.0.2 变更
- 修复客户端-服务器和 p2p 场景下的大消息处理。还将 WebSocket 载荷大小上限提高到 256 KiB,与 p2p 模式下数据通道通常协商出的大小相同。
- 将 WebSocket I/O 移至 DBus 线程,避免不必要的线程切换。
- 修复使用 p2p 时的泄漏。
15.0.3 变更
- 为 i/macOS 实现新的 frida-pipe 策略。过去直接在目标进程中设置 Mach 端口的策略会与 受保护 Mach 端口产生问题,因此现在注册一个全局可见的 Mach 服务,让目标进程联系它, 获取 socketpair 的一端。我们还会预先向沙箱查询,并在需要时签发扩展 token。如果无法 向 launchd 注册 Mach 服务,则回退到旧策略。
- 在 iOS 15 及更高版本上跳过 iOS platformized 检测,因为它与受保护 Mach 端口配合不佳。
- 将早期插桩移植到 macOS 12 和 iOS 15。
- 妥善处理 Agent 启动失败:只记录错误并卸载,而不是让目标进程崩溃。
- Linux:在支持时为 frida-pipe 使用抽象名称。
- frida-pipe 使用完 UNIX socket 后将其 unlink。
- 修复 iOS policyd Mach 消息生命周期逻辑。
15.0.4 变更
- 修复 i/macOS 注入器的数据大小逻辑。该值从一开始就被硬编码,近期入口点数据大小调整 导致它在使用 4K 内存页的系统上损坏。
- 修复 dyld < 4 上的 i/macOS 早期插桩回归。
15.0.5 变更
- 修复 dyld >= 4 上的 __CFInitialize() 代码路径。
- 修复 spawn() 期间的 i/macOS 沙箱扩展逻辑。
- 将受限 iOS 注入器移植到 iOS 15。
- 实现 Android USAP 互操作。
- 改进 DarwinGrafter 的 lazy bind 合并逻辑。感谢 @mrmacete!
15.0.6 变更
- 修复 Windows 构建回归。
15.0.7 变更
- 修复 Android 12 上的早期插桩。
- node:修复 ApplicationParameters 类型定义。感谢报告问题的 @pancake!
15.0.8 变更
- iOS:支持 unc0ver v6.1.2。
- iOS:更新 Substrate 互操作逻辑,以支持 0.9.7113。
15.0.9 变更
- iOS:恢复对旧版操作系统的支持。(已在 iOS 10.3 上验证。)
- iOS:修复对旧版 Apple usbmuxd 的支持。
- Android:处理不支持受限模式的设备。
- 修复 DarwinGrafter 对齐问题:即使 lazy bind 位于常规 bind 之后,也按 16 字节对齐。 感谢 @mrmacete!
15.0.10 变更
- 修复 Device.open_channel(“lockdown:”) 会立即关闭流的回归。
- 修复与旧版 ADB 通信时的崩溃。
15.0.11 变更
- 重写 macOS spawn gating,改用 DTrace。这意味着我们已移除对内核扩展的支持,无需再 担心未来操作系统不再支持扩展;同时也终于支持 Apple Silicon 上的 spawn gating。
- 规避 i/macOS arm64 早期插桩期间的单步延迟。
15.0.12 变更
- 修复 macOS spawn gating 的 task port 生命周期问题。不应自作聪明地保留 task port, 因为在 exec 转换后继续使用会引发严重问题。
- 移除 i/macOS task port 缓存逻辑。它在 exec 转换中很危险,会增加复杂性,而且对性能 的帮助并不大。
- 将 macOS spawn gating 的 DTrace 谓词环境变量改为可选。
- 改进 macOS spawn gating 错误消息。
15.0.13 变更
- 改进客户端,使其指定 Host 请求头并使用 TLS SNI。
- 撤销 i/macOS 单步延迟规避措施。
- iOS:修复连接请求中的 usbmux 端口号编码。
- Python、Node.js:修复 Device.spawn() 辅助选项逻辑中的所有权问题。
15.0.14 变更
- 将 iOS 崩溃报告器集成移植到 iOS 14.7.1。
- 将内部 Agent 配置为在 i/macOS 上减少侵入性。
- 改进 i/macOS 进程准备期间的错误处理。
- 恢复 i/macOS 单步延迟规避措施,事实证明它仍然必要。
- 在即将让出 JS 锁时处理可重入情况;如果 Interceptor 在 end_transaction() 期间调用 被替换的函数,就可能发生这种情况。
- 修复 GumJS 中 Interceptor 的条件式取消忽略逻辑。此前它会阻止 Interceptor 忽略内部 调用,造成噪声并降低性能。
- 增强 i/macOS DebugSymbol 后备名称,同时包含未滑动地址,方便直接放入静态分析工具。
- 修复 Stalker 代码 slab 补充逻辑。
- 添加 Stalker 统计接口。
15.0.15 变更
- 重构 i/macOS Exceptor,以支持最新 iOS 14。过去每当发生原生异常时都会导致死锁。
15.0.16 变更
- 修复 i/macOS 在 ARM 上早期插桩期间的单步处理问题,该问题会导致 attach() 对刚刚 spawn() 的进程随机失败。感谢 @mrmacete!
- 添加 Stalker backpatch 预取支持。
- 允许在 x86 上配置 Stalker 内联缓存大小。
- 优化 Stalker x86 返回处理。
- ObjC:允许代理实现方法。感谢 @hot3eed!
15.0.17 变更
- gadget:支持在 i/macOS 上打包为 framework。从“Resources/config.json”加载配置, 并相对于同一目录解析相对路径。另外,现在“certificate”和“asset_root”也支持指定 相对路径。
- web-service:实现采用 NGINX 风格输出的基本目录列表,适用于 “frida-server –asset-root=/”等场景。
- stalker:修复 32 位 x86 的 backpatch 参数。
15.0.18 变更
- 修复长期存在的内存泄漏,包括内部堆 realloc 行为配置错误导致的泄漏,以及向 QuickJS 注册 JavaScript 类的方式所导致的泄漏。感谢 @mrmacete 发现并协助追踪这些长期 存在的错误!
15.0.19 变更
- gadget:修复 iOS 上的 framework 资源查找逻辑。
oleavr