Introduction

    A few weeks ago, I participated in CTF@CIT 2026, a Jeopardy-style CTF, that featured challenges across multiple categories and was aimed at a relatively easy difficulty level. During the event, I focused exclusively on the Reversing category, solving challenges that required binary analysis, reverse engineering and a bit of patience when dealing with intentionally confusing program logic. Despite the approachable difficulty curve, several challenges still managed to be genuinely engaging and thought provoking, making the competition both enjoyable and challenging at the same time.

    I participated with the team Echelon Obscura, where I served as the team captain. By the end of the competition, we placed 120th out of 754 teams, which made the overall experience even more rewarding considering the number of participants involved. In this post series, I will go through some of the Reversing challenges I solved during the competition, discussing both the analysis process and the reasoning behind each step.

    This first post focuses on the challenge named “Escape Room”, which served as a nice warm up while still requiring enough analysis to make it interesting.

Challenge Overview

    The following description was provided as part of the challenge, along with a binary named escaperoom:

“Can you escape?

challenge Info

    The first thing I did was to use the file command to gather basic information about the binary:

$ file escaperoom
escaperoom: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=a48f82dc780190b397abf4e8595b4b466efa9380, for GNU/Linux 4.4.0, not stripped

    From this, a few key characteristics of the binary become immediately clear. It is a 64-bit ELF executable for x86-64 Linux, intended to run in a standard Linux environment.

    A more interesting detail is that it is statically linked, meaning all required library code is embedded directly into the binary instead of being resolved at runtime. This typically increases the binary size and can make analysis more noisy due to inlined library logic, but it also removes external dependencies from the equation.

    Additionally, the binary is not stripped, which is a major advantage during reversing. Symbol information is still present, so function names and internal identifiers are available, making navigation significantly easier compared to stripped binaries where analysis relies almost entirely on raw disassembly and control flow.

    As a final step before moving into the static analysis phase, I executed the binary to gather some additional context about its behavior and overall purpose:

$ ./escaperoom
Booting Room 7B egress terminal...
Tip: maintenance logs may be dirty, reflected, or rotated.

=====================================
  ROOM 7B EGRESS TERMINAL / v2.4.1
=====================================
1. read facility log
2. toggle hallway lights
3. cycle ventilation route
4. rotate camera bus
5. apply door-control patch
6. toggle emergency battery bridge
7. maintenance shell
8. enter door override token
9. status
0. quit
=====================================
> 

    The binary presents a simulated “Room 7B Egress Terminal”, exposing a menu driven interface with several actions related to the environment, i.e. reading facility logs, toggling lights, rotating the camera bus, interacting with the maintenance shell and entering a door override token.

    From the available options, it quickly became apparent that the primary objective was tied to option 8, which expects a valid override token. Based on the challenge name and the overall interface design, my initial assumption was that the token was not hardcoded directly inside the program, but instead generated or validated dynamically at runtime.

    With this initial understanding of the binary’s functionality, I then shifted focus to the static analysis phase in order to determine how the token validation mechanism actually worked and what was required to ultimately recover the flag.

Static Analysis

    I began the static analysis by opening the binary in Ghidra. Since the binary was not stripped, function names were already available, which made navigating through the program significantly easier. After locating the main function and performing some renaming, the overall structure of the challenge became much easier to understand.

    After cleaning up the decompiled output, I started by examining the main function:

Main Function part 1

Main Function part 2

Main Function part 3

    The main() function acts mostly as a dispatcher for the different menu options exposed by the terminal interface. After printing the boot message and the hint regarding “dirty, reflected, or rotated” maintenance logs, the program enters a loop where it continuously prints the menu, reads user input using std::getline and redirects execution to the corresponding handler function.

    Even from this first pass, it became clear that the challenge logic was distributed across several smaller functions rather than being implemented directly inside main(). The most interesting paths were immediately the facility logs, the maintenance shell, the room status display and especially the override token functionality.

    The next function I looked at was readFacilityLog(). This function initializes and prints rotating facility log entries. At first glance, the logs appear to be simple environmental flavor text, but they actually contain several important hints related to the required room state. Messages reference hallway lights, ventilation routing, camera buses, emergency battery bridging and door patch behavior, all of which later become important for solving the challenge:

