Bagikan melalui


Panduan Pengembang C++ untuk Saluran Sisi Eksekusi Spekulatif

Artikel ini berisi panduan bagi pengembang untuk membantu mengidentifikasi dan mengurangi kerentanan perangkat keras saluran samping eksekusi spekulatif dalam perangkat lunak C++. Kerentanan ini dapat mengungkapkan informasi sensitif di seluruh batas kepercayaan dan dapat memengaruhi perangkat lunak yang berjalan pada prosesor yang mendukung eksekusi instruksi yang spekulatif dan tidak berurutan. Kelas kerentanan ini pertama kali dijelaskan pada januari 2018 dan latar belakang dan panduan tambahan dapat ditemukan di penasihat keamanan Microsoft.

Panduan yang diberikan oleh artikel ini terkait dengan kelas kerentanan yang diwakili oleh:

  1. CVE-2017-5753, juga dikenal sebagai varian Spectre 1. Kelas kerentanan perangkat keras ini terkait dengan saluran samping yang dapat muncul karena eksekusi spekulatif yang terjadi sebagai akibat dari kesalahan penyusunan cabang kondisional. Pengkompilasi Microsoft C++ di Visual Studio 2017 (dimulai dengan versi 15.5.5) menyertakan dukungan untuk /Qspectre sakelar yang menyediakan mitigasi waktu kompilasi untuk serangkaian pola pengkodian yang berpotensi rentan terbatas yang terkait dengan CVE-2017-5753. Sakelar /Qspectre juga tersedia di Visual Studio 2015 Update 3 hingga KB 4338871. Dokumentasi untuk /Qspectre bendera memberikan informasi lebih lanjut tentang efek dan penggunaannya.

  2. CVE-2018-3639, juga dikenal sebagai Speculative Store Bypass (SSB). Kelas kerentanan perangkat keras ini terkait dengan kanal samping yang dapat muncul karena eksekusi spekulatif dari instruksi pemuatan di depan instruksi penyimpanan yang bergantung sebagai akibat dari salah prediksi akses memori.

Pengenalan yang mudah diakses untuk kerentanan saluran samping eksekusi spekulatif dapat ditemukan dalam presentasi berjudul Kasus Spectre dan Meltdown oleh salah satu tim peneliti yang menemukan kerentanan ini.

Apa itu kerentanan perangkat keras Saluran Sisi Eksekusi Spekulatif?

CPU modern memberikan tingkat performa yang lebih tinggi dengan memanfaatkan eksekusi instruksi yang spekulatif dan di luar urutan. Misalnya, ini sering dicapai dengan memprediksi target cabang (bersyarat dan tidak langsung) yang memungkinkan CPU untuk mulai secara spekulatif menjalankan instruksi pada target cabang yang diprediksi, sehingga menghindari penundaan sampai target cabang yang sebenarnya ditentukan. Jika CPU kemudian menemukan bahwa terjadi kesalahan kata sandi, semua status komputer yang dihitung secara spekulatif dibuang. Ini memastikan bahwa tidak ada efek arsitektur yang terlihat dari spekulasi yang salah diprediksi.

Meskipun eksekusi spekulatif tidak memengaruhi status yang terlihat secara arsitektur, itu dapat meninggalkan jejak sisa dalam status non-arsitektur, seperti berbagai cache yang digunakan oleh CPU. Jejak sisa dari eksekusi spekulatif inilah yang dapat menimbulkan kerentanan kanal samping. Untuk lebih memahami ini, pertimbangkan fragmen kode berikut yang memberikan contoh CVE-2017-5753 (Bounds Check Bypass):

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
    if (untrusted_index < buffer_size) {
        unsigned char value = buffer[untrusted_index];
        return shared_buffer[value * 4096];
    }
}

