← Back to list

BRK CYS CTF 2026

بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

Amr Eldhshan · 2026-04-12 09:05 · 1 claps · 4.5 min read
#reverse #crypto #ctf #ctf-writeup #brk-cys-ctf-2026
Open on Medium ↗
Wiki topics: CRY · Crypto & Web3

BRK CYS CTF 2026

بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

BRK CYS CTF 2026 — Reverse Engineering & Crypto Writeups

Challenge 1 — not_so_simple.c

Flag: BRKCYS{x0r_1s_n0t_s3cur3}

The Challenge

We’re given a C source file and asked to find the correct input that makes it print “Correct!”.

int check_password(char *input) {
    unsigned char key[] = {
        0x00, 0x10, 0x09, 0x01, 0x1b, 0x11, 0x39,
        0x3a, 0x72, 0x30, 0x1d, 0x73, 0x31, 0x1d,
        0x2c, 0x72, 0x36, 0x1d, 0x31, 0x71, 0x21,
        0x37, 0x30, 0x71, 0x3f
    };
    int len = strlen(input);
    if (len != 25) return 0;
    for (int i = 0; i < 25; i++) {
        if ((input[i] ^ 0x42) != key[i]) return 0;
    }
    return 1;
}

Solution

The check for each character is: input[i] ^ 0x42 == key[i]

XOR is self-inverse — if A ^ B = C, then C ^ B = A. So this is the same as saying input[i] == key[i] ^ 0x42. We already have the full key[] hardcoded right there, so we just XOR every byte with 0x42 to recover the original input.

key = [
    0x00, 0x10, 0x09, 0x01, 0x1b, 0x11, 0x39,
    0x3a, 0x72, 0x30, 0x1d, 0x73, 0x31, 0x1d,
    0x2c, 0x72, 0x36, 0x1d, 0x31, 0x71, 0x21,
    0x37, 0x30, 0x71, 0x3f
]
flag = ''.join(chr(b ^ 0x42) for b in key)
print(flag)
# BRKCYS{x0r_1s_n0t_s3cur3}

Challenge 2 — Dark Matter (hard_re)

Flag: BRKCYS{4ss3mbly_r3v3rs3}

The Challenge

A stripped 64-bit ELF binary. No symbols, no debug info. We have to find the correct input from the binary alone.

Reconnaissance

Running strings on the binary reveals:

/proc/self/status
TracerPid:
Wrong flag!
Enter the flag:
Correct! You got it!
ABCDEFGHIJKLMNOPQRSTUVWX
z<Ui+Nq

Two things stand out — /proc/self/status and TracerPid: tell us the binary checks for a debugger. The string z<Ui+Nq looks like 7 raw bytes that don't belong — we'll come back to that.

Anti-Debug (Layer 0)

The binary reads /proc/self/status and finds the TracerPid: field. If the value isn't 0 (meaning a debugger is attached), it exits immediately.

fd = open("/proc/self/status", O_RDONLY);
read(fd, buf, 0x1FF);
close(fd);
ptr = strstr(buf, "TracerPid:");
if (*ptr != '0') {
    write(1, "Wrong flag!\n", 12);
    _exit(1);
}

We bypass this by doing purely static analysis in IDA — the check only affects runtime.

XOR Layer (Layer 1)

After verifying the input is exactly 24 characters, the binary XORs every input byte against a key stored at .rodata:0x20C0:

13 37 42 1F 7A 3C 55 69 2B 4E 71 0D 88 3F 5C 91
27 6A B3 4D 78 1E 93 52

The key opens with 0x13 0x37 (1337 in hex). Also notice bytes 4–10 are 7A 3C 55 69 2B 4E 71 — that's exactly z<Ui+Nq, the suspicious string we saw in strings output. It was part of the XOR key all along.

Rotate Left Layer (Layer 2)

After the XOR, each byte is rotated left by a position-dependent amount:

for (int i = 0; i < 24; i++)
    tmp[i] = ROL(tmp[i], (i % 5) + 1);

The rotation amount cycles: 1, 2, 3, 4, 5, 1, 2, 3, 4, 5 …

The result is compared against a second blob at .rodata:0x20A0:

A2 95 48 C5 64 DE B8 EA 85 A7 84 81 57 35 A4 9D
55 CA 5C CF 14 B5 05 F2

Reversing It

We work backwards through both layers:

  • ROR by (i%5)+1 to undo the rotation (ROL by N = ROR by 8−N for bytes)
  • XOR with the key to undo the XOR
def ror(byte, n):
    n = n % 8
    return ((byte >> n) | (byte << (8 - n))) & 0xFF
