Added writeups

This commit is contained in:
m0rph3us1987
2026-03-08 12:22:39 +01:00
parent a566ea77d1
commit a79656b647
43 changed files with 6940 additions and 0 deletions

195
de/cryptoracle_v3.md Normal file
View File

@@ -0,0 +1,195 @@
# 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üfung `if (offset < 0x20000)`, die direkte Lesevorgänge im sicheren Speicher blockiert.
- **`get_ptr`**: Hat jetzt ein viertes Argument `is_privileged`. Wir stellen fest, dass `do_cipher` (für `enc` und `dec`) `get_ptr` mit `is_privileged=0` aufruft, 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.
```c
/* 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`:
1. **Privilegiert**: Sie kann von überall lesen, einschließlich der geheimen Flagge bei `0x10000`.
2. **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.
3. **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: `sig` auf einem einzelnen Byte `X` ist effektiv `Enc(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:
1. **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`.
2. **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.
### 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)**
```bash
# 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**
```bash
# 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.
```python
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!}`