readFacilityLog Function part 1

readFacilityLog Function part 2

readFacilityLog Function part 3

    More importantly, the function also makes use of two helper functions named reverseCopy() and rot13Copy(). This directly matches the startup hint mentioning that maintenance logs may be “reflected” or “rotated”, indicating that some clues are intentionally encoded. The helper routines themselves are fairly simple.

    Staring with the reverseCopy() function, it creates a reversed copy of a string by iterating over it in reverse order:

reverseCopy Function

    Similarly, rot13Copy() applies a standard ROT13 transformation to alphabetic characters. While these functions are technically simple, they are important because they explain how some of the hidden hints are revealed throughout the challenge:

rot13Copy Function

    After understanding the logging system, I moved on to the various state changing functions exposed through the menu. I started with thetoggleLights() function, which simply flips the hallway light state between ON and OFF:

toggleLights Function

    The cycleVentRoute() function was fairly simple, cycling between the available ventilation routes: cycleVentRoute Function

    The function rotateCameraBus() rotates through the available camera buses:

rotateCameraBus Function

    The applyDoorPatch() function increments the patch count modulo four. Interestingly, the printed messages reveal that applying the patch twice appears stable, while the third patch triggers a watchdog warning. This immediately stood out because the logs had already hinted at this exact behavior: applyDoorPatch Function

    Finally, the toggleBatteryBridge() function is used to enable or disable the emergency battery bridge:

toggleBatteryBridge Function

    The next major component was the maintenance shell. The maintenanceConsole() function exposes a small command interpreter with commands such as mirror, hush, decode and reset:

maintenanceConsole Function part 1

maintenanceConsole Function part 2

    The mirror command only succeeds if the camera bus is currently set to 3, enabling the mirror relay state. The hush command is more restrictive and requires multiple conditions to be satisfied simultaneously: the emergency battery bridge must be enabled, the hallway lights must be OFF, the ventilation route must be correctly set and the mirror relay must already be armed. Only then does the function enable the alarm muted state.

    This effectively creates a chained dependency system where multiple menu actions must be performed in the correct order before the room reaches the intended state. The maintenance shell therefore acts as the mechanism that ties together all previously observed room controls.

maintenanceConsole Function part 3

maintenanceConsole Function part 4

    The decode command was especially useful because it further confirmed the role of the reflected and rotated maintenance hints. The function decodes and prints transformed strings, reinforcing the idea that the challenge expects the player to reconstruct clues from encoded text rather than rely purely on brute force or dynamic testing.

    One particularly important observation was that the hints were actually literal instructions hidden behind simple transformations. For example, the “Mirror first. Then hush.” message survives a double ROT13 transformation unchanged, while the patch related hint confirms that the door patch should be applied exactly twice.

    With the room state logic understood, I shifted my focus to the enterOverrideToken() function, which reads the user supplied token and then calls validate(). However, one detail immediately stood out: the decompiled code does not clearly show the user input being passed into the validation routine. Instead, validate() is called with only an output buffer parameter, suggesting that the real logic is hidden deeper and likely not implemented as a simple string comparison.

enterOverrideToken Function

    This suspicion became even stronger after examining the validate() function itself:

validate Function Part 1

validate Function Part 2

    Compared to the rest of the binary, validate() looks completely different and it took me a lot of time to process. Instead of normal high level program logic, the function seems to perform VM slot initialization, self patching, XOR based transformations, dispatch table relocation and eventually transfers execution through an indirect function pointer.

    At this point, it became clear that the actual override token logic was hidden behind a custom virtual machine style interpreter. The decompiled output is essentially unreadable due to the VM architecture and self modifying behavior. Rather than validating the token directly in native code, the binary dynamically reconstructs and executes an interpreter responsible for generating or validating the expected token.

    During this process, the internal room state variables became especially important. The binary computes a room signature using the seven global flags controlled throughout the challenge, combining them with several hardcoded constants and arithmetic operations.

    This also explained why the challenge required the environment to be configured precisely. A single incorrect state variable would produce a completely different signature, resulting in an invalid override token.

    Finally, I also examined the showStatus() function:

