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

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_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

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