BlackHat MEA 2023 CTF Finals: Reverse Engineering Writeup
Detailed writeup of the Ground Hog Day reverse engineering challenge from the BlackHat MEA 2023 CTF final round in Riyadh - binary analysis, function recovery, and flag extraction.
Context
BlackHat MEA 2023 was held in Riyadh, Saudi Arabia. Our team competed in the CTF final round, placing 31st among 250+ international teams. The competition spanned three days with challenges released each day across categories including reverse engineering, pwn, cryptography, forensics, and web exploitation.
This writeup covers the Ground Hog Day challenge released on Day 3 - a reverse engineering problem that required understanding function behavior, identifying the flag generation logic, and extracting the flag either through static analysis or careful dynamic debugging.
Initial Reconnaissance
A zip file was provided containing a single binary called main. Let’s start with basic file analysis:
$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, not stripped
$ checksec --file=main
RELRO STACK CANARY NX PIE
Partial RELRO No canary found NX enabled No PIE
Key observations:
- ELF 64-bit - standard Linux binary
- Dynamically linked - uses libc, we can identify standard functions
- Not stripped - symbol names are preserved (function names visible)
- NX enabled - stack is non-executable (not relevant for RE but good to note)
- No PIE - addresses are fixed, simplifies debugging
- No canary - no stack protection (again, more relevant for pwn)
Running the Binary
$ ./main
BLACKh4t_CTF{fl4g_p4rt_
Interesting - the binary prints part of the flag but then hangs without completing. It doesn’t crash; it just stops producing output. Something in the program’s logic is preventing the full flag from being printed.
Strings Analysis
$ strings main | grep -i flag
# Nothing obvious - the flag isn't stored as a plaintext string
The flag characters are being generated at runtime, not stored statically. This confirms we need to understand the program’s logic.
Static Analysis with Ghidra
Loading the binary in Ghidra and looking at the function list, we find two key functions: main and why.
The main Function
Decompiled, main contains a loop that:
- Initializes a character array
- Iterates through the array, performing transformations on each byte
- Prints each transformed character with
putchar() - Calls the
whyfunction at some point during iteration
The transformation involves XOR operations, bit rotations, and arithmetic on each byte index. The first portion of the flag (BLACKh4t_CTF{fl4g_p4rt_) is generated by the loop before it reaches the point where why is called.
The why Function
This function was the key to the challenge. It contains a deliberate stalling mechanism - either an infinite loop, a very long sleep, or a computation that takes impractically long to complete. This is the “Ground Hog Day” concept: the program gets stuck in a loop, repeating the same state over and over.
The function performs a computation that in theory completes, but the iteration count is astronomically high - effectively an infinite loop in practical terms.
The Solution
There were two approaches:
Approach 1: Static Recovery (Preferred)
Since the flag generation logic is entirely deterministic and based on the loop index, we can extract the transformation algorithm from Ghidra and reimplement it in Python, skipping the why function entirely:
# Extracted from Ghidra's decompilation
encoded = [0x42, 0x4c, 0x41, 0x43, 0x4b, ...] # byte array from binary
key_schedule = [...] # transformation parameters
flag = ""
for i in range(len(encoded)):
b = encoded[i]
b ^= key_schedule[i % len(key_schedule)]
b = ((b >> 3) | (b << 5)) & 0xFF
flag += chr(b)
print(flag)
This immediately prints the complete flag without needing to execute the binary at all.
Approach 2: Dynamic Patching
In GDB, let the binary run until it enters the why function, then force-return from it:
$ gdb ./main
(gdb) break why
(gdb) run
# Hits breakpoint at why
(gdb) return
# Force return from why without executing it
(gdb) continue
# Binary continues printing the rest of the flag
Alternatively, NOP out the call why instruction entirely and run the patched binary.
Lessons Learned
CTF Time Pressure
In a competition setting, the fastest approach was GDB patching - identify that why is the blocking function, skip it, get the flag. Static analysis gives a cleaner understanding but takes more time.
Triage Speed
The key to solving this quickly was recognizing the pattern within the first 2 minutes:
- Binary prints partial output → something is blocking
- Two functions:
mainandwhy→whyis suspicious whycontains a loop → it’s the blocker- Skip
why→ get the flag
In CTF finals, this kind of rapid triage separates top teams from mid-tier ones. You don’t need to fully understand every instruction - you need to identify the critical path and exploit it.
This challenge was part of our 31st place finish among 250+ international teams at BlackHat MEA 2023. The reverse engineering category tested speed and pattern recognition as much as technical depth.