Thotcon 0xD
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]
- System Status
- Personnel Database
- Facility Management
- Authentication Module
- Launch Control Interface
Enter 'exit' to disconnect
Selection: 5
[LAUNCH CONTROL INTERFACE] ════════════════════════ Authenticated User: Maj. Wilson Launch Authority: Confirmed
Available Targets:
- THOTCON
- AREA-51
- PENTAGON
- 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 2886992601Output 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 ^ 42029
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