Category: Cryptography
Flag: EH4X{Cav1aR_c1Gar3TT3s_Ch3bYsH3V_05091946}
Challenge Description
She keeps her Moët et Chandon in her pretty cabinet.
Nothing she offers is accidental. Nothing she withholds is without reason.
Recommended at the price, insatiable an appetite — wanna try?
Analysis
unzip -l "Handout.zip"Archive: Handout.zip
...
Handout/Killer-Queen/Freddie.txt
Handout/Killer-Queen/Lyrics/ciggarettes.txt
Handout/Killer-Queen/Lyrics/dynamite.txt
Handout/Killer-Queen/Lyrics/fibonacci.txt
Handout/Killer-Queen/Lyrics/laserbeam.txt
Handout/Killer-Queen/Notes/antoinette.md
Handout/Killer-Queen/Notes/biography.md
Handout/Killer-Queen/Notes/curves.md
Handout/Killer-Queen/Notes/sheet_music.mdThe handout immediately looked like a guided crypto puzzle, with the notes explicitly pointing to Chebyshev composition and a two-layer lock. That matters because it shifts the approach from “break one huge primitive” to “recover the intended pipeline and replay it cleanly.”
rg -n "Chebyshev|T_m\(T_n\(x\)\)|first door|second door|SHA-256|unwrap|two voices|index" "/home/rei/Downloads/killerqueen_work/Handout/Killer-Queen".../Lyrics/fibonacci.txt:17: T_m(T_n(x)) = T_{m·n}(x)
.../Notes/sheet_music.md:58: ♩ The stream is locked behind two doors.
.../Notes/sheet_music.md:59: ♩ The first door opens with the index.
.../Notes/sheet_music.md:60: ♩ The second door is built in blocks
.../Notes/sheet_music.md:63: ♩ SHA-256 derives the second key
.../Notes/sheet_music.md:72:• The Queen speaks in two voices
.../Notes/sheet_music.md:81:You'll need to unwrap what's inside — twice.The service behavior matched those hints exactly: one query gives two related voice values for the same index, and public iv/ciphertext are fixed for that session.