Dalam contoh ini, ReadByte disediakan buffer, ukuran buffer, dan indeks ke dalam buffer tersebut. Parameter indeks, seperti yang ditentukan oleh untrusted_index, disediakan oleh konteks yang kurang istimewa, seperti proses non-administratif. Jika untrusted_index kurang dari buffer_size, maka karakter di indeks tersebut dibaca dari buffer dan digunakan untuk mengindeks ke wilayah memori bersama yang disebut oleh shared_buffer.

Dari perspektif arsitektur, urutan kode ini sangat aman karena dijamin akan untrusted_index selalu kurang dari buffer_size. Namun, dengan adanya eksekusi spekulatif, ada kemungkinan bahwa CPU akan salah menduga cabang kondisional dan menjalankan bagian dari pernyataan if bahkan ketika untrusted_index lebih besar dari atau sama dengan buffer_size. Sebagai konsekuensi dari ini, CPU dapat secara spekulatif membaca byte dari luar batas buffer (yang bisa menjadi rahasia) dan kemudian dapat menggunakan nilai byte tersebut untuk menghitung alamat beban berikutnya melalui shared_buffer.

Sementara CPU pada akhirnya akan mendeteksi kesalahan prediksi ini, efek samping residu dapat tertinggal di cache CPU yang mengungkapkan informasi tentang nilai byte yang dibaca di luar batas dari buffer. Efek samping ini dapat dideteksi oleh konteks dengan hak akses rendah yang berjalan pada sistem dalam mengukur kecepatan akses setiap baris cache di shared_buffer. Langkah-langkah yang dapat diambil untuk mencapai hal ini adalah:

  1. Panggil ReadByte beberapa kali dengan untrusted_index kurang dari buffer_size Konteks penyerangan dapat menyebabkan konteks korban memanggil ReadByte (misalnya melalui RPC) sehingga prediktor cabang dilatih untuk tidak diambil karena untrusted_index kurang dari buffer_size.

  2. Bersihkan semua baris cache di shared_buffer. Konteks serangan harus membersihkan semua baris cache di wilayah memori bersama yang dirujuk oleh shared_buffer. Karena wilayah memori dibagikan, ini mudah dan dapat dicapai menggunakan intrinsik seperti _mm_clflush.

  3. Panggil ReadByte jika untrusted_index lebih besar daripada buffer_size. Konteks yang menyerang menyebabkan konteks korban memanggil ReadByte sehingga salah dalam memprediksi bahwa cabang tersebut tidak akan diambil. Ini menyebabkan prosesor secara spekulatif mengeksekusi isi blok if dengan untrusted_index lebih besar dari buffer_size, yang mengarah pada pembacaan di luar batas dari buffer. Akibatnya, shared_buffer diindeks menggunakan nilai yang mungkin bersifat rahasia yang dibaca di luar batas yang semestinya, sehingga menyebabkan CPU memuat baris cache yang sesuai.

  4. Baca setiap baris cache dalam rangka melihat mana yang diakses paling cepat. Konteks menyerang dapat membaca setiap baris cache di shared_buffer dan mendeteksi baris cache yang dimuat secara signifikan lebih cepat daripada yang lain. Ini adalah baris cache yang kemungkinan telah dimasukkan pada langkah 3. Karena ada hubungan 1:1 antara nilai byte dan baris cache dalam contoh ini, ini memungkinkan penyerang untuk menyimpulkan nilai aktual byte yang dibaca di luar batas.

Langkah-langkah di atas memberikan contoh penggunaan teknik yang dikenal sebagai FLUSH+RELOAD bersama dengan mengeksploitasi instans CVE-2017-5753.

Skenario perangkat lunak apa yang dapat terpengaruh?

Mengembangkan perangkat lunak aman menggunakan proses seperti Security Development Lifecycle (SDL) biasanya mengharuskan pengembang mengidentifikasi batas kepercayaan yang ada di aplikasi mereka. Batas kepercayaan ada di tempat di mana aplikasi dapat berinteraksi dengan data yang disediakan oleh konteks yang kurang tepercaya, seperti proses lain pada sistem atau proses mode pengguna non-administratif dalam kasus driver perangkat mode kernel. Kelas kerentanan baru yang melibatkan saluran sisi eksekusi spekulatif relevan dengan banyak batas kepercayaan dalam model keamanan perangkat lunak yang ada yang mengisolasi kode dan data pada perangkat.