showStatus Function Part 1

showStatus Function Part 2

    This function simply prints the current room state, including the hallway lights, ventilation route, camera bus, patch count, battery bridge state, inspection mode, and alarm speaker status.

    Since validate() was still quite confusing and did not immediately reveal a clean path for generating the correct token, I continued digging through the binary for related functions. This eventually led to two very important functions that became the breakthrough for solving the challenge: buildOverrideToken() and roomSignature().

    The first one I examined was buildOverrideToken():

buildOverrideToken Function Part 1

buildOverrideToken Function Part 2

    This function finally revealed how the override token is generated. Instead of the token being stored as a static string, it is built dynamically at runtime. The function starts by initializing an alphabet string: "ABCDEFGHJKLMNPQRSTUVWXYZ23456789". This alphabet resembles a Crockford Base32 style character set, intentionally avoiding ambiguous characters such as I, O, 0, and 1. This makes sense for a token format that is expected to be typed manually.

    The initial seed for the generator is produced by calling roomSignature() and XORing the result with 0x6f70656e, which corresponds to the ASCII string open when interpreted as a 32-bit value. This immediately showed that the token depends directly on the current room state. In other words, generating the correct token is not enough on its own bacause the room must also be configured correctly before the token is valid.

    The token generation loop is based on a simple 32-bit Linear Congruential Generator (LCG), a lightweight pseudo-random number generator commonly used to produce deterministic sequences from an initial seed value. An LCG works by repeatedly updating an internal state through a multiplication and addition operation under modular arithmetic. In this case, the generator produces one token character per iteration by updating the state using a fixed multiplier, a constant increment and one value from a static spice array:

state = state * 0x19660d + spice[i] + 0x3c6ef35f;

    After each update, the top five bits of the state are used as an index into the alphabet:

token += alphabet[state >> 27];

    Since the alphabet has 32 characters, using the top five bits gives a valid index from 0 to 31. The function also inserts dashes after the third and sixth generated characters, producing a final token in the format: XXX-XXX-XXXX.

    The second important function was roomSignature():

roomSignature Function

    This function is what connects the token generation logic back to the menu actions. It builds a 32-bit signature from the program’s global room state, including the hallway lights, ventilation route, camera bus, door patch count, battery bridge, mirror relay and alarm speaker state.

    Each boolean state is first mapped to a different magic constant. For example, if the lights are off, the function uses 0x2468ace0; otherwise, it uses 0x13579bdf. Similarly, the battery bridge, mirror relay and alarm muted state each select between two different constants depending on whether they are enabled or disabled.

    The remaining numeric state values are also included in the calculation. The ventilation route, camera bus and patch count are combined with fixed multipliers, while the lights contribution is first XORed with 0xa17c3e29 and rotated left by seven bits. The final signature is then produced through a sequence of additions and XOR operations.

    This was the point where the challenge became much clearer. The purpose of the logs, maintenance shell and status menu was to guide the player toward the correct global state. Once that state is known, roomSignature() produces the seed used by buildOverrideToken() and the expected override token can be computed offline.

    One final missing piece was the spice array used inside the token generation loop. By inspecting the static data references in Ghidra, I was able to recover the array values, which were clearly hand-picked CTF-style constants:

spice = [
    0x13, 0x37, 0xc0de, 0xbeef, 0x5a,
    0xace, 0x4242, 0x900d, 0x1234, 0x777
]

    At this stage, the solving path was no longer about fully reversing the VM hidden inside validate(). Instead, the focus shifted toward reconstructing the correct room state, computing the corresponding roomSignature(), using it as the seed for buildOverrideToken() and generating the expected override token directly. With all the required pieces finally understood, the last step was simply to reproduce the correct state and compute the token offline.

