Elite

There’s a TCP server running on port 9009 that contains a hidden flag, but it’s protected by a strict firewall. The firewall can be temporarily disabled for 30 seconds by sending a “firewall disable” command to its UDP control port 2723, but this control port will only accept commands from 129.229.10.50…… However, be careful - the firewall’s EDR will automatically re-enable protection after the 30-second window expires…

The first step of this challenge was to do the "port knock". The issue is that our IP address is not anywhere in the 129.0.0.0/8 subnet. Luckily for us this is UDP and there's no handshake to help confirm we are the IP we say we are.

We can use scapy to craft a packet.

packet = IP(src="129.229.10.50", dst="172.20.2.2")/UDP(dport=2723)/"firewall disable"
send(packet)

Instinctively, I just nuked Thotcon. Seems fine:

person@blep ~/ctf [env]$ nc -vvv 172.20.2.2 57019 Warning: Inverse name lookup failed for `172.20.2.2' 172.20.2.2 57019 open

╔═════════════════════════════════════════════════════════════════════════════╗ ║ UNITED STATES STRATEGIC COMMAND ║ ║ NUCLEAR WEAPONS CONTROL INTERFACE v3.7 ║ ║ ║ ║ * WARNING * ║ ║ THIS IS A RESTRICTED UNITED STATES GOVERNMENT COMPUTER SYSTEM ║ ║ ║ ║ This system is for AUTHORIZED USE ONLY. By accessing this system, ║ ║ you are consenting to complete monitoring of all actions. Any ║ ║ unauthorized access will be prosecuted to the full extent of federal ║ ║ law under 18 USC §793 and §1030. ║ ║ ║ ║ CURRENT DEFCON STATUS: 3 ║ ║ THREAT LEVEL: ELEVATED ║ ║ CURRENT AUTHENTICATION PROTOCOL: DELTA-7 ║ ╚═════════════════════════════════════════════════════════════════════════════╝

[MAIN MENU]

  1. System Status
  2. Personnel Database
  3. Facility Management
  4. Authentication Module
  5. Launch Control Interface

Enter 'exit' to disconnect

Selection: 5

[LAUNCH CONTROL INTERFACE] ════════════════════════ Authenticated User: Maj. Wilson Launch Authority: Confirmed

Available Targets:

  1. THOTCON
  2. AREA-51
  3. PENTAGON
  4. NORAD

Enter target number: 1

╔════════════════════════════════════════════════════════╗ ║ NUCLEAR LAUNCH DETECTED ║ ║ TARGETING: THOTCON CONVENTION CENTER ║ ║ ║ ║ !!!WARHEAD DETONATED!!! ║ ║ ║ ║ YOU DESTROYED THOTCON : FLAG_WARHEADSLAMMER ║ ╚════════════════════════════════════════════════════════╝

read(net): Connection reset by peer

FLAG_WARHEADSLAMMER

IRC

YARR!!! WE ARE RUNNING AN IRC SERVER AT irc.rëdherring.com & HAVE A #VAULT OF GOLDEN COINS!!! YOU’LL NEVER TAKE ME BOOTY!!!!

Of course I used the best IRC client ERC.

There was a #vault channel.

Yes, I did pretend to be flagbot to see what other people would send me.

No, none of the flags I gave you were going to work

Was the challenge to boot @acidvegas from the channel, steal the nick @acidvegas` and become OP?

idk.

Did it yield the flag?

Yes, after joining the #opers channel

Send Noodz

Send text to Lenny at 2886992601 on extention 49

Seeing this immediatly brought back memories of the shock I had readin this malwarebytes blogpost about part of a Smoke Loader infection chain that used this fun trick.

Your terminal AND you web browser will both happily treat an integer or an octal number as an IP.

(ahhh there's no place like decimal 2130706433 or octal 017700000001)

nc -p 49 2886992601

Output mostly redacted for your safety

flag{s3nd1ng_n00dz}

Time It Right

This was a binary file that had a desription like: "run me quick!"

Listen, I'm sure that's a way to solve it. But radare2 does the trick too :)

Run analysis on the binary:

[0x000010e0]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for vtables
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information
[ ] Use -AA or aaaa to perform additional experimental analy[x] Use -AA or aaaa to perform additional experimental analysis.

Listing function yields one interesting function:

sym.generate_flag