Tabel berikut ini menyediakan ringkasan model keamanan perangkat lunak di mana pengembang mungkin perlu khawatir tentang kerentanan ini terjadi:

Batas kepercayaan Deskripsi
Batas mesin virtual Aplikasi yang mengisolasi beban kerja di komputer virtual terpisah yang menerima data yang tidak tepercaya dari komputer virtual lain mungkin berisiko.
Batas kernel Driver perangkat mode kernel yang menerima data yang tidak tepercaya dari proses mode pengguna non-administratif mungkin berisiko.
Batas proses Aplikasi yang menerima data yang tidak tepercaya dari proses lain yang berjalan pada sistem lokal, seperti melalui Panggilan Prosedur Jarak Jauh (RPC), memori bersama, atau mekanisme Komunikasi Antar Proses (IPC) lainnya mungkin berisiko.
Batas Enklave Aplikasi yang dijalankan dalam enklave aman (seperti Intel SGX) yang menerima data yang tidak tepercaya dari luar enklave mungkin berisiko.
Batas bahasa Aplikasi yang menginterpretasikan atau Just-In-Time (JIT) mengkompilasi dan menjalankan kode yang tidak tepercaya yang ditulis dalam bahasa tingkat yang lebih tinggi mungkin berisiko.

Aplikasi yang memiliki permukaan serangan yang terekspos ke salah satu batas kepercayaan di atas harus meninjau kode pada permukaan serangan untuk mengidentifikasi dan mengurangi kemungkinan instans kerentanan saluran sisi eksekusi spekulatif. Perlu dicatat bahwa batas kepercayaan yang terpapar ke permukaan serangan jarak jauh, seperti protokol jaringan jarak jauh, belum ditunjukkan berisiko terhadap kerentanan saluran samping eksekusi spekulatif.

Pola pengkodian yang berpotensi rentan

Kerentanan saluran samping eksekusi spekulatif dapat muncul sebagai konsekuensi dari beberapa pola pengodean. Bagian ini menjelaskan pola pengkodean yang berpotensi rentan dan memberikan contoh untuk masing-masing, tetapi harus dikenali bahwa variasi pada tema ini mungkin ada. Dengan demikian, pengembang disarankan untuk mengambil pola ini sebagai contoh dan bukan sebagai daftar lengkap dari semua pola pengkodian yang berpotensi rentan. Kelas kerentanan keamanan memori yang sama yang dapat ada dalam perangkat lunak saat ini mungkin juga ada di sepanjang jalur eksekusi spekulatif dan di luar urutan, termasuk tetapi tidak terbatas pada buffer overrun, akses array yang melebihi batas, penggunaan memori yang belum diinisialisasi, konflik tipe, dan sebagainya. Primitif yang sama yang dapat digunakan penyerang untuk mengeksploitasi kerentanan keamanan memori di sepanjang jalur arsitektur juga dapat berlaku untuk jalur spekulatif.

Secara umum, kanal samping pada eksekusi spekulatif yang terkait dengan salah prediksi cabang kondisional dapat muncul ketika ekspresi kondisional bekerja pada data yang dapat dikontrol atau dipengaruhi oleh konteks yang kurang tepercaya. Misalnya, ini dapat mencakup ekspresi bersyarat yang digunakan dalam if, for, while, switch, atau pernyataan ternari. Untuk masing-masing pernyataan ini, kompilator dapat menghasilkan cabang kondisional yang kemudian dapat diprediksi target cabangnya oleh CPU saat runtime.

Untuk setiap contoh, komentar dengan frasa "SPECULATION BARRIER" dimasukkan di mana pengembang dapat memasukkan penghalang sebagai pencegahan. Ini dibahas secara lebih rinci di bagian tentang mitigasi.

Beban di luar batas spekulatif

