剪贴板即信道

摘要 异步剪贴板 API 为 Web 应用提供了读、写系统剪贴板的能力,但 navigator.clipboard.write() 的异步执行与用户 paste 事件的同步触发之间,存在一个容易被忽略的原子性缺陷。攻击者可利用同一用户手势先后调用 write() 和 read(),通过 Promise.race 构造竞态条件,在浏览器实际写入新内容之前读取到剪贴板中尚未被覆盖的残留数据——而这些数据可能来自另一个域,包含密码管理器自动填充的凭据、2FA 种子或私钥。更危险的是,结合密码管理器的“复制密码”功能和 WebAuthn 条件 UI 的自动填充行为,窃取甚至不需要用户额外的粘贴操作。 异步剪贴板 API 的安全模型 从 execCommand 到 navigator.clipboard 在 navigator.clipboard 出现之前,页面只能通过 document.execCommand(‘copy’) 和 document.execCommand(‘paste’) 操作剪贴板,且后者仅在浏览器扩展或用户主动粘贴时可用。异步剪贴板 API 允许页面使用 readText() 和 writeText() 直接读取和写入文本,前提是获得用户许可(对于 read,通常要求页面可见且由用户手势触发)并且传输数据遵循安全上下文(HTTPS)。 关键设计:write() 返回一个 Promise,在数据被系统剪贴板确认后解析;read() 同样返回一个 Promise,在读取到当前剪贴板内容时解析。二者都是在“任务队列”中异步执行的,但在一次用户手势(如 click 事件)中,可以同时调用两个方法而不相互阻塞。 同一个点击下的 write 与 read 考虑以下代码: button.addEventListener('click', async () => { navigator.clipboard.writeText('A'.repeat(100)); const data = await navigator.clipboard.readText(); console.log(data); }); 开发者预期 readText() 会返回刚才写入的 ‘AAA…’,但实际结果取决于 UA 的事件循环调度:如果 writeText() 的内部实现是“先调度写入任务,再解析 Promise”,而 readText() 在写入任务实际执行之前就已经从系统剪贴板读取了内容,那么 data 将是调用 writeText()之前 剪贴板中的旧数据。这个顺序差异构成了可被利用的竞态窗口。 竞态条件的微观时序 任务队列的调度间隙 浏览器主线程执行模型: 用户点击,click 事件处理开始。 调用 writeText(),UA 创建一个“写剪贴板”任务并将其排入任务队列,同时返回一个 Promise。 继续执行,调用 readText(),UA 创建一个“读剪贴板”任务排入队列,返回…

虚悬页表

摘要 内核页表隔离 (KPTI) 是防御 Meltdown 类攻击的关键机制,通过分离用户态与内核态页表,确保用户态代码无法直接窥探内核地址空间。在 ARM64 上,KPTI 由 CONFIG_UNMAP_KERNEL_AT_EL0 和 kpti 引导参数控制,利用 TTBR0_EL1 与 TTBR1_EL1 的切换将内核映射仅在异常入口时临时暴露。然而,ARM 体系结构的维护操作(Maintenance Operations)存在一个隐蔽缺陷:部分 SoC 的 TLBI(TLB Invalidate)指令和 IC IALLU 序列仅刷新 TLB 而不冲刷页表缓存(PTC, Page Table Cache),导致在异常返回(ERET)后,TTBR1 中残留的中间页表项仍可通过推测执行或预取被用户态感知。攻击者利用性能监控单元(PMU)的精确计数器,在用户态测量内核地址访问的延迟差异,即使 KPTI 已重新隔离页表,仍可通过这些“虚悬的页表”还原内核基址,从而击碎 KASLR。 双重身份页表 两个TTBR ARM64 的虚拟地址空间分为两个区域:0x0000_0000_0000_0000 到 0x0000_FFFF_FFFF_FFFF 为用户空间,0xFFFF_0000_0000_0000 以上为内核空间。当 MMU 进行地址翻译时,根据虚拟地址的最高位自动选择页表基址寄存器:用户地址使用 TTBR0_EL1,内核地址使用 TTBR1_EL1。在未启用 KPTI 的情况下,两个寄存器都指向同一个页表(通常包含内核映射),用户态代码虽不能直接访问内核内存(受页表权限 U/S 位控制),但可以通过侧信道探测 TLB 存在性。 ======================= 正常态:用户空间 (EL0) ======================= CPU 视角: +——————–+ +——————–+ | TTBR0_EL1 (受限表) | —-> | 仅映射:用户空间 | (只读内核映射表) +——————–+ +——————–+…

