I was expecting a keygen or licence key cracking based on the title of this challenge, so revisiting some old protocols that I haven’t used in anger for ages was a nice surprise. The challenge blurb was pretty minimal:
Challenge
Serial
nc misc.chal.csaw.io 4239
Solution
Connecting to the server returned a short message and a short string of binary:
8-1-1 even parity. Respond with ‘1’ if you got the byte, ‘0’ to retransmit.
01110011001
Having a long history in messing with computers plus years of hooking up to switches over RS232 protocols, I recognised a serial transmission scheme right away. 8-1-1 means 8 data bits, one parity bit and one stop bit. We’re also told that we’re looking for even parity. This should be a breeze.
The sharp eyed observer may notice that 8+1+1 adds up to 10 and there’s 11 bits in the first chunk we’re given to check. That’s because serial links also require a start bit. The line is pulled high or low from the idle state to signify the start of a transmission and the stop bit is usually the opposite of the start bit.
Given that most of the blocks from the server seem to start with ‘0’ and end with ‘1’ we can assume that these are valid start and stop bits.
This leaves nine bits; eight data bits and an even parity bit. Parity is a simple method for spotting errors in transmitted data. In even parity, the data bits are counted and if the total is an odd number, the parity bit is set to one to make the total even. If there are an even number of data bits, the parity bit is left at zero. Using this method allows minor bit flips in damaged data to be spotted and rejected. However, it is not completely reliable as if two bits get flipped in transit, the parity check will not spot it.
Manually decoding the data from the server showed that many blocks had parity errors and needed to be re-sent. Time to break out the Python to automate testing of the blocks and requesting retransmits.
import telnetlib
import sys
def main():
tn = telnetlib.Telnet('misc.chal.csaw.io', 4239)
# Read the intro
tn.read_until(b"retransmit.\n", 1.0)
# Validate messages and ask for retransmit if required
while True:
b = getMsg(tn)
if validate(b) is False:
tn.write(b'0')
else:
tn.write(b'1')
def getMsg(c):
r = c.read_until(b"WORDS WE NEVER SEE", 0.2)
return(r.decode('utf-8'))
def validate(bits):
start = bits[0]
parity = 0
stop = bits[10]
# Check start/stop bits
if start is not '0' or stop is not '1':
return False
# Calculate parity (including parity bit)
for b in range(1, 10):
if bits[b] is '1':
parity += 1
if parity%2 is 1:
return False
# Looks like a good block, convert to ASCII and print it
sys.stdout.write(chr(int(bits[1:9], 2)))
sys.stdout.flush()
return True
if __name__ == '__main__':
main()
The code is pretty simple. After skipping over the intro text, we grab a block, check that the start and stop bits are correct, then run a parity check. Assuming that both tests pass, we have a valid block and we can print the character and request the next block. Otherwise we send a re-transmit message and repeat the test until a valid block arrives.
The final output after error correction was: flag{@n_int3rface_betw33n_data_term1nal_3quipment_and_d@t@_circuit-term1nating_3quipment}
This was one of the longest flags I’ve seen in a CTF and took an age to retrieve.