Nicht-transparente Nutzung gemeinsamer mikroarchitektureller Ressourcen
Beschreibung
Die nicht-transparente Nutzung gemeinsamer mikroarchitektureller Ressourcen tritt auf, wenn Hardware-Strukturen, die über Ausführungskontexte hinweg geteilt werden (z.B. Caches und Branch Predictors), die erwartete Architektur-Isolation zwischen Kontexten verletzen. Moderne Prozessoren verwenden Techniken zur Leistungsoptimierung wie Out-of-Order-Ausführung, Spekulation und Caching, die für Programmierer transparent arbeiten. Wenn Hardware-Implementierungen diese Ressourcen über isolierte Ausführungskontexte hinweg teilen, entstehen verdeckte Kanäle, die von Angreifern ausgenutzt werden können. Zu den spezifisch verwundbaren gemeinsam genutzten Ressourcen gehören Caches, Branch-Prediction-Logik und Load/Store-Buffer. Die Kombination aus spekulativer Ausführung und Out-of-Order-Verarbeitung verstärkt die Kontrolle des Angreifers über Datenlecks durch diese Kanäle.
Risiko
Die nicht-transparente gemeinsame Ressourcennutzung hat schwerwiegende Sicherheitsauswirkungen. Kryptografische Schlüssel können durch Cache-Timing extrahiert werden. ASLR-Offsets werden über Branch Predictors offengelegt. Beliebige Speicherinhalte sind lesbar. Prozessübergreifende Informationsoffenlegung. Spekulative Ausführungsangriffe (Spectre, Meltdown). Verdeckte Kanäle zwischen Sicherheitsdomänen. Undokumentierte gemeinsame Nutzung macht den Schutz extrem schwierig.
Lösung
Implementieren Sie partitionierte Caches, um kontextübergreifende Datenfreigabe während der Architektur- und Entwurfsphase zu verhindern. Stellen Sie neue Barriere- und Flush-Befehle für die Cache-Steuerung bereit. Deaktivieren Sie hochauflösende Leistungszähler und Timer, die Timing-Seitenkanale ermöglichen. Implementieren Sie Barrieren für spekulative Ausführung. Verwenden Sie separaten Branch-Prediction-Status pro Sicherheitsdomäne. Erwägen Sie Hardware-Partitionierung gemeinsam genutzter Ressourcen.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Vertraulichkeit | Bereich: Vertraulichkeit Sensible Informationen einschließlich kryptografischer Schlüssel, ASLR-Offsets und beliebiger Speicherinhalte sind durch mikroarchitekturelle Seitenkanale extrahierbar. |
Beispielcode und Lösung
Verwundbarer Code
// VERWUNDBAR: Code anfällig für Spectre-artige Angriffe
#include <stdint.h>
#include <string.h>
// VERWUNDBAR: Array-Grenzenprüfung kann durch spekulative Ausführung umgangen werden
uint8_t array1[16];
uint8_t array2[256 * 512]; // Probe-Array für Cache-Timing
size_t array1_size = 16;
// VERWUNDBAR: Geheime Daten im benachbarten Speicher
uint8_t secret_data[256] = "SECRET_ENCRYPTION_KEY_12345678";
uint8_t vulnerable_bounds_check(size_t x) {
// VERWUNDBAR: Spekulative Ausführung ignoriert Grenzenprüfung
if (x < array1_size) {
// Während der spekulativen Ausführung kann die CPU dies ausführen,
// auch wenn x >= array1_size ist
// VERWUNDBAR: Out-of-Bounds-Lesezugriff in secret_data
uint8_t value = array1[x]; // x könnte groß sein und Geheimnisse lesen
// VERWUNDBAR: Abhängiger Ladezugriff bringt Geheimnis in den Cache
return array2[value * 512]; // Erzeugt Cache-Seitenkanal
}
return 0;
}
// Angriff:
// 1. Branch Predictor trainieren, damit (x < array1_size) als wahr erwartet wird
// 2. Aufruf mit x = Offset zu secret_data (z.B. x = secret_data - array1)
// 3. Branch sagt falsch vorher, führt Body spekulativ aus
// 4. array1[x] liest geheimes Byte, obwohl x außerhalb der Grenzen liegt
// 5. array2[secret_byte * 512] wird in den Cache geladen
// 6. Ausführung wird zurückgesetzt, aber Cache-Zustand bleibt erhalten
// 7. Zeitmessung des Zugriffs auf array2 bestimmt, welche Zeile gecacht ist
// 8. Gecachte Zeile verrät den Wert des geheimen Bytes
// VERWUNDBAR: Gemeinsamer Branch-Predictor-Zustand
void vulnerable_indirect_branch(int index) {
// VERWUNDBAR: Indirekter Branch kann vom Angreifer trainiert werden
static void (*handlers[4])(void) = {
handler0, handler1, handler2, handler3
};
if (index >= 0 && index < 4) {
handlers[index](); // VERWUNDBAR: Spekulativer indirekter Branch
}
// Angriff: Angreifer in anderem Thread trainiert Branch Target Buffer
// Indirekter Branch des Opfers springt spekulativ zu angreifergewählter Adresse
// Angreifer kann Opfer veranlassen, spekulativ Gadgets auszuführen
}
// VERWUNDBAR: Cache-Timing-Seitenkanal
int vulnerable_cache_timing_leak(const uint8_t* secret_key, const uint8_t* input) {
uint8_t result = 0;
for (int i = 0; i < 16; i++) {
// VERWUNDBAR: Tabellen-Lookup hängt vom geheimen Schlüssel ab
// Cache-Miss/Hit-Muster verraten Schlüssel-Bits
result ^= sbox[secret_key[i] ^ input[i]];
}
return result;
}
// VERWUNDBAR: Hardware mit gemeinsam genutzten mikroarchitekturellen Ressourcen
module vulnerable_shared_cache (
input wire clk,
input wire rst_n,
// Core-0-Schnittstelle (vertrauenswürdig)
input wire [31:0] core0_addr,
input wire core0_read,
input wire [1:0] core0_security_level,
// Core-1-Schnittstelle (nicht vertrauenswürdig)
input wire [31:0] core1_addr,
input wire core1_read,
input wire [1:0] core1_security_level,
// Cache-Ausgänge
output reg [31:0] cache_data,
output reg cache_hit
);
// VERWUNDBAR: Gemeinsamer Cache zwischen Cores
// Keine Partitionierung zwischen Sicherheitsstufen
reg [31:0] cache_data_array [0:255];
reg [23:0] cache_tag_array [0:255];
reg cache_valid [0:255];
wire [7:0] core0_index = core0_addr[11:4];
wire [7:0] core1_index = core1_addr[11:4];
// VERWUNDBAR: Beide Cores teilen sich dieselben Cache-Zeilen
// Core 1 kann Cache-Zeilen von Core 0 verdrängen
// Cache-Timing verrät Zugriffsmuster von Core 0 an Core 1
always @(posedge clk) begin
// Core-0-Zugriff
if (core0_read) begin
// VERWUNDBAR: Zugriff beeinflusst gemeinsamen Cache-Zustand
// Core 1 kann Timing-Unterschiede beobachten
end
// Core-1-Zugriff
if (core1_read) begin
// VERWUNDBAR: Kann prüfen, ob Zugriffe von Core 0 erkennbar sind
// Prime+Probe-Angriff möglich
end
end
endmodule
// VERWUNDBAR: Gemeinsamer Branch Predictor
module vulnerable_branch_predictor (
input wire clk,
input wire rst_n,
// Core-0-Eingänge
input wire [31:0] core0_pc,
input wire core0_branch_taken,
input wire core0_update,
// Core-1-Eingänge
input wire [31:0] core1_pc,
input wire core1_branch_taken,
input wire core1_update,
// Vorhersagen
output wire core0_prediction,
output wire core1_prediction
);
// VERWUNDBAR: Gemeinsame Branch-History-Tabelle
reg [1:0] branch_history_table [0:1023];
wire [9:0] core0_index = core0_pc[11:2];
wire [9:0] core1_index = core1_pc[11:2];
// VERWUNDBAR: Core 1 kann Predictor-Zustand trainieren, der von Core 0 genutzt wird
// Spectre v2 (Branch Target Injection) Angriff möglich
always @(posedge clk) begin
if (core1_update) begin
// VERWUNDBAR: Branches von Core 1 beeinflussen gemeinsamen Predictor
// Können Vorhersagen für Core 0 vergiften
if (core1_branch_taken)
branch_history_table[core1_index] <= branch_history_table[core1_index] + 1;
else
branch_history_table[core1_index] <= branch_history_table[core1_index] - 1;
end
end
// VERWUNDBAR: Core 0 verwendet vergiftete Vorhersagen
assign core0_prediction = branch_history_table[core0_index][1];
assign core1_prediction = branch_history_table[core1_index][1];
endmodule
Sichere Lösung
// SICHER: Schutzmassnahmen gegen mikroarchitekturelle Angriffe
#include <stdint.h>
#include <string.h>
// SICHER: Spectre-Schutz mit Spekulationsbarriere
#if defined(__x86_64__)
#define speculation_barrier() __asm__ __volatile__("lfence" ::: "memory")
#elif defined(__aarch64__)
#define speculation_barrier() __asm__ __volatile__("csdb" ::: "memory")
#else
#define speculation_barrier() __asm__ __volatile__("" ::: "memory")
#endif
uint8_t array1[16];
uint8_t array2[256 * 512];
size_t array1_size = 16;
uint8_t secure_bounds_check(size_t x) {
if (x < array1_size) {
// SICHER: Spekulationsbarriere verhindert spekulative Ausführung
speculation_barrier();
uint8_t value = array1[x];
return array2[value * 512];
}
return 0;
}
// SICHER: Index-Maskierung (Grenzen-Clipping)
uint8_t secure_bounds_mask(size_t x) {
// SICHER: Maske erstellen, die 0 ist wenn x >= array1_size
size_t mask = ~(x - array1_size) >> (sizeof(size_t) * 8 - 1);
mask = mask - 1; // Alle 1en wenn gültig, alle 0en wenn ungültig
// SICHER: Maske auf Index anwenden - außerhalb der Grenzen wird 0
x = x & mask;
uint8_t value = array1[x];
return array2[value * 512];
}
// SICHER: Retpoline für indirekte Branches (Spectre-v2-Schutz)
#define RETPOLINE_CALL(func, arg) \
__asm__ __volatile__( \
"call retpoline_call_target\n" \
: : "D" (arg), "S" (func) : "memory")
void secure_indirect_branch(int index) {
static void (*handlers[4])(void) = {
handler0, handler1, handler2, handler3
};
if (index >= 0 && index < 4) {
// SICHER: Retpoline verwenden, um spekulative indirekte Branch-Angriffe zu verhindern
// Oder IBRS (Indirect Branch Restricted Speculation) verwenden, falls verfügbar
speculation_barrier();
handlers[index]();
}
}
// SICHER: Konstant-Zeit-Tabellen-Lookup (kein Cache-Timing-Seitenkanal)
uint8_t secure_constant_time_lookup(const uint8_t* table, size_t index, size_t table_size) {
uint8_t result = 0;
// SICHER: ALLE Tabelleneinträge zugreifen, den richtigen per Maskierung auswählen
for (size_t i = 0; i < table_size; i++) {
// Konstant-Zeit-Vergleich: Maske ist alle 1en wenn i == index, sonst alle 0en
uint8_t mask = -((i ^ index) == 0);
result |= table[i] & mask;
}
return result;
}
// SICHER: Cache-Timing-resistentes AES (Bit-Sliced-Implementierung)
void secure_aes_no_cache_timing(const uint8_t* key, const uint8_t* input,
uint8_t* output) {
// SICHER: Bit-Sliced-Implementierung ohne Tabellen-Lookups verwenden
// Alle Operationen sind arithmetisch/logisch, keine speicherabhängigen Zugriffsmuster
aes_bitsliced_encrypt(key, input, output);
}
// SICHER: Hardware mit partitionierten mikroarchitekturellen Ressourcen
module secure_partitioned_cache (
input wire clk,
input wire rst_n,
// Core-0-Schnittstelle (vertrauenswürdig)
input wire [31:0] core0_addr,
input wire core0_read,
input wire [1:0] core0_security_level,
// Core-1-Schnittstelle (nicht vertrauenswürdig)
input wire [31:0] core1_addr,
input wire core1_read,
input wire [1:0] core1_security_level,
// Cache-Ausgänge
output reg [31:0] core0_data,
output reg core0_hit,
output reg [31:0] core1_data,
output reg core1_hit
);
// SICHER: Separate Cache-Partitionen pro Sicherheitsdomäne
// Way-Partitionierung: Core 0 nutzt Ways 0-1, Core 1 nutzt Ways 2-3
reg [31:0] cache_data_way0 [0:127]; // Core-0-Partition
reg [31:0] cache_data_way1 [0:127];
reg [31:0] cache_data_way2 [0:127]; // Core-1-Partition
reg [31:0] cache_data_way3 [0:127];
reg [23:0] cache_tag_way0 [0:127];
reg [23:0] cache_tag_way1 [0:127];
reg [23:0] cache_tag_way2 [0:127];
reg [23:0] cache_tag_way3 [0:127];
// SICHER: Jeder Core kann nur seine eigene Partition beeinflussen
// Keine domainübergreifende Cache-Interferenz
wire [6:0] core0_index = core0_addr[10:4];
wire [6:0] core1_index = core1_addr[10:4];
always @(posedge clk) begin
// SICHER: Core 0 greift nur auf Ways 0-1 zu
if (core0_read) begin
// Cache-Lookup in partitionierten Ways
// Kann Zeilen von Core 1 nicht verdrängen
end
// SICHER: Core 1 greift nur auf Ways 2-3 zu
if (core1_read) begin
// Kann Zugriffsmuster von Core 0 nicht beobachten
end
end
endmodule
// SICHER: Kontextbezogener Branch Predictor
module secure_branch_predictor (
input wire clk,
input wire rst_n,
// Core-0-Eingänge
input wire [31:0] core0_pc,
input wire core0_branch_taken,
input wire core0_update,
input wire [7:0] core0_context_id,
// Core-1-Eingänge
input wire [31:0] core1_pc,
input wire core1_branch_taken,
input wire core1_update,
input wire [7:0] core1_context_id,
// Vorhersagen
output wire core0_prediction,
output wire core1_prediction
);
// SICHER: Separater Branch-Predictor-Zustand pro Kontext
reg [1:0] bht_context0 [0:1023]; // Core 0 / vertrauenswürdig
reg [1:0] bht_context1 [0:1023]; // Core 1 / nicht vertrauenswürdig
wire [9:0] core0_index = core0_pc[11:2];
wire [9:0] core1_index = core1_pc[11:2];
// SICHER: Aktualisierungen betreffen nur den eigenen Kontext-Predictor
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// Predictoren initialisieren
end else begin
// SICHER: Core 0 aktualisiert nur seinen Predictor
if (core0_update) begin
if (core0_branch_taken)
bht_context0[core0_index] <= bht_context0[core0_index] + 1;
else
bht_context0[core0_index] <= bht_context0[core0_index] - 1;
end
// SICHER: Core 1 aktualisiert nur seinen Predictor
if (core1_update) begin
if (core1_branch_taken)
bht_context1[core1_index] <= bht_context1[core1_index] + 1;
else
bht_context1[core1_index] <= bht_context1[core1_index] - 1;
end
end
end
// SICHER: Jeder Core verwendet nur seinen eigenen Vorhersage-Zustand
assign core0_prediction = bht_context0[core0_index][1];
assign core1_prediction = bht_context1[core1_index][1];
// SICHER: Kontextwechsel löscht/wechselt Predictor-Zustand
// SICHER: IBPB (Indirect Branch Prediction Barrier) Unterstützung für Flushes
endmodule
CVE-Beispiele
- CVE-2017-5753: Spectre Variante 1 - Bounds Check Bypass durch spekulative Ausführung.
- CVE-2017-5715: Spectre Variante 2 - Branch Target Injection.
- CVE-2017-5754: Meltdown - Rogue Data Cache Load.
- CVE-2018-3639: Speculative Store Bypass.
- CVE-2019-1125: SWAPGS-Angriff.
Verwandte CWEs
- CWE-203: Observable Discrepancy (Eltern)
- CWE-1189: Improper Isolation of Shared Resources on System-on-a-Chip (Eltern)
- CWE-1198: Privilege Separation and Access Control Issues (Kategorie)
- CWE-1300: Improper Protection of Physical Side Channels (verwandt)
Referenzen
- MITRE Corporation. "CWE-1303: Non-Transparent Sharing of Microarchitectural Resources." https://cwe.mitre.org/data/definitions/1303.html
- Kocher, P., et al. "Spectre Attacks: Exploiting Speculative Execution"
- Lipp, M., et al. "Meltdown: Reading Kernel Memory from User Space"