[0x000010e0]> afl 0x000010e0 1 37 entry0 0x00001110 4 41 -> 34 sym.deregister_tm_clones 0x00001140 4 57 -> 51 sym.register_tm_clones 0x00001180 5 57 -> 54 sym.__do_global_dtors_aux 0x00001080 1 10 fcn.00001080 0x000011c0 1 9 entry.init0 0x000011c9 1 104 sym.generate_flag 0x000010c0 1 10 sym.imp.snprintf 0x00001374 1 13 sym._fini 0x00001231 9 320 main 0x00001000 3 27 sym._init 0x00001090 1 10 sym.imp.localtime 0x000010a0 1 10 sym.imp.puts 0x000010b0 1 10 sym.imp.printf 0x000010d0 1 10 sym.imp.time

Thank you for not stripping your binaries!

[0x000010e0]> s sym.generate_flag [0x000011c9]> pdf ; CALL XREF from main @ 0x12e1 ┌ 104: sym.generate_flag (char *arg1, char *arg2); │ ; var char *size @ rbp-0x20 │ ; var char *s @ rbp-0x18 │ ; var char *var_10h @ rbp-0x10 │ ; var char *format @ rbp-0x8 │ ; arg char *arg1 @ rdi │ ; arg char *arg2 @ rsi │ 0x000011c9 f30f1efa endbr64 │ 0x000011cd 55 push rbp │ 0x000011ce 4889e5 mov rbp, rsp │ 0x000011d1 4883ec20 sub rsp, 0x20 │ 0x000011d5 48897de8 mov qword [s], rdi ; arg1 │ 0x000011d9 488975e0 mov qword [size], rsi ; arg2 │ 0x000011dd 488d053c0e00. lea rax, str.flagdynamic_ ; 0x2020 ; "flag{dynamic_" │ 0x000011e4 488945f8 mov qword [format], rax │ 0x000011e8 488d053f0e00. lea rax, str._flag ; 0x202e ; "_flag}" │ 0x000011ef 488945f0 mov qword [var_10h], rax │ 0x000011f3 bae9070000 mov edx, 0x7e9 ; 2025 │ 0x000011f8 b804000000 mov eax, 4 │ 0x000011fd 89d7 mov edi, edx │ 0x000011ff 31c7 xor edi, eax │ 0x00001201 488b4df0 mov rcx, qword [var_10h] │ 0x00001205 488b55f8 mov rdx, qword [format] │ 0x00001209 488b75e0 mov rsi, qword [size] ; size_t size │ 0x0000120d 488b45e8 mov rax, qword [s] │ 0x00001211 4989c9 mov r9, rcx │ 0x00001214 4189f8 mov r8d, edi │ 0x00001217 4889d1 mov rcx, rdx │ 0x0000121a 488d15140e00. lea rdx, str._s_d_s ; 0x2035 ; "%s%d%s" ; const char *format │ 0x00001221 4889c7 mov rdi, rax ; char *s │ 0x00001224 b800000000 mov eax, 0 │ 0x00001229 e892feffff call sym.imp.snprintf ; int snprintf(char *s, size_t size, const char *format, …) │ 0x0000122e 90 nop │ 0x0000122f c9 leave └ 0x00001230 c3 ret

The're a couple important bits to pull from this.

We're going to be using snprintf to format a string with the format %s%d%s

We can see radare showing the strings at 0x2020 and 0x202e and can infer the flag is going to look like:

flag{dynamic_XXXXXX_flag}

the XXXX here looks like it should be 2025.. and that would make sense since it's 2025

│ 0x000011f3 bae9070000 mov edx, 0x7e9 ; 2025

But we actually XOR this with 4

│ 0x000011f3 bae9070000 mov edx, 0x7e9 ; 2025 │ 0x000011f8 b804000000 mov eax, 4 │ 0x000011fd 89d7 mov edi, edx │ 0x000011ff 31c7 xor edi, eax

return 2025 ^ 4
2029

flag{dynamic_2029_flag}

Inception

There is a zip file. in a zip file. in a zip file. in a zip file… zizek voice and so on and so on (1019 times)

#!/usr/bin/env bash

echo "Original Md5"
md5sum ./Inception_32x32.zip
while true;
do unzip -o ./Inception_32x32.zip && md5sum ./Inception_32x32.zip 
done;

flag{inc3ption_by_compr355i0n}

Fuscation