Kategori pola pengkodean ini melibatkan salah prediksi cabang kondisional yang mengarah pada akses memori spekulatif di luar batas.

Array di luar batas memuat beban

Pola pengodean ini adalah pola pengodean rentan yang awalnya dijelaskan untuk CVE-2017-5753 (Bounds Check Bypass). Bagian latar belakang artikel ini menjelaskan pola ini secara rinci.

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
    if (untrusted_index < buffer_size) {
        // SPECULATION BARRIER
        unsigned char value = buffer[untrusted_index];
        return shared_buffer[value * 4096];
    }
}

Demikian pula, pemuatan array di luar batas dapat terjadi bersama dengan loop yang melebihi kondisi penghentiannya karena prediksi salah. Dalam contoh ini, cabang bersyarat yang terkait dengan x < buffer_size ekspresi dapat salah prediksi dan secara spekulatif menjalankan blok for perulangan ketika x lebih besar dari atau sama dengan buffer_size, sehingga menghasilkan pemuatan di luar batas yang bersifat spekulatif.

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadBytes(unsigned char *buffer, unsigned int buffer_size) {
    for (unsigned int x = 0; x < buffer_size; x++) {
        // SPECULATION BARRIER
        unsigned char value = buffer[x];
        return shared_buffer[value * 4096];
    }
}

Array di luar batas beban memberi makan cabang tidak langsung

Pola pengkodean ini melibatkan kasus di mana kesalahan prediksi cabang bersyarat dapat menyebabkan akses di luar batas pada array penunjuk fungsi, yang kemudian mengakibatkan lompatan tidak langsung ke alamat target yang dibaca di luar batas. Cuplikan berikut memberikan contoh yang menunjukkan hal ini.

Dalam contoh ini, pengidentifikasi pesan yang tidak tepercaya disediakan untuk DispatchMessage melalui untrusted_message_id parameter . Jika untrusted_message_id kurang dari MAX_MESSAGE_ID, maka akan digunakan untuk mengindeks ke dalam array pointer fungsi dan melompat ke target cabang yang sesuai. Kode ini aman secara arsitektur, tetapi jika CPU salah memprediksi cabang kondisional, hal itu dapat mengakibatkan DispatchTable diindeks oleh untrusted_message_id ketika nilainya sama dengan atau lebih besar dari MAX_MESSAGE_ID, sehingga mengarah ke akses di luar batas. Ini dapat mengakibatkan eksekusi spekulatif dari alamat target cabang yang berasal dari luar batas array, yang bisa menyebabkan pengungkapan informasi tergantung pada kode yang dieksekusi secara spekulatif.

#define MAX_MESSAGE_ID 16

typedef void (*MESSAGE_ROUTINE)(unsigned char *buffer, unsigned int buffer_size);

const MESSAGE_ROUTINE DispatchTable[MAX_MESSAGE_ID];

void DispatchMessage(unsigned int untrusted_message_id, unsigned char *buffer, unsigned int buffer_size) {
    if (untrusted_message_id < MAX_MESSAGE_ID) {
        // SPECULATION BARRIER
        DispatchTable[untrusted_message_id](buffer, buffer_size);
    }
}

Seperti halnya pemanggilan array di luar batas yang menyebabkan pemanggilan lain, kondisi ini juga dapat muncul bersamaan dengan perulangan yang melebihi kondisi penghentiannya karena prediksi yang salah.

Array di luar batas menyimpan umpan cabang tidak langsung

Meskipun contoh sebelumnya menunjukkan bagaimana pemuatan di luar batas spekulatif dapat memengaruhi target percabangan tidak langsung, juga dimungkinkan bagi penyimpanan di luar batas untuk memodifikasi target percabangan tidak langsung, seperti penunjuk fungsi atau alamat pengembalian. Hal ini berpotensi menyebabkan eksekusi spekulatif dari alamat yang ditentukan penyerang.

