BlackHatUSA2026:PLaTypus执行围栏"

admin 2026-10-16 13:48:28 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

Black Hat USA 2026:PLaTypus执行围栏

原创

Max Luo
Max Luo

白帽子罗棋琛

2026年10月14日 09:39
中国香港

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

把动态库关进执行围栏:PLaTypus 如何收紧跨模块控制流

现代 Linux 进程很少是一个完整、封闭的二进制。主程序、glibc、OpenSSL、NSS、压缩库和业务插件共同占据同一地址空间。硬件控制流保护可以挡住经典 ROP,却没有回答一个更细的问题:模块 A 中被劫持的函数指针,为什么可以指向模块 B 中任意一个满足硬件入口约束的函数?

PLaTypus 给出的答案不是维护一张全进程、全类型的精确控制流图,而是复用 ELF 动态链接已经存在的依赖信息:一个 DSO 只能间接到达自身代码,以及它确实声明过的跨模块入口。课件所描述的原型基于 LLVM 20.1.0 与 glibc 2.41,通过编译器、链接器和动态加载器协作,把模块边界从“共享地址空间中的逻辑概念”变成运行时可执行的控制流约束。

本文依据公开课件、论文配套材料和工具链文档整理。重点放在防护机制、ELF 细节和落地验证,不把它写成一次会议见闻。

1、CET 挡住了返回地址,却没有隔离动态库

Intel CET 主要由两部分构成:Shadow Stack 保护 backward edge,要求普通栈与影子栈中的返回地址一致;Indirect Branch Tracking(IBT)保护 forward edge,要求间接 call 或 jmp 的目标以 ENDBR64 开头。前者针对返回地址破坏,后者显著减少可作为间接分支目标的指令位置。

问题是,IBT 判断的是“这里能否成为一个间接入口”,不是“当前模块是否有权调用这个入口”。只要另一个 DSO 中的函数带有 ENDBR64,被覆盖的函数指针就可能跳过去。libc 中的 system、execve、文件操作和 syscall wrapper 因而仍是可用的整函数 gadget。

图 1:CET 缩小了合法入口集合,却没有建立调用模块与目标模块之间的授权关系

可在一台启用 CET 工具链的测试机上检查二进制属性和入口指令:

bash

cc -O2 -fPIC -fcf-protection=full -Wl,-z,relro,-z,now \\   -o demo demo.c  readelf -n ./demo | sed -n '/GNU_PROPERTY/,+8p' objdump -d -M intel ./demo | rg -n 'endbr64|call.*\\*|jmp.*\\*'

-fcf-protection=full 生成分支保护相关代码;-z relro,-z now 让动态重定位在启动阶段完成并将相关表项转为只读。它们都是 PLaTypus 威胁模型的组成部分,但都不等价于模块级授权。

2、FOP 利用的是“整个函数都合法”

Function-Oriented Programming(FOP)不依赖传统的短 ROP gadget。攻击者寻找能循环分派函数指针的 dispatcher,再把一组完整函数当作 gadget 调用。每个目标本来就是合法函数入口,因此可以通过 IBT;调用与返回仍按正常指令配对,也不必破坏 Shadow Stack。

图 2:dispatcher 从攻击者可控内存连续取出函数地址,完整函数本身承担 gadget 角色

课件使用 glibc loader 中的 _dl_call_fini 说明这类调度器为何危险:调度循环位于 ld-linux,被调用的函数可以来自 libc。控制流跨越 DSO 边界,而现有 CET 只检查每个目标是不是合法 IBT 入口。

把这一过程抽象成伪代码,漏洞不在 ENDBR64 是否存在,而在 next 的候选集合过大:

c

