Немного поиграв со сборкой, я обнаружил, что вы можете использовать ретполины с CET, но это далеко не идеально. Вот как. Для справки рассмотрим этот код C:
extern void (*fp)(void);
int f(void) {
fp();
return 0;
}
Компиляция его с помощью 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
Оказывается, вы можете сделать это работать, просто изменяя преобразователи, чтобы они выглядели так:
__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 , и они действительно устранили ошибки потока управления go.
Это были хорошие новости. Вот плохие новости:
- В отличие от
endbr64
, инструкции CET, которые я использовал в преобразователях, не являются NOP на процессорах, которые не поддерживают CET (они приводят к SIGILL
). Это означает, что вам понадобятся два разных набора преобразователей, и вам нужно будет использовать диспетчерскую диспетчеризацию ЦП, чтобы выбрать нужные, в зависимости от того, доступен ли CET. - Использование ретполинов вообще означает, что вас больше нет выполняя любые непрямые ветки, поэтому, хотя вы по-прежнему получаете преимущества SS, вы полностью отрицаете IBT. Я полагаю, вы могли бы обойти это, сделав
__x86_indirect_thunk_rax
проверку на наличие инструкции endbr64
, но это действительно неэлегантно и, вероятно, будет очень медленным.