1. Challenge Setup
The story says Mildred Milwaukee is listening to an undersea communications cable near
CASCADA. We only get a hostname, challs.undutmaning.se, but not the exact
port. We are told the challenge is not running on CTFd, which matters
later because it means a normal undut{...} submission assumption is not always
valid during the middle of the solve.
In practice, this solve happened over several sessions. The first session was mostly about understanding the hints and establishing a reliable way to talk to the service. Later sessions focused on collecting larger captures, testing framing theories, and separating the misleading intermediate message from the actual final payload.
Given
- Host:
challs.undutmaning.se - Binary:
sc - Two free hints
Unknown
- Correct port
- Whether the service uses TLS or plaintext
- How the signal is encoded
- What the final answer format is
2. Free Hints
The challenge provides two hint strings:
xyzxyzxdyzjxyzxuyzxypzetxyäzrxyzpxoyzxrteyzxyzxyznxyz
ÅTdjTIOuTRpeEÅtäTrpoTIrOten
Hint 1
Remove every xyz from the first string:
djupetärporten
This translates to: “the depth is the port”.
Hint 2
The second string is mixed-case. Taking the lowercase letters again gives
djupetärporten. Taking the uppercase letters gives:
ÅTTIOTREÅTTIO
That is Swedish for eighty-three eighty, so the port is:
3. Finding The Port
The intended first solve step is simply to derive 8380 from the hints.
At this point the natural first try was to use the supplied binary:
./sc challs.undutmaning.se:8380
That failed with a TLS error:
tls: first record does not look like a TLS handshake
This was important: the port was correct, but sc was the wrong transport
for this service.
4. Protocol Mismatch
The local sc binary is a TLS/SNI tunneling helper. Its help output shows
options such as -bind, -servername, and -insecure.
So when it connected to 8380, it tried to speak TLS.
The remote side answered in raw bytes, not TLS. So the correct client for initial
exploration was plain nc:
nc challs.undutmaning.se 8380
This produced a continuous binary-looking stream rather than readable text.
5. Capturing The Signal
Dumping the stream to files was the only practical next step. Early captures were:
nc challs.undutmaning.se 8380 | head -c 8192 > a.bin
nc challs.undutmaning.se 8380 | head -c 8192 > b.bin
cmp -l a.bin b.bin
The first observation was that a.bin and b.bin were identical.
That suggested a fixed stream, but a larger capture later showed the more accurate
interpretation: each new connection starts at the same place, but the
stream keeps progressing if you stay connected.
This was one of the main session-to-session corrections. In the early pass, the equal small captures made the service look like a short repeating loop. After returning with longer recordings, it became clear that the challenge behaves more like a broadcast that rewinds to the start on reconnect. That changed the goal from “find the loop” to “stay connected long enough to reach the rest of the message.”
Larger captures
nc challs.undutmaning.se 8380 | head -c 65536 > long.bin
nc challs.undutmaning.se 8380 | head -c 167848 > huge.bin
a.bin and b.bin happened because both connections
started at the beginning of the same broadcast.
6. Framing Analysis
The data did not match any normal file format. file and signature scans
found nothing useful. The crucial structural observation was that the stream splits
cleanly into 33-byte frames.
The dominant idle frame was:
d820a2e15a50004924924924500049249249245000492492492450004924924924
Active payload appeared in long bursts separated by large idle regions, with three tiny singleton frames between most major bursts. At first this looked like some kind of display or lane-coded modem stream. A large amount of exploratory work went into:
- bitmaps from raw bits
- XOR against idle
- triplet/ternary interpretations
- burst-family averaging
- marker-frame overlays
Those visualizations were useful for understanding the structure, but the solve breakthrough came from ignoring the image interpretation and looking at the timing of each major burst.
That detail is worth stressing because it explains the investigative path. Several sessions were spent building images, comparing burst families, and checking whether the data hid some sort of rasterized text. None of that directly produced the flag, but it did show that the stream was highly structured and that the bursts were consistent enough to support a timing analysis.
7. Decoding The Morse Layer
Each major active burst can be reduced to a 1-bit signal over time. The on-run lengths naturally cluster into short and long pulses, with small off-gaps between them. That is exactly Morse-style timing.
Example mapping
| Burst | Pulse lengths | Morse | Letter |
|---|---|---|---|
(0, 64) |
[17, 6, 8, 7] |
-... |
B |
(116, 54) |
[7, 18, 7] |
.-. |
R |
(221, 45) |
[7, 17] |
.- |
A |
Doing that across the initial section gave:
B R A J O B B A T A V K O D A
or:
At first it was tempting to treat that phrase as the final answer. That was wrong. It is only an instruction: “Good job, decode …”
Longer capture
With the longer capture, the Morse transmission continued beyond the first phrase and decoded to:
BRAJOBBATAVKODANUOVXGI5LUPNZGK3LPOJZWK7I
The key insight is to split this as:
BRA JOBBAT AVKODA NU OVXGI5LUPNZGK3LPOJZWK7I
In Swedish, NU means now. So the message reads:
“Good job, decode now …”
This was the point where the later session finally resolved the biggest ambiguity from the earlier work. The first recovered phrase looked polished enough to be a possible answer, but the longer recording showed that it was only an instruction embedded in the transmission. Without that extra session and the longer capture, it would have been easy to stop one layer too early.
8. Second-Stage Ciphertext
The real second-stage payload is therefore not the whole tail after
BRAJOBBATAVKODA, but the substring after NU:
OVXGI5LUPNZGK3LPOJZWK7I
This immediately becomes suspicious because:
- it is uppercase alphanumeric
- it fits the Base32 alphabet
- its length is
23, which becomes valid Base32 with one=pad
The next test is therefore:
python3 - <<'PY'
import base64
s = "OVXGI5LUPNZGK3LPOJZWK7I"
print(base64.b32decode(s + "="))
PY
9. Final Base32 Decode
The result is:
b'undut{remorse}'
undut{remorse}
Why the earlier guesses were wrong
BRA JOBBAT AVKODAwas only the first-layer instruction.- The alphanumeric string including
NUwas not the final token either. NUwas part of the instruction text, not part of the Base32 ciphertext.
10. Step-By-Step Summary
- Use the two free hints to derive the port
8380. - Notice that
scfails because it expects TLS, while the service is plaintext. - Connect with
ncand save the output to binary files. - Discover that the stream uses a 33-byte frame structure with a dominant idle frame.
- Separate the stream into major active bursts and tiny singleton marker frames.
- Reduce each major burst to a binary timing trace and decode it as Morse.
- Read the first-layer message:
BRA JOBBAT AVKODA. - Capture a longer stream and continue the Morse decode.
- Read the extended message:
BRAJOBBATAVKODANUOVXGI5LUPNZGK3LPOJZWK7I. - Interpret
NUas Swedish “now”, separating the actual ciphertext:OVXGI5LUPNZGK3LPOJZWK7I. - Base32-decode that ciphertext with one padding character.
- Recover the real flag:
undut{remorse}.
11. Useful Commands
# Find the signal
nc challs.undutmaning.se 8380 | head -c 65536 > long.bin
# Get a larger capture
nc challs.undutmaning.se 8380 | head -c 167848 > huge.bin
# Base32 decode final payload
python3 - <<'PY'
import base64
s = "OVXGI5LUPNZGK3LPOJZWK7I"
print(base64.b32decode(s + "=").decode())
PY