5.3 KiB
CryptOracle v2
CryptOracle v2 ist die "gehärtete" Version des HSM-Simulators. Die Schwachstelle des direkten Speicherlesens aus v1 wurde behoben, aber eine neue "Kryptographische Engine" wurde hinzugefügt. Das Ziel ist dasselbe: den geheimen Schlüssel aus dem sicheren Speicherbereich bei 0x10000 zu dumpen.
Informationsbeschaffung
Nachdem wir uns mit dem Server verbunden haben, sehen wir eine vertraute Oberfläche mit neuen Befehlen wie enc und dec.
CryptOracle v2.0 (Hardened!)
Setting up memory...
0x10000 - 0x11000 : Secure Memory (Keys/ROM)
0x20000 - 0x28000 : User Memory
Type 'help' for commands.
Reverse Engineering
Wir öffnen die Binärdatei in Ghidra und überprüfen die Funktionen, die sich geändert haben.
1. Der Patch (do_read)
Die do_read-Funktion enthält jetzt eine explizite Prüfung, die jeden Versuch blockiert, von Adressen unterhalb des Benutzerspeicherbereichs zu lesen.
void do_read(uint32_t offset,uint32_t len)
{
uint8_t *data;
// Diese Prüfung verhindert, dass wir 0x10000 direkt lesen.
if (offset < 0x20000) {
puts("ERR_ACCESS_VIOLATION");
}
else {
// ... (Rest der Funktion)
}
return;
}
Das einfache rm 0x10000 32 aus v1 funktioniert nicht mehr.
2. Die Kryptographische Engine (do_cipher)
Da die Schwachstelle des direkten Lesens aus v1 gepatcht wurde, müssen wir nun andere Befehle als potenzielle Angriffsfläche untersuchen. Die Befehle enc und dec, die zuvor schon vorhanden waren, rücken nun in unseren Fokus. Sie werden von der do_cipher-Funktion behandelt. Sie ermöglicht das Verschlüsseln oder Entschlüsseln von Daten von einer Quelladresse zu einer Zieladresse unter Verwendung eines Schlüssels, der in einem sicheren "Slot" gespeichert ist.
void do_cipher(char mode, uint32_t src_off, uint32_t len, int slot, uint32_t dst_off)
{
// ...
// Zeiger auf Quellpuffer abrufen (kann überall sein)
src_ptr = get_ptr(src_off, len, 0);
// Zeiger auf Zielpuffer abrufen (muss beschreibbar sein)
dst_ptr = get_ptr(dst_off, len, 1);
// Zeiger auf den Schlüssel aus einem sicheren Slot abrufen
// Adresse wird berechnet als 0x10000 + slot * 32
key_ptr = get_ptr((slot + 0x800) * 0x20, 0x20, 0);
if (/* alle Zeiger sind gültig */) {
// ... führt AES-Verschlüsselung/-Entschlüsselung blockweise durch ...
}
}
Die wichtigste Erkenntnis liegt darin, wie die Zeiger validiert werden, was alles innerhalb der get_ptr-Funktion geschieht.
3. Schwachstellenanalyse (get_ptr)
Hier ist die dekompilierte get_ptr-Funktion aus Ghidra. Sie nimmt einen Offset, eine Länge und ein Flag is_write entgegen, das 1 für Schreiboperationen und 0 für Leseoperationen ist.
uint8_t * get_ptr(uint32_t offset,uint32_t len,int is_write)
{
uint8_t *puVar1;
if (len + offset < offset) {
puVar1 = (uint8_t *)0x0;
}
else if ((offset < 0x10000) || (0x11000 < len + offset)) {
if ((offset < 0x20000) || (0x28000 < len + offset)) {
puVar1 = (uint8_t *)0x0;
}
else {
puVar1 = (uint8_t *)(ulong)offset;
}
}
else if ((is_write == 0) || (0x7ff < offset - 0x10000)) {
puVar1 = (uint8_t *)(ulong)offset;
}
else {
puVar1 = (uint8_t *)0x0;
}
return puVar1;
}
Die Schwachstelle:
Die Logik im letzten else if-Block ist fehlerhaft. Verfolgen wir sie für eine Leseoperation (is_write == 0) im sicheren Flaggenbereich (offset = 0x10000).
- Der Ausdruck ist
(is_write == 0) || (0x7ff < offset - 0x10000). - Da
is_write0ist, ist der erste Teil des ODER(0 == 0)wahr. - Der gesamte Ausdruck wird
wahr, und der Zugriff wird gewährt, wodurch ein gültiger Zeiger zurückgegeben wird.
Die Prüfung, die das Schreiben in die ersten 2KB des sicheren Speichers (0x7ff < offset - 0x10000) verhindern sollte, wird für jede Leseoperation vollständig umgangen. Die do_cipher-Funktion missbraucht dies, indem sie ein Lesen (is_write=0) auf der sicheren Flagge anfordert, was get_ptr erlaubt. Dies erzeugt ein Confused Deputy-Szenario, bei dem wir die legitimen Leseberechtigungen des Orakels nutzen, um Daten zu exfiltrieren.
Lösung
Der Angriff ist ein zweistufiger Prozess:
- Verschlüsseln: Verwenden Sie den
enc-Befehl, um das Orakel anzuweisen, aus dem sicheren Flaggenbereich (0x10000) zu lesen und das verschlüsselte Ergebnis (Chiffretext) in den benutzerzugänglichen Speicher (0x20000) zu schreiben. - Entschlüsseln: Verwenden Sie den
dec-Befehl, um das Orakel anzuweisen, den Chiffretext aus dem Benutzerspeicher (0x20000) zu lesen und das entschlüsselte Ergebnis (Klartext) zurück an eine andere Stelle im Benutzerspeicher (0x20100) zu schreiben. - Lesen: Verwenden Sie den jetzt reparierten
rm-Befehl, um die Klartext-Flagge von0x20100zu lesen.
Ausführung
# Schritt 1: Verschlüsseln der Flagge aus dem sicheren Speicher in den Benutzerspeicher
> enc 0x10000 32 30 0x20000
OK
# Schritt 2: Entschlüsseln des Chiffretextes aus dem Benutzerspeicher an eine andere Stelle im Benutzerspeicher
> dec 0x20000 32 30 0x20100
OK
# Schritt 3: Lesen der Klartext-Flagge aus dem Benutzerspeicher
> rm 0x20100 32
00020100: 7b 66 6c 61 67 3a 20 73 65 6c 66 5f 65 6e 63 72 {flag: self_encr
00020110: 79 70 74 69 6f 6e 5f 69 73 5f 6e 69 63 65 21 7d yption_is_nice!}
Die Flagge wird enthüllt: {flag: self_encryption_is_nice!}