08/02/2026
ZinadIT Cyber Champions CTF 2026 | Reverse Engineering
CTF Writeup: Zinad Cyber Champions - Reverse Engineering Challenge
بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

Easy Level
Challenge 01: Calc
The main idea of this binary is to take a string input from the user and respond with Wrong or correct flag!
Lets try to reverse the binary and see how it works, I will start with Ghidra as static analysis
Browse to the entry point of the application to get the main function of the application

As we see the FUN_00101159 is the main function, double click to see it.

As we see here's the main logic of the binary, if you struggle with reading the function, you can use AI to make it more human readable like this:
1uint64_t check_flag(uint64_t unused_arg, long user_input_struct)
2{
3 char *flag_buffer;
4 int cmp_result;
5
6 int i;
7 int j;
8 int k;
9
10 flag_buffer = *(char **)(user_input_struct + 8);
11
12 for (i = 0; i < 6; i++) {
13 flag_buffer[i] += 5;
14 }
15
16 for (j = 6; j < 11; j++) {
17 flag_buffer[j] -= 5;
18 }
19
20 for (k = 11; k < 29; k++) {
21 flag_buffer[k] ^= 0x10;
22 }
23
24 cmp_result = strcmp(flag_buffer, "_nHmruvO++Z]ESXOSP\\SE\\PDY ^Jm");
25
26 if (cmp_result == 0) {
27 printf("Correct Flag");
28 } else {
29 puts("Wrong Flag!!");
30 }
31
32 return 0;
33}
34The binary take the input from the user, apply three transformations to the input string
- Add 5 to the first 6 characters
- Subtract 5 from characters 6 to 11
- XOR characters 11 to 29 with 0x10
And then compare it with an expected value to determine if the flag is correct or not. which means that this string is the corrected flag
To get the flag, take this string and reverse the transformation applied in this binary by this code and thats it
1#!/usr/bin/env python3
2
3target = input("Enter the target string: ")
4
5
6flag = []
7
8print("\nReversing positions 0-5 (subtract 5):")
9for i in range(6):
10 original_char = chr(ord(target[i]) - 5)
11 flag.append(original_char)
12
13print("Reversing positions 6-10 (add 5):")
14for i in range(6, 11):
15 original_char = chr(ord(target[i]) + 5)
16 flag.append(original_char)
17
18print("Reversing positions 11-28 (XOR with 0x10):")
19for i in range(11, len(target)):
20 original_char = chr(ord(target[i]) ^ 0x10)
21 flag.append(original_char)
22
23
24flag_string = ''.join(flag)
25print("\n" + "="*60)
26print(f"FLAG: {flag_string}")
27print("="*60)
28
Challenge 02: Broken
Challenge is about the concept of broken binary which make us can't get the flag and always return [*] Something went wrong!!
Firstly, I tried to search in the strings of the binary about any error message, suspecious strings or any hints, but i got nothing, so i repeated the previous steps and opened the binary on ghidra and analyse it