Getting the flag

    After understanding how the room state influenced the token generation process, the remaining task was to place the terminal into the exact state expected by roomSignature().

    Starting from a fresh launch of the binary, all global flags are initialized to zero. Based on the analysis of the maintenance shell and the state dependent logic, the required sequence of actions became the following:

  1. Press 6: Enable the emergency battery bridge.

  2. Press 3: Set the ventilation route to the east bypass.

  3. Press 4 three times: Rotate the camera bus to 3.

  4. Enter 7 -> mirror: Arm the mirror relay.

  5. Enter back: Return to the main menu.

  6. Press 2: Turn the hallway lights OFF.

  7. Enter 7 -> hush: Mute the alarm speaker.

  8. Enter back: Return to the main menu.

  9. Press 5 twice: Apply the door patch exactly two times.

  10. Press 8: Enter the generated override token.

    With the correct state now known, generating the token became straightforward. I reproduced the logic from roomSignature() and buildOverrideToken() in a small Python script:

import struct

def u32(x): return x & 0xffffffff

def rotl32(x, n):
    x = u32(x)
    return u32((x << n) | (x >> (32 - n)))

# Target state: lights=0, battery=1, mirror=1, hush=1, vent=1, cam=3, patch=2
lights_contrib  = 0x2468ace0
battery_contrib = 0xa5a55a5a
mirror_contrib  = 0x31415926
alarm_contrib   = 0xdeadbeef

vent_route = 1
camera_bus = 3
patch_count = 2

tmp   = u32(lights_contrib ^ 0xa17c3e29)
rot   = rotl32(tmp, 7)
step1 = u32(rot + u32((vent_route + 1) * 0x1f123bb5))
step2 = u32(step1 ^ u32((camera_bus + 3) * 0x45d9f3b))
step3 = u32(step2 + u32((patch_count + 5) * 0x27d4eb2d))
step4 = u32(step3 ^ battery_contrib)
sig   = u32(u32(step4 + mirror_contrib) ^ alarm_contrib)
seed  = u32(sig ^ 0x6f70656e)

spice = [0x13, 0x37, 0xc0de, 0xbeef, 0x5a, 0xace, 0x4242, 0x900d, 0x1234, 0x777]
alpha = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'

state = seed
token = ''

for i in range(10):
    state = u32(state * 0x19660d + spice[i] + 0x3c6ef35f)
    token += alpha[state >> 27]

    if i in (2, 5):
        token += '-'

print(f"[+] Override token: {token}")

    Running the script produced the following override token:

python get_token.py
[+] Override token: RHY-QVT-KAXJ

After entering the token into the terminal, the validation succeeded and the binary finally revealed the flag:

./escaperoom
Booting Room 7B egress terminal...
Tip: maintenance logs may be dirty, reflected, or rotated.

=====================================
  ROOM 7B EGRESS TERMINAL / v2.4.1
=====================================
1. read facility log
2. toggle hallway lights
3. cycle ventilation route
4. rotate camera bus
5. apply door-control patch
6. toggle emergency battery bridge
7. maintenance shell
8. enter door override token
9. status
0. quit
=====================================
> 6
[power] emergency battery bridge ENGAGED.

=====================================
  ROOM 7B EGRESS TERMINAL / v2.4.1
=====================================
1. read facility log
2. toggle hallway lights
3. cycle ventilation route
4. rotate camera bus
5. apply door-control patch
6. toggle emergency battery bridge
7. maintenance shell
8. enter door override token
9. status
0. quit
=====================================
> 3
[vents] rerouted to east bypass.

=====================================
  ROOM 7B EGRESS TERMINAL / v2.4.1
=====================================
1. read facility log
2. toggle hallway lights
3. cycle ventilation route
4. rotate camera bus
5. apply door-control patch
6. toggle emergency battery bridge
7. maintenance shell
8. enter door override token
9. status
0. quit
=====================================
> 4
[cameras] switched to bus 1 / corridor.

=====================================
  ROOM 7B EGRESS TERMINAL / v2.4.1
=====================================
1. read facility log
2. toggle hallway lights
3. cycle ventilation route
4. rotate camera bus
5. apply door-control patch
6. toggle emergency battery bridge
7. maintenance shell
8. enter door override token
9. status
0. quit
=====================================
> 4
[cameras] switched to bus 2 / vault.