This is the script.. and WHILE AI might be dogwater at figuring out cyphers… it's great at understanding code and can easily solve this. BUT THIS IS MALÖRTWARE!!!!

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import base64
import zlib
import random
import time
import sys
from itertools import cycle

def d_func(r):
    return ''.join(chr(((ord(c) - 97 + r) % 26) + 97) if 'a' <= c <= 'z' else 
                  (chr(((ord(c) - 65 + r) % 26) + 65) if 'A' <= c <= 'Z' else c) 
                  for c in ''.join([chr(x) for x in [102, 108, 97, 103, 123, 104, 49, 100, 100, 51, 110, 95, 98, 51, 104, 49, 110, 100, 95, 108, 52, 121, 51, 114, 115, 95, 48, 102, 95, 48, 98, 102, 117, 115, 99, 52, 116, 49, 48, 110, 125]]))

class Scrambler:
    def __init__(self, data, key=None):
        self.data = data
        self.key = key or random.randint(1, 25)
    
    def encrypt(self):
        result = []
        for char, key_char in zip(self.data, cycle(str(self.key))):
            shifted = ord(char) + int(key_char)
            result.append(chr(shifted))
        return ''.join(result)
    
    def decrypt(self):
        result = []
        for char, key_char in zip(self.data, cycle(str(self.key))):
            shifted = ord(char) - int(key_char)
            result.append(chr(shifted))
        return ''.join(result)

def matrix_transform(input_str, rows=5, cols=None):
    if not cols:
        cols = (len(input_str) + rows - 1) // rows
    
    # Pad the string to fill the matrix
    padded = input_str.ljust(rows * cols)
    
    # Create the matrix
    matrix = [padded[i:i+cols] for i in range(0, len(padded), cols)]
    
    # Read by columns
    result = ''.join(''.join(matrix[r][c] for r in range(rows) if c < len(matrix[r])) 
                     for c in range(cols))
    
    return result

def encode_layer1(text):
    # XOR each character with its position
    result = ''.join(chr(ord(c) ^ (i % 7)) for i, c in enumerate(text))
    return result

def encode_layer2(text):
    # Reverse and apply Caesar cipher
    reversed_text = text[::-1]
    caesar_text = ''.join(chr((ord(c) + 3) % 256) for c in reversed_text)
    return caesar_text

def encode_layer3(text):
    # Convert to bytes, compress, and base64 encode
    compressed = zlib.compress(text.encode('utf-8'))
    return base64.b85encode(compressed).decode('utf-8')

_0xflag = lambda _: ''.join(chr(x ^ 42) for x in [107, 69, 76, 65, 109, 125, 73, 82, 78, 65, 76, 117, 78, 85, 73, 82, 86, 78, 117, 76, 86, 89, 86, 89, 81, 117, 84, 87, 117, 84, 71, 71, 94, 84, 70, 82, 66, 83, 84, 74, 111])

def generate_decoy():
    decoys = [
        "flag{this_is_not_the_real_flag}",
        "flag{nice_try_but_keep_looking}",
        "flag{almost_there_keep_digging}",
        "flag{red_herring_nothing_to_see_here}",
        "flag{you_found_a_fake_flag}"
    ]
    return random.choice(decoys)

class Challenge:
    def __init__(self):
        self.value = None
        self._hidden = [ord(c) for c in d_func(13)]
        self._decoy1 = generate_decoy()
        self._decoy2 = _0xflag(None)
    
    def _transform(self, s):
        return ''.join(chr((ord(c) + 7) % 256) for c in s)
    
    def _inverse_transform(self, s):
        return ''.join(chr((ord(c) - 7) % 256) for c in s)
    
    def run(self):
        print("Welcome to the CTF challenge!")
        print("Solving this will require multiple layers of analysis.")
        
        while True:
            command = input("> ").strip().lower()
            
            if command == "exit" or command == "quit":
                print("Goodbye!")
                break
                
            elif command == "help":
                print("Available commands: help, hint, run, analyze, decrypt, exit")
                
            elif command == "hint":
                print("Look for patterns. Not everything is as it seems.")
                print("The real flag follows the format flag{...}")
                
            elif command == "run":
                value = encode_layer3(encode_layer2(encode_layer1(self._decoy1)))
                print(f"Output: {value}")
                
            elif command == "analyze":
                scrambler = Scrambler(self._decoy2, 5)
                encrypted = scrambler.encrypt()
                print(f"Analysis result: {encrypted}")
                time.sleep(1)
                print("Memory dump:")
                for i in range(0, 41, 10):
                    chunk = self._hidden[i:i+10]
                    if chunk:
                        print(f"Offset {i}: {' '.join(f'{x:02x}' for x in chunk)}")
                
            elif command == "decrypt":
                key = input("Enter decryption key: ")
                if not key.isdigit():
                    print("Invalid key format")
                    continue
                    
                try:
                    original = bytes([((x - int(key)) % 256) for x in self._hidden]).decode('utf-8')
                    print(f"Decryption attempt: {original}")
                except:
                    print("Decryption failed")
            
            else:
                print("Unknown command. Type 'help' for available commands.")

