导语:通过这篇文章我将深入分析最近的一个Specter漏洞补丁程序,还会介绍我们是如何独立挖到此漏洞的以及我们的Respectre插件是如何自动修复这个底层漏洞的。
通过这篇文章我将深入分析最近的一个Specter漏洞补丁程序,其中一个补丁是手动打到Linux内核中的。我将介绍此修复程序所采取的方法,从其发出警告到向后移植到Long Term Support (LTS) kernels时被破坏。我们将研究后端漏洞的原理以及导致这种backporting失败的upstream过程中的bug。还会介绍我们是如何独立挖到此漏洞的以及我们的Respectre插件是如何自动修复这个底层漏洞的。
我能够找到的最早版本的补丁程序是来自于Dianzhang Chen的这个补丁,它是在2019年5月24日被发布的。它通过ptrace系统调用来解决具有用户控制索引的数组的推测访问。最初的解决方案还是挺好的,但Thomas Gleixner提出补丁可以被绕过,一个月后,他们又发布了补丁的第二个版本。
由于补丁的细节很重要,我做了一个比较:
diff --git a/arch/x86/kernel/ptrace.c b/arch/x86/kernel/ptrace.c index a166c96..cbac646 100644 --- a/arch/x86/kernel/ptrace.c +++ b/arch/x86/kernel/ptrace.c @@ -25,6 +25,7 @@ #include <linux/rcupdate.h> #include <linux/export.h> #include <linux/context_tracking.h> +#include <linux/nospec.h> #include <linux/uaccess.h> #include <asm/pgtable.h> @@ -643,9 +644,11 @@ static unsigned long ptrace_get_debugreg(struct task_struct *tsk, int n) { struct thread_struct *thread = &tsk->thread; unsigned long val = 0; + int index = n; if (n < HBP_NUM) { - struct perf_event *bp = thread->ptrace_bps[n]; + index = array_index_nospec(index, HBP_NUM); + struct perf_event *bp = thread->ptrace_bps[index]; if (bp) val = bp->hw.info.address;
可以看到,代码块的开头在声明bp指针之前初始化了索引变量。
这导致下面的编译器警告:
arch/x86/kernel/ptrace.c: In function 'ptrace_get_debugreg': arch/x86/kernel/ptrace.c:705:3: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement] struct perf_event *bp = thread->ptrace_bps[index]; ^~~~~~
尽管有这个警告,但这段代码被逐字地合并到了Thomas Gleixner的x86 / tip tree中,可以在这里看到。此前合并的5.3-RC1的补丁,Linus Torvalds发现警告的LKML邮件列表。但是,当对tree做实际合并时,没有提到对补丁的更新。
LTS内核中采用的修复方法是通过简单地交换顺序:
+ struct perf_event *bp = thread->ptrace_bps[index]; + index = array_index_nospec(index, HBP_NUM);
为了解释为什么说这个补丁是失效的,先看一下array_index_nospec()API。此函数获取索引值以及索引的第一个较大的越界值。如果不使用条件控制流,它会将该索引转换为原始索引值(如果在边界内)或零(如果超出边界)。通过确保原始索引的所有后续使用改为使用从此API返回的值,可以防止具有越界索引值的推测路径的破坏。
但是,在索引array_index_nospec ()宏之前,“index”用作ptrace_bps数组的索引。此外,由于“索引”在函数中没有后续使用,整个array_index_nospec()操作通常被编译器视为dead store,并通过称为死存储消除(DSE)的优化传递消除。由于在执行array_index_nospec()的某些内联汇编中使用了“volatile”关键字,后一种效果最终不会发生。
我们可以通过查看函数的反汇编来看下一:
.text:0000000000000648 ptrace_get_debugreg proc near ; CODE XREF: getreg32+EF .text:0000000000000648 ; arch_ptrace+6E .text:0000000000000648 push rbp .text:0000000000000649 cmp esi, 3 ; esi = index (or n), this is the if (n < HBP_NUM) check .text:000000000000064C mov rbp, rsp .text:000000000000064F jg short loc_673 .text:0000000000000651 movsxd rsi, esi ; rsi = sign-extended index .text:0000000000000654 ; rdx is 'bp' here, using rsi derived from user-provided one, with no speculation barrier .text:0000000000000654 mov rdx, [rdi+rsi*8+800h] .text:000000000000065C ; here begins the unused array_index_nospec() .text:000000000000065C cmp rsi, 4 ; from arch/x86/include/asm/barrier.h asm volatile .text:0000000000000660 sbb rsi, rsi ; from arch/x86/include/asm/barrier.h asm volatile .text:0000000000000663 xor eax, eax ; fallout from arch/x86/include/asm/barrier.h asm volatile .text:0000000000000665 test rdx, rdx .text:0000000000000668 jz short loc_68F .text:000000000000066A mov rax, [rdx+138h] .text:0000000000000671 jmp short loc_68F .text:0000000000000673 ; --------------------------------------------------------------------------- .text:0000000000000673 .text:0000000000000673 loc_673: ; CODE XREF: ptrace_get_debugreg+7 .text:0000000000000673 cmp esi, 6 .text:0000000000000676 jnz short loc_681 .text:0000000000000678 mov rax, [rdi+820h] .text:000000000000067F jmp short loc_68F .text:0000000000000681 ; --------------------------------------------------------------------------- .text:0000000000000681 .text:0000000000000681 loc_681: ; CODE XREF: ptrace_get_debugreg+2E .text:0000000000000681 xor eax, eax .text:0000000000000683 cmp esi, 7 .text:0000000000000686 jnz short loc_68F .text:0000000000000688 mov rax, [rdi+828h] .text:000000000000068F .text:000000000000068F loc_68F: ; CODE XREF: ptrace_get_debugreg+20 .text:000000000000068F ; ptrace_get_debugreg+29 ... .text:000000000000068F pop rbp .text:0000000000000690 retn .text:0000000000000690 ptrace_get_debugreg endp
Respectre编译器插件是世界上最先进,最有效,最高效的防御 CPU Spectre 侧信道攻击的工具。该插件使用高级静态分析自动查找潜在的Specter实例,并通过高性能检测消除它们。值
通过获取当前代码并修复漏洞后,可以得到以下输出:
arch/x86/kernel/ptrace.c: In function 'ptrace_get_debugreg': arch/x86/kernel/ptrace.c:717:22: note: Spectre v1 array index bound '3' struct perf_event *bp = thread->ptrace_bps[n]; ^ arch/x86/kernel/ptrace.c:717:22: note: Spectre v1 array index mask adjust: inc constbound: yes
我们为客户提供像Respectre这样的防御工具可以填补个人的临时手动修补留下的空白,这确实是有好处的。
最后要说的是,上游社区的开发者并没有发现这个漏洞,这么多人都参与其中,因此应该存在很多机会来阻止这种糟糕的修复技术被引入。然而,这个补丁漏洞不仅被发布,而且传播到了所有支持的稳定内核中,这表明上游的开发社区根本不像公众所认为的那样厉害。