RISC-V 物理内存保护漏洞

摘要 RISC-V 架构通过物理内存保护与基于页表的虚拟内存两套机制协同控制内存访问。PMP 由机器模式配置,提供物理地址上的粗粒度权限;MMU 由监管者模式管理,实现细粒度的页级保护。然而,RISC-V 特权规范对两者权限叠加规则的定义存在模糊空间——部分微架构实现采用“最严格”策略,部分采用“最宽松”或“时序竞争”策略。攻击者可利用这一裂隙,在 S‑mode 下构造特殊的 PMP 与页表配置组合,使物理内存保护被虚拟内存权限意外覆盖,进而从 S‑mode 提升至 M‑mode 权限等级,读取或篡改安全监控固件。 RISC-V 的两层内存保护 PMP 物理内存保护是 RISC-V 机器模式下的安全基石。PMP 单元由最多 64 个配置寄存器(pmpcfgX)和地址寄存器(pmpaddrX)组成,支持三种地址匹配模式:自然对齐 4 字节区域(NA4)、自然对齐 2 的幂区域(NAPOT)和范围模式(TOR)。每个区域的访问权限由独立的 R(读)、W(写)、X(执行)位控制,并有一个 L(锁定)位。一旦 L 位被置位,PMP 配置对于更低特权级(包括 S‑mode)变为只读,即便 M‑mode 软件也无法修改,除非系统复位。 PMP 检查发生在物理地址完成转换后:当一条 load/store/指令 fetch 请求抵达物理地址总线前,硬件会遍历所有 PMP 条目,找到第一个匹配的区域,并根据访问类型和当前特权级(M、S、U)决定是否允许。若检查失败,对于 load/store 会触发访问错误异常,对于指令 fetch 则产生指令访问错误异常。 MMU RISC‑V S‑mode 通过页表(通常是 Sv39/Sv48 格式)实现虚拟地址到物理地址的转换。页表项包含 U(用户)、R、W、X 位,权限检查发生在翻译阶段。对于 S‑mode…

eBPF 验证器盲点

