剪贴板即信道

摘要 异步剪贴板 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 (受限表) | —-> | 仅映射:用户空间 | (只读内核映射表) +——————–+ +——————–+…

WNF 的幽灵状态

摘要 Windows 通知设施(Windows Notification Facility)是内核向用户态分发系统事件的异步通道,负责传递诸如电池状态、网络连接变化、Shell 执行触发等大量低延迟通知。WNF 的状态数据以键值对形式存储在内核池中,并按临时或永久两种生命周期管理。然而,这一设计隐藏了一个鲜为人知的反取证库房:永久性 WNF 状态即使关联进程退出,其数据依然存活于内核内存中,且重启后仍可从注册表或其他持久化位置恢复。攻击者可通过 NtCreateWnfStateName 与 NtUpdateWnfStateData 在内核池中建立隐形存储,将 Shellcode、C2 配置或加密密钥藏匿于传统取证工具无法触及的角落,并在特定系统事件触发时通过 WNF_SHELLEXECUTE 等 Shell 触发器在用户态启动恶意进程。 WNF架构 轻量级通知引擎 Windows 内核需要向用户态组件推送大量异步事件,例如电源管理通知、Shell 文件关联变更、网络地址变化等。早期实现主要依赖 ETW 提供者和回调,但 ETW 的启用和解析较为笨重。Windows 8 引入 WNF 作为更轻量的替代方案,其设计目标是在内核与用户态之间提供高效、基于状态的订阅通知机制,且无需用户态主动轮询。 WNF 的整体组件架构与交互过程如下: +——————————————————-+ | 用户态层 | | +——————–+ +———————+ | | | Publisher (发布者) | | Subscriber (订阅者) | | | +———+———-+ +———-+———-+ | +————|—————————|————–+ | | NtUpdateWNFStateData| |NtSubscribeWNFStateChange…

单向链表缺陷与 AuthzBasep SecurityDescriptor 传播攻击