Dalam contoh ini, indeks yang tidak tepercaya diteruskan melalui untrusted_index parameter . Jika untrusted_index kurang dari jumlah pointers elemen array (256 elemen), maka nilai pointer yang disediakan dalam ptr ditulis ke pointers array. Kode ini aman secara arsitektur, tetapi jika CPU salah menduga cabang kondisional, itu dapat mengakibatkan ptr ditulis secara spekulatif di luar batas array yang dialokasikan pointers tumpukan. Ini dapat menyebabkan kerusakan spekulatif dari alamat pengembalian untuk WriteSlot. Jika penyerang dapat mengontrol nilai ptr, mereka mungkin dapat menyebabkan eksekusi spekulatif dari alamat sembarang ketika WriteSlot kembali di jalur spekulatif.

unsigned char WriteSlot(unsigned int untrusted_index, void *ptr) {
    void *pointers[256];
    if (untrusted_index < 256) {
        // SPECULATION BARRIER
        pointers[untrusted_index] = ptr;
    }
}

Demikian pula, jika variabel lokal penunjuk fungsi bernama func dialokasikan pada tumpukan, maka mungkin dapat secara spekulatif memodifikasi alamat yang func acukan ketika misprediksi cabang kondisional terjadi. Ini dapat mengakibatkan eksekusi spekulatif dari alamat arbitrer ketika penunjuk fungsi dipanggil melalui.

unsigned char WriteSlot(unsigned int untrusted_index, void *ptr) {
    void *pointers[256];
    void (*func)() = &callback;
    if (untrusted_index < 256) {
        // SPECULATION BARRIER
        pointers[untrusted_index] = ptr;
    }
    func();
}

Perlu dicatat bahwa kedua contoh ini melibatkan modifikasi spekulatif dari pointer cabang tidak langsung yang dialokasikan pada tumpukan. Ada kemungkinan bahwa modifikasi spekulatif juga dapat terjadi pada variabel global, memori yang dialokasikan pada heap, dan bahkan memori baca-saja pada beberapa CPU. Untuk memori yang dialokasikan tumpukan, pengkompilasi Microsoft C++ sudah mengambil langkah-langkah untuk membuatnya lebih sulit untuk memodifikasi target cabang tidak langsung yang dialokasikan tumpukan secara spekulatif, seperti dengan menyusun ulang variabel lokal sehingga buffer ditempatkan berdekatan dengan cookie keamanan sebagai bagian /GS dari fitur keamanan kompilator.

Kebingungan jenis spekulatif

Kategori ini berkaitan dengan pola pengkodian yang dapat memunculkan kebingungan jenis spekulatif. Ini terjadi ketika memori diakses menggunakan tipe yang tidak tepat di sepanjang jalur tidak arsitektural selama eksekusi spekulatif. Baik misprediksi cabang kondisional dan bypass penyimpanan spekulatif dapat berpotensi menyebabkan kebingungan jenis spekulatif.

Untuk pengabaian penyimpanan spekulatif, ini dapat terjadi dalam skenario di mana kompilator menggunakan kembali lokasi memori tumpukan untuk variabel dari beberapa jenis. Ini karena penyimpanan arsitektur dari variabel jenis A dapat dilewati, sehingga memungkinkan beban jenis A untuk dijalankan secara spekulatif sebelum variabel ditetapkan. Jika variabel yang disimpan sebelumnya memiliki jenis yang berbeda, maka ini dapat menciptakan kondisi untuk kebingungan jenis spekulatif.

Untuk misprediksi cabang bersyarat, cuplikan kode berikut akan digunakan untuk menggambarkan berbagai kondisi yang dapat menimbulkan kebingungan jenis spekulatif.

enum TypeName {
    Type1,
    Type2
};

class CBaseType {
public:
    CBaseType(TypeName type) : type(type) {}
    TypeName type;
};

class CType1 : public CBaseType {
public:
    CType1() : CBaseType(Type1) {}
    char field1[256];
    unsigned char field2;
};

