4.7 KiB
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.
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".
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.
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_writeis0, 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:
- Encrypt: Use the
enccommand to instruct the oracle to read from the secure flag region (0x10000) and write the encrypted result (ciphertext) into user-accessible memory (0x20000). - Decrypt: Use the
deccommand 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). - Read: Use the now-fixed
rmcommand to read the plaintext flag from0x20100.
Execution
# 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!}