摘要 Windows 安全引用监视器是对象访问的最后仲裁者,其访问检查逻辑沿 AuthzBasep 单向链表回溯安全描述符,为用户态权限评估提供“足够快”的缓存答案。然而,该缓存与内核态真实安全描述符之间存在可被精确操纵的传播延迟。攻击者通过在 NtSetSecurityObject 修改 DACL 与 AuthzAccessCheck 读取缓存之间制造 TOCTOU 窗口,可迫使 SRM 基于过期的权限授予访问令牌,造成“用户态通过、内核态拒绝”的悖论权限提升。 Windows 访问检查的分裂模型 安全引用监视器的两条路径 Windows 对对象(文件、注册表、进程)的访问控制由内核模式安全引用监视器集中裁决。当用户态应用程序请求 CreateFile 时,最终会调用 SeAccessCheck(内核态)进行权限评估。然而,大量的用户态授权框架(如 AuthzAccessCheck、AccessCheckByType)并不直接发起内核调用,而是依赖 AuthzBasep 内部的“安全描述符缓存”来模拟访问判断。 这种设计产生了天然的时间裂隙:内核真实安全描述符(位于 OBJECT_HEADER->SecurityDescriptor)由内核线程在 ObpReferenceSecurityDescriptor 中同步更新,而用户态缓存(AuthzBasepCachedSecurityDescriptor)的刷新是异步、节流的,甚至受制于单向链表遍历效率。 AuthzBasep 的单向链表缓存架构 AuthzBasep 是 Windows authz.dll 的核心引擎,内部维护一个单向链表(AuthzBasepSecurityDescriptorList),每个节点包含对象的引用名称、缓存的安全描述符副本、以及上一次刷新时间戳。当应用程序调用 AuthzAccessCheck 时,它遍历该单向链表,查找同名对象的最新缓存描述符,然后基于缓存执行访问检查,不会实时查询内核状态。 单向链表的天然缺陷在于:查找与刷新操作为 O(n) 复杂度,且没有原子性保证。刷新线程(AuthzBasepUpdateThread)周期性唤醒,逐个节点检查 LastUpdateTime,若超过阈值(默认 15 秒)则重新从内核拉取安全描述符。如果攻击者在刷新线程遍历到目标节点之前修改内核安全描述符并触发用户态访问检查,旧版缓存仍然被信任。 传播延迟 内核 SeAccessCheck 与 SepAccessCheck 的调用链分裂 理解分裂链条对于攻击窗口的精准打击至关重要。 用户态请求 →SeAccessCheck (ntoskrnl.exe):直接读取对象内核安全描述符,基于调用者 Token 进行即时权限判定。任何修改(如 NtSetSecurityObject)均立即在此路径中体现。 用户态 Authz 请求 →SepAccessCheck (authz.dll):是 SeAccessCheck 的用户态模拟。它使用缓存的 SecurityDescriptor 副本,并与内核 Token 句柄交互。SepAccessCheck 不与 SeAccessCheck 同步。 分裂意味着:同一对象在同一时刻,SeAccessCheck 可能返回“拒绝”,而 SepAccessCheck 可能返回“允许”。这正是缓存延迟攻击的根基。 AuthzBasepCachedSecurityDescriptor 的刷新触发器 AuthzBasepCachedSecurityDescriptor 结构的关键字段: typedefstruct _AUTHZ_BASEP_CACHED_SD { LIST_ENTRY Link;…

通过 AER 滥用和 ECRC 劫持实现 DMA 重定向

摘要 PCI Express 链路上传输的每一条 DMA 读写请求都被封装为 TLP(事务层数据包),携带设备标识符和目标物理地址。IOMMU 通过 BDF 编号和地址转换表对这些请求进行合法性检查,阻止未经授权的内存访问。然而,TLP 层存在一个容易被忽视的攻击面:高级错误报告(AER)允许设备向根复合体报告各类错误,而端到端 CRC(ECRC)本用于检测传输过程中的比特翻转,却可能被恶意设备主动操纵,迫使接收端进入错误恢复流程。在错误恢复的重传机制中,如果攻击者能够制造 TLP 的“合法重放”,并将其中的物理地址字段替换为攻击目标,便能在 IOMMU 检查之前完成 DMA 重定向。 PCIe TLP 与 DMA 的权限模型 TLP 的结构与路由 PCIe 采用分层协议,事务层负责在设备与主机之间传输数据。 每个 TLP 包含: TLP 前缀(可选):用于扩展头信息。 TLP 头:包含格式(Fmt)和类型(Type)字段,决定 TLP 是存储器读写、配置读写、消息还是完成包。对于存储器写 TLP,头中还有地址字段(32 位或 64 位物理地址)。 数据载荷:实际传输的有效数据。 TLP 摘要:可选字段,包含 32 位的 ECRC,用于检测端到端的比特错误。 在 DMA 操作中,设备主动向主机内存发起读写请求。主机芯片组中的根复合体(Root Complex)根据 TLP 头中的 Requester ID(Bus/Device/Function,BDF)和地址进行路由,并由 IOMMU…

CT日志、SCT签发时间与证书透明度的时间回拨攻击

摘要 Certificate Transparency 通过 Merkle Tree 哈希链保证证书的不可抵赖性,SCT 记录了 CA 签发证书的精确时间,二者共同构成了现代 TLS 信任体系的审计根基。然而,CT 日志服务器的时间戳可信吗?如果攻击者能够操控 NTP 同步路径,便可在证书签发时间与 SCT 时间之间制造足以掩盖入侵窗口的差异,使证书吊销检查和 CT 监控双双失效。 Merkle Tree 与 SCT 的时间锚点 Certificate Transparency 的设计初衷 2011 年,荷兰证书颁发机构 DigiNotar 遭入侵,攻击者为其未控制的域名(包括 Google 和 CIA)签发了超过 500 张伪造证书,随后在伊朗用于大规模中间人攻击。事件曝光后,DigiNotar 宣告破产,浏览器厂商紧急撤销其根证书。这一事件暴露了 TLS 信任模型的结构性缺陷:CA 可以为其未被授权的域名签发证书,而依赖方(浏览器)无法实时获知滥用行为。 Certificate Transparency 正是为解决这一“CA 信任无限”问题而生。CT 要求 CA 将签发的每一张证书提交到至少一个公开的 CT 日志服务器,日志服务器将该证书追加到一条仅可追加、不可删除的 Merkle Tree 哈希链中,并返回 Signed Certificate Timestamp(SCT)作为证书已被记录的证明。依赖方(浏览器)在验证证书时检查其是否包含有效的…