前段时间,一个和之前一些 CPU 漏洞比如 Meltdown、Spectre 之类完全不同作用机制的漏洞:CVE-2024-56161,又名 EntrySign,被 Google 的安全研究员发掘了出来。笔者也因他们在发布漏洞信息时的冷幽默而写了一期文章:
随机数、XKCD 漫画与 ZEN 系列最新 CPU 漏洞 CVE-2024-56161
笔者顺便继续强调网络安全的无底洞,有必要把 CPU 微码更新纳入到日常的漏洞补丁管理范围内:
笔者:国际注册信息系统审计师、软考系统分析师、软件工程硕士
言归正传。在 CVE-2024-56161 被披露的时候,Google 研究员说会在3月5日继续发布一些相关信息。随着时间到了,他们在如下地址发布了名为 zentool 的工具:
https://github.com/google/security-research/tree/master/pocs/cpus/entrysign/zentool
该工具应该是第一个公开的、非厂商的 CPU 微码操纵工具。使用该工具,可以把自己编写的 CPU 微码存入 CPU 的微码更新内存区域,从而改变 CPU X86 指令的行为。
随同工具一起发布的,还包括了对 CPU 微码实现机制的介绍以及 AMD X86 CPU 内部 RISC86 指令集的探索。
可能有些读者还不知道,现代 X86 CPU 实际上早已经不是纯粹的 CISC 结构。因为 CISC 结构固有的问题是难以提升执行效率的,所以 Intel 从 Pentium Pro 开始,AMD 从 K6 开始(这两玩意笔者都有实物收藏),除了一些最基础的指令比如 add,sub,mov这些是直接译码执行外,复杂的指令比如本次被 Google 研究员专门拿来演示漏洞作用的 rdrand 指令,实际都是因为 CPU 内部已经另行实现了一套 RISC 指令集,把 X86 指令用 RISC 指令解释执行的结果。
话说现在的程序员大部分都不懂 X86 汇编,也不愿意学。但缺了这门基础,所谓站在巨人肩膀上的高屋建瓴也就成了无本之木。
Intel 和 AMD 这两家各自的具 RISC 实现互不相同,也不对外公开。据 Google 安全研究员介绍,他们是在《The Anatomy of a High-Performance Microprocessor: A Systems Perspective》(ISBN 0818684003) 这本书[1]以及《Reverse Engineering x86 Processor Microcode》[2]这篇论文的帮助下,通过一点点的尝试和观察而总结出一些 AMD RISC86 指令的行为。
至于 AMD CPU 的微码更新,实际就是用 AMD RISC86 指令写一小段程序,重新实现某个 X86 指令的行为,并且在计算机启动时加载到 CPU 内一块容量极少的内存区域。通过类似操作系统钩子函数的机制,当 CPU 执行被重现的 X86 指令时,就会转到该内存区域执行新写的 RISC86 指令程序段。
鉴于这个区域据说只有1KB字节这么点,所以稍微大点的程序都不可能放进去利用。不过,回溯到 CVE-2024-56161 本身,一个能随意改变 X86 指令执行结果的漏洞,本身就已经具有极大的意义和利用价值。
zentool 本身并不是一个容易使用的工具:使用者需要先学习 RISC86 指令,了解清楚微码更新工作机制,然后才能通过该工具把 RISC86 汇编编译为二进制机器码,并最终加载到 CPU 的微码更新存储区域 -- 同时还可能需要保持与已有的微码更新的兼容和保留。
需要掌握的背景知识一点不少。
整个过程最关键的步骤在于对修改后的微码补丁文件重新签名使其有效:这就是 EntrySign 这个漏洞的作用。未加载 CVE-2024-56161 漏洞微码补丁的 ZEN 系列CPU,可以绕过 CPU 对微码更新的校验,实现自行调整 CPU 微码指令。
不过,基础还是要先掌握,不然就算想折腾,除了让 CPU 短暂变砖之外实际无处下手。
首先,研究员们发现,64位的微码指令是四个一组地存储和执行的,如果实际只需要执行3个指令,则第4个依然不可缺,要塞进去一个 nop (无操作)指令。懂汇编、有加解密经验的读者对 NOP 指令应该毫不陌生。
然后,这种四指令单元,还不是简单地顺序执行,而是通过关联到顺序字(Sequence Word),由顺序字告诉 CPU 下一个要执行的四指令单元在哪里。倒是与常见 X86 汇编相似的是,会通过一些属于 RISC86 自己的进位标志比如 ECF (Emulated Carry Flag)控制执行的流向。
再和常见 X86 汇编不同的是,因为 RISC 的特点是大量使用寄存器进行运算,所以 RISC86 汇编基本上每个指令助记符带3个操作数,操作数可以是从8位的单字节到64位的四字,通过在指令助记符带修饰去表达。
例如 sub.d rax, rbx, 0x123 这条指令,对 rbx 寄存器减去双字 0x123,运算结果保存到第一个操作数即 rax 寄存器。据研究员分析,RISC86 总共有32个内部寄存器,少数有特殊用途而大部分可以被微码指令使用 -- 学过 8086 汇编的读者,是否还记得只有 AX~DX 4个通用寄存器,加上其他也总共才14个?
最后一项基础知识是内存分段。所有的内存地址均带有分段信息,重要区别是要区分主要用于物理内存的线性分段 ls 和虚拟内存的 os 分段。
基础知识了解之后(实际当然还不够,学无止境,别信什么“穷人掉的最深的坑就是不停地学习”这种标题党文章,关键是学什么和有无行动),要具体尝试还得先找一下现成的微码更新文件,这就是前面说的,要考虑保持和厂商已给出的微码更新兼容。
在这里有 AMD 给出的 CPU 微码更新补丁:
https://github.com/platomav/CPUMicrocodes/tree/master/AMD
然后就是利用现有微码更新文件,使用 zentool 进行解密、删除实现钩子关系的 ROM 地址,甚至删除全部内容(用 nop 填充),从而产生适合自己使用的模版文件。
接下来就是找一个不常用的 X86 指令去尝试修改其行为。研究员建议比如修改 f2xm1 或者 fpatan 这些浮点数运算指令,因为操作系统核心、测试期间的运行环境一般都不会使用到浮点数运算指令。
确定修改目标 X86 指令后,通过设置匹配寄存器,也就是钩子,使微码更新和修改目标 X86 指令之间攀上关系。
再然后就是修改微码更新文件,向其添加自己用 RISC86 汇编编写的命令,然后设置顺序字,告诉 CPU 该如何执行。
最后为修改后的微码更新文件选择一个与 CPU 型号匹配且符合规律的版本号,就完成修改了 -- 而且这些操作还可以一次性完成,zentool 这工具实现得颇为到位,例如:
zentool edit --nop all --match all=0 --hdr-revlow 0xff --match 0=@fpatan --seq 0=7 --insn q0i0="add rax, rax, 0x1337" input.bin
本文不说太具体,读者可以自行查阅 GITHUB 上 zentool 的文档。另外还可以留意到这又有个 0x1337 的梗,懂的都懂。
最后就是对微码更新文件重新签名和校验,校验通过后,可以用 zentool 直接加载微码更新文件,然后用自己写的程序嵌入 X86 汇编运行,实现校验修改是否生效。
zentool 怎么用介绍完了,但关键在哪里呢?
关键就是这工具实现了对微码更新文件的有效签名。
要深究 zentool 是如何在不知道厂家签名私钥的情况下实现重新签名的,答案就在项目其中 factor.c 的 crypt_factor_patch() 函数,以及其调用的 factor_produce_key()、crypt_calc_modhash() 、crypt_resign_patch () 以及 crypt_find_preimage() 等函数。
过程逻辑大致就是:
1)先调用 crypt_calc_modhash() 计算出原补丁的签名哈希,也就是目标哈希值。
2)调用 factor_produce_key() 找出新的模数和私钥,使得新模数的哈希值与目标哈希值相同。
3)再次调用 crypt_calc_modhash() 验证生成的新模数。
4)最后用新的模数和私钥对补丁进行重新签名。
需要注意,factor_produce_key() 函数内部循环反复碰撞寻找具有和目标哈希相同数值的新模数和私钥的过程也就是 CVE-2024-56161 这个漏洞的具体原理在攻击侧的体现。
熟悉公钥加密算法原理的读者都知道,非对称加密要破解私钥难过上天,要用另一个私钥产生有效碰撞也殊为不易,而 factor_produce_key() 函数能够相对容易成功的原因可以归结为以下几点:
1)目的并不是破解出私钥,而是利用已知有效的哈希值实现重新签名而欺骗 CPU 内部的微码更新校验逻辑认为微码补丁有效,从而简化了整个破解场景。
2)哈希函数的压缩特性:目标哈希值的长度较短(128 位),存在多个模数可能映射到同一个哈希值。
3)模数生成的约束:模数被限制为可以被已知素数分解的形式,减少了搜索空间。
4)私钥的计算依赖于模数:只要找到一个有效的模数,就可以轻松计算出对应的私钥。
5)候选密钥的调整机制:通过 crypt_find_preimage() 函数基于前向计算和逆向推导调整候选密钥,提高了找到符合条件的模数的效率。
这些因素共同作用,从而极大地提高了实现重新签名的可能性,也就构成了漏洞的作用机制。
虽然按程序里面的逻辑可以知道,这个寻找过程并不一定成功,但只要懂得公钥加密算法的原理,就不难修改 factor.c 里面这些逻辑而加大搜索的范围,提高成功率。
而如果把观察角度转回到 CPU 也即防守一侧,造成能被绕过对补丁的原厂性验证的原因大致有以下几点:
1)CPU 仅依赖于 128 位的哈希值进行校验,在特定场景下容易产生碰撞。
2)CPU 没有实现完整的公钥签名验证机制。
3)CPU 对模数的验证不够严格,或者没有进行验证。
4)微码补丁文件本身的完整性检查较为局限。
~ 完 ~
注:题图为笔者自行拍摄。
参考引用:
[1] The Anatomy of a High Performance Microprocessor: A Systems Perspective with CdromAugust
https://dl.acm.org/doi/book/10.5555/552093
[2] Reverse Engineering x86 Processor Microcode
https://www.usenix.org/system/files/conference/usenixsecurity17/sec17-koppe.pdf
本站微信订阅号:
本页网址二维码: