第一次看到这个标题的时候,我的第一反应是:KASLR 又又又被击穿了。在 2020 年前后那批“瞬态执行类漏洞”研究里,Intel TSX 几乎成了绕过内核地址随机化的标配放大器,每次出现都让人觉得“哦,居然还能这样玩”。Kernel Address Space Layout Randomization(KASLR)一直被当成缓解内存破坏攻击的重要防线,而 TSX 本意是为了事务内存,结果被安全研究者拿来当作一把高精度的“探针”,用来确认内核地址空间里哪些地址是映射过的。这篇文章不是要复述某个特定漏洞的全部攻击细节,而是想把“TSX 为什么能打破 KASLR”这件事拆透了:先讲 KASLR 到底在防什么,再讲 TSX 的事务机制为什么天然适合做侧信道探测,然后给出一个可以自己搭建的测量环境与实验思路,最后从防御者的角度聊聊怎么削弱这类探测。无论你是做内核安全、系统加固,还是单纯想理解“为什么一个 CPU 特性会被安全社区反复拿来用”,这篇都能给你一套完整的思维框架。
1. 先把 KASLR 这一层“随机化”拆开看:它到底在保护什么
1.1 内核地址随机化的有效性与有限熵
KASLR 的核心想法很直接:操作系统内核每次启动时,把内核自身代码段、数据段加载到不同的虚拟地址上,而不是固定在某一个基地址。传统漏洞利用一般需要知道某个内核函数的地址,比如commit_creds、prepare_kernel_cred这类敏感函数,或者至少要能定位到内核的.text段基址。如果每次启动地址都不一样,攻击者在不知道基址的情况下就难以构造稳定的 ROP 链或内核读写原语。
x86-64 下的 Linux KASLR 并不是把整个内核地址空间重新洗牌,而是对几个主要区域分别在一定的候选窗口内做随机化。以主流实现为例:内核_text段基址按 2MB 粒度在相对有限的窗口里移动,page_offset_base、vmalloc_base、vmemmap_base这几个直接映射和线性映射区域也有各自的随机范围。它们彼此之间存在约束关系,所以实际可利用的熵并没有看起来那么高。早期实现的有效熵甚至只有个位数比特,后面几代内核才逐步增加随机范围。
很多人对 KASLR 有个误解:以为“地址随机化 = 地址不可预测”。实际上它只能提高攻击成本,不能保证永远不被探测。因为虚拟地址空间是跟页表直接对应的,一个地址是否被映射、映射在哪一级,在内存子系统里是有“物理痕迹”的。只要攻击者能找到一种方法来观察这些痕迹,KASLR 的随机化就会被降维打击。
1.2 为什么 KASLR 不是万能钥匙
KASLR 属于“缓解措施”而不是“安全边界”。它跟 ASLR 在用户态的定位一样:让漏洞利用过程变贵、变慢、变不稳定,而不是从根上阻止控制流劫持。只要攻击者能够被观察到“某个地址是不是映射”,KASLR 的随机性就可以被消解。
观察地址是否映射的途径可以粗分为两大类:一类是“信息泄露”,也就是直接通过内核输出的 bug 泄露某个地址,比如未初始化的指针、dmesg打印的内核地址;另一类是“侧信道”,也就是不直接拿地址,而是通过 CPU 缓存、时序、事务中止等间接效应推断映射状态。TSX 走的是后一条路,而且因为事务的“试错-恢复”特性,它比普通用户态程序探测内核地址要优雅得多。
1.3 攻击 KASLR 的典型思路:信息泄露 vs 侧信道
传统上,绕过 KASLR 最省事的办法就是找一处信息泄露。内核里任意一个%p打印,如果被格式化成裸地址,攻击者就白捡一个指针,直接算出内核基址。为此内核开发者做了大量工作,把敏感地址相关的日志输出全部换成%pK或哈希化地址。于是攻击者开始把目光转向 CPU 微架构层:缓存命中与否、执行时间差异、分支预测行为,这些都和“目标地址是否在内核页表里”有相关性。
TSX 的价值在于,它给用户态程序提供了一个“不会崩溃的探测窗口”。常规情况下,用户态去读一个内核地址会立刻触发段错误,进程直接挂掉;但在 TSX 事务里,这个页错误不会立即杀死进程,而是让事务中止,程序可以捕获中止状态并继续运行。这样一个进程就能反复扫描大量候选地址,既不崩溃,又能得到类似“这个地址是否存在页表映射”的回答。把这种探测原语和 KASLR 的有限熵结合起来,就形成了标题里的那种突破效果。
2. Intel TSX 为何会被安全研究者盯上
2.1 TSX 的工作机制与中止路径
Intel TSX 全称 Transactional Synchronization Extensions,从 Haswell 时代的处理器开始提供,包含两种使用方式:硬件事务内存(HLE,Hardware Lock Elision)和受限事务内存(RTM,Restricted Transactional Memory)。安全研究里用到的基本都是 RTM,也就是通过XBEGIN、XEND、XABORT这一组指令手动开启和结束事务。
XBEGIN会标记一个事务区域的开始,处理器把这之后一段逻辑上“原子”的读写操作暂存在事务缓冲区里。如果事务顺利执行到XEND,所有修改一次性提交;如果中途发生冲突、异常、中断、缓存容量不足等情况,事务中止,处理器回到XBEGIN之前保存的检查点,并在EBX/EAX寄存器里返回一个状态码。状态码能区分中止原因:显式中止、事务冲突、容量溢出、调试异常、嵌套中止等等。
这个机制本身就是为“乐观并发”设计的:多个线程可以大胆地同时写共享数据,遇到冲突再回滚重试,而不是一开始就用锁来串行化。但安全研究者看到的是另一面:事务的“回滚”能力让程序可以安全地执行一些平时会崩溃的指令,然后像什么都没发生一样继续。这在异常处理语义上非常特殊。
2.2 事务内存怎么变成侧信道“探针”
把 TSX 当探针的出发点很简单:在事务里执行一条访存指令,如果地址不可访问,普通情况下应该触发页错误,但在事务里这个异常被转换为事务中止,程序不会收到 SIGSEGV,而是拿到一个状态码。换句话说,TSX 把一个“一碰到就崩”的操作,变成了“碰一下,然后告诉我结果”的实验。
上面这种机制直接用于探测某个地址是否可读其实是可行的。但真正让它成为“侧信道”的,是它结合了大量微架构细节后能区分更细腻的状态:一个地址是完全没有页表项,还是有页表项但权限不足,还是映射为不可缓存类型,还是命中已经缓存的页表项,都可能体现在事务成功的时间、中止的延迟、甚至中止状态码的概率分布上。
用得比较多的一个技巧是:在事务内对目标地址发起一个无副作用但能触发页表遍历的访问,然后用高精度计时器测量从XBEGIN到XEND或到 abort 处理程序的耗时。如果一个地址的页表路径已经驻留在缓存中,耗时就会明显低于冷遍历的耗时。这些细微的时间差,经过几千次重复采样取均值或中位数,就能从噪声里提取出“该地址被映射在内核页表”的信号。
2.3 用 TSX 探测地址映射状态的基本模型
从抽象层面看,一个地址探测任务可以用下面的伪代码框架来描述,这也是我在自己的实验环境里验证过的一块核心逻辑:
#include <stdint.h> int probe_with_tsx(uint64_t addr) { uint32_t status; uint64_t start, end; start = rdtsc(); status = _xbegin(); if (status == _XBEGIN_STARTED) { // 在事务内发起一个可能触发页错误/权限错误的访存 (void)(*(volatile uint8_t *)addr); _xend(); end = rdtsc(); return 1; // 事务成功:目标地址可读且映射可用 } end = rdtsc(); // 事务中止:延迟和 status 可用于进一步推断中止原因 return 0; }注意这只是一个研究用的测量框架,不是完整利用代码。真正的研究工作还要做候选区域扫描、统计过滤、地址对齐和噪声抑制。但从这个框架能看出 TSX 探测与传统mincore、prefetch、signal handler探测法的本质区别:探测动作和恢复动作是绑定在一个硬件原语里的,不依赖信号分发,也不依赖系统调用,因此速度极快、噪声相对小、不容易给内核留下“进程反复访问非法地址”的观测痕迹。
在支持 TSX 的环境里,一次事务的成功/失败可能在几十到几百纳秒内完成。扫描一轮候选窗口也许只需要毫秒到秒级的时间。相比内核打印日志泄露地址这类软缺陷,TSX 这种硬件辅助探测更像“看着地图找房间”,确定性高,稳定性也高。
3. 复现思路:在受控环境里验证 TSX 对 KASLR 的影响
3.1 实验环境与前期检查
如果你也想在自己的测试机上做一次这样的实验,第一件事不是写代码,而是确认环境配不配合。TSX 被微码禁用已经不是新闻,尤其是在某些处理器爆出 TSX 异步中止问题之后,不少厂商默认在 BIOS/微码层面关掉了它。Linux 内核里也有对应的处理开关。
建议先跑几条命令检查:
grep -o '\brtm\b' /proc/cpuinfo | head -1 grep -o '\bhle\b' /proc/cpuinfo | head -1 dmesg | grep -i tsx如果/proc/cpuinfo里能看到rtm和hle标志,说明当前系统还暴露了 TSX 能力。如果dmesg中显示 TSX 已禁用或已被 CPU 微码修复覆盖,那就需要检查 CPU 型号、BIOS 设置和内核启动参数。部分环境需要在内核命令行里显式加tsx=on,但我不建议在生产机器上这么做,这种操作只适用于隔离的测试机器。
内核版本也很关键。启用 KPTI(内核页表隔离)之前和之后,用户态页表中是否还包含内核映射,直接影响 TSX 探测的效果。在开启了完整内核页表隔离的系统上,用户进程的页表里只有很小的一跳区域映射了内核入口,其他内核地址根本不在用户态页表里。此时即便用 TSX 去探测,也大概率得到“未映射”的结果。所以复现这类实验时,要记录好内核版本号、KPTI 开关状态、CPU 微码版本,否则实验结果会很难解释。
3.2 测量程序的设计骨架与计时策略
测量程序不需要很复杂,但几个关键点必须处理好。
第一是绑定 CPU 核心。上下文切换和中断会让计时严重失真,最好用sched_setaffinity把进程绑在一个核心上,并且避免该核心上有大量其他任务。第二是使用高精度计时,优先读 TSC 而不是clock_gettime,前者开销更低,粒度更高。第三是批量采样。单个地址的一次事务成功/失败并不能区分“未映射”和“偶然冲突”,必须对同一个地址反复探测几百上千次,再对成功率和时序分布做统计。第四是冷热状态控制,探测前用CLFLUSH清理目标地址所在的缓存行。注意:如果你在一轮测量里反复探测同一个地址,页表项本身可能已经被加载到缓存,后续测量的信号就变了。所以多数研究性代码会交错扫描一批地址,让每个被探测地址都处于相对平等的缓存状态下。
下面是一个更细化的测量循环骨架,仍然只展示研究框架,不包含任何针对特定内核版本的扫描策略:
#include <x86intrin.h> #include <sched.h> #include <stdio.h> static inline uint64_t time_tsx_probe(void *addr) { uint32_t status; uint64_t t0, t1; _mm_clflush(addr); _mm_mfence(); t0 = __rdtsc(); status = _xbegin(); if (status == _XBEGIN_STARTED) { (void)(*(volatile char *)addr); _xend(); t1 = __rdtsc(); return t1 - t0; // 成功路径耗时 } t1 = __rdtsc(); return t1 - t0; // 中止路径耗时 }在实际研究里,可能还要配合prefetcht0、prefetchnta之类的指令做变体探测,因为有些地址不一定允许显式读取,但prefetch的失败路径又能反映映射状态。TSX 和 prefetch 的组合,可以探测出“映射但不可读”这类更细的层次,这对判断内核段属性很有用。
3.3 从探测数据里提炼内核基址信号
假设你已经拿到了大量候选地址的成功/失败数据,怎么从中找到内核基址?原理并不神秘:你需要在内核可能所在的候选范围内按对齐粒度扫描,例如按 2MB 粒度扫描整个候选窗口。凡是内核实际映射到的地址,TSX 探测显示“可访问/事务成功”的概率显著更高;凡是空洞或保留区域,探测结果基本全是“事务中止”。
但这里要注意,内核不是一整块连续存在的大单位,它有代码段、数据段、只读数据、直接映射区等多个区域。直接扫描“哪个地址可读”得到的往往是一大片连续映射区间,还需要结合内核布局中的已知偏移来推算真正的基址。这个推算过程不是百分之百稳定的,因为内核版本不同、配置不同,各段之间的偏移也不完全一样。
更有意思的信号不是“事务是否成功”,而是“中止类型分布”。某些地址会让事务因为页错误而中止,某些因为权限错误而中止,某些因为缓存容量冲突而中止。把大量探测结果按中止状态码做直方图统计,能看出一个地址是“根本没有页表项”“仅有上级页表项但叶子缺失”,还是“叶子存在但权限不够”。这些信息比单一的成功/失败更能帮助攻击者缩小基址候选范围。作为防御方,理解这种信号的维度,才能真正理解为什么单纯 KASLR 挡不住这类探测。
3.4 需要避开的坑和测量误差来源
这类实验的坑比想象中多,我把自己踩过的几个典型问题列一下。
中断造成的异步中止是最烦人的干扰源。TSX 事务会因为任何中断而中止,所以哪怕你只是探测一个合法且可读的地址,如果当前核心发生了本地定时器中断,事务也会失败。解决办法是反复采样并用统计量过滤,不能只看单次结果。频率缩放也很讨厌,TSC 在现代处理器上一般是恒定频率的,但 CPU 进入高功耗状态或降频状态时,计时结果会偏离正常范围,最好在 BIOS 里锁定最高性能和 C-state,或者至少用turbostat观察确认。
另一个容易忽略的点是超线程。同一个物理核心的两个逻辑处理器共用事务存储和缓存资源,如果另一个逻辑核心上的程序在忙跑,你的事务中止率会异常升高。实验最好绑核执行,同时确认兄弟逻辑核心处于空闲或同样被控制住。
页表缓存状态对结果影响巨大。现代 CPU 的页表遍历结果会保存在 TLB 和 paging-structure cache 里。第一次探测某个地址时,页表项要从内存读,速度很慢;但第二次探测同一个地址时,页表路径可能已被缓存,速度明显变快。如果攻击者想要利用“内核页表路径是否在缓存中”这个信号来定位基址,就必须在测量中控制缓存状态,而这一点很容易因为测量顺序不当而崩溃。一个常见策略是随机化探测顺序,或者先用一个大的探针集合把缓存“污染”掉,再逐个读取目标地址的时间差异。
最后是 KPTI 的影响。启用 KPTI 后,用户态页表里基本没有内核映射,TSX 这种方法会变得不那么直接。但这不代表研究失去意义——它反过来提醒我们,缓解措施的部署变更会对攻击面产生多大影响。
4. 攻击面之外的防御思考:如何让 TSX 探测失效
4.1 KPTI 与内核页表隔离的边界
内核页表隔离(KPTI,Kernel Page Table Isolation)最初主要是为缓解熔断类漏洞而引入的,但它阴差阳错地也对 TSX 类型的用户态内核地址探测造成了很大干扰。KPTI 的基本思路是:用户态运行时,页表里只保留少量必需的 kernel trampoline 映射,把绝大多数内核地址从用户态页表中移除;进入内核态时再切换回完整内核页表。这样攻击者想从用户态直接探测“内核的某个地址是否被映射”,会得到“几乎都不存在”的结果,因为它们在当前页表里根本不在。
KPTI 不是无代价的:每次系统调用、中断、异常都要切换页表,CR3 写操作和 TLB 冲刷带来了显著的性能损耗。所以并不是所有系统都默认开启它,尤其是某些场景下管理员会因为性能原因关闭它。这提醒我们,一个缓解措施的有效性高度依赖部署状态。如果你在部署决策中以“我们内核很新”或“我们已经开了 KASLR”为由忽略 KPTI,那么 TSX 这类用户态探测的威胁就不会真正消失。
不过也要清醒一点:KPTI 并不能在所有场景下完全消灭 TSX 侧信道。只要内核在某个时候必须访问某些地址,而这些访问会留下微架构痕迹,那么攻击者就可能用侧信道去间接推断。KPTI 改变的是“用户态能不能直接试错”,而不是“内核的地址布局还能不能被观察”。它提高了门槛,但不是永久保险。
4.2 升级内核与关闭/限制 TSX 的取舍
既然 TSX 这么能惹事,最朴素的做法就是关掉它。Linux 内核在部分处理器上通过微码更新暴露了tsx_ctrl能力,管理员可以用tsx=off或者tsx=0这类启动参数来禁用 TSX。对于不需要事务内存性能的服务器来说,这是一个非常有效的缓解动作,付出的代价主要是数据库或某些并发库可能用到的 HLE/RTM 加速能力。
但要注意,关掉 TSX 并不等于“所有这类攻击都失效”。TSX 只是放大器和稳定器,没有 TSX,攻击者还有prefetch、信号处理器、页表遍历时序等替代方案,只是稳定性和效率会下降。安全加固从来不是“关掉某个功能就天下太平”,而是做纵深防御。
内核升级同样重要。每一代内核都在收紧地址泄露路径、增加随机化粒度和改善页表隔离。比如部分内核版本进一步把内核段随机化的粒度从 2MB 提升到更细的级别,还有功能粒度随机化(FGKASLR)方案,把函数级别的位置随机化也纳入考量。不过这些方案离大规模默认启用还有距离,而且面对微架构侧信道时,效果如何仍然需要逐一评估。
4.3 随机性变强之后:FGKASLR 与更多熵
传统 KASLR 的熵其实没那么大,这是我反复强调的。提高 KASLR 的熵是这类攻击的天然“解药”之一。FGKASLR 的思路是:不再只随机化整个内核镜像的基址,而是把内核里的函数重排一遍,让函数级位置也随机化。即使攻击者拿到了内核.text段基址,如果不确定目标函数的具体偏移,也很难构造精确的利用链。
FGKASLR 的推广遇到不少现实困难:重排函数会影响内核的启动流程、链接脚本、对 cache 局部性的优化,而且对某些依赖函数指针地址的模块也不是完全透明。它更像一个需要整体配套的加固措施,而不是一个简单的启动参数。
从防御角度看,最有效的组合拳仍然是:KPTI 默认开启 + 尽可能关闭/限制 TSX + KASLR/FGKASLR 提供足够熵 + 严格过滤内核日志地址 + 及时更新微码。任何单一措施都会有边界,但叠加起来就能把 TSX 型探测的成本推到不可接受的高度。
5. 我从这个题目里读到的东西:安全研究的方法论
5.1 侧信道不是新东西,但新平台给了它新的放大器
“地址是否映射”这个话题其实很古老。早在 TSX 之前,攻击者就尝试过用mprotect加信号处理、用prefetch加时序统计来探测内核地址。TSX 的贡献不在于创造了侧信道,而在于提供了一个非常干净、快速、高成功率的探测原语。它把以前需要进行异常处理和信号恢复的笨重流程,压缩成几条指令和一次状态检查。
这给安全研究者最大的启示是:任何新的 CPU 特性、任何新的“异步执行/回滚/事务化”原语,都值得被重新审视。因为一个特性在诞生时解决的是性能或并发问题,但它对安全模型的冲击往往要到很多年后才被公众认识到。TSX 就是一个典型例子:它最初绝对不是为了安全研究设计的,却成了侧信道研究工具箱里的一把利器。
5.2 评估一个缓解措施时该问的三句话
面对 KASLR 这类缓解措施,我评估时通常会问自己三句话。
第一句:这个措施假设攻击者缺少什么能力?KASLR 的假设是攻击者不知道内核基址,并且不容易观察到地址映射状态。第二句:这个假设在当前威胁模型下成立吗?如果有 TSX 这类硬件原语可以重复探测,假设就不再成立。第三句:如果假设不成立,还有哪些纵深防线兜底?对内核安全来说,兜底可能来自权限隔离、函数级随机化、更严格的地址泄露过滤、甚至硬件特性开关。
这套提问方式也适用于评估其他缓解措施,比如 CFI、堆喷隔离、用户态指针加密。每个措施都有自己的隐式假设,而攻击研究本质上就是去挑战这些假设。TSX 打破 KASLR 的过程,等于把 KASLR 的隐式假设摆到台面上,让防卫者看清边界在哪里。
5.3 做这类实验时的合规与边界意识
最后必须强调合规问题。TSX 探测内核地址的实验属于安全研究范畴,但如果你把它延伸到真实目标系统、未经授权的系统上,性质就完全不同了。我自己只会在隔离的测试机、专门的实验内核、明确授权的环境里做这类验证。哪怕是研究 KASLR 和 TSX 的交互原理,也应该遵守漏洞披露和实验伦理的底线。
写这篇文章的目的不是提供攻击武器,而是帮防御方理解攻击的原理,从而更好地设计缓解策略。安全社区的一条基本原则是:不了解攻击细节,就无法做出有效防御。但“了解”不等于“滥用”,尤其是涉及内核级地址探测、硬件特性滥用的话题,实验边界和授权边界一定要划清楚。
最后再分享一点实际操作中的体会
如果你真的要在自己的测试机上复现这类实验,我的建议是不要去追求“打破”某一个具体内核版本,而是先把测量基础设施做扎实:绑核、锁频、压住超线程、批量采样、统计建模,每一步都直接决定信号质量。我在早期实验里最大的浪费不是写探测逻辑,而是在没有绑核和没有清缓存的情况下反复跑,得到一堆谁也无法复现的“噪声图”。
另一个有用的习惯是把每一步测量结果可视化出来:按地址为横轴、事务成功率为纵轴画一张图。你会非常直观地看到哪些地址区间是“实心”的,哪里是“空心”的,哪里是“半透明”的。KASLR 的随机化在这张图面前,真的就是一层窗户纸。
如果你对后续方向感兴趣,可以继续研究几个延伸点:TSX 与 prefetch 探测的对比、KPTI 开关前后信号强度的差异、FGKASLR 是否真的能抗住这种探测、以及在没有 TSX 的老平台上这类侧信道还能不能稳定触发。每一块都值得单独开一篇实验记录。先把自己手头的测量平台打磨好,后面这些方向都会顺利很多。