Knative冷启动劫持

摘要 Knative 通过保留最小数量的常驻 Pod 来加速无服务器函数的冷启动,但其容器复用策略在安全性与性能之间制造了一道危险的裂缝:当某个函数容器被重用时,其内存中可能残留上一个请求的变量、环境密钥、数据库连接池或临时文件。攻击者可以在 Serverless 平台上发起一个合法请求,利用容器复用时未清理的 tmpfs 或环境变量读取上一个租户的残留数据。部分平台为了降低延迟,甚至不重启 Node.js 或 Python 进程,仅清空 HTTP 上下文,使得跨请求的数据残留成为系统性缺陷。 Knative 冷启动优化与实例复用 冷启动的代价与优化 Knative Serving 基于 Kubernetes,为每个函数实例(Revision)创建 Pod 来服务请求。当函数长时间未收到请求时,Knative 会将其 Pod 缩减到零以节省资源。下一个请求到达时,需要重新创建 Pod、拉取镜像、启动容器、初始化运行时环境——这一“冷启动”过程可能耗费数秒,远高于用户体验可接受的延迟。 为了降低冷启动延迟,Knative 引入了 容器实例保持 策略:即使没有活跃请求,也保留一个或多个“温暖”的 Pod 在内存中等待。 当新请求到达时,这些温暖的 Pod 可以立即处理请求,避免镜像拉取和运行时初始化。这些温暖的 Pod 可能被同一个函数实例的多个请求复用,甚至在某些配置下被同一命名空间内的不同函数实例复用。 实例复用时的内存语义 当一个温暖的 Pod 被复用时,Knative 不会重新启动容器进程,而是直接向现有进程发送新的 HTTP 请求。对于 Node.js、Python 等解释型语言运行时,这意味着同一进程的内存空间将被多个请求共享。tmpfs、环境变量、全局变量和堆中的对象都保持原样,只有 HTTP 上下文被重新初始化。 这种“进程级复用”带来了显著的安全隐患:如果前一个请求在内存中留下了敏感数据,这些数据可能被后续请求读取。例如: 用户 A 提交了一个包含 API 密钥的函数请求,该密钥被存储在某个全局变量或 tmpfs 文件中。 用户 B 随后向同一函数实例发起请求,如果用户…

eBPF 程序间隐蔽信道

摘要 eBPF 映射为内核态程序提供了高效的数据交换机制,其原子性保证仅限于单个 64 位操作。当多个 eBPF 程序共享同一个 BPF_MAP_TYPE_ARRAY 或 BPF_MAP_TYPE_HASH 时,涉及多个映射字段的复合操作存在天然的竞态窗口。攻击者可在一个容器的 eBPF 程序中快速修改映射的多个字段,与此同时,另一个容器的 eBPF 程序在读取前观察到不一致的中间状态,从而建立基于时序的隐蔽信道,绕过容器网络隔离与安全策略。更关键的是,bpf_spin_lock 的适用范围有限——它仅保护映射值内部字段的原子性,无法阻止攻击者利用映射操作之间的竞态。 eBPF 映射的并发语义 原子性保证的边界 eBPF 映射是内核中存储键值对的数据结构,支持 BPF_MAP_TYPE_ARRAY、BPF_MAP_TYPE_HASH、BPF_MAP_TYPE_PERCPU_ARRAY 等多种类型。对于哈希映射,bpf_map_update_elem 和 bpf_map_lookup_elem 在单个条目上提供原子读写——这意味着一个 64 位的值在任一时刻要么被完全写入,要么被完全读出,不会出现撕裂读写。 然而,复合操作的原子性并不存在。当攻击者需要同时修改两个或多个映射条目,或者在一个映射条目上执行“读取-修改-写入”序列时,多个指令之间存在可被其他程序观察到的窗口。例如: // 非原子复合操作 __u64 *counter = bpf_map_lookup_elem(&stats_map, &key); if (counter) { __u64 old = *counter; *counter = old + 1; // 读-改-写 竞态 __u64 *timestamp = bpf_map_lookup_elem(&time_map, &key); if (timestamp) { *timestamp = bpf_ktime_get_ns(); // 与 counter…

镜像仓库幻影层

