TheLeopard65
Published on

Nullcon GOA CTF 2025 Challenges Writeup

AUTHORS
  • avatar
    NAME
    Yasir Mehmood
    TWITTER

REVERSE ENGINEERING


1. Flag-Checker


flag-checker-challenge.png

  1. Running the ELF prompted us to enter a flag.
  2. If we entered any random string it simply said Incorrect!

flag-checker-info.png

  1. I opened up the ELF in Ghidra and asked for it to analyze it.
  2. I analyzed all the stripped Functions. and found the the 3 necessary functions; the rest being useless.
  3. I analyzed the code and renamed essentials variables for understanding (also renamed function names).
  4. Here is the cleaned-up code for the 3 functions: main, flag-checker, flag-checker-2.


void main(int argc,char **argv){
  int check_value;
  size_t input_len;
  long canary_offset;
  char flag_buffer [40];
  long stack_canary;
  stack_canary = *(long *)(canary_offset + 0x28);
  printf("Enter the flag: ");
  fgets(flag_buffer,35,stdin);
  input_len = strcspn(flag_buffer,"\n");
  flag_buffer[input_len] = '\0';
  flag_checker(flag_buffer);
  if (check_value == 0) {
    puts("Incorrect!");
  }
  else {
    puts("Correct!");
  }
  if (stack_canary != *(long *)(canary_offset + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}


void flag_checker(char *flag_input){
  size_t length-of-flag;
  long SF;
  int i;
  char buffer [40];
  long SC;
  SC = *(long *)(SF + 0x28);
  length-of-flag = strlen(flag_input);
  if (length-of-flag == 34) {
    flag-checker-2(flag_input,buffer);
    for (i = 0; (i < 34 && (buffer[i] == (&DAT_00102020)[i])); i = i + 1) {
    }
  }
  if (SC == *(long *)(SF + 0x28)) {
    return;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}


void flag-checker-2(char *flag_input,char *buffer){
  int i;
  for (i = 0; i < 34; i = i + 1) {
    buffer[i] = (flag_input[i] ^ 90U) + (char)i;
    buffer[i] = buffer[i] << 3 | (byte)buffer[i] >> 5;
  }
  return;
}

  1. But even after analyzing all these things. i couldn’t get the flag because the DAT_00102020 is not disassembled.
  2. So i opened the file in Radare2 and started analyzing it. Here is the analyzing breakdown/summary:

(kali@LEOPARD-PC)-[~/CTF-Events/Nullcon-CTF-2025/REV]$ r2 -A flag-checker                // Analyze entire file
...
[0x00001100]> afl                        // List all Functions
...
0x00001100    1     37 entry0
0x00001318    6    173 main
0x00001090    1     11 fcn.00001090
0x00001130    4     34 fcn.00001130
0x000011e9    4    145 fcn.000011e9
0x0000127a   11    158 fcn.0000127a
...
[0x00001100]> s main                  //  Seek Main
[0x00001318]> pdf                     // Print disassembly of this function
...
0x0000137a      488d45d0       lea rax, [s1]
0x0000137e      4889c7         mov rdi, rax                ; char *arg1
0x00001381      e8f4feffff     call fcn.0000127a
0x00001386      85c0           test eax, eax
│       ┌─< 0x00001388      7411           je 0x139b
│       │   0x0000138a      488d05c40c..   lea rax, str.Correct_       ; 0x2055 ; "Correct!"
...
[0x00001318]> s fcn.0000127a        // Seek this function
[0x0000127a]> pdf                   // Print disassembly of this function
...
│     ╎││   0x000012dd      488d0d3c0d..   lea rcx, [0x00002020]
│     ╎││   0x000012e4      0fb60408       movzx eax, byte [rax + rcx]
│     ╎││   0x000012e8      38c2           cmp dl, al
│    ┌────< 0x000012ea      7407           je 0x12f3
│    │╎││   0x000012ec      b800000000     mov eax, 0
│   ┌─────< 0x000012f1      eb0f           jmp 0x1302
│   ││╎││   ; CODE XREF from fcn.0000127a @ 0x12ea(x)
...
[0x0000127a]> px 34 @0x12f3         // Print 34 bytes containing the flag in encrypted form.
- offset -  F3F4 F5F6 F7F8 F9FA FBFC FDFE FF 0  1 2  3456789ABCDEF012
0x000012f3  8345 cc01 837d cc21 7ed1 b801 0000 0048  .E...}.!~......H
0x00001303  8b55 f864 482b 1425 2800 0000 7405 e8aa  .U.dH+.%(...t...
0x00001313  fdff                                     ..
[0x0000127a]>

  1. Now we have the flag but it is in encrypted form. So quicky wrote this script to decode the flag:

def reverse_flag_checker(buffer):
    flag_input = bytearray(34)
    for i in range(34):
        temp = buffer[i]
        temp = (temp >> 3) | (temp << 5) & 0xFF
        flag_input[i] = (temp - i) ^ 0x5A
    return bytes(flag_input)

buffer_hex = "f8a8b8216073908380c39b80ab0959d321d3dbd8fb4999e0793c4c492c29ccd4dc42"
buffer = bytes.fromhex(buffer_hex)
original_flag = reverse_flag_checker(buffer)
print(original_flag.decode())

  1. Executing this python3 script gave me the flag.
FLAG: ENO{R3V3R53_3NG1N33R1NG_M45T3R!!!}

2. Scrambled


scrambled-challenge.png

  1. The two files that they provided us with were:
# main.py
import random

def encode_flag(flag, key):
    xor_result = [ord(c) ^ key for c in flag]
    chunk_size = 4
    chunks = [xor_result[i:i+chunk_size] for i in range(0, len(xor_result), chunk_size)]
    seed = random.randint(0, 10)
    random.seed(seed)
    random.shuffle(chunks)
    scrambled_result = [item for chunk in chunks for item in chunk]
    return scrambled_result, chunks

def main():
    flag = "REDACTED"
    key = REDACTED
    scrambled_result, _ = encode_flag(flag, key)
    print("result:", "".join([format(i, '02x') for i in scrambled_result]))

if __name__ == "__main__":
    main()

result: 1e78197567121966196e757e1f69781e1e1f7e736d6d1f75196e75191b646e196f6465510b0b0b57

  1. I wrote a script to brute force the key and seed with the given result. After analyzing all 2560 results found that the key was 42 and seed was 10. Here is the script code:
import random

def reverse_scramble(scrambled_result, seed, chunk_size=4):
    random.seed(seed)
    num_chunks = len(scrambled_result)
    chunks = [scrambled_result[i:i + chunk_size] for i in range(0, len(scrambled_result), chunk_size)]
    indices = list(range(num_chunks))
    random.shuffle(indices)
    unshuffled_chunks = [None] * num_chunks
    for i, idx in enumerate(indices): unshuffled_chunks[idx] = chunks[i]
    unscrambled_result = [item for chunk in unshuffled_chunks for item in chunk]
    return unscrambled_result

def decode_flag(scrambled_result, key, seed):
    unscrambled_result = reverse_scramble(scrambled_result, seed)
    decoded_result = [chr(byte ^ key) for byte in unscrambled_result]
    return ''.join(decoded_result)

scrambled_hex = "1e78197567121966196e757e1f69781e1e1f7e736d6d1f75196e75191b646e196f6465510b0b0b57"
scrambled_result = [int(scrambled_hex[i:i+2], 16) for i in range(0, len(scrambled_hex), 2)]
key = 42
seed = 10
# for key in range(256):
	# for seed in range (11):
flag = decode_flag(scrambled_result, key, seed)
print("Decoded flag:", flag)
  1. Executing the Script gave me the flag:
FLAG: ENO{5CR4M83L3D_3GG5_4R3_1ND33D_T45TY!!!}

MISC


1. USBnet


usbnet-challenge.png

  1. I download the usbnet.pcapng file and directly uploaded it to CyberChef .
  2. and Imported the Extract Files Module from the list.
  3. CyberChef found a PNG file embed inside of a packet.

usbnet-cyberchef.png

  1. Downloaded and Opened up the file. It was a QR Code.

usbnet-qrcode.png

  1. Scanning the QR Code gave me the Flag.
FLAG: ENO{USB_ETHERNET_ADAPTER_ARE_COOL_N!C3}

WEB


1. Paginator


paginator-challenge.png

  1. Opening the link showed a simple interface, representing pages of the book.
  2. It showed all the pages from 2 to 10 except 1.

paginator-demo.png

  1. The source code was just simple PHP connecting SQL Database to the frontend.
  2. It check if the min is less then or equal to 1 or max is greater then 10.

<?php
ini_set("error_reporting", 0);
ini_set("display_errors",0);
if(isset($_GET['source'])) {
    highlight_file(__FILE__);
}
include "flag.php";
$db = new SQLite3('/tmp/db.db');
try {
  $db->exec("CREATE TABLE pages (id INTEGER PRIMARY KEY, title TEXT UNIQUE, content TEXT)");
  $db->exec("INSERT INTO pages (title, content) VALUES ('Flag', '" . base64_encode($FLAG) . "')");
  $db->exec("INSERT INTO pages (title, content) VALUES ('Page 1', 'This is not a flag, but just a boring page.')");
  $db->exec("INSERT INTO pages (title, content) VALUES ('Page 2', 'This is not a flag, but just a boring page.')");
  $db->exec("INSERT INTO pages (title, content) VALUES ('Page 3', 'This is not a flag, but just a boring page.')");
  $db->exec("INSERT INTO pages (title, content) VALUES ('Page 4', 'This is not a flag, but just a boring page.')");
  $db->exec("INSERT INTO pages (title, content) VALUES ('Page 5', 'This is not a flag, but just a boring page.')");
  $db->exec("INSERT INTO pages (title, content) VALUES ('Page 6', 'This is not a flag, but just a boring page.')");
  $db->exec("INSERT INTO pages (title, content) VALUES ('Page 7', 'This is not a flag, but just a boring page.')");
  $db->exec("INSERT INTO pages (title, content) VALUES ('Page 8', 'This is not a flag, but just a boring page.')");
  $db->exec("INSERT INTO pages (title, content) VALUES ('Page 9', 'This is not a flag, but just a boring page.')");
  $db->exec("INSERT INTO pages (title, content) VALUES ('Page 10', 'This is not a flag, but just a boring page.')");
} catch(Exception $e) {
  //var_dump($e);
}

if(isset($_GET['p']) && str_contains($_GET['p'], ",")) {
  [$min, $max] = explode(",",$_GET['p']);
  if(intval($min) <= 1 ) {
    die("This post is not accessible...");
  }
  try {
    $q = "SELECT * FROM pages WHERE id >= $min AND id <= $max";
    $result = $db->query($q);
    while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
      echo $row['title'] . " (ID=". $row['id'] . ") has content: \"" . $row['content'] . "\"<br>";
    }
  }catch(Exception $e) {
    echo "Try harder!";
  }
} else {
    echo "Try harder!";
}
?>

<html>
    <head>
        <title>Paginator</title>
    </head>
    <body>
        <h1>Paginator</h1>
        <a href="/?p=2,10">Show me pages 2-10</a>
        <p>To view the source code, <a href="/?source">click here.</a>
    </body>
</html>

  1. Simply using modular arithmetic to get 1 as result revealed the base64 encoded flag.

paginator-base64-flag.png

  1. Decoded the flag with CyberChef.
FLAG: ENO{SQL1_W1th_0uT_C0mm4_W0rks_SomeHow!}