From d5ec5327da33e442c13dfd347dfcf994102e43e4 Mon Sep 17 00:00:00 2001 From: haskal Date: Tue, 26 May 2020 03:08:34 -0400 Subject: [PATCH] 56k --- comms/56k/README.md | 210 ++++++++++++++++++++++++++++++++++++++++++++ comms/56k/decode.py | 38 ++++++++ 2 files changed, 248 insertions(+) create mode 100644 comms/56k/README.md create mode 100644 comms/56k/decode.py diff --git a/comms/56k/README.md b/comms/56k/README.md new file mode 100644 index 0000000..296e9a2 --- /dev/null +++ b/comms/56k/README.md @@ -0,0 +1,210 @@ +# 56K Flex Magic + +**Category:** Communication Systems +**Points (final):** 205 +**Solves:** 13 + +>Anyone out there speak modem anymore? We were able to listen in to on, maybe you can ask it for a +>flag… +> +>UPDATE: Since everyone is asking, yes...a BUSY signal when dialing the ground station is expected +>behavior. + +## Write-up + +by [haskal](https://awoo.systems) + +An audio file is included that contains a session where a number is dialed, and then some modem data +is exchanged (it's a very distinctive sound). Additionally, there is a note included with the +following text. + +``` +---=== MY SERVER ===--- +Phone #: 275-555-0143 +Username: hax +Password: hunter2 + +* Modem implements a (very small) subset of 'Basic' commands as + described in the ITU-T V.250 spec (Table I.2) + +---=== THEIR SERVER ===--- + +Ground station IP: 93.184.216.34 +Ground station Phone #: 458-XXX-XXXX ...? +Username: ? +Password: ? + +* They use the same model of modem as mine... could use +++ATH0 + ping of death +* It'll have an interactive login similar to my server +* Their official password policy: minimum requirements of + FIPS112 (probably just numeric) + * TL;DR - section 4.1.1 of 'NBS Special Publication 500-137' +``` + +ITU-T V.250 is essentially a formalization of the [Hayes command +set](https://en.wikipedia.org/wiki/Hayes_command_set), so we can use basic Hayes commands to +interact with our local modem, such as +``` +ATDTXXXXXXXXXX - dial number XXX... +ATH0 - hang up ++++ - get the local modem's attention while in a remote session +``` + +The first step is to try to get information about the ground station server. We can decode the dial +tones from the audio file, which are +[DTMF](https://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling) tones. Once decoded we +obtain a phone number for the ground station of `458-555-0142`. However dialing this number results +in an error - `BUSY`. Presumably, the ground station is already dialed into somewhere, and we need +to disconnect it. + +The "ping of death" refers to injecting a modem command into a packet sent to a remote server in +order to cause the server's modem to interpret a hang up command contained in the packet. This can +be achieved by pinging with the data `+++ATH0`, but we need to escape it with hex to avoid having +our local modem hang up instead. Once in the session, we dial the number in the text file to get an +initial shell session + +``` +ATDT2755550143 +``` + +Next, issue a ping of death to the provided server IP +```bash +ping -v 0x2b2b2b415448290d 93.184.216.34 +``` + +Now the ground station should be disconnect so it is available for us to dial. + +``` ++++ATH0 +ATDT45845550142 +``` + +We get a login prompt for SATNET + +``` +* * . * * * . * * . * * . + . * * . * . . * . * + * +------------------------------+ + . | SATNET | * + +------------------------------+ . + . | UNAUTHORIZED ACCESS IS | + | STRICTLY PROHIBITED | +. +------------------------------+ . + . . + . + +Setting up - this will take a while... + +LOGIN +Username: +``` + +However we still need the username and password. Maybe the provided audio file has the credentials +somewhere in the dialup modem exchange. By analyzing the spectrum in Audacity (or any analyzer of +choice) we discover that it has peaks around 980 Hz, 1180 Hz, 1650 Hz, and 1850 Hz. This is +consistent with the [ITU V.21 standard](https://www.itu.int/rec/T-REC-V.21-198811-I/en) which uses +dual-channel Frequency Shift Keying at 300 bits/second. We can use +[minimodem](https://github.com/kamalmostafa/minimodem) to decode the modem traffic. We can provide +the two FSK frequencies (the "mark" and "space", representing each bit of the data) for channel 1 +and then for channel 2 to get both sides of the exchange. We also need to provide the bit rate. + +```bash +minimodem -8 -S 980 -M 1180 -f recording.wav 300 +minimodem -8 -S 1650 -M 1850 -f recording.wav 300 +``` + +This data looks like garbage but it contains some strings, notably `rocketman2674`. We assume from +the notes file that the password is a 4-digit number, but trying the username `rocketman` and +password `2674` didn't work. We need to look closer. This is the beginning of one side of the +exchange in hex: + +```hexdump +00000000: 7eff 7d23 c021 7d21 7d20 7d20 7d34 7d22 ~.}#.!}!} } }4}" +00000010: 7d26 7d20 7d20 7d20 7d20 7d25 7d26 28e5 }&} } } } }%}&(. +00000020: 4c21 7d27 7d22 7d28 7d22 e193 7e7e ff7d L!}'}"}(}"..~~.} +00000030: 23c0 217d 217d 217d 207d 347d 227d 267d #.!}!}!} }4}"}&} +00000040: 207d 207d 207d 207d 257d 2628 e54c 217d } } } }%}&(.L!} +``` + +It starts with `7eff`, which is characteristic of [Point-to-Point +Protocol](https://en.wikipedia.org/wiki/Point-to-Point_Protocol). We can decode the packets with +[scapy](https://github.com/secdev/scapy), a framework for network protocol analysis. However, first +we have to de-frame the PPP frames since there doesn't seem to be a tool for this automatically. +There are two main tasks, first split up the frames by the `7e` delimiters, and then remove the byte +stuffing within the frame, since PPP will escape certain bytes with the `7d` prefix followed by the +byte XOR `0x20`. Finally, the frame can be passed to scapy for analysis. This is a VERY lax +de-framer because sometimes frames seemed to not be started or terminated properly. + + def decode(ch): + buf2 = b"" + esc = False + + for x in ch: + if x == 0x7e: + if buf2 != b"\xFF" and buf2 != b"": + print(PPP(buf2).__repr__()) + buf2 = b"" + esc = False + elif esc: + esc = False + buf2 += bytes([x^0x20]) + elif x == 0x7d: + esc = True + else: + buf2 += bytes([x]) + + if len(buf2) > 0: + print(PPP(buf2).__repr__()) + +Now we can see what the packets mean. In particular, we spot these ones: + +``` + ===================== CH 1 + +, , , , , ] |>>> +>> + + ===================== CH 2 + +>> +>> +``` + +We can see in this exchange that the client has negotiated `MS-CHAP` authentication and then +authenticates to the server successfully. MS-CHAP uses NetNTLMv1 hashes, which can be cracked very +easily. We just need the username (`rocketman2674`), the "challenge" which is used as a salt for the +hash, and the hash itself. The format of the response in MS-CHAP (according to RFC2433) is 49 bytes, +including 24 bytes of stuff we ignore, 24 bytes of hash, and one byte of stuff we also ignore. We +can now convert the data into a [John-the-Ripper](https://www.openwall.com/john/) compatible hash +like + +``` +username:$NETNTLM$challenge$hash + +rocketman2674:$NETNTLM$12810ab88c7f1c74$6c2e3af0f2f77602e9831310b56924f3428b05ad60c7a2b4 +``` + +Technically, you can use hashcat as well but I didn't want to bother with the hashcat flags. Put +this hash in a text file and run `john file.txt`. No need to specify 4 digit pins because john will +complete in literal seconds anyway. + +``` +Proceeding with incremental:ASCII +9435 (rocketman2674) +1g 0:00:00:08 DONE 3/3 (2020-05-26 03:07) 0.1212g/s 10225Kp/s 10225Kc/s 10225KC/s 97xx..94b4 +Use the "--show --format=netntlm" options to display all of the cracked passwords reliably +Session completed +``` + +Use `rocketman2674` with password `9435` to log in to the ground station, then execute the `flag` +command to get the flag. + +## Resources and other writeups + + * + * + * + * + * + * diff --git a/comms/56k/decode.py b/comms/56k/decode.py new file mode 100644 index 0000000..43f442d --- /dev/null +++ b/comms/56k/decode.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +from scapy.all import * + +with open("ch1", "rb") as f: + ch1 = f.read() + +with open("ch2", "rb") as f: + ch2 = f.read() + +# print(PPP(ch2[0x16a:0x186]).show()) +# print(PPP(ch1[0x171:0x170+70]).show()) + +def decode(ch): + buf2 = b"" + esc = False + + for x in ch: + if x == 0x7e: + if buf2 != b"\xFF" and buf2 != b"": + print(PPP(buf2).__repr__()) + buf2 = b"" + esc = False + elif esc: + esc = False + buf2 += bytes([x^0x20]) + elif x == 0x7d: + esc = True + else: + buf2 += bytes([x]) + + if len(buf2) > 0: + print(PPP(buf2).__repr__()) + +print("\n", "=====================", "CH 1", "\n") +decode(ch1) +print("\n", "=====================", "CH 2", "\n") +decode(ch2)