222 lines
7.1 KiB
Markdown
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!}`
|