【问题标题】:Is it possible to temporarily suppress Intel CET for a single ret instruction, or otherwise use retpolines with it?是否可以暂时禁止单个 ret 指令的英特尔 CET,或者以其他方式使用 retpolines?
【发布时间】:2020-10-07 16:40:24
【问题描述】:

英特尔 CET(控制流执行技术)由两部分组成:SS(影子堆栈)和 IBT(间接分支跟踪)。如果您需要间接分支到由于某种原因无法放置endbr64 的某个位置,您可以使用notrack 抑制单个jmpcall 指令的IBT。是否有等效的方法来抑制单个 ret 指令的 SS?

对于上下文,我正在考虑这将如何与 retpolines 交互,其关键控制流或多或少类似于push real_target; call retpoline; pop junk; ret。如果没有办法抑制 ret 的 SS,那么在启用 CET 时是否还有其他方法可以让 retpolines 工作?如果没有,我们会有什么选择?我们是否需要为所有东西维护两组二进制包,一组用于需要 retpolines 的旧 CPU,另一组用于支持 CET 的新 CPU?如果英特尔证明是错误的,我们最终仍然需要在他们的新 CPU 上进行 retpolines 怎么办?我们是否必须放弃 CET 才能使用它们?

【问题讨论】:

    标签: assembly x86-64 intel spectre


    【解决方案1】:

    在玩了一会儿程序集之后,我发现您可以在 CET 中使用 retpolines,但这并不理想。这是如何做。作为参考,请考虑以下 C 代码:

    extern void (*fp)(void);
    
    int f(void) {
        fp();
        return 0;
    }
    

    Compiling it with gcc -mindirect-branch=thunk -mfunction-return=thunk -O3 产生这个:

    f:
            subq    $8, %rsp
            movq    fp(%rip), %rax
            call    __x86_indirect_thunk_rax
            xorl    %eax, %eax
            addq    $8, %rsp
            jmp     __x86_return_thunk
    __x86_return_thunk:
            call    .LIND1
    .LIND0:
            pause
            lfence
            jmp     .LIND0
    .LIND1:
            lea     8(%rsp), %rsp
            ret
    __x86_indirect_thunk_rax:
            call    .LIND3
    .LIND2:
            pause
            lfence
            jmp     .LIND2
    .LIND3:
            mov     %rax, (%rsp)
            ret
    

    事实证明,您只需将 thunk 修改为如下所示即可完成这项工作:

    __x86_return_thunk:
            call    .LIND1
    .LIND0:
            pause
            lfence
            jmp     .LIND0
    .LIND1:
            push    %rdi
            movl    $1, %edi
            incsspq %rdi
            pop     %rdi
            lea     8(%rsp), %rsp
            ret
    
    __x86_indirect_thunk_rax:
            call    .LIND3
    .LIND2:
            pause
            lfence
            jmp     .LIND2
    .LIND3:
            push    %rdi
            rdsspq  %rdi
            wrssq   %rax, (%rdi)
            pop     %rdi
            mov     %rax, (%rsp)
            ret
    

    通过使用incsspqrdsspqwrssq 指令,您可以修改影子堆栈以使您的更改与实际堆栈相匹配。我用Intel SDE 测试了那些修改过的thunk,它们确实使控制流错误消失了。

    这是个好消息。坏消息是:

    1. endbr64 不同,我在thunk 中使用的CET 指令在不支持CET 的CPU 上不是NOP(它们导致SIGILL)。这意味着您需要两组不同的 thunk,并且您需要使用 CPU 调度来根据 CET 是否可用来选择正确的。
    2. 完全使用 retpolines 意味着您不再执行任何间接分支,因此虽然您仍将获得 SS 的好处,但您已经完全否定了 IBT。我想你可以通过让__x86_indirect_thunk_rax 检查是否存在endbr64 指令来解决这个问题,但这真的很不雅,而且可能真的很慢。

    【讨论】:

    • R11 是 call-clobbered 并且不用于 arg 传递;即使在像那些 thunk 这样的透明包装器中,您也应该能够将它而不是 RDI 用于 incsspq。 (x86-64 SysV 具有一个不用于传递/返回任何内容的调用破坏 reg 的一个原因正是包装器/thunk 具有临时 reg,例如用于惰性动态链接。)跨度>
    • @PeterCordes 我考虑过这一点,但是 GCC uses the same thunk 用于函数调用,就像它对 labels as values (goto *x;) 所做的那样,我不能 100% 确定它不会假设 r11 通过他们。
    • 啊,我明白了。我想这就是为什么 GCC 在其 thunk 中使用 lea 而不是 add 的原因,所以它甚至不会破坏 FLAGS。它确实已经踩到了红色区域,所以它不能随意将它用于叶函数中的switch。如果你正在修改 GCC 来发出这个,希望你能告诉它它破坏了 R11。
    • @PeterCordes Linux 内核提供了自己的 thunk 和一些调用,但 AFAIK 编译器仍然会生成一些对它们的调用,调用者决定将哪个寄存器用于目标地址。 (并且GENERATE_THUNK 定义在它的使用位置上方。)
    • 对了。如果他们纯粹使用内联汇编,他们只会使用一个虚拟的"=r" 输出作为临时注册,或者一个固定的选择,并且不需要为每个可能的选择发出定义。由于 Linux 是一个独立的内核,它需要定义通常位于 libgcc 中的 GCC 可以发出引用的内容。
    猜你喜欢
    • 2022-12-24
    • 2017-11-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-03
    • 2015-06-07
    • 2014-05-25
    相关资源
    最近更新 更多