if __name__ == "__main__":
    Challenge().run()

Let's start by simplifying this script :)

All useless paths are removed and anything that turns into a static string has been resolved

import base64
import zlib
import random
import time
import sys
from itertools import cycle

class Challenge:
    def __init__(self):
        self.value = None
        self._hidden = 'synt{u1qq3a_o3u1aq_y4l3ef_0s_0oshfp4g10a}'

    def run(self):
        while True:
            key = input("Enter decryption key: ")

            # Must be a digit
            if not key.isdigit():
                print("Invalid key format")
                continue

            try:
                original = bytes([((x - int(key)) % 256) for x in self._hidden]).decode('utf-8')
                print(f"Decryption attempt: {original}")
            except:
                print("Decryption failed")


if __name__ == "__main__":
    Challenge().run()

OK so all that matters here is that this function original = bytes([((x - int(key)) % 256) for x in self._hidden]).decode('utf-8') is being applied to our "encrypted" flag synt{u1qq3a_o3u1aq_y4l3ef_0s_0oshfp4g10a}

You can note that in the function we're not doing any sort of iteration over the key so it's a single bit key encrypting this. This is functionally a ROT cypher… and in fact is the classic ROT13

flag{h1dd3n_b3h1nd_l4y3rs_0f_0bfusc4t10n}

(RIP TO OBFUSCATION PUZZLES- YOU WERE THE BEST)

Math Riddle

I live in threes, not in trees. I convert but I am not covert. I am a “nightmare” but it is right where I would be. Don’t add, else you will be mad. Connect is the effect. With love: Reuben Louis Goodstein 511504 = (²2 × 5 × 7 × 11 × 83) + 224 = a 10125 = (3⁴ × 5³) = e 3109052 = (²2 × 109 × 1,783) -500 = g 9506704 = (3 × ²2 × 23 × 79 × 109) + 160 = h 9710312 = (2³ × 13 × 109 × 857) - 4640 = i 1145111 = (3 × 419 × 911) - 16 = m 1021080 = (2³ × 15 × 8,509) = n 90481 = (2 × 7 × 23 × 281) -1 = r 8109112 = (2³ × 5² × 11 × 29 × 127) + 6512 = t

This was a fun little riddle. The weirdest part was the "exponents" that looked like this ²2 which is certainly not standard math vocab (for a non-math guy). This is called Tetration. So ³2 would be 2^2^2. With this understanding we can do the calculations and you find that the first two components of each "equal" comparison are numerically equal. So it would make sense to just simplify:

511504 = a 10125 = e 3109052 = g 9506704 = h 9710312 = i 1145111 = m 1021080 = n 90481 = r 8109112 = t

Then we can see that the clue "i am nightmare" means we should probably rearrange:

1021080 = n 9710312 = i 3109052 = g 9506704 = h 8109112 = t 1145111 = m 511504 = a 90481 = r 10125 = e

Then with the clue Don’t add, else you will be mad. Connect is the effect. suggests we should concat the numbers

1021080971031231090529506704810911211451115115049048110125

I live in threes, not in trees. suggests these should be in groups of three:

102 108 097 103 123 109 052 95 067 048 109 112 114 51 115 115 049 048 110 125

You'll notice that some of these are not in groups of 3. If the number would fall out of ascii range you should break the group earlier.

In CyberChef.

flag{m4_C0mpr3ss10n}

Thotcon-TV and Thotcon-FM

The SDR challenges did not make an appearance at Thotcon 0xD. For previous SDR writeups check out our blog on the 0xA badge

But if you like niche operating systems as much as you like niche bitter chicago liquors here's how you WOULD get your BladeRF set up to do a SDR challenge at Thotcon if you were using Guix