class CType2 : public CBaseType {
public:
    CType2() : CBaseType(Type2) {}
    void (*dispatch_routine)();
    unsigned char field2;
};

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ProcessType(CBaseType *obj)
{
    if (obj->type == Type1) {
        // SPECULATION BARRIER
        CType1 *obj1 = static_cast<CType1 *>(obj);

        unsigned char value = obj1->field2;

        return shared_buffer[value * 4096];
    }
    else if (obj->type == Type2) {
        // SPECULATION BARRIER
        CType2 *obj2 = static_cast<CType2 *>(obj);

        obj2->dispatch_routine();

        return obj2->field2;
    }
}

Kebingungan jenis spekulatif yang mengarah ke beban di luar batas

Pola pengkodean ini melibatkan situasi di mana kebingungan tipe spekulasi dapat mengakibatkan akses bidang yang di luar batas atau tipe yang membingungkan, di mana nilai yang dimuat memberi umpan ke alamat muatan berikutnya. Ini mirip dengan pola pengkodan array di luar batas tetapi dimanifestasikan melalui urutan pengkodan alternatif seperti yang ditunjukkan di atas. Dalam contoh ini, konteks penyerangan dapat menyebabkan konteks korban menjalankan ProcessType beberapa kali dengan objek jenis CType1 (field type di mana sama dengan Type1). Hal ini akan memiliki efek dalam melatih cabang bersyarat agar pernyataan pertama if diprediksi tidak akan diambil. Konteks menyerang kemudian dapat menyebabkan konteks korban dijalankan ProcessType dengan objek jenis CType2. Ini dapat mengakibatkan kebingungan tipe spekulatif jika cabang kondisional untuk pernyataan pertama if salah memprediksi dan menjalankan isi pernyataan if, sehingga mengkonversi objek jenis CType2 ke CType1. Karena CType2 lebih kecil dari CType1, akses memori ke CType1::field2 akan menghasilkan beban data yang di luar batas spekulatif yang mungkin rahasia. Nilai ini kemudian digunakan dalam pengambilan data dari shared_buffer yang dapat membuat efek samping yang terlihat, seperti yang dijelaskan dalam contoh array di luar batas sebelumnya.

Kebingungan tipe spekulatif yang mengarah ke percabangan tidak langsung

Pola pengodian ini melibatkan kasus di mana kebingungan jenis spekulatif dapat mengakibatkan cabang tidak langsung yang tidak aman selama eksekusi spekulatif. Dalam contoh ini, konteks penyerangan dapat menyebabkan konteks korban menjalankan ProcessType beberapa kali dengan objek tipe CType2 (kolom type sama dengan Type2). Hal ini akan berdampak pada pelatihan cabang bersyarat agar pernyataan pertama if dijalankan dan pernyataan else if tidak dijalankan. Konteks penyerang kemudian dapat menyebabkan konteks korban mengeksekusi ProcessType dengan objek tipe CType1. Ini dapat mengakibatkan kebingungan jenis spekulatif jika cabang bersyarat untuk pernyataan pertama if diprediksi diambil dan pernyataan else if diprediksi tidak diambil, sehingga mengeksekusi isi else if dan mengubah objek dari jenis CType1 ke CType2. CType2::dispatch_routine Karena kolom tumpang tindih dengan char array CType1::field1, ini dapat mengakibatkan cabang spekulatif tidak langsung menuju target cabang yang tidak diharapkan. Jika konteks menyerang dapat mengontrol nilai byte dalam CType1::field1 array, mereka mungkin dapat mengontrol alamat target cabang.

Penggunaan spekulatif yang tidak diinisialisasi

Kategori pola pengkodian ini melibatkan skenario di mana eksekusi spekulatif dapat mengakses memori yang tidak diinisialisasi dan menggunakannya untuk memberi makan beban berikutnya atau cabang tidak langsung. Agar pola pengkodian ini dapat dieksploitasi, penyerang harus dapat mengontrol atau secara bermakna memengaruhi konten memori yang digunakan tanpa diinisialisasi oleh konteks tempatnya digunakan.

