cit@ctf 2025 writeups

catacombs [rev]

just strings it

Maybe they forgot sth, let’s solve it in the “intended” way

what if it’s a remote challenge

This feels like a maze challenge where we have to reach all the nodes. Once we’re done, call submit to get the flag

Digging into IDA: main -> runScript -> applyVisibleStep. I found EDGE_TABLE, which (as confirmed through testing) basically maps out all the edges of the “catacombs”.

escaperoom [rev]

This is another game/riddle challenge

I tried out all the commands. It seems that once we achieve the correct room state, we can 8. enter door override token to get the flag.

The tasks are listed in the facility log:

I also found the state variables for these things:

  1. gstate = 0 light off
  2. dword_5D90E4 = 1 ventilation route east
  3. dword_5D90E8 = 3 cam 3
  4. dword_5D90EC = 2 door patch 2 times
  5. inside maintainance shell: dword_5D90F1 = 1 mirror first then dword_5D90F2 = 1 hush
  6. dword_5D90F0 = 1 emegency battery on

Alright, thing is set up

But i still need the door token; where could it be? Looking at the references of those state variables, I found roomSignature() -> buildOverrideToken()

I couldn’t find where it was called, so i replicated it:

def rol32(v, n):
    v &= 0xFFFFFFFF
    return ((v << n) | (v >> (32 - n))) & 0xFFFFFFFF

def room_signature(g_state, e4, e8, ec, f0, f1, f2):
    v0 = 324508639 if g_state else 610839776
    inner = (521288629 * (e4 + 1) + rol32(v0 ^ 0xA17C3E29, 7)) & 0xFFFFFFFF
    v5 = (668265261 * (ec + 5) + ((73244475 * (e8 + 3)) ^ inner)) & 0xFFFFFFFF
    v1 = 0xFFFFFFFF & -1515890086 if f1 else 1515890085
    v6 = (v1 ^ v5) & 0xFFFFFFFF
    v2 = 826366246 if f0 else 655894552
    v7 = (v2 + v6) & 0xFFFFFFFF
    v3 = 0xFFFFFFFF & -559038737 if f2 else 0xFFFFFFFF & -1160724258
    return (v3 ^ v7) & 0xFFFFFFFF

def build_override_token(g_state, e4, e8, ec, f0, f1, f2):
    alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
    spice = [19, 55, 49374, 48879, 90, 2766, 16962, 36877, 4660, 1911]
    sig = room_signature(g_state, e4, e8, ec, f0, f1, f2)
    v6 = (sig ^ 0x6F70656E) & 0xFFFFFFFF
    token = []
    for i in range(10):
        v6 = (1664525 * v6 + spice[i] + 1013904223) & 0xFFFFFFFF
        token.append(alphabet[v6 >> 27])
        if i == 2 or i == 5:
            token.append('-')
    return ''.join(token)

print(build_override_token(
    g_state=0, e4=1, e8=3, ec=2, f0=1, f1=1, f2=1
))

Running it prints: RHY-QVT-KAXJ

I wonder if there is a neat way to reverse the validate() in enterOverrideToken() for a better approach.

trivia

I was fed up at this point of writing. Some other challenges follow a similar game-like pattern:

  • find & setup the correct state
  • derive the token
  • submit token & get flag Except for the reallysecurepasswordmanager, where we have to find a way to bypass the user check.

Let’s take a look at the interesting thing i found in these challenges:

mangled look

For example, this is from challenge say my name:

Upon looking it up, this was done with anonymous namespace. Let me explain:

A namespace is just a named container that groups related code together to avoid name collisions.
namespace math {
    int add(int a, int b) { return a + b; }
}

namespace mycode {
    int add(int a, int b) { return a + b; }
}

math::add(1, 2);    // unambiguous
mycode::add(1, 2);  // unambiguous
An **anonymous namespace** is the same thing but with no name:
namespace {
    int add(int a, int b) { return a + b; }
}

The C++ standard gives symbols in anonymous namespaces internal linkage, IDA just labels them with the prefix 'anonynmous namespace'::

looks fun.

gdb start & ni denial

can’t run with start

in certain parts. the ni doesn’t work

i haven’t figured out why yet, be updating soon.