← Back to list

🎭 ZiChmp CTF 2026: cyber champions 2026

Or: How I Learned to Stop Worrying and Love the Reversing

Amr Eldhshan · 2026-02-07 18:08 · 10 claps · 8.6 min read
#cybersecurity #reverse-engineering #zinad #cyber-champions #reverse
Open on Medium ↗
Wiki topics: 🔒 · Cybersecurity 💑 · Relationships

Cu🎭 ZiChmp CTF 2026: A Journey Through Pain, Suffering, and XOR

Or: How I Learned to Stop Worrying and Love the Reversing

📖 Table of Contents

  • Challenge 1: calc — Baby’s First XOR
  • Challenge 2: IloveC — The Guessing Game Nobody Asked For
  • Challenge 3: tangled — Lua? In MY C Binary?
  • Challenge 4: broken — The One That Actually Worked
  • Challenge 5: hard — GPU Goes Brrr

Challenge 1: calc — Difficulty: Tutorial Island

The Setup

We’re greeted with an encrypted flag that looks like someone fell asleep on their keyboard:

enc flag
_nHmruvO++Z]ESXOSP\SE\PDY ^Jm

And a C program that’s basically saying: “Hey, I’m going to do some math to your input, and if it equals this garbage string, you win!”

The Code (Simplified)


__int64 __fastcall main(int a1, char **a2, char **a3)
{
  int i; // [rsp+1Ch] [rbp-14h]
  int j; // [rsp+20h] [rbp-10h]
  int k; // [rsp+24h] [rbp-Ch]
  char *s1; // [rsp+28h] [rbp-8h]

  s1 = a2[1];
  for ( i = 0; i <= 5; ++i )
    s1[i] += 5;
  for ( j = 6; j <= 10; ++j )
    s1[j] -= 5;
  for ( k = 11; k <= 28; ++k )
    s1[k] ^= 0x10u;
  if ( !strcmp(s1, "_nHmruvO++Z]ESXOSP\\SE\\PDY ^Jm") )
    printf("Correct Flag");
  else
    puts("Wrong Flag!!");
  return 0LL;
}
ZiChmp{T00_MUCH_C@LCUL@TI0NZ}

You know what’s great about simple math? It’s reversible!

If they added 5, we subtract 5. If they subtracted 5, we add 5. If they XORed with 0x10… we XOR with 0x10 again (because XOR is its own inverse, and that’s the only time I’ll sound smart in this writeup).

The Solution

cipher = list("_nHmruvO++Z]ESXOSP\\SE\\PDY ^Jm")

cipher = list("_nHmruvO++Z]ESXOSP\\SE\\PDY ^Jm")

# reverse s1[i] += 5  → subtract 5
for i in range(0, 6):
    cipher[i] = chr(ord(cipher[i]) - 5)

# reverse s1[j] -= 5  → add 5
for j in range(6, 11):
    cipher[j] = chr(ord(cipher[j]) + 5)

# reverse s1[k] ^= 0x10 → XOR again
for k in range(11, 29):
    cipher[k] = chr(ord(cipher[k]) ^ 0x10)

flag = "".join(cipher)
print(flag)

Flag: ZiChmp{T00_MUCH_C@LCUL@TI0NZ}

It was easy in a strange way

💔 Challenge 2: IloveC — Difficulty: Stockholm Syndrome

The Plot Twist

This challenge uses qsort() as an encryption mechanism. Yes, you read that right. Someone looked at a sorting function and thought: "You know what this needs? MALICIOUS INTENT."

The Evil Genius Move

The comparison function doesn’t just compare — it modifies the input buffer as a side effect. Every time qsort() compares two bytes, it XORs the input with another byte.

This is like if your calculator also did your taxes while you were trying to multiply 2×2.

The Solution (Sort Of)

I wrote a Python script using ctypes to literally call the C library and emulate the exact sorting behavior:

