319 points | 11 solves
Description:
Email is great and all, but what if there is no internet?
**P.S. **There is no guess work, stego and trial/error involved. If you know signals, then the file is all you need.
Challenge author: elafargue
Files: sus.flac
No, but I'm going to decode it anyway.
I, operator of call sign KE8KAS, have dabbled in amateur radio on and off for a few years, interested especially in digital modes such as packet radio and FreeDV. Having listened to APRS frequencies before just to hear what it should sound like, I immediately recognized the data as being encoded with the 1200-baud Bell 202 scheme, though my initial attempts at decoding it left me doubtful at first. Eventually, I stumbled accross UZ7HO's soundmodem, which effortlessly decoded the AX.25 frames streamed in through the sacred but mysterious, and sometimes elusive, Stereo Mix device. Check out the results:

I immediately noticed "WL2K" in the third frame which, given the context, made me think of WinLink2000, nowadays more commonly known as Winlink. Upon further digging, it seems that the "B2F" in the same frame stands for the B2F protocol, which we will explore further later on. Unfortunately, however, "effortlessly" rings a bit too true in the case of non-ASCII data, for which this humble application cannot natively dump to a 7-bit safe format. Luckily, it provides a mode to emulate a KISS TNC.

KISS is a serial protocol designed for interacting with a TNC, which in a post-KISS world is a fancy word for a packet radio modem (in the days before it was practical to offload the cost of networking to the computer, a TNC would also decode the frames and handle them accordingly). It's just a rebadged SLIP (even uses the same byte values) with an extra control byte prepended to each frame. While the protocol is bidirectional, we don't really need to worry about data going to the virtualized TNC as it seems to spit out some good data by default. Using an interactive Python session, you can connect to the TNC and dump the data into a bytes object.
>>> import socket
>>> sock = socket.create_connection(('127.0.0.1', 8100))
>>> tncdump = b''
>>> sock.settimeout(0.5)
>>> while True:
... try:
... tncdump += sock.recv(4096)
... except socket.timeout:
... pass # Hack. The timeout allows KeyboardInterrupt to break through.
... except KeyboardInterrupt:
... break
...
Traceback (most recent call last):
File "<pyshell#16>", line 3, in <module>
tncdump += sock.recv(4096)
socket.timeout: timed out
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<pyshell#16>", line 3, in <module>
tncdump += sock.recv(4096)
KeyboardInterrupt
>>> print(tncdump.hex())
c000ae6c8a98824060ae6ca6868c40f573c0c000ae6c8a988240e0ae6ca6868c407500f0524d53205061636b65742e205669736974207777772e7363346172632e6f726720666f7220696e666f726d6174696f6e2e0dc0c000ae6c8a988240e0ae6ca6868c407502f05b574c324b2d352e302d4232465749484a4d245d0dc0c000ae6c8a988240e0ae6ca6868c407504f03b50513a2030353432323238360dc0c000ae6c8a988240e0ae6ca6868c407506f0434d5320766961205736534346203e0dc0c000ae6c8a98824060ae6ca6868c40f551c0c000ae6c8a988240e0ae6ca6868c407548f03b504d3a205736454c412050524a595a325145314e5444203533362077736d6974683937353235343840676d61696c2e636f6d20536f207768617420697320746869733f0dc0c000ae6c8a988240e0ae6ca6868c40754af0464320454d2050524a595a325145314e5444203730322035333620300dc0c000ae6c8a988240e0ae6ca6868c40754cf0463e2035330dc0c000ae6c8a98824060ae6ca6868c40f571c0c000ae6c8a988240e0ae6ca6868c40756ef00113536f207768617420697320746869733f00300002fa9005be020000ecf57a1c6d6773bdd6f2f9b7ddde8ef7b5e0c3cce5acfb403e3858377bca971bbaee2d5d6aaa547814a6a36b69d2ff7eff34aa77f16db02103ffcefd6c3e92787c192528789ccf9c1b199fc6ebdf3d21bc26ed9f6bc7c2b9ec7362c49c01f77dabe31bc0c000ae6c8a988240e0ae6ca6868c407560f0dc06309a9b985cf9b5d34ea0a9c9fe59e8af7e7a135c488491e71ff00564a9e09013ebad7a8dbee1808d5cf8cd42c7844a7978e68523143081aa8c32029127f3be01f44b12230145186266105fc84c9a49418bfcf3e8fd71ebadfa03477e28bff9bfa1824fc30062816ff05ffe0c2151ba0c92bd33e4e7198ddc06ceed9736f4c0c000ae6c8a988240e0ae6ca6868c407562f01f363ee37e40eea1ca1cc1f954eefa658902fa19f43978cbef3190319fbe0005daf1f9102dc5eb073f5dbebf75a4edfe77ab112af39c0d606cdbdddbddaafb0dde4576dd4b4aebaa8b0befe1b9c3c951bffe22b85f54fa76884e29efae568ac7de994883fa669dde77b2372a9ab08a6116aeb98ca89167439f66f4a2da98b2b7d9c1c0c000ae6c8a988240e0ae6ca6868c407564f08f9d173eaea9ef73b0c90aa5bb6058b2285e9336dcbe1506736894b4ef4f059c0be117872794053d7d21793771bc6d2b73b646411309267b0bad7a2fcbd9f6b0d29f392aea1453b304c6c544df2779df8426ca7462a85fa2d1ac7f583c3556ef8ed4b941d558f3be8eb2564df38a54da1673d2dbdd28281d6988bbbeab65b8e69fc0c000ae6c8a988240e0ae6ca6868c407566f0245272f48da2ff459eb86d7cca0224f84d284ed6ea498b2f1f016a5e1e57571b1ab5c277b0c6aa3de60a83285d1a4efe15a680047dc0c000ae6c8a98824060ae6ca6868c40f591c0c000ae6c8a988240e0ae6ca6868c407588f046510dc0c000ae6c8a98824060ae6ca6868c40f573c0
Now that we have a raw dump of the TNC output, we can decode each frame using the KISS spec at the Wikipedia page.
>>> frames = []
>>> currentframe = bytearray()
>>> esc = False
>>> for c in tncdump:
... if esc:
... esc = False
... if c == 0xDC:
... currentframe.append(0xC0)
... elif c == 0xDD:
... currentframe.append(0xDB)
... else:
... raise Exception("insert unfunny spanish inquisition joke here")
... elif c == 0xDB:
... esc = True
... elif c == 0xC0:
... if len(currentframe) > 0:
... frames.append(bytes(currentframe))
... currentframe.clear()
... else:
... currentframe.append(c)
...
>>> assert all(frame[0] == 0 for frame in frames) # ensure all control bytes are 0, as this is coming from the TNC
>>> frames = [frame[1:] for frame in frames] # strip off control bytes
>>> print('\n'.join(f"{str(i).rjust(2)}: {frame.hex()}" for i, frame in enumerate(frames)))
0: ae6c8a98824060ae6ca6868c40f573
1: ae6c8a988240e0ae6ca6868c407500f0524d53205061636b65742e205669736974207777772e7363346172632e6f726720666f7220696e666f726d6174696f6e2e0d
2: ae6c8a988240e0ae6ca6868c407502f05b574c324b2d352e302d4232465749484a4d245d0d
3: ae6c8a988240e0ae6ca6868c407504f03b50513a2030353432323238360d
4: ae6c8a988240e0ae6ca6868c407506f0434d5320766961205736534346203e0d
5: ae6c8a98824060ae6ca6868c40f551
6: ae6c8a988240e0ae6ca6868c407548f03b504d3a205736454c412050524a595a325145314e5444203533362077736d6974683937353235343840676d61696c2e636f6d20536f207768617420697320746869733f0d
7: ae6c8a988240e0ae6ca6868c40754af0464320454d2050524a595a325145314e5444203730322035333620300d
8: ae6c8a988240e0ae6ca6868c40754cf0463e2035330d
9: ae6c8a98824060ae6ca6868c40f571
10: ae6c8a988240e0ae6ca6868c40756ef00113536f207768617420697320746869733f00300002fa9005be020000ecf57a1c6d6773bdd6f2f9b7ddde8ef7b5e0c3cce5acfb403e3858377bca971bbaee2d5d6aaa547814a6a36b69d2ff7eff34aa77f16db02103ffcefd6c3e92787c192528789ccf9c1b199fc6ebdf3d21bc26ed9f6bc7c2b9ec7362c49c01f77dabe31b
11: ae6c8a988240e0ae6ca6868c407560f0dc06309a9b985cf9b5d34ea0a9c9fe59e8af7e7a135c488491e71ff00564a9e09013ebad7a8dbee1808d5cf8cd42c7844a7978e68523143081aa8c32029127f3be01f44b12230145186266105fc84c9a49418bfcf3e8fd71ebadfa03477e28bff9bfa1824fc30062816ff05ffe0c2151ba0c92bd33e4e7198ddc06ceed9736f4
12: ae6c8a988240e0ae6ca6868c407562f01f363ee37e40eea1ca1cc1f954eefa658902fa19f43978cbef3190319fbe0005daf1f9102dc5eb073f5dbebf75a4edfe77ab112af39c0d606cdbdbaafb0dde4576dd4b4aebaa8b0befe1b9c3c951bffe22b85f54fa76884e29efae568ac7de994883fa669dde77b2372a9ab08a6116aeb98ca89167439f66f4a2da98b2b7d9c1
13: ae6c8a988240e0ae6ca6868c407564f08f9d173eaea9ef73b0c90aa5bb6058b2285e9336dcbe1506736894b4ef4f059c0be117872794053d7d21793771bc6d2b73b646411309267b0bad7a2fcbd9f6b0d29f392aea1453b304c6c544df2779df8426ca7462a85fa2d1ac7f583c3556ef8ed4b941d558f3be8eb2564df38a54da1673d2db28281d6988bbbeab65b8e69f
14: ae6c8a988240e0ae6ca6868c407566f0245272f48da2ff459eb86d7cca0224f84d284ed6ea498b2f1f016a5e1e57571b1ab5c277b0c6aa3de60a83285d1a4efe15a680047d
15: ae6c8a98824060ae6ca6868c40f591
16: ae6c8a988240e0ae6ca6868c407588f046510d
17: ae6c8a98824060ae6ca6868c40f573
Note how the first 14 bytes of each frame are largely similar. They differ only in the following parts (all indices starting from 0):
This is the address field. The soundmodem has already collected the callsigns, SSIDs, and "C" bit of each frame from this field in accordance with the AX.25 specification, so there is no need to decode this ourselves.
Byte 14 of the frame is the control field. Bit 0 determines whether the frame is an "I" frame if it is 0 or an "S" or "U" frame if it is 1. "I" frames contain actual user data, whereas "S" and "U" are mainly used for layer 2 link control and therefore out of scope for this challenge.
Byte 15 of an "I" frame is the PID field, which identifies the layer 3 protocol. This is invariably 0xF0 for this transmission, which signifies no layer 3 protocol (pedantic note: Winlink is to some extent a layer 3 protocol, but obviously not one as general-purpose as IP or AppleTalk).
These 16 bytes form the header of the frame and can be stripped off the beginning.
Going back to the soundmodem screenshot, one can interpret the messages in accordance with the B2F protocol. Of particular note is frame 7, reading FC EM PRJYZ2QE1NTD 702 536 0. The FC proposal signifies data on the horizon; this one can be interpreted as "encapsulated message with ID PRJYZ2QE1NTD, uncompressed length 702, and compressed length 536" (I couldn't figure out what the 0 on the end was for; possibly a Winlink-specific extension?). The following frame 8 reading F> 53 signifies the end of the proposal, as specified in the FBB forwarding protocol which B2F extends. (Again, a mysterious out-of-spec number at the end.)
Following a non-"I" frame 9, there are 5 frames of lengths 128, 128, 128, 128, and 53, at indices 10 through 14. They can be strung together to recover the packet.
>>> packet = b''.join(frame[16:] for frame in frames[10:15]) # strip the header off before joining
>>> len(packet)
565
>>> print(packet.hex())
0113536f207768617420697320746869733f00300002fa9005be020000ecf57a1c6d6773bdd6f2f9b7ddde8ef7b5e0c3cce5acfb403e3858377bca971bbaee2d5d6aaa547814a6a36b69d2ff7eff34aa77f16db02103ffcefd6c3e92787c192528789ccf9c1b199fc6ebdf3d21bc26ed9f6bc7c2b9ec7362c49c01f77dabe31bdc06309a9b985cf9b5d34ea0a9c9fe59e8af7e7a135c488491e71ff00564a9e09013ebad7a8dbee1808d5cf8cd42c7844a7978e68523143081aa8c32029127f3be01f44b12230145186266105fc84c9a49418bfcf3e8fd71ebadfa03477e28bff9bfa1824fc30062816ff05ffe0c2151ba0c92bd33e4e7198ddc06ceed9736f41f363ee37e40eea1ca1cc1f954eefa658902fa19f43978cbef3190319fbe0005daf1f9102dc5eb073f5dbebf75a4edfe77ab112af39c0d606cdbdbaafb0dde4576dd4b4aebaa8b0befe1b9c3c951bffe22b85f54fa76884e29efae568ac7de994883fa669dde77b2372a9ab08a6116aeb98ca89167439f66f4a2da98b2b7d9c18f9d173eaea9ef73b0c90aa5bb6058b2285e9336dcbe1506736894b4ef4f059c0be117872794053d7d21793771bc6d2b73b646411309267b0bad7a2fcbd9f6b0d29f392aea1453b304c6c544df2779df8426ca7462a85fa2d1ac7f583c3556ef8ed4b941d558f3be8eb2564df38a54da1673d2db28281d6988bbbeab65b8e69f245272f48da2ff459eb86d7cca0224f84d284ed6ea498b2f1f016a5e1e57571b1ab5c277b0c6aa3de60a83285d1a4efe15a680047d
This packet should be interpreted in the B2 block format, which is defined in a different document from the B2F control protocol. Since there are 536 compressed bytes to be recovered and each B2 block contains up to 250 bytes, we can get away with traversing the three blocks manually.
>>> packet[0] # Should equal SOH or 1
1
>>> packet[1] # Length of remaining header
19
>>> packet[21] # Should equal STX or 2
2
>>> packet[22] # Length of remaining block
250
>>> packet[273]
2
>>> packet[274]
250
>>> packet[525]
2
>>> packet[526]
36
>>> packet[563] # Should equal EOT or 4
4
>>> compressed = packet[23:273] + packet[275:525] + packet[527:563]
>>> len(compressed)
536
Winlink's (de)compression routines are written in Visual Basic .NET, so we must now exit the land of Python.
>>> print(f"{{{','.join(str(c) for c in compressed)}}}")
{144,5,190,2,0,0,236,245,122,28,109,103,115,189,214,242,249,183,221,222,142,247,181,224,195,204,229,172,251,64,62,56,88,55,123,202,151,27,186,238,45,93,106,170,84,120,20,166,163,107,105,210,255,126,255,52,170,119,241,109,176,33,3,255,206,253,108,62,146,120,124,25,37,40,120,156,207,156,27,25,159,198,235,223,61,33,188,38,237,159,107,199,194,185,236,115,98,196,156,1,247,125,171,227,27,220,6,48,154,155,152,92,249,181,211,78,160,169,201,254,89,232,175,126,122,19,92,72,132,145,231,31,240,5,100,169,224,144,19,235,173,122,141,190,225,128,141,92,248,205,66,199,132,74,121,120,230,133,35,20,48,129,170,140,50,2,145,39,243,190,1,244,75,18,35,1,69,24,98,102,16,95,200,76,154,73,65,139,252,243,232,253,113,235,173,250,3,71,126,40,191,249,191,161,130,79,195,0,98,129,111,240,95,254,12,33,81,186,12,146,189,51,228,231,25,141,220,6,206,237,151,54,244,31,54,62,227,126,64,238,161,202,28,193,249,84,238,250,101,137,25,244,57,120,203,239,49,144,49,159,190,0,5,218,241,249,16,45,197,235,7,63,93,190,191,117,164,237,254,119,171,17,42,243,156,13,96,108,219,219,170,251,13,222,69,118,221,75,74,235,170,139,11,239,225,185,195,201,81,191,254,34,184,95,84,250,118,136,78,41,239,174,86,138,199,222,153,72,131,250,102,157,222,119,178,55,42,154,176,138,97,22,174,185,140,168,145,103,67,159,102,244,162,218,152,178,183,217,193,143,157,23,62,174,169,239,115,176,201,10,165,187,96,88,178,40,94,147,54,220,190,21,6,115,104,148,180,239,79,5,156,11,225,23,135,39,148,5,61,125,33,121,55,113,188,109,43,115,182,70,65,19,9,38,123,11,173,122,47,203,217,246,176,210,159,57,42,234,20,83,179,4,198,197,68,223,39,121,223,132,38,202,116,98,168,95,162,209,172,127,88,60,53,86,239,142,212,185,65,213,88,243,190,142,178,86,77,243,138,84,218,22,115,210,219,40,40,29,105,136,187,190,171,101,184,230,159,36,82,114,244,141,162,255,69,158,184,109,124,202,248,77,40,78,214,234,73,139,47,31,1,106,94,30,87,87,27,26,181,194,119,176,198,170,61,230,10,131,40,93,26,78,254,21,166,128}
I created a VB.NET project and added WinlinkSupport.vb to it. I also wrote a Main() subroutine to decompress the data and print it to the console:
Sub Main()
Dim Inp As Byte() = {144,5,190,2,0,0,236,245,122,28,109,103,115,189,214,242,249,183,221,222,142,247,181,224,195,204,229,172,251,64,62,56,88,55,123,202,151,27,186,238,45,93,106,170,84,120,20,166,163,107,105,210,255,126,255,52,170,119,241,109,176,33,3,255,206,253,108,62,146,120,124,25,37,40,120,156,207,156,27,25,159,198,235,223,61,33,188,38,237,159,107,199,194,185,236,115,98,196,156,1,247,125,171,227,27,220,6,48,154,155,152,92,249,181,211,78,160,169,201,254,89,232,175,126,122,19,92,72,132,145,231,31,240,5,100,169,224,144,19,235,173,122,141,190,225,128,141,92,248,205,66,199,132,74,121,120,230,133,35,20,48,129,170,140,50,2,145,39,243,190,1,244,75,18,35,1,69,24,98,102,16,95,200,76,154,73,65,139,252,243,232,253,113,235,173,250,3,71,126,40,191,249,191,161,130,79,195,0,98,129,111,240,95,254,12,33,81,186,12,146,189,51,228,231,25,141,220,6,206,237,151,54,244,31,54,62,227,126,64,238,161,202,28,193,249,84,238,250,101,137,25,244,57,120,203,239,49,144,49,159,190,0,5,218,241,249,16,45,197,235,7,63,93,190,191,117,164,237,254,119,171,17,42,243,156,13,96,108,219,219,170,251,13,222,69,118,221,75,74,235,170,139,11,239,225,185,195,201,81,191,254,34,184,95,84,250,118,136,78,41,239,174,86,138,199,222,153,72,131,250,102,157,222,119,178,55,42,154,176,138,97,22,174,185,140,168,145,103,67,159,102,244,162,218,152,178,183,217,193,143,157,23,62,174,169,239,115,176,201,10,165,187,96,88,178,40,94,147,54,220,190,21,6,115,104,148,180,239,79,5,156,11,225,23,135,39,148,5,61,125,33,121,55,113,188,109,43,115,182,70,65,19,9,38,123,11,173,122,47,203,217,246,176,210,159,57,42,234,20,83,179,4,198,197,68,223,39,121,223,132,38,202,116,98,168,95,162,209,172,127,88,60,53,86,239,142,212,185,65,213,88,243,190,142,178,86,77,243,138,84,218,22,115,210,219,40,40,29,105,136,187,190,171,101,184,230,159,36,82,114,244,141,162,255,69,158,184,109,124,202,248,77,40,78,214,234,73,139,47,31,1,106,94,30,87,87,27,26,181,194,119,176,198,170,61,230,10,131,40,93,26,78,254,21,166,128}
Dim Outp As Byte()
Dim N As Int32 = Compression.Decode(Inp, Outp, True, 702)
Console.WriteLine(Encoding.Default.GetString(Outp))
Console.ReadKey(True)
End Sub
The console output is as follows:
MID: PRJYZ2QE1NTD
Date: 2021/09/29 04:21
From: SMTP:wsmith9752548@gmail.com
To: W6ELA
Subject: So what is this?
Mbo: SMTP
Body: 561
Yes, there is a worldwide system out there that can be used for sending
email over radio waves called "Winlink". A bit old school but hey, it
actually works great. Iridium and other satellite technologies have
replaced it to a large extent with the sailing crowd who used
to be a big user group, but it is still there.
... and in case of emergency, nothing beats HF or VHF, if you are a HAM
radio operator.
Glad you made it all the way here and maybe learned something new today.
Here is what you came for: cGJjdGZ7OTA4MjNqc2RnaGtfODAxM2tzNzIzNH0=
After reading this nice ode to Winlink, we can decode that interesting Base64-like string which, as it turns out, is bona fide Base64, and yield pbctf{90823jsdghk_8013ks7234}. Part of me wants to know if that flag actually means anything, but I've had enough decoding for today.
© 2021 Robert B. Langer, CC BY 4.0