printf '0\n1\n2\n3\n4\n5\nexit\n' | nc 20.244.7.184 7331...
p = 7073193951809819973664306187302601643156849222029017483853417297144476949947829139698672438579337709916095808633124126138796016767532929021864211602000001
v = 5587994941705590424272649068295916108274072829805484356511387258719009236678476179597670504915988561703594735329013208151470008715612024345889541886986304
iv = f9c60e2a62756a6ebcd59c589dfd61b6
ciphertext = ad218e83bffe076fbceeddc17f540cd3089e3c4e6309a0e63c7f7cbed1c8b0f9fd2aced60c79a00de855acb4d5047bcd
[20 left] q> pretty_cabinet = 1
moet_chandon = 1
[19 left] q> pretty_cabinet = 5587994941705590424272649068295916108274072829805484356511387258719009236678476179597670504915988561703594735329013208151470008715612024345889541886986304
moet_chandon = 6224016679887966716101625712054632071019252852377882598120412242331086101200983099281554722925606107557863470191553080919315116795993075331627024609220227
...From there the final solve path was to derive material from moet_chandon(q) in index order, decrypt the AES-CBC outer layer, unpad, then do the second unwrap as a blockwise XOR stream where each 16-byte block is SHA256(str(moet_chandon(i)))[:16]. I tried heavier DLP-centric routes first, but those dead ends were useful because they confirmed the challenge was more about composition and serialization correctness than defeating the largest subgroup factor.
python3.12 "/home/rei/Downloads/killerqueen_work/Handout/Killer-Queen/solve_killerqueen.py"[+] p bits: 512
[+] v: 11305618717155759150724013649828687503261898264977063258438514099154408236632078431105146562036594498971222989731073226625866287035766764084910955827909772
[+] iv: 83f4896579e6e4a8f29272ae5feef4ff
[+] ciphertext bytes: 48
[+] plaintext: EH4X{Cav1aR_c1Gar3TT3s_Ch3bYsH3V_05091946}
[+] FLAG: EH4X{Cav1aR_c1Gar3TT3s_Ch3bYsH3V_05091946}Running it against the real remote session verified the whole chain end-to-end and produced the real flag in plaintext.
Solution
# solve.py
import re
import socket
from hashlib import sha256
from Crypto.Cipher import AES
HOST = "20.244.7.184"
PORT = 7331
def must_match(pattern: str, data: str, flags: int = 0) -> re.Match[str]:
match = re.search(pattern, data, flags)
if match is None:
raise ValueError(f"pattern not found: {pattern}")
return match
def recv_until(sock: socket.socket, marker: bytes) -> bytes:
data = b""
while marker not in data:
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
return data
def parse_public(blob: str):
p = int(must_match(r"^\s*p\s*=\s*(\d+)\s*$", blob, re.M).group(1))
v = int(must_match(r"^\s*v\s*=\s*(\d+)\s*$", blob, re.M).group(1))
iv = bytes.fromhex(must_match(r"^\s*iv\s*=\s*([0-9a-f]+)\s*$", blob, re.M).group(1))
ciphertext = bytes.fromhex(
must_match(r"^\s*ciphertext\s*=\s*([0-9a-f]+)\s*$", blob, re.M).group(1)
)
return p, v, iv, ciphertext
def parse_oracle_reply(blob: str):
pretty = int(must_match(r"pretty_cabinet\s*=\s*(\d+)", blob).group(1))
moet = int(must_match(r"moet_chandon\s*=\s*(\d+)", blob).group(1))
return pretty, moet
def pkcs7_unpad(data: bytes) -> bytes:
pad = data[-1]
if pad < 1 or pad > 16 or data[-pad:] != bytes([pad]) * pad:
raise ValueError("invalid PKCS#7 padding")
return data[:-pad]
def main() -> None:
with socket.create_connection((HOST, PORT), timeout=10) as sock:
sock.settimeout(5)
banner = recv_until(sock, b"q> ").decode(errors="replace")
p, v, iv, ciphertext = parse_public(banner)
print(f"[+] p bits: {p.bit_length()}")
print(f"[+] v: {v}")
print(f"[+] iv: {iv.hex()}")
print(f"[+] ciphertext bytes: {len(ciphertext)}")
moet_values = {}
for q in range(1, 21):
sock.sendall(f"{q}\n".encode())
reply = recv_until(sock, b"q> ").decode(errors="replace")
if "yawns" in reply or "Goodbye" in reply:
break
_, moet = parse_oracle_reply(reply)
moet_values[q] = moet
outer_key = sha256(str(moet_values[1]).encode()).digest()[:16]
inner = AES.new(outer_key, AES.MODE_CBC, iv).decrypt(ciphertext)
inner = pkcs7_unpad(inner)
block_count = (len(inner) + 15) // 16
stream = b"".join(
sha256(str(moet_values[i]).encode()).digest()[:16]
for i in range(1, block_count + 1)
)
plaintext = bytes(a ^ b for a, b in zip(inner, stream)).decode(errors="replace")
flag_match = re.search(r"EH4X\{[^}]+\}", plaintext)
if flag_match is None:
raise RuntimeError(f"flag not found in plaintext: {plaintext!r}")
print(f"[+] plaintext: {plaintext}")
print(f"[+] FLAG: {flag_match.group(0)}")
if __name__ == "__main__":
main()python3.12 solve.py[+] p bits: 512
[+] v: 11305618717155759150724013649828687503261898264977063258438514099154408236632078431105146562036594498971222989731073226625866287035766764084910955827909772
[+] iv: 83f4896579e6e4a8f29272ae5feef4ff
[+] ciphertext bytes: 48
[+] plaintext: EH4X{Cav1aR_c1Gar3TT3s_Ch3bYsH3V_05091946}
[+] FLAG: EH4X{Cav1aR_c1Gar3TT3s_Ch3bYsH3V_05091946}