安全

WebAssembly 的安全模型有两个重要目标:(1) 使用故障隔离技术保护用户免受错误或恶意模块的攻击,以及 (2) 在 (1) 的限制内,为开发者提供有用的原语和缓解措施,以便开发安全的应用程序。

用户

每个 WebAssembly 模块都在一个沙盒环境中执行,该环境使用故障隔离技术与主机运行时分离。这意味着

  • 应用程序独立执行,并且无法逃脱沙盒,除非通过适当的 API。
  • 应用程序通常以确定性方式执行 除有限例外情况外

此外,每个模块都受其嵌入环境的安全策略的约束。在 Web 浏览器 中,这包括对通过 同源策略 的信息流的限制。在 非 Web 平台上,这可能包括 POSIX 安全模型。

开发者

WebAssembly 的设计通过从其执行语义中消除危险特性来促进安全程序,同时保持与为 C/C++ 编写的程序的兼容性。

模块必须在加载时声明所有可访问的函数及其关联类型,即使使用 动态链接 时也是如此。这允许通过结构化控制流隐式强制执行 控制流完整性 (CFI)。由于编译后的代码是不可变的,并且在运行时不可观察,因此 WebAssembly 程序受到保护,免受控制流劫持攻击。

  • 函数调用 必须指定一个目标的索引,该索引对应于 函数索引空间表索引空间 中的有效条目。
  • 间接函数调用 在运行时会受到类型签名检查;所选间接函数的类型签名必须与调用站点指定的类型签名匹配。
  • 一个受保护的调用堆栈,它不受模块堆中缓冲区溢出的影响,可确保安全函数返回。
  • 分支 必须指向封闭函数内的有效目标。

C/C++ 中的变量可以根据其作用域降低为 WebAssembly 中的两种不同原语。具有固定作用域的 局部变量全局变量 表示为通过索引存储的固定类型的值。前者默认初始化为零,并存储在受保护的调用堆栈中,而后者位于 全局索引空间 中,并且可以从外部模块导入。具有 不明确的静态作用域 (例如,被取地址运算符使用,或者类型为 struct 并按值返回)的局部变量存储在 线性内存 中的单独的用户可寻址堆栈中,在编译时。这是一个隔离的内存区域,具有固定的最大大小,默认情况下初始化为零。对该内存的引用以无限精度计算,以避免溢出并简化边界检查。将来,将实现对 多个线性内存部分更细粒度的内存操作 (例如,共享内存、页面保护、大页面等)的支持。

陷阱 用于立即终止执行并将异常行为信号发送到执行环境。在浏览器中,这表示为 JavaScript 异常。将来将实现对 模块定义的陷阱处理程序 的支持。可能导致陷阱的操作包括

  • 在任何索引空间中指定无效索引,
  • 使用不匹配的签名执行间接函数调用,
  • 超过受保护的调用堆栈的最大大小,
  • 访问 超出边界 线性内存中的地址,
  • 执行非法算术运算(例如,除零或取余运算、有符号除法溢出等)。

内存安全

与传统的 C/C++ 程序相比,这些语义消除了 WebAssembly 中某些类别的内存安全错误。缓冲区溢出发生在数据超出对象的边界并访问相邻内存区域时,不会影响存储在索引空间中的局部或全局变量,它们是固定大小的,并通过索引寻址。存储在线性内存中的数据可以覆盖相邻的对象,因为边界检查是在线性内存区域粒度上执行的,并且不是上下文相关的。但是,控制流完整性和受保护的调用堆栈的存在阻止了直接代码注入攻击。因此,WebAssembly 程序不需要常见的缓解措施,例如 数据执行保护 (DEP) 和 堆栈粉碎保护 (SSP)。

另一类常见的内存安全错误涉及不安全的指针使用和 未定义的行为。这包括对未分配内存(例如 NULL)或已释放内存分配的指针进行解引用。在 WebAssembly 中,指针的语义已针对函数调用和具有固定静态作用域的变量被消除,允许对任何索引空间中的无效索引的引用在加载时触发验证错误,或者在最坏的情况下在运行时触发陷阱。对线性内存的访问在区域级别进行边界检查,这可能会导致在运行时发生陷阱。这些内存区域与运行时的内部内存隔离,并且默认情况下设置为零,除非另行初始化。

然而,WebAssembly 的语义并没有消除其他类别的错误。虽然攻击者无法执行直接代码注入攻击,但可以使用针对间接调用的代码重用攻击来劫持模块的控制流。但是,传统的 返回导向编程 (ROP) 攻击使用短指令序列(“小工具”)在 WebAssembly 中是不可能的,因为控制流完整性确保调用目标是在加载时声明的有效函数。同样,竞争条件(例如 检查时到使用时 (TOCTOU) 漏洞)在 WebAssembly 中是可能的,因为除了按顺序执行和 后 MVP 原子内存原语 :unicorn: 外,没有提供执行或调度保证。类似地,侧信道攻击 会发生,例如针对模块的计时攻击。将来,运行时或工具链可能会提供额外的保护,例如代码多样化或内存随机化(类似于 地址空间布局随机化 (ASLR)),或 有界指针 (“胖”指针)。

控制流完整性

控制流完整性的有效性可以通过其完整性来衡量。通常,有三种类型的外部控制流转换需要保护,因为被调用者可能不可信

  1. 直接函数调用,
  2. 间接函数调用,
  3. 返回值。

(1) 和 (2) 通常被称为“前向边”,因为它们对应于有向控制流图中的前向边。同样 (3) 通常被称为“后向边”,因为它对应于有向控制流图中的后向边。更专门的函数调用(例如尾调用)可以看作是 (1) 和 (3) 的组合。

通常,这是使用运行时检测来实现的。在编译期间,编译器会生成程序执行的预期控制流图,并在每个调用站点插入运行时检测,以验证转换是否安全。从程序中所有可能的调用目标集构建预期调用目标集,为每个集合分配唯一标识符,并且检测检查当前调用目标是否为预期调用目标集的成员。如果该检查成功,则允许原始调用继续,否则将执行失败处理程序,该处理程序通常会终止程序。

在 WebAssembly 中,执行语义隐式地保证了 (1) 通过使用显式函数部分索引,以及 (3) 通过受保护的调用堆栈的安全性。此外,间接函数调用的类型签名已经在运行时进行了检查,实际上为 (2) 实施了粗粒度类型化控制流完整性。所有这些都无需在模块中显式使用运行时检测即可实现。但是,如 前面 所述,这种保护并不能防止针对间接调用的具有函数级粒度的代码重用攻击。

Clang/LLVM CFI

Clang/LLVM 编译器基础设施包括对细粒度控制流完整性的 内置实现,该实现已扩展以支持 WebAssembly 目标。它在 Clang/LLVM 3.9+ 中可用,带有 新的 WebAssembly 后端

启用细粒度控制流完整性(通过将 -fsanitize=cfi 传递给 emscripten)比默认的 WebAssembly 配置具有许多优势。这不仅能更好地防御利用间接函数调用 (2) 的代码重用攻击,还能通过在 C/C++ 类型级别操作来增强内置的函数签名检查,这在语义上比 WebAssembly 类型级别 更丰富,后者仅包含四种值类型。目前,启用此功能会对每次间接调用产生较小的性能成本,因为使用整数范围检查来验证目标索引是否可信,但这将在将来通过利用 WebAssembly 中对 多个间接表 的内置支持(具有同构类型)来消除。