team-logo
Published on

No Hack No CTF 2025 - Reverse challenges

Authors

Introduction

The Taiwan No Hack No CTF 2025, organized by ICEDTEA, held a jeopardy-style CTF from July 5th to 7th. Our team initially placed 5th, then climbed to 3rd, and finally finished 4th ;) We managed to solve 4 out of 5 reverse challenges. More information about this CTF can be found here. all-rev

flag checker

rev1 Writeup author: Grzechu
import struct

def sub1189(x):
    # Convert float or IEEE754 bit-pattern to int
    if isinstance(x, float):
        return int(x)
    return int(struct.unpack('<f', struct.pack('<I', x & 0xffffffff))[0])

def sub119d(x):
    return x & 0xffffffff

def rol8(x, n):
    x &= 0xff
    return ((x << n) & 0xff) | (x >> (8 - n))

def ror8(x, n):
    x &= 0xff
    return ((x >> n) | ((x << (8 - n)) & 0xff)) & 0xff

def sub11dc(x, n):
    return rol8(x, n)

def sub120d(x, n):
    return ror8(x, n)

# Initialize result array
res = [None] * 46

# Apply each constraint to determine characters
res[0]  = next(c for c in range(256) if sub1189(c * 3.1415) == 0xf5)
res[1]  = 0x48
res[2]  = (sub11dc(sub1189(0x419cf5c3), 3) - 0x4a) & 0xff
res[3]  = (((sub119d(0x411b4673) >> 16) & 0xff) + 0x28) & 0xff
res[4]  = next(c for c in range(256) if (sub1189(c * 2.5) + 0x48) & 0xff == c)
res[5]  = (sub120d(sub1189(0x420fe9e1), 1) - 0x27) & 0xff
res[6]  = (sub1189(0x41316c22) * 2 + 0x5f) & 0xff
res[7]  = (((sub119d(0x42607127) >> 8) & 0xff) + 2) & 0xff
res[8]  = next(c for c in range(256) if (sub11dc(sub1189(c * 1.5), 2) - 0x12) & 0xff == c)
res[9]  = (sub1189(0x416c902e) // 3 + 0x5b) & 0xff
res[0xa]= (sub119d(0x40b37b4a) + 0x29) & 0xff
res[0xb]= next(c for c in range(256) if (sub1189(c * 7.76999998) % 5 + 0x2e) & 0xff == c)
res[0xc]= (sub120d(sub1189(0x42b16c22), 4) - 0x18) & 0xff
res[0xd]= (sub1189(0x41082681) + 0x2b) & 0xff
res[0xe]= (((sub119d(0x41f66b86) >> 24) & 0xff) + 0x1e) & 0xff
res[0xf]= (sub11dc(sub1189(0x413313aa), 2) + 0x37) & 0xff
res[0x10]= (sub1189(0x406c902e) * 3 + 0x27) & 0xff
res[0x11]= (sub120d(sub1189(0x3f800000), 1) - 0x50) & 0xff
res[0x12]= (sub1189(0x40a00000) + 0x67) & 0xff
res[0x13]= ((sub119d(0x3f800000) >> 24) & 0xff) + 0x20
res[0x14]= (sub11dc(sub1189(0x40c00000), 1) + 0x5a) & 0xff
res[0x15]= ((sub1189(0x40e00000) + 0x14) << 2) & 0xff
res[0x16]= ((sub1189(0x41000000) >> 1) + 0x6b) & 0xff
res[0x17]= (((sub119d(0x40000000) >> 16) & 0xff) + 0x34) & 0xff
res[0x18]= (sub120d(sub1189(0x40400000), 3) + 0x14) & 0xff
res[0x19]= (sub1189(0x40a00000) % 7 + 0x2c) & 0xff
res[0x1a]= ((sub1189(0x40c80000) + 0x31) * 2) & 0xff
res[0x1b]= (((sub119d(0x3fc00000) >> 8) & 0xff) + 0x67) & 0xff
res[0x1c]= (sub11dc(sub1189(0x41100000), 4) - 0x31) & 0xff
res[0x1d]= (sub1189(0x41200000) // 5 + 0x6e) & 0xff
res[0x1e]= (((sub119d(0x3f000000) >> 24) & 0xff) - 0xf) & 0xff
res[0x1f]= (sub120d(sub1189(0x40000000), 2) - 0x17) & 0xff
res[0x20]= ((sub1189(0x41100000) % 10 + 0x2e) * 2) & 0xff
res[0x21]= (sub119d(0x3fa00000) + 0x37) & 0xff
res[0x22]= (sub11dc(sub1189(0x40b00000), 5) - 0x41) & 0xff
res[0x23]= (sub1189(0x40d00000) + 0x2a) & 0xff
res[0x24]= (sub120d(sub1189(0x40f00000), 6) + 0x54) & 0xff
res[0x25]= ((sub1189(0x41080000) & 3) + 0x33) & 0xff
res[0x26]= (((sub119d(0x40000000) >> 16) & 0xff) + 0x72) & 0xff
res[0x27]= (sub11dc(sub1189(0x408ccccd), 1) + 0x59) & 0xff
res[0x28]= (sub1189(0x40d33334) * 5 + 0x19) & 0xff
res[0x29]= (((sub119d(0x3f800000) >> 8) & 0xff) + 0x69) & 0xff
res[0x2a]= (sub120d(sub1189(0x400ccccd), 3) + 0x2f) & 0xff
res[0x2b]= (sub1189(0x4013d70a) + 0x6c) & 0xff
res[0x2c]= (sub11dc(sub1189(0x406ccccd), 7) - 0x4c) & 0xff
res[0x2d]= 0x7d

# Join and print the flag
flag = ''.join(chr(c) for c in res)
print(flag)

NHNC{jus7_s0m3_c00l_flo4t1ng_p0in7_0p3ra7ion5}

b@by_r3v3rs3

rev2 Writeup author: michalBB
from Crypto.Cipher import AES

# Dane z modułu WASM
encrypted = b'\x06F\x88\x88"\xbb\x1d\x1d\x9d\xd4T6M\xc1\xefB' \
            b'\xf9\xeb\xd2\xd8\xd4\x93\x13\xc3@wr\x88\xe2\xeaIG' \
            b'\x91?\xe5\x9f<S]\xd2{\xbc&\xec\xc3)\xa9\x10'
# Klucz i IV (16 bajtów każdy), pobrane z zakodowanych danych w module
key = b"secretkey???!!!~"       # 16-bajtowy klucz AES
iv  = b"Hi_I_am_iv_owo!!"       # 16-bajtowy wektor inicjalizacyjny

# Deszyfrujemy w trybie AES-CBC (AES-128) – otrzymamy tekst jawny (flagę)
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(encrypted)

# Odrzucamy ewentualne wypełnienie i konwertujemy na tekst
# (zakładamy wypełnienie PKCS#7; w przykładzie są 8 bajtów 0x08 na końcu)
pad_len = plaintext[-1]
flag = plaintext[:-pad_len].decode('utf-8', errors='ignore')

print("Flaga:", flag)

encrypted: the original 48-byte encrypted string (from the WASM module data).
key, iv: the AES key and IV extracted from the module's strings.
Decryption: AES-128 in CBC mode decrypts the data.
Padding: removes PKCS#7 padding (the last byte indicates how many padding bytes were added).

NHNC{3@sy_R3v3rs3_f0r_fr33_fl@9!!!!!!!!}

Yep Another Snake Game

rev3 Writeup author: rvr

We are given an attachment with several files: yep another snake game - files

The name of the task suggests that it's a snake game. We can confirm this by running the code in a web browser:

yep another snake game - snake game
Analyzing some files, we can see that the game was created using godot engine: yep another snake game - godot
Thus, we can simply open the index.pck file in the godot decompiler, eg. gdsdecomp. We get: yep another snake game - gdsdecomp

The important parts of this script are as follows:

func flag1():
	return "he_tol" + "d_"

func flag2():
	var flag = "where_is_my_snake"
	var flag100 = "100_score_correct"
	if flag == "whale120":
		return flag100
	elif 1337 == 1337:
		var a1 = 123
		a1 ^= 100
	if 1 == 1:
		return "me" + "_n" + "ot_to"

func flag3():
	var test = 1337
	test += 1
	test += 3
	if test == 0:
		return "user_root" + ":pa"
	elif test == 3:
		return flag1()
	else:
		return "_use"

func flag4():
	var meow = 100
	for i in range(100):
		meow += i
	if meow:
		return "_che" + "at_eng" + "ine"
	else:
		return "my_fir" + "st" + "_game" + "_hackIng"

func Score(x):
	get_node("Score").clear()
	if x >= 1333337:
		get_node("Score").add_text(flag1() + flag2() + flag3() + flag4())
	else:
		get_node("Score").add_text(str("Score : ", x))

Now, we can just replace func with def, remove the var keyword and at the end add print(flag1() + flag2() + flag3() + flag4()). Then, just save it as a python file and run it to get the flag:

NHNC{he_told_me_not_to_use_cheat_engine}

Encrypted(?

rev4 Writeup author: kerszi

This task seemed difficult at first—there was a lot of code and many checks to analyze. I approached it a bit differently. I didn’t even need encoded examples, just the encoded flag that we received with the challenge. Besides that, there was a binary that encoded a file. If the file contained the string "AA", the letter "A" in the first position was always encoded the same way, and the letter "A" in the second position was also always encoded the same way, although it differed from the first one. The first letter was independent of the other. Since the flag file flag2_enc.txt contained 507 bytes, it was possible to map 507 positions and then restore them. It worked!!! As you know, I really like bruteforce ;)

from pwn import *
import threading

context.log_level = 'warning'

context.update(arch='x86_64', os='linux')
context.terminal = ['wt.exe','wsl.exe']

BINARY_NAME="./encrypter"
binary = context.binary = ELF(BINARY_NAME, checksec=False)

zmapowana_tablica={}
mapped_bytes = []

mapping_all = {}
for length in range(1, 508):  # od 1 do 507 bajtów
    mapped_bytes_n = []
    for j in range(0x20, 0x7f):  # tylko pisemne znaki ASCII
        data = bytes([0x41] * (length - 1) + [j])
        with open('plik.txt', 'wb') as f:
            f.write(data)
        p = process([binary.path, 'plik.txt'])
        p.recvall(timeout=2)
        p.close()
        with open('plik.txt', 'rb') as f:
            encrypted_bytes = f.read(length)
        mapped_bytes_n.append((j, encrypted_bytes[-1:]))  # interesuje nas ostatni bajt
        mapping_all[(length, j)] = encrypted_bytes[-1:]
    # Wyświetl mapowanie dla danej długości
    print(f"\n--- Mapowanie dla długości {length} bajtów ---")
    for j, b in mapped_bytes_n:
        print(f"{length}th: {j:02x} -> {b.hex()}")

# Na końcu wypisz całość
print("\n=== Pełne mapowanie wszystkich długości ===")
for (length, j), b in mapping_all.items():
    print(f"{length}th: {j:02x} -> {b.hex()}")


# Odczytaj zakodowany plik
with open('flag2_enc.txt', 'rb') as f:
    encrypted_data = f.read()

# Odwrotne mapowanie: (length, zaszyfrowany_bajt) -> oryginalny_bajt
reverse_mapping = {}
for (length, orig_byte), enc_byte in mapping_all.items():
    reverse_mapping[(length, enc_byte)] = orig_byte

# Dekodowanie bajtów
decoded = []
for idx, enc_byte in enumerate(encrypted_data):
    length = idx + 1 if idx + 1 <= 507 else 507  # dla pozycji >507 używaj długości 507
    key = (length, bytes([enc_byte]))
    if key in reverse_mapping:
        decoded.append(reverse_mapping[key])
    else:
        decoded.append(ord('?'))  # znak zapytania jeśli nie znaleziono

# Zamień na string
decoded_str = ''.join(chr(b) for b in decoded)
print("Odkodowany tekst:")
print(decoded_str)

Bonus

Sometimes these binaries are simply no longer available, but occasionally you can find them on GitHub, so you can download and test them. Download binaries.zip