174 lines
5.0 KiB
Markdown
174 lines
5.0 KiB
Markdown
# CryptOracle v1
|
|
|
|
`CryptOracle v1` is an introductory cryptography and reverse engineering challenge that simulates a Hardware Security Module (HSM). The objective is to retrieve a secret flag stored in a "secure" memory region that is supposedly isolated from the user.
|
|
|
|
## Information Gathering
|
|
|
|
We are provided with a `cryptOracle_v1.tar.xz` archive containing the `crypt_oracle_v1` binary. Initial analysis confirms it is a statically linked 64-bit ELF executable.
|
|
|
|
```bash
|
|
$ file crypt_oracle_v1
|
|
crypt_oracle_v1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ...
|
|
```
|
|
|
|
Upon connecting to the challenge server, we are greeted with the following interface:
|
|
|
|
```
|
|
CryptOracle v1.0
|
|
Setting up memory...
|
|
0x10000 - 0x11000 : Secure Memory (Keys/ROM)
|
|
0x20000 - 0x28000 : User Memory
|
|
Type 'help' for commands.
|
|
```
|
|
|
|
The challenge description hints that sensitive keys are stored at address `0x10000`.
|
|
|
|
## Reverse Engineering
|
|
|
|
We use Ghidra to analyze the binary and understand how it manages memory access.
|
|
|
|
### 1. Main Interaction Loop (`main`)
|
|
|
|
The `main` function initializes the HSM simulation and processes user commands.
|
|
|
|
```c
|
|
undefined8 main(void)
|
|
|
|
{
|
|
int iVar1;
|
|
char *pcVar2;
|
|
long in_FS_OFFSET;
|
|
undefined4 local_428;
|
|
undefined4 local_424;
|
|
undefined4 local_420;
|
|
undefined4 local_41c;
|
|
char local_418 [512];
|
|
undefined1 local_218 [520];
|
|
long local_10;
|
|
|
|
local_10 = *(long *)(in_FS_OFFSET + 0x28);
|
|
setvbuf((FILE *)stdout,(char *)0x0,2,0);
|
|
puts("CryptOracle v1.0");
|
|
setup_memory();
|
|
puts("Type \'help\' for commands.");
|
|
while( true ) {
|
|
pcVar2 = fgets(local_418,0x200,(FILE *)stdin);
|
|
if (pcVar2 == (char *)0x0) break;
|
|
iVar1 = strncmp(local_418,"rm",2);
|
|
if (iVar1 == 0) {
|
|
iVar1 = __isoc99_sscanf(local_418,"rm 0x%x %d",&local_420,&local_41c);
|
|
if (iVar1 == 2) {
|
|
do_read(local_420,local_41c);
|
|
}
|
|
}
|
|
// ... (other commands) ...
|
|
}
|
|
// ...
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
The `rm` command takes a hexadecimal address and a decimal size, then calls the `do_read` function.
|
|
|
|
### 2. Memory Initialization (`setup_memory`)
|
|
|
|
The `setup_memory` function reveals where the flag is placed.
|
|
|
|
```c
|
|
void setup_memory(void)
|
|
|
|
{
|
|
void *pvVar1;
|
|
FILE *__stream;
|
|
size_t sVar2;
|
|
|
|
puts("Setting up memory...");
|
|
pvVar1 = mmap64((void *)0x10000,0x1000,3,0x32,-1,0);
|
|
// ...
|
|
pvVar1 = mmap64((void *)0x20000,0x8000,3,0x32,-1,0);
|
|
// ...
|
|
memset((void *)0x10000,0,0x1000);
|
|
memset((void *)0x20000,0,0x8000);
|
|
__stream = fopen64("flag.bin","rb");
|
|
if (__stream != (FILE *)0x0) {
|
|
sVar2 = fread((void *)0x10000,1,0x800,__stream);
|
|
fclose(__stream);
|
|
// ...
|
|
return;
|
|
}
|
|
// ...
|
|
}
|
|
```
|
|
|
|
We confirm that the flag from `flag.bin` is loaded directly into the "Secure Memory" region at `0x10000`.
|
|
|
|
### 3. Vulnerability Analysis (`get_ptr`)
|
|
|
|
The `do_read` function calls a helper called `get_ptr` to validate memory access before printing data.
|
|
|
|
```c
|
|
void do_read(undefined4 param_1,undefined4 param_2)
|
|
|
|
{
|
|
long lVar1;
|
|
|
|
lVar1 = get_ptr(param_1,param_2,0);
|
|
if (lVar1 == 0) {
|
|
puts("ERR_ACCESS_VIOLATION");
|
|
}
|
|
else {
|
|
print_hex(lVar1,param_2);
|
|
}
|
|
return;
|
|
}
|
|
```
|
|
|
|
Now let\'s examine the logic in `get_ptr`:
|
|
|
|
```c
|
|
uint get_ptr(uint param_1,int param_2,int param_3)
|
|
|
|
{
|
|
// 1. Integer overflow check: ensure address + size doesn't wrap around
|
|
if (param_2 + param_1 < param_1) {
|
|
param_1 = 0;
|
|
}
|
|
// 2. Range Validation: check if access is within mapped regions
|
|
else if ((param_1 < 0x10000) || (0x11000 < param_2 + param_1)) {
|
|
// If not in Secure Memory (0x10000-0x11000), check User Memory (0x20000-0x28000)
|
|
if ((param_1 < 0x20000) || (0x28000 < param_2 + param_1)) {
|
|
param_1 = 0;
|
|
}
|
|
}
|
|
// 3. Security Check: block access to flag region (0x10000 - 0x10800)
|
|
// This is ONLY enforced if param_3 (check_secure) is non-zero
|
|
else if ((param_3 != 0) && (param_1 - 0x10000 < 0x800)) {
|
|
param_1 = 0;
|
|
}
|
|
return param_1;
|
|
}
|
|
```
|
|
|
|
The validation logic in `get_ptr` works as follows:
|
|
1. **Integer Overflow**: It checks if `size + addr` overflows.
|
|
2. **Range Validation**: It ensures the access is within the Secure Memory (`0x10000-0x11000`) or User Memory (`0x20000-0x28000`).
|
|
3. **Secure Memory Restriction**: It explicitly blocks access to the first `0x800` bytes of Secure Memory (where the flag is stored) **ONLY IF** `param_3` (the `check_secure` flag) is non-zero.
|
|
|
|
Crucially, in the `do_read` function (which implements the `rm` command), the third argument passed to `get_ptr` is **hardcoded to `0`**. This means the specific check that protects the secret keys is bypassed when using the "read memory" command.
|
|
|
|
## Solution
|
|
|
|
Because the `rm` command bypasses the isolation check in `get_ptr`, we can directly dump the secure RAM.
|
|
|
|
1. Connect to the server.
|
|
2. Read the memory at address `0x10000`.
|
|
|
|
```bash
|
|
> rm 0x10000 32
|
|
00010000: 7b 66 6c 61 67 3a 20 74 68 61 74 5f 77 61 73 5f {flag: that_was_
|
|
00010010: 74 6f 6f 5f 65 61 73 79 5f 72 69 67 68 74 3f 7d too_easy_right?}
|
|
```
|
|
|
|
The flag is revealed: `{flag: that_was_too_easy_right?}`
|
|
|
|
``` |