08/02/2026

ZinadIT Cyber Champions CTF 2026 | Reverse Engineering

CTF Writeup: Zinad Cyber Champions - Reverse Engineering Challenge

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

Zinad Cyber champions CTF | Reverse Engineering | Easy level

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

Home page

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

Home page

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:

check_flag.c
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} 34

The 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

reverse_flag.py
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
Home page

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

Home page

Same as previous, lets take it to AI to make it human readable

c
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.

python
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)
Home page

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:

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

Home page

After Refinement:

validate_flag.c
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 47

I 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

String bytes

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

Comparison function

After Refinement:

c
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

c
#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); }
bash
gcc -shared -fPIC -o trace.so trace.c -ldl LD_PRELOAD=./trace.so ./ilovec AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Output:

Comparison function

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).

decrypt_flag.py
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
Comparison function

That's it for this writeup, hope you enjoyed it and learned something new. See you in the next one!