=====================================
  ROOM 7B EGRESS TERMINAL / v2.4.1
=====================================
1. read facility log
2. toggle hallway lights
3. cycle ventilation route
4. rotate camera bus
5. apply door-control patch
6. toggle emergency battery bridge
7. maintenance shell
8. enter door override token
9. status
0. quit
=====================================
> 4
[cameras] switched to bus 3 / mirror relay.

=====================================
  ROOM 7B EGRESS TERMINAL / v2.4.1
=====================================
1. read facility log
2. toggle hallway lights
3. cycle ventilation route
4. rotate camera bus
5. apply door-control patch
6. toggle emergency battery bridge
7. maintenance shell
8. enter door override token
9. status
0. quit
=====================================
> 7

[maintenance shell]
commands: help, mirror, hush, decode, reset, back
svc> mirror
[svc] mirror relay aligned. inspection mode enabled.
svc> back 

=====================================
  ROOM 7B EGRESS TERMINAL / v2.4.1
=====================================
1. read facility log
2. toggle hallway lights
3. cycle ventilation route
4. rotate camera bus
5. apply door-control patch
6. toggle emergency battery bridge
7. maintenance shell
8. enter door override token
9. status
0. quit
=====================================
> 2
[lights] hallway lights now OFF.

=====================================
  ROOM 7B EGRESS TERMINAL / v2.4.1
=====================================
1. read facility log
2. toggle hallway lights
3. cycle ventilation route
4. rotate camera bus
5. apply door-control patch
6. toggle emergency battery bridge
7. maintenance shell
8. enter door override token
9. status
0. quit
=====================================
> 7

[maintenance shell]
commands: help, mirror, hush, decode, reset, back
svc> hush
[svc] alarm speaker muted.
svc> back

=====================================
  ROOM 7B EGRESS TERMINAL / v2.4.1
=====================================
1. read facility log
2. toggle hallway lights
3. cycle ventilation route
4. rotate camera bus
5. apply door-control patch
6. toggle emergency battery bridge
7. maintenance shell
8. enter door override token
9. status
0. quit
=====================================
> 5
[door] patch layer 1 accepted.

=====================================
  ROOM 7B EGRESS TERMINAL / v2.4.1
=====================================
1. read facility log
2. toggle hallway lights
3. cycle ventilation route
4. rotate camera bus
5. apply door-control patch
6. toggle emergency battery bridge
7. maintenance shell
8. enter door override token
9. status
0. quit
=====================================
> 5
[door] patch layer 2 accepted. controller timing looks stable.

=====================================
  ROOM 7B EGRESS TERMINAL / v2.4.1
=====================================
1. read facility log
2. toggle hallway lights
3. cycle ventilation route
4. rotate camera bus
5. apply door-control patch
6. toggle emergency battery bridge
7. maintenance shell
8. enter door override token
9. status
0. quit
=====================================
> 8
override token> RHY-QVT-KAXJ
CIT{Vc282vlhCxIJ}

Conclusion

    Overall, this was a very enjoyable challenge that combined several different reversing concepts in a clean and approachable way. Even though the binary initially looked intimidating because of the custom VM logic inside validate(), the actual solving path relied more on understanding the program’s state machine and reconstructing the token generation process than on fully reversing the interpreter itself.

    What I particularly liked about this challenge was how the hints were embedded naturally into the environment. The facility logs, the maintenance shell and the encoded messages all contributed meaningful information without feeling forced. The reflected and rotated text paths were simple ideas technically, but they fit very well with the overall theme of the terminal simulation.

    Another interesting aspect was how the challenge encouraged changing perspective during analysis. My initial instinct was to focus entirely on the VM inside validate(), but eventually the better approach was to step back, analyze the surrounding logic and identify the functions responsible for generating the token directly. Once roomSignature() and buildOverrideToken() were understood, the entire challenge became much more manageable.

    In the end, the challenge provided a good balance between static analysis, state reconstruction, lightweight obfuscation and dynamic reasoning, making it a very solid introductory reversing task for the competition.