Added writeups
This commit is contained in:
133
cryptoracle_v2.md
Normal file
133
cryptoracle_v2.md
Normal file
@@ -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!}`
|
||||
Reference in New Issue
Block a user