在玩了一会儿程序集之后,我发现您可以在 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
通过使用incsspq、rdsspq 和wrssq 指令,您可以修改影子堆栈以使您的更改与实际堆栈相匹配。我用Intel SDE 测试了那些修改过的thunk,它们确实使控制流错误消失了。
这是个好消息。坏消息是:
- 与
endbr64 不同,我在thunk 中使用的CET 指令在不支持CET 的CPU 上不是NOP(它们导致SIGILL)。这意味着您需要两组不同的 thunk,并且您需要使用 CPU 调度来根据 CET 是否可用来选择正确的。
- 完全使用 retpolines 意味着您不再执行任何间接分支,因此虽然您仍将获得 SS 的好处,但您已经完全否定了 IBT。我想你可以通过让
__x86_indirect_thunk_rax 检查是否存在endbr64 指令来解决这个问题,但这真的很不雅,而且可能真的很慢。