Satellite hacking.

That’s it. That’s the pitch.

DDSAT-1

This year the folks at Defense Digital Service partnered with the Aerospace Village to bring four unique interactive workshops to DEFCON 28.

The DDSAT-1 challenge demonstrates how an unencrypted control channel can be abused to make satellites execute attacker supplied commands.

The challenge page gives us all the information needed to complete the workshop.

  1. Manchester encoded data is posted as if it has passed through a BPSK filter. No need to worry about RF.
  2. We need to reverse the message protocol in order to craft our own messages.
  3. If we’re successful we should be able to change the satellite telemetry, flash some lights, and move the camera.
  4. We can’t remove the ground station and so we’ll need to account for any corrections it attempts to make.

Decoding Messages

Messages between the ground station and the satellite are posted in Twitch. They look something like this.

!msg ZVpVplWWZVllVmZlWlVaVmVWWlZaZlpmplWlZpmaZak=

The !msg command is used to signal the Twitch bot. Base64 decoding the message gives us the following junk but we already know that data is Manchester encoded.

eZU¦U.eYeVfeZUZVeVZVZfZf¦U¥f..e©

Let’s write a function to decode Manchester data. Instead of worrying about bits in Python we’ll use string representations of binary numbers.

def manchester_decode(message):
    bit_map = {'0': '01',
               '1': '10'}
    bits = ''
    # Split the encoded bits into pairs.
    b = [message[i:i+2] for i in range(0, len(message), 2)]
    for i in b:
        for k, v in bit_map.items():
            if i == v:
                bits += k
    return bits

And an encoding function too.

def manchester_encode(message):
    bit_map = {'0': '01',
               '1': '10'}
    bits = ''
    # Split the string into a list of bits.
    b = [message[i:i+1] for i in range(0, len(message), 1)]
    for i in b:
        for k, v in bit_map.items():
            if i == k:
                bits += v
    return bits

Test it and make sure it works.

>>> manchester_decode("1001")
10
>>> manchester_encode("10")
1001

Now we need to decode a base station message. We’ll do this one byte at a time converting each byte to a binary string as we go.

def message_decode(message):
    # Base64 decode the message returns a python bytes object.
    message_bytes = base64.b64decode(message)
    decoded = ''
    # Convert each byte to a binary string.
    for i in message_bytes:
        b = "{0:08b}".format(i)
        decoded += manchester_decode(b)
    # Decoding from Manchester format means we've shortened the message length by half.
    decoded_message_len = len(message_bytes)//2
    # Turn it back into a Python bytes object.
    decoded_message = int(decoded, 2).to_bytes(decoded_message_len, byteorder='big')
    return decoded_message

>>> message_decode(message)
b'CN\nPAY02UCYANRk\x0b\xd0'

Looks like there’s something going on here!

Reversing the Message Protocol

The message protocol is very simple.

If we observe enough messages it becomes clear what we’re looking at. Here’s a sample for perspective.

b'C\xdb\tCAM01A108r\xb9\xf7\x1c'
b'C\xdc\x08CAM01A632h\x80\n'
b'C\xdd\x0cPAY02UYELLOW\xe2\x8a\x9e~'
b'C\xde\nPAY02AGOLDO\xf6\x80z'
b'C\xdf\x08BAT02A28\xf71,\xda'
b'C\xe0\tBAT01A180-\xbb\tr'
b'C\r\x08BAT01A82\xc4\xfc\xbc\x14'
b'C\x0e\x08BAT01A24\xf7\xea\x1c\xaf'
b'C\x0f\x07BAT01A4\xc8:\x19\xa8'
b'C\x10\x06PAY02Lo\xe8&\xe7'
b'C\x11\tCAM01A168\x9a\x1f\xc8\x16'
b'C*\tBAT01A167\x9f\xfe:\x9e'
b'Cn\x0cSATTETEMP100'

We see that each message is prefixed with 3 bytes, then is followed by some kind of payload, and is terminated with 4 bytes.