typedefvoid(*fop_gadget)(void *ctx);  structchain {     fop_gadget *entries;   // 已被攻击者控制size_t count;     void *ctx; };  staticvoiddispatcher(struct chain *c) {     for (size_t i = 0; i < c->count; ++i) {         c->entries[i](c->ctx);  // CET 只确认目标是合法函数入口     } }

因此,PLaTypus 不试图再次证明每个函数的语义安全性,而是删除 dispatcher 最依赖的能力:从一个模块任意跳到另一个模块的函数集合。

3、PLT 本来就是 ELF 的跨模块调用清单

ELF 中的 Procedure Linkage Table(PLT)是主程序或 DSO 调用外部符号的标准跳板。编译器生成对 puts@plt 的直接调用,PLT stub 再经 GOT 中已解析的地址转向 libc。启用 Full RELRO 后,运行时重定位表不再适合被直接覆盖,但 PLT 仍保留了一个重要事实:这个模块声明自己需要 puts。

图 3:对外部函数的正常调用经由当前模块自己的 PLT stub 完成

不需要先理解 LLVM 内部结构,也能从现有制品导出一个粗略的“外部能力清单”:

bash

# 动态未定义符号:模块请求动态链接器提供的能力 readelf --dyn-syms --wide ./libparser.so \\   | awk '$7 == \"UND\" { print $8 }' \\   | sed 's/@.*//' | sort -u  # PLT 中实际存在的跳板 objdump -d -j .plt -j .plt.sec ./libparser.so \\   | awk '/<.*@plt>:/ { gsub(/[<>:]/, \"\", $2); print $2 }'# 动态依赖关系 readelf -d ./libparser.so | rg 'NEEDED|BIND_NOW|FLAGS'

这份清单并非传统意义上的 allowlist 配置文件,而是链接结果的一部分。PLaTypus 的关键设计就是让间接控制流服从这份已存在的依赖声明:模块若没有相应 PLT,就不能借一个被劫持的函数指针调用该外部函数。

4、Execution Jail 用两条位运算约束间接目标

PLaTypus 为每个 DSO 划定 Execution Jail(EJ)。对模块内的间接 call/jmp,编译器在使用目标地址前加入 or 和 and 两个掩码,把高位固定为当前模块执行区域的前缀,并限制可变地址范围。处理后的目标只能落入当前 DSO 的可执行区域,不能带着原地址直接越过模块边界。

图 4:掩码把间接目标压回当前 DSO 的执行区域,图中示例使用 28 位模块前缀

课件中的汇编可以写成下列等价形式:

asm

; 原始间接调用 call rax  ; PLaTypus 插桩后的概念形式 or   rax, OR_MASK       ; 注入当前 Execution Jail 的地址前缀 and  rax, AND_MASK      ; 清除 Jail 之外的可变位 call rax

它不是边界检查失败后再 abort(),而是对目标地址做强制归一化。工程上应明确三个前提:加载器要按预定布局放置代码;掩码必须与每个 DSO 的实际映射一致;可执行区和普通 r--/rw- 映射要分离,防止归一化后的地址落入非代码页。

图 5:普通间接分支经过掩码后只能留在本模块;合法跨模块路径需要专门出口

威胁模型同样值得注意:课件假设攻击者拥有任意读写、可覆盖函数指针且可多次触发漏洞,同时 DEP、Full RELRO 和 CET 已启用。PLaTypus 是利用缓解层,不是内存安全替代品;如果攻击者已经能修改代码页、加载任意 DSO 或控制加载器,它所依赖的边界也会被改变。

5、Fake PLT 解决地址被取用的外部符号

并非所有外部函数引用都表现为 call puts@plt。下面这种代码取得 puts 的地址后再间接调用,链接器会产生非 PLT relocation:

c

#include<stdio.h>intmain(void) {     int (*fp)(constchar *) = &puts;     return fp(\"hello\"); }

图 6:函数地址被装入指针时,目标可能直接解析为另一个 DSO 中的真实函数地址

如果直接对 fp 做 Execution Jail 掩码,真实的 libc 地址会被改写为主程序地址空间内的无关位置。PLaTypus 的处理方式是为这类 address-taken symbol 生成 Fake PLT,并把对应 relocation 改为指向本模块 .fakeplt.sec 中的 stub。

图 7:为非 PLT relocation 补出本模块跳板,再把取地址结果重定向到跳板

asm

; .fakeplt.sec 中的概念 stub puts_fake_plt:     endbr64     jmp qword ptr [rip + puts_got_slot]     nop

函数指针现在保存的是 Fake PLT 地址。它位于本模块 EJ 内,因此掩码后仍可到达;stub 再以受控方式跳向解析后的外部函数。最终可到达集合变成:本模块的内部函数,加上本模块为 address-taken 外部符号生成的 Fake PLT。

图 8:被劫持的指针不能遍历全进程函数,只能到达本 DSO 与其 Fake PLT 集合

普通 PLT 和 Fake PLT 必须区别布局。普通 PLT 由编译器生成的直接 call 使用,不应成为被劫持指针的间接目标;Fake PLT 恰恰需要被间接调用。因此设计把普通 PLT 放在 EJ 外,只把 Fake PLT 留在 EJ 内。

6、回调是模块边界策略最难处理的反向路径

qsort()、event loop、GUI toolkit 和 RPC runtime 都允许调用方把本模块函数地址交给外部库。控制流先从模块 A 进入 libc,再由 libc 间接回调 A。如果只按“当前 DSO 只能留在自身 EJ”执行,libc 无法合法返回到 compare()。

图 9:callback 形成与普通导入相反的边,callee 模块需要回到调用方提供的函数

PLaTypus 在编译/链接阶段按模块收集 callback symbol,并识别执行回调的 call site;这些位置使用扩展掩码,允许到达与该回调关系对应的目标区域。课件把实现概括为两个动作:逐模块收集 callback symbols;对 callback invocation site 做扩展 masking。

图 10:策略不是全局放开 libc 的所有间接调用,而是在实际回调点扩展目标集合

工程审计可以先从高风险 callback API 建清单,再检查每个回调函数是否意外暴露了敏感逻辑:

yaml

callback_review:libc:-api:qsortcallback_arg:4expected_signatures: [\"int(const void *, const void *)\"]     -api:pthread_createcallback_arg:3expected_signatures: [\"void *(void *)\"]   project:reject_writable_executable_callbacks:truerequire_declared_callback_sites:trueflag_variadic_or_untyped_callbacks:true

这里仍需保留类型审计。模块边界可以阻止 libc 跳回完全无关的 DSO,却不天然区分同一允许区域内签名不兼容的函数;PLaTypus 与 type-based CFI 解决的是不同维度的问题。

7、dlopen 与 dlsym 迫使策略面对运行时依赖

静态链接信息无法预知任意 dlopen() 路径和 dlsym() 字符串。课件的原型选择在启动时预加载必要模块,并为通过 dlsym 获取的符号生成合适 PLT stub。glibc 经常动态加载 NSS 等库,因此这不是只影响插件程序的边角案例。

图 11:预加载和补充 PLT stub 将运行时发现的边重新纳入模块能力模型

上线前可以用 loader 审计与系统调用跟踪找出动态依赖;不要只查看 DT_NEEDED:

bash

# 测试环境记录加载器决策 LD_DEBUG=libs,bindings ./service 2>ld-debug.log  # 观察运行期间打开的 DSO;按发行版调整路径过滤 strace -f -e trace=openat,mmap,mprotect ./service 2>&1 \\   | rg '\\.so(\\.|\\\")|PROT_EXEC'# 源码侧定位显式运行时解析 rg -n '\\b(dlopen|dlmopen|dlsym|dlclose)\\s*\\(' src/ third_party/

预加载会改变启动成本与懒加载语义,也可能让原本按需加载的插件提前执行 constructor。更稳妥的发布流程应分别验证冷启动、插件发现、NSS/认证、locale、TLS provider 与卸载路径,而不是只跑主程序 happy path。

8、评估数据说明它缩小的是 gadget 可达性

课件从正确性、安全收益和性能三个维度评估:19 个真实应用的测试套件未观察到失败;另外插桩了 16 个库,包括 glibc 与 OpenSSL;按模块计算的 cross-DSO gadget reduction 超过 98%。

图 12:PLaTypus 把每个模块可借用的跨 DSO gadget 集合压缩了 98% 以上

对 _dl_call_fini 这一 FOP 链,loader 模块内没有通往所需 libc gadget 的 PLT,跨模块 transition 因而不可达。

图 13:dispatcher 仍可能存在,但其所需外部函数不再属于该模块的允许出口

这组数据不应被解读成“消除了 98% 的全部代码复用”。它衡量的是每个模块的跨 DSO gadget 可达集合;本模块内部的合法间接目标仍需 CET、编译器分析、内存安全和进一步的 intra-DSO isolation 共同约束。课件还提出把从不被间接调用的敏感函数移出当前 EJ,例如 libc 中的 system(),进一步降低同模块滥用。

性能图覆盖 Bftpd、Memcached、Lighttpd、Redis、SQLite、Nginx 和 Nginx(ramfs),柱状结果均低于 0.5%,其中部分接近零。

图 14:课件报告的运行时开销低于 0.5%;该结果应在本企业 workload 上重新测量

可按固定构建、固定 CPU 和稳定样本量做 A/B:

bash

# baseline 与 protected 必须使用相同业务配置和数据集 taskset -c 2-5 hyperfine --warmup 5 --runs 30 \\   './baseline/bin/benchmark --dataset ./corpus' \\   './protected/bin/benchmark --dataset ./corpus' \\   --export-json platypus-ab.json  # 服务型 workload 同时比较 p50/p95/p99、吞吐、启动时间和 RSS wrk2 -t4 -c128 -d120s -R20000 http://127.0.0.1:8080/api/test

只有均值开销很小还不够。掩码插桩位于间接调用热路径,必须关注尾延迟、指令缓存、branch miss,以及 callback 密集场景。

9、受保护与未受保护模块可以共存,但不是对称信任

PLaTypus 支持渐进部署。未插桩模块仍可到达地址空间内的任意函数;已插桩模块只能到达自己拥有 PLT 的未保护函数。换言之,它可以限制“从受保护代码向外”的能力,却无法替第三方二进制约束“从未保护代码向内”的任意间接跳转。

图 15:互操作保留了兼容性,同时明确留下未保护模块这一信任缺口

因此落地顺序不应只按编译难度决定。更合理的优先级是:先保护持有复杂解析器、网络输入和高价值函数指针的 DSO;再处理 loader、libc、TLS 与认证库;最后才是低风险插件。对于无法重编译的模块,至少建立清单、签名和加载路径控制。

bash

# 生成发布制品的 DSO 与 CET/RELRO 基线for elf in ./dist/bin/* ./dist/lib/*.so*; dotest -f \"$elf\" || continue   file \"$elf\" | rg -q 'ELF' || continueprintf'%s\\t'\"$elf\"   readelf -n \"$elf\" 2>/dev/null | rg -q 'IBT' \\     && printf'IBT=yes\\t' || printf'IBT=no\\t'   readelf -lW \"$elf\" 2>/dev/null | rg -q 'GNU_RELRO' \\     && printf'RELRO=yes\
' || printf'RELRO=no\
'done

若未保护模块允许从用户目录、临时目录或可写插件路径加载,攻击者甚至不必挑战 Execution Jail,只需引入新的合法代码模块。模块签名、只读文件系统、noexec 挂载和依赖供应链校验仍是必要条件。

10、落地评审要验证边,而不只是确认编译成功

PLaTypus 原型修改 LLVM 20.1.0 的编译器与链接器,并修改 glibc 2.41 loader。它说明模块边界硬化可以做到兼容复杂库和低运行时开销,但这仍不是在普通发行版上加一个编译参数就能获得的通用能力。公开材料同时给出 ARM 与 Windows 的可迁移方向:ARM 用 BTI 加 PAC 承担前后向保护;Windows 可将 CFG 与 IAT 作为相近基础,并需要修改平台加载路径。

图 16:设计原则可迁移,但 ELF PLT、Windows IAT 和各架构 backward-edge 机制并不相同

对准备评估该方案或同类模块级 CFI 的团队,release gate 至少应覆盖:

yaml

module_cfi_release_gate:build:cet_ibt_and_shadow_stack_verified:truefull_relro_verified:trueno_rwx_load_segments:truereachability:per_dso_plt_inventory_reviewed:trueaddress_taken_symbols_have_fake_plt:truenormal_plt_not_indirectly_reachable:trueruntime:callbacks_tested:truedlopen_dlsym_paths_tested:truenss_tls_locale_plugins_tested:truesecurity:known_fop_chain_blocked:truecross_dso_gadget_delta_measured:trueuninstrumented_modules_documented:trueperformance:startup_and_rss_measured:truep50_p95_p99_and_throughput_measured:true

图 17:方案的实际价值是关闭任意跨 DSO transition,同时保留受保护与未保护代码的互操作

最终值得带回工程实践的不是某个固定掩码,而是一个更一般的原则:链接依赖本身就是最小权限信息。 当模块只声明了 memcpy、malloc 和 puts,一个被破坏的函数指针不应自动获得调用 system、execve 和任意 TLS 私钥操作的能力。PLaTypus 把这条原则下沉到了控制流层,而且没有要求先构造一张覆盖整个进程的完美 CFG。


资料:

  • Black Hat 官方 Session 页面
  • Black Hat USA 2026 Session
  • PLaTypus 论文与开源材料(Zenodo)
  • CISPA 项目介绍
  • LLVM Control Flow Integrity Design
  • Intel CET 技术说明

原始会议材料(仓库内)

  • 演讲课件 PDF

开源资料与原始议题 PDF

本文对应的 Markdown 原稿、Black Hat 原始议题 PDF 与配图已整理到 GitHub,可按文章编号查找和下载。

https://github.com/cybermaxluo/black-hat-usa-2026-talks

也可以点击文末“阅读原文”进入仓库。欢迎 Star、提交 Issue 或参与勘误。

BlackHatUSA2026:PLaTypus执行围栏" 网络安全文章

BlackHatUSA2026:PLaTypus执行围栏"

本文介绍BlackHatUSA2026上提出的Platypus技术,通过复用ELF动态链接依赖信息,为每个DSO划定执行围栏,用掩码约束间接分支目标,使被劫持的函数指针只能到达本模块内部函数及为address-taken符号生成的fakeplt,从而将跨模块gadget可达集合压缩98%以上,同时处理回调与dlopen等动态场景。
拉黑后消息重放漏洞" 网络安全文章

拉黑后消息重放漏洞"

本文介绍拉黑后消息重放漏洞,源于服务端未校验拉黑状态,攻击者可重放旧请求绕过拉黑继续发送消息,导致拉黑功能失效并可能投递恶意内容。文章提供测试流程与危害分析,建议服务端校验拉黑关系。
马来西亚寻求深化对华合作,强化跨境网络诈骗调查与追赃协作" 网络安全文章

马来西亚寻求深化对华合作,强化跨境网络诈骗调查与追赃协作"

文档汇总了多项网络安全事件:Citrix修复高危漏洞CVE-2026-107406;加拿大Aldo集团遭Clop勒索团伙暗网泄露424GB数据;WolfSSH修复严重身份验证绕过漏洞;恶意npm包窃取浏览器密码等;黑客滥用必应重定向发起ClickFix攻击;勒索软件攻击致日本IDCF云服务中断;Atlassian存在未授权文件读取漏洞。建议关注相关补丁与威胁情报。
评论:0   参与:  0