Files
HIP7CTF_Writeups/cryptoracle_v3.md
m0rph3us1987 a79656b647 Added writeups
2026-03-08 12:22:39 +01:00

222 lines
7.1 KiB
Markdown

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