The first byte is always 0x43 which could stand for control or command but it doesn’t matter. What’s important is that this value is static and it is always present in messages sent from the base station to the satellite.

The next byte increments each time the base station sends a message. It’s a reasonable guess that this is a sequence number and so we’ll need to account for it later when we send our own messages up the satellite.

In a large enough sample of messages we’ll notice the third byte sometimes repeats itself. It does so when the payload is the same or very similar. Calling len() on the payload confirms that this byte indicates the payload length.

If you’re going through the trouble of putting a bird in the sky you probably want some assurance of message integrity. It’s a good guess that the last 4 bytes is a 32 bit checksum.

So far we have a pretty good idea of what the message header looks like and a reasonable guess about the checksum. We don’t yet understand the payload but we’re in a good enough place that we should be able to form messages the satellite can understand.

Let’s start a function to encode a message.

def message_encode(cmd):
    # This is the static "C" byte.
    header = b'\x43'
    # The sequence number.
    header += b'\x7c'
    # The length of the payload.
    header += bytes([len(cmd)])

    message = header + cmd
    # Convert to hex representation of binary data.
    # We'll come back to why this is the case later.
    hex_message = binascii.b2a_hex(message)
    # Generate the CRC.
    crc = binascii.crc32(hex_message)
    # Convert hex back to bytes.
    crc = (crc).to_bytes(4, byteorder='big')
    
    message += crc
    encoded = ''
    
    for i in message:
        b = "{0:08b}".format(i)
        manchester_encoded_byte = manchester_encode(b)
        encoded += manchester_encoded_byte
    
    # Manchester encode + base64. Manchester encoding doubles the message length.
    encoded_message = base64.b64encode(int(encoded, 2).to_bytes(len(message)*2, byteorder='big'))
    return encoded_message

Now we should be able to craft a valid message but there are two caveats; we need to find the correct sequence number, and we don’t know the payload format yet.

After a bit of trial and error we find that the satellite accepts any sequence greater than the last message received from the base station provided it’s not more than +0x20.

Let’s decode a message from the base station, increment the sequence by \x10 and pass it the same payload we just decoded. If message is not valid the satellite will complain with the following error.

b'ERROR, INVALID MESSAGE'

There’s a catch to how the CRC is calculated.

In Python, b'AAAA' is equivalent to b'\x41\x41\x41\x41'. What we need to do is calculate the CRC of the hex encoded binary representation of the message. This means if our payload is b'AAAA' we need to actually calculate the CRC for b'41414141' (which in Python is equivalent to b'\x52\x49\x52\x49\x52\x49\x52\x49').

Base Station Commands

Every so often the satellite responds to the base station with a status message. By decoding this message we can identify names of physical modules on the satellite.

PAY01TEMP: 30.3539 
PAY01LIGHT: 1904 
PAY02TEMP: 29.2744 
PAY02LIGHT: 1888 
SATTETEMP: 43.329 
BAT01ANGLE: 4 
BAT02ANGLE: 74 
CAM01ANGLE: 159

We’ve already seen the base station issue commands to these modules so now we try creating our own messages. Luckily, the satellite kindly informs us of our errors. If the command or arguments are not valid we’ll see this message.

b'Error, something has gone horribly wrong'

Yes. Yes it has.

Decode enough messages from the base station and we’ll notice that an A is used as a delimiter between the device name and integer argument it accepts (except for the lights). Payloads look like this.

[device type][device ID][b'A'][arg]

For example, if we want to move the solar panels we’ll issue a command that indicates the device and the position.

[b'BAT'][b'01'][b'A'][b'90']

Moon Shot

Our goal is to reposition the camera to take a photograph of the moon.

First, we’ll find the correct device name which is likely CAM01. To move the camera angle to 120 we would form a payload of CAM01A120.

Fix the sequence number and generate the message. Assuming your sequence is \x7c you should get the following message. ZVpqpVWWZVplVmWmWlVaVmVWWlZaWVpVmpmqpqmlVpU=

Play around with the lights, move the solar panels, have fun!