Penggunaan spekulatif yang tidak diinisialisasi yang mengarah ke beban di luar batas

Penggunaan spekulatif yang tidak diinisialisasi berpotensi menyebabkan beban di luar batas menggunakan nilai yang dikontrol penyerang. Dalam contoh di bawah ini, nilai index ditetapkan trusted_index pada semua jalur arsitektur dan trusted_index diasumsikan kurang dari atau sama dengan buffer_size. Namun, tergantung pada kode yang dihasilkan oleh pengkompilasi, ada kemungkinan bahwa pengabaian penyimpanan spekulatif dapat terjadi yang memungkinkan pengambilan dari buffer[index] dan ekspresi dependen untuk dieksekusi sebelum penetapan ke index. Jika ini terjadi, nilai yang tidak diinisialisasi untuk index akan digunakan sebagai offset ke dalam buffer yang dapat memungkinkan penyerang membaca informasi sensitif di luar batas dan menyampaikan ini melalui saluran samping dengan memuat data dari shared_buffer yang bergantung.

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

void InitializeIndex(unsigned int trusted_index, unsigned int *index) {
    *index = trusted_index;
}

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int trusted_index) {
    unsigned int index;

    InitializeIndex(trusted_index, &index); // not inlined

    // SPECULATION BARRIER
    unsigned char value = buffer[index];
    return shared_buffer[value * 4096];
}

Penggunaan yang tidak diinisialisasi secara spekulatif yang mengarah ke cabang tidak langsung

Penggunaan yang belum diinisialisasi secara spekulatif berpotensi menyebabkan cabang tidak langsung di mana target cabang dikendalikan oleh penyerang. Dalam contoh di bawah ini, routine ditetapkan ke salah satu DefaultMessageRoutine1 atau DefaultMessageRoutine tergantung pada nilai mode. Pada jalur arsitektur, ini akan mengakibatkan routine selalu diinisialisasi sebelum cabang tidak langsung. Namun, tergantung pada kode yang dihasilkan oleh kompilator, pengabaian penyimpanan spekulatif dapat terjadi yang memungkinkan cabang tak langsung melalui routine untuk dijalankan secara spekulatif sebelum penugasan ke routine. Jika ini terjadi, penyerang mungkin dapat secara spekulatif mengeksekusi dari alamat sembarang, dengan asumsi penyerang dapat memengaruhi atau mengontrol nilai yang tidak diinisialisasi dari routine.

#define MAX_MESSAGE_ID 16

typedef void (*MESSAGE_ROUTINE)(unsigned char *buffer, unsigned int buffer_size);

const MESSAGE_ROUTINE DispatchTable[MAX_MESSAGE_ID];
extern unsigned int mode;

void InitializeRoutine(MESSAGE_ROUTINE *routine) {
    if (mode == 1) {
        *routine = &DefaultMessageRoutine1;
    }
    else {
        *routine = &DefaultMessageRoutine;
    }
}

void DispatchMessage(unsigned int untrusted_message_id, unsigned char *buffer, unsigned int buffer_size) {
    MESSAGE_ROUTINE routine;

    InitializeRoutine(&routine); // not inlined

    // SPECULATION BARRIER
    routine(buffer, buffer_size);
}

Opsi mitigasi

Kerentanan saluran sisi eksekusi spekulatif dapat dimitigasi dengan membuat perubahan pada kode sumber. Perubahan ini dapat melibatkan mitigasi contoh spesifik dari kerentanan, seperti dengan menambahkan pembatas spekulasi, atau dengan mengubah desain aplikasi untuk menjadikan informasi sensitif tidak dapat diakses oleh eksekusi spekulatif.

Hambatan spekulasi melalui instrumentasi manual