target = bytes([
    0xA2, 0x95, 0x48, 0xC5, 0x64, 0xDE, 0xB8, 0xEA,
    0x85, 0xA7, 0x84, 0x81, 0x57, 0x35, 0xA4, 0x9D,
    0x55, 0xCA, 0x5C, 0xCF, 0x14, 0xB5, 0x05, 0xF2
])
key = bytes([
    0x13, 0x37, 0x42, 0x1F, 0x7A, 0x3C, 0x55, 0x69,
    0x2B, 0x4E, 0x71, 0x0D, 0x88, 0x3F, 0x5C, 0x91,
    0x27, 0x6A, 0xB3, 0x4D, 0x78, 0x1E, 0x93, 0x52
])
flag = bytes([ror(target[i], (i%5)+1) ^ key[i] for i in range(24)])
print(flag.decode())
# BRKCYS{4ss3mbly_r3v3rs3}

Challenge 3 — Keygen

Flag: BRKCYS{k3yg3n_m4st3r_r3v3rs3r_ots}

The Challenge

A remote service that sends a unique binary per connection as a hex dump. The binary validates a username and a license key. The constants change every connection, so the solution has to be fully automated.

Algorithm

From the decompiled validation:

// Username (14 chars)
for (i = 0; i < 14; ++i)
    expected_user[i] = byte_2068[i] ^ 0x3F;
// License key (17 chars)
for (j = 0; j < 17; ++j)
    expected_license[j] = byte_2050[j] ^ 0x5A;

Same pattern as before — hardcoded byte arrays XORed with a constant. The only difference is the arrays live in .rodata and are randomized each connection.

So:

  • byte_2068 lives at offset 0x2068 from the start of .rodata (which starts at 0x2000)
  • byte_2050 lives at offset 0x2050

We parse the binary, extract both arrays, XOR them with their constants, and we have the answer.

Solution

def solve_from_hex(hex_dump: str) -> tuple:
    binary = bytes.fromhex(''.join(hex_dump.split()))
    RODATA_OFFSET = 0x2000
    byte_2050 = binary[RODATA_OFFSET + 0x50 : RODATA_OFFSET + 0x50 + 17]
    byte_2068 = binary[RODATA_OFFSET + 0x68 : RODATA_OFFSET + 0x68 + 14]
    username    = ''.join(chr(b ^ 0x3F) for b in byte_2068)
    license_key = ''.join(chr(b ^ 0x5A) for b in byte_2050)
    return username, license_key

Example output for one of the binaries:

Username    : jorgd525b
License Key : LICENSE-74D1-969D
[+] License accepted!
[+] Flag: BRKCYS{k3yg3n_m4st3r_r3v3rs3r_ots}

Challenge 4 — Pixel Perfect (Crypto)

Flag: BRKCYS{c0lumnar_tr4ns0s1t10n_r3qu1r3s_p4t13nc3}

The Challenge

We’re given a ciphertext and a block of fake image metadata:

Ciphertext:

R0_srsnYm41u4}Snn01t_Curtqp3Klt13_cBcr0_33{asnr1_

Metadata:

Comment : Taken with my old SERPENT-brand camera on a rainy day.

Finding the Cipher

The ciphertext is 49 characters long — and 49 = 7 × 7, a perfect square. The word SERPENT in the comment is 7 letters. That combination points directly to a columnar transposition cipher with a 7×7 grid and the keyword SERPENT.

Solving It

Write the ciphertext row by row into a 7×7 grid:

R  0  _  s  r  s  n
Y  m  4  1  u  4  }
S  n  n  0  1  t  _
C  u  r  t  q  p  3
K  l  t  1  3  _  c
B  c  r  0  _  3  3
{  a  s  n  r  1  _

Reading column 0 downward gives R Y S C K B { — an anagram of B R K C Y S {, the start of every flag in this CTF. The grid is correct.

Now brute-force all 7! = 5040 possible row orderings until we get a column-read that starts with BRKCYS{:

from itertools import permutations
ciphertext = "R0_srsnYm41u4}Snn01t_Curtqp3Klt13_cBcr0_33{asnr1_"
grid = [list(ciphertext[r*7:(r+1)*7]) for r in range(7)]
for perm in permutations(range(7)):
    result = ''
    for col in range(7):
        for row in perm:
            result += grid[row][col]
    if result.startswith('BRKCYS{'):
        print(f"Row order: {perm}")
        print(f"Flag: {result.strip('_')}")
        break
Row order: (5, 0, 4, 3, 1, 2, 6)
Flag: BRKCYS{c0lumnar_tr4ns0s1t10n_r3qu1r3s_p4t13nc3}

메타데이터
post_id
bb8e0e8faada
slug
brk-cys-ctf-2026-bb8e0e8faada
url
https://medium.com/@amrkhaledv2171516/brk-cys-ctf-2026-bb8e0e8faada
canonical_url
https://medium.com/@amrkhaledv2171516/brk-cys-ctf-2026-bb8e0e8faada
author_url
https://medium.com/@amrkhaledv2171516
status
ok
fetched_at
2026-07-26 09:46:03