7.0 KiB
CryptOracle v3
CryptOracle v3 ist das "Gold Master" Release des HSM-Simulators. Die Schwachstellen aus v1 (direktes Lesen) und v2 (Confused Deputy) wurden gepatcht. Die Herausforderung besteht darin, die neue Schwachstelle in den kryptographischen Funktionen zu finden, um die geheime Flagge zu dumpen.
Informationsbeschaffung
Nachdem wir uns mit dem Server verbunden haben, sehen wir eine vertraute Oberfläche:
CryptOracle v3.0 (Bullet proof!)
...
Type 'help' for commands.
Die Binärdatei ist nicht gestrippt, was unsere Analyse in Ghidra erleichtert.
Reverse Engineering
1. Die Patches
Zuerst überprüfen wir die Fixes.
do_read: Hat immer noch die Prüfungif (offset < 0x20000), die direkte Lesevorgänge im sicheren Speicher blockiert.get_ptr: Hat jetzt ein viertes Argumentis_privileged. Wir stellen fest, dassdo_cipher(fürencunddec)get_ptrmitis_privileged=0aufruft, was bedeutet, dass es überhaupt nicht mehr auf sicheren Speicher zugreifen kann. Der Confused Deputy Angriff ist behoben.
2. Die neue Angriffsfläche (do_sign)
Die Challenge-Beschreibung deutet an, dass der sig-Befehl unser neuer Fokus ist. Die do_sign-Funktion wird aufgerufen, und im Gegensatz zu do_cipher ist sie privilegiert.
/* Dekompilierte do_sign Funktion */
void do_sign(uint32_t src_off, uint32_t len, int slot, uint32_t dst_off)
{
long lVar1;
uint8_t *src_ptr;
uint8_t *dst_ptr;
uint8_t *key_ptr;
AES_KEY k;
uint8_t block [16]; // Ein 16-Byte-Puffer auf dem Stack
// Alle Zeiger werden mit privilegiertem Zugriff angefordert (letztes Argument ist 1)
src_ptr = get_ptr(src_off, len, 0, 1);
dst_ptr = get_ptr(dst_off, 0x10, 1, 1);
key_ptr = get_ptr((slot + 0x800) * 0x20, 0x20, 0, 1);
if (/* Zeiger sind gültig */) {
AES_set_encrypt_key(key_ptr, 0x80, &k);
// Der Stack-Puffer wird ausgenullt
memset(block, 0, 0x10);
// Die Eingabelänge wird auf 16 Byte begrenzt
if (0x10 < len) {
len = 0x10;
}
// Die (bis zu) 16 Bytes von der Quelle werden in den Block kopiert
memcpy(block, src_ptr, (ulong)len);
// Der Block wird verschlüsselt und an das Ziel geschrieben
AES_encrypt(block, dst_ptr, &k);
puts("OK");
}
}
Wichtige Erkenntnisse aus do_sign:
- Privilegiert: Sie kann von überall lesen, einschließlich der geheimen Flagge bei
0x10000. - Deterministisch: Das "Signieren" ist einfach eine AES-ECB-Verschlüsselung. Für einen gegebenen Schlüssel und einen gegebenen Eingabeblock ist die Ausgabe immer gleich.
- Padding: Die Funktion nimmt bis zu 16 Bytes Eingabe, kopiert sie in einen mit Nullen aufgefüllten 16-Byte-Block und verschlüsselt dann den Block. Das ist entscheidend:
sigauf einem einzelnen ByteXist effektivEnc(Key, [X, 0, 0, ...]).
3. Die Schwachstelle: Deterministisches Orakel
Da das Signieren deterministisch ist, können wir es als Verschlüsselungsorakel verwenden. Wir können es bitten, beliebige Daten, die wir wollen, mit einem der Hauptschlüssel (z.B. Slot 0) zu "signieren" (verschlüsseln). Wenn wir jedes mögliche Byte von 0-255 verschlüsseln, können wir eine Lookup-Tabelle erstellen, die das ursprüngliche Byte auf seine Signatur abbildet.
Dies ermöglicht einen klassischen Rainbow Table Angriff.
Lösung
Der Angriff besteht aus zwei Phasen:
-
Erstellen einer Rainbow Table:
- Iteriere durch alle 256 möglichen Bytewerte.
- Schreibe jedes Byte in den Benutzerspeicher (z.B.
0x20000). - Verwende den
sig-Befehl mit einem Hauptschlüssel (Slot 0), um dieses einzelne Byte zu verschlüsseln. - Lies die 16-Byte-Signatur aus dem Ausgabepuffer.
- Speichere die Zuordnung
Signatur -> Byte.
-
Dumpen des sicheren Speichers:
- Iteriere durch die Adressen der geheimen Flagge (
0x10000,0x10001, usw.). - Verwende für jede Adresse den
sig-Befehl, um die Signatur des einzelnen geheimen Bytes an dieser Adresse zu erhalten. - Suche die resultierende Signatur in unserer Rainbow Table, um das ursprüngliche geheime Byte zu finden.
- Iteriere durch die Adressen der geheimen Flagge (
Schritt-für-Schritt-Ausführung
Hier ist, wie wir das Lookup für das erste Byte der Flagge manuell durchführen würden.
1. Tabelle für ein bekanntes Byte erstellen, z.B. 'A' (0x41)
# Schreibe 'A' in den Benutzerspeicher
> wm 0x20000 1 41
OK
# Signiere dieses Byte mit dem Hauptschlüssel in Slot 0
> sig 0x20000 1 0 0x20100
OK
# Lies die Signatur
> rm 0x20100 16
00020100: d85de6195410...
Jetzt wissen wir, dass die Signatur d85de6... dem Klartext-Byte A entspricht. Wir wiederholen dies für alle 256 Bytes.
2. Signatur des ersten geheimen Bytes abrufen
# Signiere das Byte am Anfang des sicheren Bereichs
> sig 0x10000 1 0 0x20100
OK
# Lies seine Signatur
> rm 0x20100 16
00020100: 555c441c2...
3. Nachschlagen und wiederholen
Wir finden heraus, welches Klartext-Byte der Signatur 555c44... in unserer vorgefertigten Tabelle entspricht. Dies enthüllt das erste Byte der Flagge. Wir wiederholen dies für alle 32 Bytes, um die gesamte Flagge zu dumpen.
Finales Solver-Skript
Das solve.py Skript automatisiert diesen gesamten Prozess.
from pwn import *
import sys
# --- Konfiguration ---
HOST = '127.0.0.1'
PORT = 1339
# Speicherkarte
SECRET_BASE = 0x10000
USER_BASE = 0x20000
USER_SCRATCH = USER_BASE + 0x200
def solve():
io = remote(HOST, PORT)
io.recvuntil(b"Type 'help' for commands.
")
log.info(f"Connected. dumping memory from 0x{SECRET_BASE:x}...")
# --- Phase 1: Rainbow Table erstellen (0x00 - 0xFF) ---
log.info("Phase 1: Building Rainbow Table...")
rainbow_table = {}
prog = log.progress("Mapping")
for b in range(256):
byte_hex = f"{b:02x}"
io.sendline(f"wm 0x{USER_BASE:x} 1 {byte_hex}".encode())
io.recvuntil(b"OK\n")
io.sendline(f"sig 0x{USER_BASE:x} 1 0 0x{USER_SCRATCH:x}".encode())
io.recvuntil(b"OK\n")
io.sendline(f"rm 0x{USER_SCRATCH:x} 16".encode())
signature = io.recvline().strip().decode()
rainbow_table[signature] = b
if b % 16 == 0: prog.status(f"{b}/255")
prog.success(f"Done. ({len(rainbow_table)} entries)")
# --- Phase 2: Sicheren Speicher dumpen ---
log.info("Phase 2: Dumping first 32 bytes of Secure Memory...")
dumped_bytes = []
for i in range(32):
target_addr = SECRET_BASE + i
io.sendline(f"sig 0x{target_addr:x} 1 0 0x{USER_SCRATCH:x}".encode())
io.recvuntil(b"OK\n")
io.sendline(f"rm 0x{USER_SCRATCH:x} 16".encode())
secret_sig = io.recvline().strip().decode()
if secret_sig in rainbow_table:
val = rainbow_table[secret_sig]
dumped_bytes.append(val)
else:
dumped_bytes.append(0)
# ASCII-Darstellung drucken
ascii_repr = "".join([chr(b) if 32 <= b <= 126 else '.' for b in dumped_bytes])
log.success(f"Flag: {ascii_repr}")
io.close()
if __name__ == "__main__":
solve()
Das Ausführen des Skripts liefert die Flagge:
{flag: self_encryption_is_nice!}