5.0 KiB
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.
$ 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.
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.
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.
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:
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:
- Integer Overflow: It checks if
size + addroverflows. - Range Validation: It ensures the access is within the Secure Memory (
0x10000-0x11000) or User Memory (0x20000-0x28000). - Secure Memory Restriction: It explicitly blocks access to the first
0x800bytes of Secure Memory (where the flag is stored) ONLY IFparam_3(thecheck_secureflag) 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.
- Connect to the server.
- Read the memory at address
0x10000.
> 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?}