摘要 OCI 镜像以层为单位分发,每层通过 tar 包与白文件(whiteout)控制文件删除与覆盖。容器运行时在解压镜像时,runc 的 mountToRootfs 会逐层应用变更,此过程对符号链接的处理存在边界歧义:攻击者可构造恶意镜像,先创建指向宿主根目录的符号链接,再利用后续层的挂载与白文件操作,绕过文件系统的访问控制,在容器启动阶段完成宿主机文件系统的逃逸。 OCI 镜像与容器运行时基础 镜像分层与 tar 层 OCI 镜像由多个只读层叠加而成,每一层是一个 tar 归档,保存相对于根文件系统的变化。镜像构建时,每个 RUN、ADD 等指令会生成一个新的层,最终通过联合文件系统(如 OverlayFS)合并成一个完整的根文件系统视图。 容器启动时,containerd 调用 runc 创建容器。runc 根据 OCI 配置,先挂载根文件系统(通常是 overlay),然后执行 rootfs 的准备工作,包括处理各层的文件操作。 runc 的 mountToRootfs 流程 runc 的 libcontainer/rootfs_linux.go 中,mountToRootfs 负责将各层挂载到容器的根文件系统路径。 流程大致如下: 挂载根文件系统(例如通过 overlay)到临时目录或直接使用 pivot_root。 应用各层:对于每个镜像层,runc 会遍历 tar 包中的条目,执行创建、修改、删除等操作。 处理 whiteout 文件(.wh. 前缀)和 opaque 目录文件(.wh..wh..opq)以隐藏或删除底层内容。 挂载卷、设备等。 在应用层的过程中,runc 会处理符号链接。具体逻辑在 mountToRootfs 中调用 mountToRootfs 的辅助函数 doMount,并依赖 os.Lstat 和 os.MkdirAll 等。对于符号链接,runc 可能在某些路径上直接跟随链接,导致对宿主机文件系统的意外操作。 符号链接与白文件的滥用 符号链接的跟随与逃逸 OCI 镜像的 tar 包可以包含符号链接。如果攻击者在一个层中创建一个指向宿主根目录(例如 /)的符号链接,然后在后续层中通过该符号链接写入文件,由于解压过程通常在宿主命名空间中执行(在挂载命名空间隔离之前),写入操作可能逃逸到宿主机文件系统。 runc 在处理各层时,会先应用层的 tar 包到容器的 rootfs 目录。这个过程发生在 pivot_root 之前,此时 rootfs 是一个宿主机路径(例如 /var/lib/containerd/rootfs/…)。如果 tar 包中包含符号链接并指向 /,那么后续在该符号链接下的写入操作可能影响到宿主机根目录。 白文件清空机制 白文件(以 .wh. 开头的文件)用于在更高层中删除低层的文件或目录。runc 在处理白文件时,会将低层的目标路径替换为删除操作。如果目标路径是一个符号链接,且该链接指向宿主目录,则删除操作可能删除宿主上的文件。 攻击者可以利用这种“删除”操作来掩盖逃逸行为,或者在逃逸后清理痕迹。 嵌套挂载与保护目录的绕过…

DNSSEC 密钥标签碰撞

摘要 DNSSEC 通过数字签名保证 DNS 数据的完整性,其信任锚点由 DNSKEY 记录的密钥标签标识。解析器在验证签名时,根据授权签名者(Signer)字段中的密钥标签查找对应的 DNSKEY。然而,16 位的密钥标签空间仅有 65536 种可能,攻击者可以暴力生成一个与信任锚标签相同的恶意密钥,并将其植入精心构造的子域 NSEC3 记录中。部分递归解析器(如 Unbound)在解析路径上的密钥选择逻辑存在缺陷:当响应中包含与当前信任锚标签匹配的非权威 DNSKEY 时,可能错误地将其用于验证签名,导致攻击者伪造的签名被接受,整个 DNSSEC 验证降级为“无保护”状态。 DNSSEC 密钥标签与解析器信任模型 密钥标签的计算方法 DNSSEC 使用 DNSKEY 记录存储区的公钥,每个密钥由 16 位的密钥标签唯一标识(在 RRSIG 和 DS 记录中引用)。密钥标签的计算过程如下(RFC 4034 附录 B):将 DNSKEY 的 RDATA(标志位、协议、算法、公钥)按网络字节序拼接,然后逐 16 位累加,最后取低 16 位作为标签。算法简单,无密码学强度,仅作为快速索引。 由于算法可逆性弱,攻击者无法直接计算出与特定标签匹配的公钥,但可以反复生成密钥对,计算标签,直到碰出目标标签。对于 16 位空间,平均需要约 32768 次尝试,现代 CPU 在毫秒级内即可完成。因此,密钥标签的碰撞是现实可行的。 解析器的密钥选择路径 以 Unbound 为例,验证签名时的密钥选择流程: 解析器收到包含 RRSIG 的响应,提取 Signer 字段,其中含有密钥标签。…

剪贴板即信道

摘要 异步剪贴板 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 创建一个“读剪贴板”任务排入队列,返回…

C++ 元编程的定时炸弹

摘要 C++ 的模板与 constexpr 机制赋予开发者编译期计算的能力,其图灵完备性使得任意算法均可在编译阶段执行。然而,编译器的实现必须为这种计算设定资源上限——-ftemplate-depth 和 -fconstexpr-steps 等选项维持着编译期世界的脆弱平衡。攻击者在提交至 CI/CD 的源代码中暗藏精心构造的递归模板或 constexpr 循环,将合法代码转化为耗尽编译服务器 CPU 与内存的定时炸弹。更隐蔽的威胁在于:通过 __builtin_is_constant_evaluated 或编译器差异注入条件分支,同一份代码可在 Clang 下悄然通过,却在 GCC 上触发堆耗尽,实现对特定工具链的定向打击。 编译资源 模板递归与 constexpr 函数的计算模型 C++ 模板元编程通过递归实例化处理类型或值。例如,一个计算阶乘的模板: template <unsigned N> structFactorial { staticconstexprunsigned value = N * Factorial<N – 1>::value; }; template <> structFactorial<0> { staticconstexprunsigned value = 1; }; 编译器为每个不同的模板参数生成一个新的类特化,递归深度达到 N。constexpr 函数则允许在编译期执行常规 C++ 函数,但其求值同样受制于实现定义的步数限制。 这两种机制都是图灵完备的,但本质上是编译器的解释器——没有硬件强制执行严格的栈深度或时间片。编译器采用固定的内部计数器防止无限循环,这些计数器的默认值构成了攻击面。 编译器限制 各主流编译器均提供控制编译期计算深度的选项: 编译器 模板递归深度限制 constexpr 求值步数限制 默认值 最大可设置值 GCC -ftemplate-depth=n…