摘要 eBPF 验证器通过静态分析确保 BPF 程序在正常执行路径上不会越界访问内存,但其检查模型有一个根本性的假设:CPU 严格按照指令序列执行。Spectre v1 攻击利用分支预测器在条件跳转方向未确定时沿推测路径继续执行,这条路径不受验证器约束。攻击者可编写完全合法的 eBPF 程序,精心布局数组索引和条件分支,使 CPU 在推测执行时绕过边界检查,将内核敏感内存的数据加载到缓存中,再通过侧信道(如缓存时间测量)将数据泄露至用户态。由于验证器无法检测推测执行路径,且 eBPF 程序一旦加载便在内核中持续运行,Spectre v1 在 BPF 上下文中成为一种极为隐蔽的持久化信息窃取通道。 eBPF 验证器与 Spectre eBPF 验证器的安全模型 eBPF 程序运行在内核特权级,但受到严格的验证过程约束。验证器执行控制流分析和数据流分析,确保: 程序在有限步数内终止(无无限循环)。 所有内存访问(如 bpf_map_lookup_elem 返回的指针)均在合法边界内。 指针不会被泄漏或用于非法算术。 调用的辅助函数与声明的 BPF 程序类型兼容。 验证器通过跟踪每个寄存器的类型和可能的值范围(value range)来确保安全。例如,对于 if (index < map->max_entries),验证器在“真”分支中记录 index 的上界为 max_entries-1,从而允许使用 index 访问该 map。在“假”分支中,index 的范围则不包括合法区域。 推测执行与 Spectre v1 现代 CPU 的分支预测器在遇到条件跳转时,会根据历史预测最可能的分支并沿推测路径继续执行指令。 当分支方向最终确定后,如果预测错误,推测执行的结果会被丢弃,但该过程可能已在微架构状态中留下痕迹——最典型的是缓存。 Spectre v1 利用这一特性:攻击者训练分支预测器,然后故意提供一个会导致越界访问的输入,使 CPU 在推测路径上从越界地址加载数据,该数据被缓存。随后,攻击者通过测量合法地址的访问时间差异推断被缓存的数据值。 在 eBPF 的语境中,验证器确保架构层面的正常执行不会越界,但它无法控制推测路径上的指令流。如果一个 BPF 程序包含 if (index…

WebAssembly 线性内存幻境

摘要 WebAssembly 通过线性内存和严格的类型系统为宿主环境提供安全隔离,但当 Wasm 模块被编译为原生代码(如通过 wasm2c)嵌入宿主时,安全边界完全依赖于自动生成的“沙箱胶水代码”的正确性。这些胶水代码负责将 Wasm 的虚拟指令映射为 C 语言操作,强制进行边界检查和类型转换。然而,胶水代码中用于模拟 Wasm 内存和函数调用的 memcpy 与 wasm_rt_typecheck 存在一类隐蔽的 Type Confusion 漏洞:在零长度拷贝、类型嵌套及 OOB 指针算术等边界条件下,胶水代码可能错误地信任 Wasm 模块内部提供的类型标签,导致将攻击者控制的整数视为指针或函数地址,从而打破宿主进程的内存安全约束。 Wasm 线性内存与 wasm2c 沙箱模型 隔离 WebAssembly 的安全模型核心是线性内存——一个单一的、字节寻址的、从零开始的地址空间,与宿主进程的堆栈和堆严格隔离。Wasm 模块不能直接访问宿主内存,所有内存操作(i32.load、i64.store 等)都必须经过编译后的边界检查:i32.load 必须验证 address + offset + 4 不超过当前内存大小,否则抛出陷阱。这种机制在设计上杜绝了越界读写。 wasm2c wasm2c 是 WABT 工具集中的一个工具,可将 WebAssembly 二进制模块(.wasm)转换为等效的 C 源文件(.c 和 .h)。生成的 C 代码不依赖任何外部运行时,可被直接编译并与宿主程序链接。其核心包括: 操作数栈模拟:Wasm 值栈被转换为 C 局部变量或结构体字段。 函数调度:通过函数指针表(wasm_rt_func_table)实现间接调用,并通过签名索引进行类型检查。 内存访问:所有 load/store 由内联的边界检查守卫,访问超出 mem_size 则调用 wasm_rt_trap。 类型判断:胶水代码中大量使用 wasm_rt_typecheck 宏,比较 Wasm 模块提供的函数签名是否与调用点期望的类型匹配。 整个转换与运行的流程如下: 沙箱的安全性此时完全迁移到了这些胶水代码的正确性上:C 编译器不再理解 Wasm…

WebAuthn 信任链坍缩

摘要 FIDO2/WebAuthn 为无密码认证筑起了坚固的防线,其认证器认证声明通过 X.509 证书链将用户公钥与硬件安全模块绑定。然而,证书路径验证这一经典 X.509 弱点依然潜伏于此:认证声明支持多种格式,部分格式对证书链的深度、签名算法和扩展验证存在容错空间;更危险的是,若攻击者在制造或固件更新阶段向认证器注入私钥,便可伪造认证声明,使恶意硬件通过安全校验,伪装成符合 FIPS 140‑2 的安全元件。 认证声明 WebAuthn 注册与认证声明 当用户向依赖方(Relying Party)注册一个 FIDO2 认证器时,认证器会生成一对公私钥,并将公钥连同认证声明(attestation)一起返回。 认证声明的目的是向依赖方证明: 公钥确实由真实的认证器生成; 认证器具有声称的安全属性(例如,密钥存储在安全元件中); 认证器的型号和制造商可被验证。 依赖方通过验证认证声明中的证书链或签名,来决定是否信任该认证器。这一步骤是 WebAuthn 信任模型的第一道关口。 四种认证声明格式 WebAuthn 规范定义了四种认证声明格式,其信任根各异: 格式 信任根 证书链 说明 Packed 认证器制造商 CA 证书 可选(X.509 链或自签名) 最常见,支持多种算法 TPM TPM 制造商证书 是(TPM EK/AIK 证书链) 基于 TPM 2.0 的远程证明 Android Key Android 安全硬件证书 是(Android Keystore 证书)…