def compar(a1, a2):
    if dword_4088.value <= 30:
        input_buf[dword_4088.value] ^= a1[dword_4088.value]
        dword_4088.value += 1
    return (a1[0] % 7) - (a2[0] % 7)
cmp_cb = CMPFUNC(compar)
libc.qsort(s, n, 1, cmp_cb)
```python
zeroaccess@amr /m/d/C/c/r/ilovec [0|1]> python
Python 3.13.9 (main, Oct 15 2025, 14:56:22) [GCC 15.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> #!/usr/bin/env python3
... import ctypes
...
... libc = ctypes.CDLL("libc.so.6")
...
... # original string from binary
... s_bytes = b"KXXKX3XJXXXXXXNXXXXXBXXXXXAXXYY"
... n = len(s_bytes)
...
... # convert s to ctypes array
... s = (ctypes.c_ubyte * n)(*s_bytes)
...
... # byte_4060 extracted from IDA (yours)
... byte_4060 = (ctypes.c_ubyte * n)(*[
...     0x02, 0x31, 0x1B, 0x30, 0x35, 0x28, 0x23, 0x1A,
...     0x6B, 0x07, 0x0F, 0x18, 0x1C, 0x6B, 0x07, 0x68,
...     0x1E, 0x07, 0x0A, 0x31, 0x44, 0x33, 0x5F, 0x6A,
...     0x46, 0x46, 0x33, 0x43, 0x0C, 0x0A, 0x7D
... ])
...
... # argv[1] buffer (start from byte_4060, reverse XOR into it)
... input_buf = (ctypes.c_ubyte * n)(*byte_4060)
...
... dword_4088 = ctypes.c_int(0)
...
... CMPFUNC = ctypes.CFUNCTYPE(
...     ctypes.c_int,
...     ctypes.POINTER(ctypes.c_ubyte),
...     ctypes.POINTER(ctypes.c_ubyte),
... )
...
... def compar(a1, a2):
...     if dword_4088.value <= 30:
...         input_buf[dword_4088.value] ^= a1[dword_4088.value]
...         dword_4088.value += 1
...     return (a1[0] % 7) - (a2[0] % 7)
...
... cmp_cb = CMPFUNC(compar)
...
... libc.qsort(s, n, 1, cmp_cb)
...
... flag = bytes(input_buf)
... print(flag.decode())
...
ZiChmp{B3_W@R3_0F_S0D2_3FF3CTS}
>>> fish: Job 1, 'python' has stopped

**Output:** `ZiChmp{B3_W@R3_0F_S0D2_3FF3CTS}`

# The Problem

This was **wrong**.

Specifically, characters 20 and 22 were incorrect. It should say “SIDE EFFECTS” not “SOD2 EFFECTS” (whatever that means).

# The Fix

I tried: `ZiChmp{B3_W@R3_0F_S1D3_3FF3CTS}`

$ ./ilovec ZiChmp{B3_W@R3_0F_S1D3_3FF3CTS} Flag is correct.


**ARE YOU KIDDING ME?**

I literally **guessed** two characters because my qsort emulation wasn’t perfect.

**Flag:** `ZiChmp{B3_W@R3_0F_S1D3_3FF3CTS}`

Moderate. My ego is bruised.

**Lesson Learned:** “Guessing is not funny” — *Me, in my notes, somehow predicting my own fate*

# Challenge 3: tangled — Difficulty: Spaghetti Code Nightmare

# First Impressions

The binary name is “tangled” and boy, they weren’t lying. This thing has more layers than an onion. An onion that also runs Lua scripts. **Inside a C binary.**

Because why make things simple when you can make them *incomprehensible*?

# The Encryption Layers (In Order of Pain)
1. **Initial XOR** with key `[0x5A, 0x69, 0x43, 0x68, 0x6D, 0x70, 0x7B, 0x7D]` (which spells "ZiChmp{}", very subtle)
2. **Bit manipulation** with rotation (ROL1) and magic constants
3. **Lua transformation** that includes:
- XOR with position-dependent keys
- Nibble swapping (because regular swapping is too mainstream)
- Conditional arithmetic based on position modulo 3

# The Solution Strategy

Reverse. Everything. In. Reverse. Order.

Step 1: Undo Lua nonsense

for i in range(34): byte = expected[i] idx = (i + 1) % 8

# Reverse position XOR
pos_key = ((i + 1) * 7 + 13) & 0xFF
byte ^= pos_key

# Reverse nibble swap
byte = ((byte & 0xF0) >> 4) | ((byte & 0x0F) << 4)

# Reverse conditional math (the fun part)
if (i + 1) % 3 == 0:
    byte = (byte - phase2[idx]) & 0xFF
elif (i + 1) % 3 == 1:
    byte = (byte + phase2[(idx + 4) % 8]) & 0xFF
else:
    byte ^= phase2[(idx + 2) % 8]

byte ^= phase1[idx]
before_lua.append(byte)

Step 2: Undo bit manipulation with SIMD constants

(Yes, they used SIMD instructions. For a flag check.)

for i in range(34): byte = before_lua[i]

if i < 16:
    byte ^= xmmword_27E0[i]
    byte = (byte - xmmword_27D0[i]) & 0xFF
    v2_byte = ((byte >> 2) | ((byte & 0x03) << 6)) & 0xFF
# ... more bit twiddling ...

Step 3: Undo initial XOR

flag = [chr(v2[0] ^ 0x5A)] for i in range(1, 34): flag.append(chr(v2[i] ^ xor_key[i & 7]))


**Full solver**

def solve(): expected = [ 0xB3, 0xCB, 0x4F, 0x80, 0x79, 0xE4, 0x7B, 0xAF, 0x47, 0x45, 0x4B, 0xDA, 0xC4, 0x6F, 0xFA, 0x55, 0x24, 0x1B, 0x71, 0xEB, 0x25, 0x7C, 0x85, 0xCE, 0xD2, 0x5D, 0x20, 0x3A, 0x10, 0x00, 0xE7, 0x07, 0x71, 0x2D ]

# Constants
xmmword_27D0 = [0x07, 0x0A, 0x0D, 0x10, 0x13, 0x16, 0x19, 0x1C, 0x1F, 0x22, 0x25, 0x28, 0x2B, 0x2E, 0x31, 0x34]
xmmword_27E0 = [0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55]
xmmword_27F0 = [0x37, 0x3A, 0x3D, 0x40, 0x43, 0x46, 0x49, 0x4C, 0x4F, 0x52, 0x55, 0x58, 0x5B, 0x5E, 0x61, 0x64]

# Reverse Lua transformation
phase1 = [0x13, 0x37, 0x42, 0x69, 0xDE, 0xAD, 0xBE, 0xEF]
phase2 = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80]

before_lua = []
for i in range(34):
    byte = expected[i]
    idx = (i + 1) % 8

    pos_key = ((i + 1) * 7 + 13) & 0xFF
    byte ^= pos_key

    byte = ((byte & 0xF0) >> 4) | ((byte & 0x0F) << 4)

    if (i + 1) % 3 == 0:
        byte = (byte - phase2[idx]) & 0xFF
    elif (i + 1) % 3 == 1:
        byte = (byte + phase2[(idx + 4) % 8]) & 0xFF
    else:
        byte ^= phase2[(idx + 2) % 8]

    byte ^= phase1[idx]
    before_lua.append(byte)

print(f"After Lua reverse: {[hex(x) for x in before_lua[:10]]}")

# Reverse bit manipulation with correct constants
v2 = []

for i in range(34):
    byte = before_lua[i]

    if i < 16:
        # *v3 = _mm_xor_si128(v10, si128) where si128 = xmmword_27E0
        byte ^= xmmword_27E0[i]
        # v10 = _mm_add_epi8(..., xmmword_27D0)
        byte = (byte - xmmword_27D0[i]) & 0xFF
        # Reverse: ((v >> 6) & 0x03) | (v << 2)
        v2_byte = ((byte >> 2) | ((byte & 0x03) << 6)) & 0xFF

    elif i < 32:
        byte ^= xmmword_27E0[i - 16]
        byte = (byte - xmmword_27F0[i - 16]) & 0xFF
        v2_byte = ((byte >> 2) | ((byte & 0x03) << 6)) & 0xFF

    elif i == 32:
        # v3[32] = (-104 - ROL1(v2[32], 2)) ^ 0x55
        byte ^= 0x55
        rol_result = (-104 - byte) & 0xFF
        v2_byte = ((rol_result >> 2) | (rol_result << 6)) & 0xFF

    else:  # i == 33
        # v3[33] = (ROL1(v2[33], 2) + 106) ^ 0x55
        byte ^= 0x55
        rol_result = (byte - 106) & 0xFF
        v2_byte = ((rol_result >> 2) | (rol_result << 6)) & 0xFF

    v2.append(v2_byte)

print(f"v2 values: {[hex(x) for x in v2[:10]]}")

# Reverse initial XOR
xor_key = [0x5A, 0x69, 0x43, 0x68, 0x6D, 0x70, 0x7B, 0x7D]

flag = [chr(v2[0] ^ 0x5A)]
for i in range(1, 34):
    flag.append(chr(v2[i] ^ xor_key[i & 7]))

result = ''.join(flag)
print(f"\nFlag: {result}")
return result

solve()


**Flag:** `ZiChmp{I_L0VE_LU@_SCRIPTING_AND_C}`

After Lua reverse: ['0xad', '0x5f', '0xa7', '0x45', '0xb9', '0x43', '0xb3', '0xb9', '0x99', '0xe3'] v2 values: ['0x0', '0x0', '0x0', '0x0', '0x0', '0x0', '0x0', '0x34', '0x5', '0x25']

Flag: ZiChmp{I_L0VE_LU@_SCRIPTING_AND_C}


**Emotional Damage:** Severe. I now have trust issues with the letter ‘C’.

**Things I Learned:**
- Lua can run inside C binaries
- I hate Lua running inside C binaries
- Nibble swapping is never necessary
- I should have studied harder in assembly class

# Challenge 4: broken — Difficulty: Wholesome

# A Pleasant Surprise

After the Lua nightmare, I needed a win. And “broken” delivered.

The binary is called “broken” but it’s actually the only challenge that **ISN’T** broken. It’s just… *deceptive*.

# The Analysis

$ strings broken [*] Something went wrong!! \x%x


That’s it. Two strings. This is either:
- A trap
- Really simple
- Both

I will take the necessary steps

$ file broken broken: ELF 64-bit LSB pie executable, x86-64, dynamically linked, stripped


# The Code

if (strlen(asc_2004) != 22) { printf("[*] Something went wrong!!"); exit(0); }

for (i = 0; i < strlen(asc_2004); ++i) { asc_2004[i] ^= 0x41u; printf("\x%x", (unsigned int)asc_2004[i]); }

printf("\n%s\n", asc_2004);


# Translation

“We have a 22-byte string. We XOR each byte with 0x41 (the letter ‘A’). That’s it. That’s the challenge.”

# The Encrypted Bytes

data = [ 0x1B, 0x28, 0x02, 0x29, 0x20, 0x2C, 0x31, 0x3A, 0x19, 0x71, 0x13, 0x08, 0x0F, 0x06, 0x1E, 0x08, 0x1B, 0x1E, 0x02, 0x71, 0x71, 0x0D, 0x3C ]


# Decompiled Main

int64 fastcall main(int a1, char a2, char a3) { int i;

if ( strlen(asc_2004) != 22 ) { printf("[*] Something went wrong!!"); exit(0); }

for ( i = 0; i < strlen(asc_2004); ++i ) { asc_2004[i] ^= 0x41u; printf("\x%x", (unsigned int)asc_2004[i]); }

printf("\n%s\n", asc_2004); return 0; }


# The Solution

data = [ 0x1B, 0x28, 0x02, 0x29, 0x20, 0x2C, 0x31, 0x3A, 0x19, 0x71, 0x13, 0x08, 0x0F, 0x06, 0x1E, 0x08, 0x1B, 0x1E, 0x02, 0x71, 0x71, 0x0D, 0x3C]

flag = ''.join(chr(b ^ 0x41) for b in data) print(flag)

Flag: ZiChamp{X0RING_IZ_C00L}


Actually this was easy one

# Challenge 5: hard — Difficulty

# The Marketing

Challenge name: “hard”

Me: “Oh no.”

Reality: **Literally the easiest challenge.**

# The Twist

This challenge uses **OpenCL** to verify the flag. On the GPU.

Why? Because apparently checking 29 characters on a CPU is too mainstream.

# The Flow
1. Program loads `kernel.enc` (encrypted OpenCL kernel)
2. Decrypts it at runtime
3. Sends your flag to the GPU
4. GPU does… something
5. Result comes back as `int[29]`
6. Compared against hardcoded expected array

# The Kernel Logic (After Decryption)

result[i] = flag[i] ^ key[i % 6];


That’s it. The GPU is doing a **repeating XOR cipher**.

They literally sent data to the GPU just to XOR it with a 6-byte key.

# The Key

key = [0x5A, 0x69, 0x43, 0x68, 0x6D, 0x70] # "ZiChmp"


Of course it spells the CTF name.

# The Expected Output

expected = [ 0,0,0,0,0,0, # "ZiChmp{" 33,48,115,61,50,55, # "Y0U_G0" 106,61,23,40,50,60, # "TT@_L0" 106,63,6,55,93,32, # "VE_0P3" 105,39,0,36,16 # "NCL}" ]


# The Solution

flag = ''.join(chr(expected[i] ^ key[i % 6]) for i in range(29)) print(flag)



**Flag:** `ZiChmp{Y0U_G0TT@_L0VE_0P3NCL}`

None. Pure comedy.

I spend about 30 minutes (mostly getting rid of the Kernel)

**Actual Difficulty:** Tutorial level

**Perceived Difficulty Based on Name:** “HARD”

**Reality:** They used a GPU to XOR 29 bytes

# Why This Is Peak Comedy
- Uses OpenCL (enterprise-grade parallel computing framework)
- Encrypts the kernel at rest
- Sends data to GPU
- **Does a 6-byte repeating XOR**
- Could have been done with: `flag[i] ^ "ZiChmp"[i % 6]`

This is like using a flamethrower to light a birthday candle.

# Lessons Learned
1. **XOR is life** — It appeared in every single challenge
2. **qsort() can be weaponized** — Never trust library functions again
3. **“hard” is relative** — Sometimes it means “uses GPU”, sometimes it means “actually easy”
4. **Guessing is funny** — Despite what past me wrote in the notes
5. **Lua in C is cursed** — This should be illegal
6. **Sometimes the broken thing is you** — *looking at you, challenge 2*

# Acknowledgments
- **ZiChmp CTF organizers** for creating challenges that hurt in creative ways
- **IDA Pro** for showing me assembly I didn’t want to see
- **Python** for making reversing bearable
- **My qsort() emulation** for trying its best (even if it failed)
- **OpenCL** for being absolute overkill
- **Caffeine** for keeping me conscious

*Thanks for reading! If you enjoyed this writeup, you can find more in my portfolio*

![](https://miro.medium.com/v2/resize:fit:734/1*zgIRS-5zVyA6ZnibWW_lUw.png)

or visit my

[Amr-Khaled-Ahmed (Amr El-Dahshan )](https://github.com/Amr-Khaled-Ahmed)

*Remember: In CTF, we don’t make mistakes, we just create “unexpected features” in our solve scripts.*

**~ zeroaccess / amr**

*“I blame time” — me, probably*

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