# 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!}`