Pengembang dapat secara manual memasukkan hambatan spekulasi untuk mencegah eksekusi spekulatif melanjutkan ke jalur yang tidak sesuai dengan arsitektur. Misalnya, pengembang dapat menyisipkan barrier spekulatif sebelum pola pengkodean berbahaya di dalam blok bersyarat, baik di awal blok (setelah percabangan kondisional) atau sebelum pemuatan pertama yang menjadi perhatian. Ini akan mencegah kesalahan prediksi cabang bersyarat dari menjalankan kode berbahaya pada jalur non-arsitektural dengan cara men-serialisasi eksekusi. Urutan penghalang spekulasi berbeda dengan arsitektur perangkat keras seperti yang dijelaskan oleh tabel berikut:

Arsitektur Hambatan spekulasi bawaan untuk CVE-2017-5753 Penghalang spekulasi intrinsik untuk CVE-2018-3639
x86/x64 _mm_lfence() _mm_lfence()
ARM saat ini tidak tersedia __dsb(0)
ARM64 saat ini tidak tersedia __dsb(0)

Misalnya, pola kode berikut dapat dimitigasi dengan menggunakan _mm_lfence intrinsik seperti yang ditunjukkan di bawah ini.

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
    if (untrusted_index < buffer_size) {
        _mm_lfence();
        unsigned char value = buffer[untrusted_index];
        return shared_buffer[value * 4096];
    }
}

Hambatan spekulasi melalui penginstrumentasian saat kompilasi

Pengkompilasi Microsoft C++ di Visual Studio 2017 (dimulai dengan versi 15.5.5) menyertakan dukungan untuk /Qspectre sakelar yang secara otomatis menyisipkan hambatan spekulasi untuk serangkaian pola pengkodian yang berpotensi rentan terbatas yang terkait dengan CVE-2017-5753. Dokumentasi untuk /Qspectre bendera memberikan informasi lebih lanjut tentang efek dan penggunaannya. Penting untuk dicatat bahwa bendera ini tidak mencakup semua pola pengkodean yang berpotensi rentan dan karena itu pengembang tidak boleh mengandalkannya sebagai mitigasi komprehensif untuk kelas kerentanan ini.

Masking indeks array

Dalam kasus di mana pemuatan out-of-bounds spekulatif dapat terjadi, indeks array dapat dibatasi dengan ketat pada jalur arsitektur dan non-arsitektur dengan menambahkan logika untuk secara eksplisit membatasi indeks array. Misalnya, jika array dapat dialokasikan ke ukuran yang sejajar dengan pangkat dua, maka masker sederhana dapat diperkenalkan. Ini diilustrasikan dalam sampel di bawah ini di mana diasumsikan buffer_size selaras dengan pangkat dari dua. Ini memastikan bahwa untrusted_index selalu kurang dari buffer_size, bahkan jika terjadi prediksi cabang bersyarat yang salah dan untrusted_index diteruskan dengan nilai yang lebih besar dari atau sama dengan buffer_size.

Perlu dicatat bahwa masking indeks yang dilakukan di sini dapat tunduk pada bypass penyimpanan spekulatif tergantung pada kode yang dihasilkan oleh pengkompilasi.

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
    if (untrusted_index < buffer_size) {
        untrusted_index &= (buffer_size - 1);
        unsigned char value = buffer[untrusted_index];
        return shared_buffer[value * 4096];
    }
}

Menghapus informasi sensitif dari memori

Teknik lain yang dapat digunakan untuk mengurangi kerentanan saluran samping eksekusi spekulatif adalah menghapus informasi sensitif dari memori. Pengembang perangkat lunak dapat mencari peluang untuk merefaktor aplikasi mereka sehingga informasi sensitif tidak dapat diakses selama eksekusi spekulatif. Ini dapat dicapai dengan merefaktor desain aplikasi untuk mengisolasi informasi sensitif ke dalam proses terpisah. Misalnya, aplikasi browser web dapat mencoba mengisolasi data yang terkait dengan setiap asal web ke dalam proses terpisah, sehingga mencegah satu proses mengakses data lintas asal melalui eksekusi spekulatif.

Lihat juga

Panduan untuk mengurangi celah keamanan eksekusi spekulatif
Mengurangi kerentanan perangkat keras saluran samping eksekusi spekulatif