Same as previous, lets take it to AI to make it human readable
uint64_t decode_and_print_secret(void)
{
size_t encoded_length;
int index;
const char *encoded_reference =
"\x1b(\x02) ,1:\x19q\x13\b\x0f\x06\x1e\b\x1b\x1e\x02qq\r<";
char *encoded_buffer = (char *)&DAT_00102004;
encoded_length = strlen(encoded_reference);
if (encoded_length != 22) {
printf("[*] Something went wrong!!");
exit(0);
}
for (index = 0; index < encoded_length; index++) {
encoded_buffer[index] ^= 0x41;
printf("\x%x", (unsigned char)encoded_buffer[index]);
}
printf("\n%s\n", encoded_buffer);
return 0;
}The main idea of the binary, validate the length of a specific encoded string and if it's valid, the program will decode it with XOR operation using 0x41 as the key.
Now we can see the bug that cause the problem, is the program always check if the string's length equal to 22 Bytes (which is not), so it always prints the error message and exits.
To Fix the problem we can do in different ways, we can edit on the check instruction and batch the file, or simply take the string as it and decode it manually
Manual Decoding Approach
To decode the string, we can use a simple script that decode each byte using the XOR key 0x41.
target = "\x1b(\x02) ,1:\x19q\x13\b\x0f\x06\x1e\b\x1b\x1e\x02qq\r<"
flag = []
for i in range(len(target)):
original_char = chr(ord(target[i]) ^ 0x41)
flag.append(original_char)
flag_string = ''.join(flag)
print("
" + "="*60)
print(f"FLAG: {flag_string}")
print("="*60)
Medium/Hard Level
Challenge 01: Ilovec
And this challenge was very tricky due to multiple steps and layers - dynamic analysis and more, so please focus with me
Firstly, i executed the binary ./ilovec "test" to see the output
Output:
Wrong!!Wrong!!Wrong!!Wrong!!Wrong!!Wrong!!Wrong!!Wrong!!
Wrong!!Wrong!!Wrong!!Wrong!!Wrong!!Wrong!!Wrong!!Wrong!!
Wrong!!Wrong!!Wrong!!Wrong!!Wrong!!Wrong!!Wrong!!Wrong!!
Wrong!!Wrong!!Wrong!!Wrong!!Wrong!!Wrong!!Wrong!!Flag is correct.Lets start with the static analysis using Ghidra

After Refinement:
1uint64_t validate_flag(uint64_t unused_arg, long argv_ptr)
2{
3 size_t pattern_length;
4 int i;
5
6 char pattern_buffer[40];
7 char *user_input;
8
9 // Stack canary
10 long stack_cookie = *(long *)(in_FS_OFFSET + 0x28);
11
12 // argv[1] → user input
13 user_input = *(char **)(argv_ptr + 8);
14 DAT_00104090 = (long)user_input; // Used by custom qsort comparator
15
16 // Initialize pattern
17 strncpy(
18 pattern_buffer,
19 "KXXKX3XJXXXXXXNXXXXXBXXXXXAXXYY",
20 0x20
21 );
22
23 pattern_length = strlen(pattern_buffer);
24 qsort(
25 pattern_buffer,
26 pattern_length,
27 sizeof(char),
28 FUN_00101179
29 );
30
31 for (i = 0; i < 31; i++) {
32 if (user_input[i] != (&DAT_00104060)[i]) {
33 printf("Wrong!!");
34 }
35 }
36
37 puts("Flag is correct.");
38
39 // Stack integrity check
40 if (stack_cookie != *(long *)(in_FS_OFFSET + 0x28)) {
41 __stack_chk_fail();
42 }
43
44 return 0;
45}
46
47I initially understood that the binary take the input from the user `stored in DAT_00104090` and pass it to qsort function with custom comparator and then compare the result with the expected pattern `stored in DAT_00104060` to validate the flag.
I got the value of the expected pattern from the Bytes window (May be the pattern is the flag) by copying the 31 bytes starts at 0x00104060

We got 02311b303528231a6b070f181c6b07681e070a3144335f6a464633430c0a7d , which may be the flag in an encrypted form and we need to decrypt it
Now Lets analyze the Comparison function FUN_00101179

After Refinement:
int custom_char_comparator(unsigned char *a, unsigned char *b)
{
// Side-effect counter (global)
if (xor_index < 31) {
// XOR user input with pattern character
user_input[xor_index] ^= a[xor_index];
xor_index++;
}
// Comparator logic:
// sort by (char % 7)
return ((a[0] % 7) - (b[0] % 7));
}I asked the AI to understand the function and from the previous comparison logic, we got that it's not a traditional comparison, the function edit on the user input with XOR operations each time (Side Effect) and the comparison logic not fixed (it depends on the value of the characters modulo 7) which is not constant.
So, we need a dynamic analysis to fully understand the behavior of the comparator and know how it edits on the user input
Asked AI and it told me to use something called LD_PRELOAD that replace the comparison function and use another one (Same behavior but with logging) so we can see which position will be XORed and with which value
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#include <stddef.h>
static void (*real_qsort)(
void*, size_t, size_t,
int (*)(const void*, const void*)
);
static void *array_base;
static size_t call_num = 0;
static int xor_index = 0;
int wrapper_compare(const void *a, const void *b)
{
unsigned char xor_byte = 0;
if (xor_index < 31) {
xor_byte = *((unsigned char *)a + xor_index);
fprintf(stderr,
"XOR %02d: using a[%02d] = 0x%02x\n",
xor_index, xor_index, xor_byte
);
xor_index++;
}
return (*(unsigned char*)a % 7) - (*(unsigned char*)b % 7);
}
void qsort(
void *base,
size_t n,
size_t size,
int (*cmp)(const void*, const void*)
)
{
real_qsort = dlsym(RTLD_NEXT, "qsort");
array_base = base;
call_num = 0;
xor_index = 0;
real_qsort(base, n, size, wrapper_compare);
}gcc -shared -fPIC -o trace.so trace.c -ldl
LD_PRELOAD=./trace.so ./ilovec AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOutput:

So, what i understood that the binary will take the input and apply the XOR pattern on it and then compare with a stored string/expected value, which means
user_input ^ xor_pattern = expected_value --- (user_input is the correct flag) and (expected_value is the encoded form of it)
We have the XOR pattern and the expected value, so we can XOR them to get the user input (flag).
1xor_pattern = [
2 0x58, 0x58, 0x58, 0x58, 0x58,
3 0x58, 0x58, 0x58, 0x58, 0x58,
4 0x58, 0x58, 0x4e, 0x58, 0x58,
5 0x58, 0x58, 0x58, 0x59, 0x00,
6 0x00, 0x00, 0x00, 0x59, 0x00,
7 0x00, 0x00, 0x00, 0x58, 0x59,
8]
9
10# Expected bytes (from the binary at address 0x104060)
11expected = bytes.fromhex("02311b303528231a6b070f181c6b07681e070a3144335f6a464633430c0a7d")
12
13
14flag = ""
15for i in range(30):
16 flag += chr(xor_pattern[i] ^ expected[i])
17flag += chr(expected[30]) # Last byte not XORed
18
19print("\n"+"="*40)
20print(f"Flag: {flag}")
21print("="*40)
22
23
That's it for this writeup, hope you enjoyed it and learned something new. See you in the next one!