\newpage
* Attitude Adjustment
  :PROPERTIES:
  :CUSTOM_ID: attitude-adjustment
  :END:

*Category*: Astronomy, Astrophysics, Astrometry, Astrodynamics, AAAA
*Points (final)*: 69 points *Solves*: 62

#+BEGIN_QUOTE
  Our star tracker has collected a set of boresight reference vectors,
  and identified which stars in the catalog they correspond to. Compare
  the included catalog and the identified boresight vectors to determine
  what our current attitude is.

  Note: The catalog format is unit vector (X,Y,Z) in a celestial
  reference frame and the magnitude (relative brightness)
#+END_QUOTE

*Given files*: =attitude-papa21503yankee.tar.bz2=

** Writeup
   :PROPERTIES:
   :CUSTOM_ID: writeup
   :END:

by [[https://imer.in][erin (=barzamin=)]].

For this problem, we have two sets of N vectors which are paired; all
points in the first set are just those in the second set up to rotation;
we want to find the rotation which maps the first set onto the other
one. Since we already know which point in the observation set maps to
which vector in the catalog set, we can use the
[[https://en.wikipedia.org/wiki/Kabsch_algorithm][Kabsch algorithm]] to
find the rotation matrix (note that this is called an /orthogonal
Procrustes problem/). I'd only vaguely heard of the Kabsch algorithm
before, and in the context of bioinformatics, so I didn't immediately
identify it as a good path to the solution. Instead, I just googled
"/align two sets of vectors/", for which it's the third result.

Since nobody has time to implement computational geometry during a ctf,
I grabbed an existing Kabsch implementation. For some reason, I didn't
notice that =scipy.spatial= has
[[https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.align_vectors.html][a
Kabsch implementation]] built in, so I used some random external
project, [[https://github.com/charnley/rmsd][=rmsd=]].

First, load the star catalog:

#+BEGIN_SRC python
  catalog = {}
  with open('./attitude-papa21503yankee/test.txt') as f:
      i = 0
      for line in f:
          [x, y, z, m] = [float(s.strip()) for s in line.split(',')]
          catalog[i] = {'v': np.array([x,y,z]), 'm':m}
          i += 1
#+END_SRC

Set up some helpers for parsing the output of the challenge server and
solving an orientation:

#+BEGIN_SRC python
  def parse_stars(stardata):
      stars = {}
      for line in stardata.strip().split('\n'):
          line = line.strip()
          star_id = int(line.split(':')[0].strip())
          direction = np.array([float(x) for x in line.split(':')[1].split(',\t')])
          stars[star_id] = direction
      return stars

  def solve_orientation(stars, catalog):
      P = np.vstack(list(stars.values()))
      Q = np.vstack([catalog[i]['v'] for i in stars.keys()])
      print("rmsd: {}".format(calculate_rmsd.kabsch_rmsd(P,Q)))
      rotation_mtx = calculate_rmsd.kabsch(P, Q)
      rotation = Rotation.from_matrix(np.linalg.inv(rotation_mtx))
      return rotation
#+END_SRC

Note that I threw in an inversion of the rotation matrix; this is
because I should've been aligning from the catalog /to/ the current star
locations. Switching P to be the catalog and Q to be stars would've done
the same thing.

Then we just grabbed each challenge from the computer, aligned the sets,
and spat the orientation of the satellite back at the server:

#+BEGIN_SRC python
  TICKET = 'THE_TICKET'
  r = tubes.remote.remote('attitude.satellitesabove.me', 5012)
  r.send(TICKET+'\n')
  time.sleep(0.5)
  for _ in range(20):
      r.recvuntil(b'--------------------------------------------------\n', drop=True)
      stars = parse_stars(r.recv().decode())
      rotation = solve_orientation(stars, catalog)
      r.send(','.join([str(x) for x in rotation.as_quat()]) + '\n')
      time.sleep(0.1)
  print(r.clean())
#+END_SRC

The flag should get printed out on stdout by the final line.

*** Full code
    :PROPERTIES:
    :CUSTOM_ID: full-code
    :END:

#+BEGIN_SRC python
  import numpy as np
  from pwnlib import tubes
  import time
  import matplotlib.pyplot as plt
  from rmsd import calculate_rmsd
  from scipy.spatial.transform import Rotation

  catalog = {}
  with open('./attitude-papa21503yankee/test.txt') as f:
      i = 0
      for line in f:
          [x, y, z, m] = [float(s.strip()) for s in line.split(',')]
          catalog[i] = {'v': np.array([x,y,z]), 'm':m}
          i += 1

  def parse_stars(stardata):
      stars = {}
      for line in stardata.strip().split('\n'):
          line = line.strip()
          star_id = int(line.split(':')[0].strip())
          direction = np.array([float(x) for x in line.split(':')[1].split(',\t')])
          stars[star_id] = direction
      return stars

  def solve_orientation(stars, catalog):
      P = np.vstack(list(stars.values()))
      Q = np.vstack([catalog[i]['v'] for i in stars.keys()])
      print("rmsd: {}".format(calculate_rmsd.kabsch_rmsd(P,Q)))
      rotation_mtx = calculate_rmsd.kabsch(P, Q)
      rotation = Rotation.from_matrix(np.linalg.inv(rotation_mtx))
      return rotation

  TICKET = 'THE_TICKET'
  r = tubes.remote.remote('attitude.satellitesabove.me', 5012)
  r.send(TICKET+'\n')
  time.sleep(0.5)
  for _ in range(20):
      r.recvuntil(b'--------------------------------------------------\n', drop=True)
      stars = parse_stars(r.recv().decode())
      rotation = solve_orientation(stars, catalog)
      r.send(','.join([str(x) for x in rotation.as_quat()]) + '\n')
      time.sleep(0.1)
  print(r.clean())
#+END_SRC

** Resources and other writeups
   :PROPERTIES:
   :CUSTOM_ID: resources-and-other-writeups
   :END:

- [[https://en.wikipedia.org/wiki/Orthogonal_Procrustes_problem]]
- [[https://en.wikipedia.org/wiki/Kabsch_algorithm]]
- [[https://github.com/charnley/rmsd/tree/master]]

\newpage
* Digital Filters, Meh
  :PROPERTIES:
  :CUSTOM_ID: digital-filters-meh
  :END:

*Category*: Astronomy, Astrophysics, Astrometry, Astrodynamics, AAAA
*Points (final)*: 104 points *Solves*: 37

#+BEGIN_QUOTE
  Included is the simulation code for the attitude control loop for a
  satellite in orbit. A code reviewer said I made a pretty big mistake
  that could allow a star tracker to misbehave. Although my code is
  flawless, I put in some checks to make sure the star tracker can't
  misbehave anyways.

  Review the simulation I have running to see if a startracker can still
  mess with my filter. Oh, and I'll be giving you the attitude of the
  physical system as a quaternion, it would be too much work to figure
  out where a star tracker is oriented from star coordinates, right?
#+END_QUOTE

*Given files*: =src.tar.gz=.

** Writeup
   :PROPERTIES:
   :CUSTOM_ID: writeup-1
   :END:

by [[https://imer.in][erin (=barzamin=)]].

As part of this challenge, we're given a file =src.tar.gz= by the
scoreboard. This contains Octave code which simulates the satellite
kinetics and control loop, and, presumably, is what's running on the
challenge server.

Digging into the main file, =challenge.m=, we note a few interesting
things. The satellite is running a Kalman filter on the gyroscope data
(velocity and acceleration) and star tracker orientation data. Near the
start of each control system iteration, there's a check on the Kalman
filter error; if greater than a threshold, the controller crashes out:

#+BEGIN_SRC matlab
  % Check error bounds
  if max(abs(err)) > err_thresh
      disp("Error: Estimator error too large... Goodbye");
      disp(q_est);
      disp(target.q_att);
      break;
  endif
#+END_SRC

Note =err_thresh= is defined upscript as

#+BEGIN_SRC matlab
  err_thresh = 1*pi/180. ;   % 1 Degree error max
#+END_SRC

Further down, we see that a PID update step on for the correction is
only run every five iterations; this is weird, but doesn't help us as
far as we could tell. Even further downscript, we see that every
iteration, a check is performed between target and actual orientations
(note that /actual orientation/ means, here, the true simulated physical
pose of the satellite):

#+BEGIN_SRC matlab
  % Check we're still safe...
  [v,a] = q2rot(quat_diff(actual.q_att, target.q_att));
  if abs(v(2)*a) >  (pi/8)
      disp("Uh oh, better provide some information!");
      disp(getenv("FLAG"))
      break;
  endif
#+END_SRC

If this check is true (ie, there's >π/8 radians of rotation error on the
Y axis), we get the flag! So the challenge here is making the satellite
think it's drifted when it hasn't, without making the Kalman filter
angry. How can we do that?

The star filter observations are pulled right after the previous check,
with the following code:

#+BEGIN_SRC matlab
  % Get Observations
  q_att = startracker(actual);
#+END_SRC

Looking inside =startracker()=, we see that it pretty clearly indicates
that we /are/ the star tracker; every timestep, the code tells us, the
adversary, what the true physical orientation is as a quaternion. We can
act as the star tracker and send back a wxyz-format quaternion on stdin,
which it will use as the star-tracker output (note that, for some
reason, the code they give us uses space-separated floats and the actual
challenge uses comma-separated floats):

#+BEGIN_SRC matlab
  % Model must have a q_att member

  function [ q ] = startracker(model) 
    q = model.q_att;
    disp([q.w, q.x, q.y, q.z]);
    fflush(stdout);
    % Get Input
    q = zeros(4,1);
    for i = 1:4
      q(i) = scanf("%f", "C");
    endfor 
    q = quaternion(q(1), q(2), q(3), q(4));
    %q.w = q.w + normrnd(0, 1e-8);
    %q.x = q.x + normrnd(0, 1e-8);
    %q.y = q.y + normrnd(0, 1e-8);
    %q.z = q.z + normrnd(0, 1e-8);
    
    q = q./norm(q);
    
  endfunction
#+END_SRC

Also note that in =challenge.m=, immediately after the star tracker
call, checking the return value for consistency with the physical model
is /commented out/. We can tell the satellite that it's pointing
anywhere we like and it will believe us, although Kalman error might be
bad:

#+BEGIN_SRC matlab
  %err = quat2eul(quat_diff(q_att, target.q_att))';
  %if max(abs(err)) > err_thresh
  %    disp("Error: No way, you are clearly lost, Star Tracker!");
  %    break;
  %endif
#+END_SRC

So we control the star tracker. What can we do?

We immediately noticed the vast count of =eul2quat()= and =quat2eul()=
calls and wasted so much time trying to get something to gimbal lock.
Turns out this problem is deceptively easy, and you don't need to do
that at all.

We can't make the discrepancy between the true position and what the
star tracker says too great, nor make it vary quickly; the gyroscope is
reporting gaussian noise close to zero with very low variance, so any
big delta in star tracker orientation will incur error in the Kalman
filter. So what can we do?

Turns out all we have to do hold the Y orientation constant and report
that. The satellite's true Y euler angle gradually rotates over time due
to system dynamics, accumulating controller error on the Y axis, and we
eventually get the flag.

Hook up to the server:

#+BEGIN_SRC python
  sep = ','
  r = remote('filter.satellitesabove.me', 5014)
  r.clean()
  r.send('THE_TICKET')
  time.sleep(0.1)
#+END_SRC

Write a little function that pretends to be the star tracker (note:
=lie= was determined by playing with the local simulator a bunch):

#+BEGIN_SRC python
  def adversary(true_pose):
      lie = 0.25

      euler = true_pose.as_euler('xyz')
      euler[1] = lie

      return R.from_euler('xyz',euler)
#+END_SRC

And talk to the server, pretending to be the tracker every indication,
until we see a string indicating we got the flag:

#+BEGIN_SRC python
  while True:
      rl = r.readline(timeout=3)
      if rl.startswith(b'Uh oh,'):
          r.interactive()
      log.info(f'<== [{i}] {rl}')
      
      [w,x,y,z] = [float(x) for x in rl.decode().strip().split()]
      true_pose = R.from_quat([x,y,z,w])

      new_pose = adversary(true_pose)

      [x,y,z,w] = new_pose.as_quat()
      msg = sep.join(map(str,[w,x,y,z]))
      log.info(f'==> {msg}')
      r.send(msg+'\n')
#+END_SRC

When we see that string, the script jumps to =pwnlib.tubes=' interactive
mode and we see the flag in the dumped buffer.

*** Full code
    :PROPERTIES:
    :CUSTOM_ID: full-code-1
    :END:

#+BEGIN_SRC python
  import numpy as np
  import matplotlib.pyplot as plt
  from pwn import *
  from scipy.spatial.transform import Rotation as R
  import time

  q_att_ts = []
  badnesses = []

  LOCAL = False

  if LOCAL:
      sep = ' '
      r = process('octave challenge.m', shell=True)
  else:
      sep = ','
      r = remote('filter.satellitesabove.me', 5014)
      r.clean()
      r.send('THE_TICKET')
      time.sleep(0.1)

  def adversary(true_pose):
      lie = 0.25

      euler = true_pose.as_euler('xyz')
      euler[1] = lie

      return R.from_euler('xyz',euler)


  for i in range(10000):
      if LOCAL:
          badness = float(r.readline().decode().strip())
          log.info(f'[!] badness: {badness}')
          badnesses.append(badness)

      rl = r.readline(timeout=3)
      if rl.startswith(b'Uh oh,'):
          r.interactive()
      log.info(f'<== [{i}] {rl}')
      
      [w,x,y,z] = [float(x) for x in rl.decode().strip().split()]
      true_pose = R.from_quat([x,y,z,w])

      new_pose = adversary(true_pose)

      [x,y,z,w] = new_pose.as_quat()
      msg = sep.join(map(str,[w,x,y,z]))
      log.info(f'==> {msg}')
      r.send(msg+'\n')
#+END_SRC

\newpage
* Seeing Stars
  :PROPERTIES:
  :CUSTOM_ID: seeing-stars
  :END:

*Category*: Astronomy, Astrophysics, Astrometry, Astrodynamics, AAAA
*Points (final)*: 23 *Solves*: 213

#+BEGIN_QUOTE
  Here is the output from a CCD Camera from a star tracker, identify as
  many stars as you can! (in image reference coordinates) Note: The
  camera prints pixels in the following order (x,y): (0,0), (1,0),
  (2,0)... (0,1), (1,1), (2,1)...

  Note that top left corner is (0,0)
#+END_QUOTE

** Write-up
   :PROPERTIES:
   :CUSTOM_ID: write-up
   :END:

by [[https://qtp2t.club/][hazel (=arcetera=)]]

The CCD image given by the netcat is a 128x128 matrix of comma-separated
values.

We read the data into a NumPy array, and pass that into OpenCV.

#+BEGIN_SRC python
  data = []
  for line in rawdat.strip().split('\n'):
      data.append([int(x) for x in line.split(',')])

  x = np.array(data, dtype='uint8').T

  im = x
#+END_SRC

We then run a filter on the data, only grabbing values in [127, 255] to
filter out data that is /obviously not/ stars. We then run two dilates
on the image post-filter, because otherwise we end up with a division by
zero on centroid finding later for =M["m00"]=. Finally, we grabbed the
contour of every object visible in the image.

#+BEGIN_SRC python
  ret, thresh = cv2.threshold(im.copy(), 127, 255, 0)
  kernel = np.ones((5, 5), np.uint8)
  dilated = cv2.dilate(thresh.copy(), kernel, iterations = 2)

  cnts, hier = cv2.findContours(dilated.copy(), \
                                cv2.RETR_TREE, \
                                cv2.CHAIN_APPROX_NONE)
#+END_SRC

For each contour, we grabbed its centroid:

#+BEGIN_SRC python
  solve = ''
  for c in cnts:
      M = cv2.moments(c)
      cX = int(M["m10"] / M["m00"])
      cY = int(M["m01"] / M["m00"])

      solve += (str(cX) + "," + str(cY)+'\n')
  return solve
#+END_SRC

We then automated this entire process using pwnlib to connect to the
server and read the data.

*** Full code
    :PROPERTIES:
    :CUSTOM_ID: full-code-2
    :END:

#+BEGIN_SRC python
  #!/usr/bin/env python3
  import cv2
  import math
  import numpy as np
  from pwnlib import tubes
  import time

  def solve(rawdat):
      data = []
      for line in rawdat.strip().split('\n'):
          data.append([int(x) for x in line.split(',')])

      x = np.array(data, dtype='uint8').T

      im = x # cv2.imread("output.png", cv2.IMREAD_GRAYSCALE)
      ret, thresh = cv2.threshold(im.copy(), 127, 255, 0)
      kernel = np.ones((5, 5), np.uint8)
      dilated = cv2.dilate(thresh.copy(), kernel, iterations = 2)

      cnts, hier = cv2.findContours(dilated.copy(), \
                                    cv2.RETR_TREE, \
                                    cv2.CHAIN_APPROX_NONE)

      edit = thresh.copy()
      cv2.drawContours(edit, cnts, -1, (0, 255, 0), 3)

      solve = ''
      for c in cnts:
          M = cv2.moments(c)
          cX = int(M["m10"] / M["m00"])
          cY = int(M["m01"] / M["m00"])

          solve += (str(cX) + "," + str(cY)+'\n')
      return solve

  TICKET = 'THE_TICKET'
  r = tubes.remote.remote('stars.satellitesabove.me', 5013)
  r.recvline()
  r.send(TICKET+'\n')
  going = True
  while going:
      rawdat = r.recvuntil('Enter', drop=True)
      time.sleep(0.5)
      r.clean()
      solution = solve(rawdat.decode())
      r.send(solution+'\n')
      time.sleep(0.1)
      if r.recvuntil('Left...\n') == b'0 Left...\n':
          time.sleep(0.1)
          print(r.clean())
#+END_SRC

Run it, and the flag should be printed as a bytestring.

** Resources and other writeups
   :PROPERTIES:
   :CUSTOM_ID: resources-and-other-writeups-1
   :END:

- [[https://docs.opencv.org/trunk/d9/d61/tutorial_py_morphological_ops.html]]
- [[https://docs.opencv.org/trunk/dd/d49/tutorial_py_contour_features.html]]

\newpage
* 56K Flex Magic
  :PROPERTIES:
  :CUSTOM_ID: k-flex-magic
  :END:

*Category:* Communication Systems *Points (final):* 205 *Solves:* 13

#+BEGIN_QUOTE
  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.
#+END_QUOTE

** Write-up
   :PROPERTIES:
   :CUSTOM_ID: write-up-1
   :END:

by [[https://awoo.systems][haskal]]

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.

#+BEGIN_EXAMPLE
  ---=== 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'
#+END_EXAMPLE

ITU-T V.250 is essentially a formalization of the
[[https://en.wikipedia.org/wiki/Hayes_command_set][Hayes command set]],
so we can use basic Hayes commands to interact with our local modem,
such as

#+BEGIN_EXAMPLE
  ATDTXXXXXXXXXX - dial number XXX...
  ATH0           - hang up
  +++            - get the local modem's attention while in a remote session
#+END_EXAMPLE

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
[[https://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling][DTMF]]
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=, because as the server
replies to the ping with the same data, its local modem will interpret
the command inside the ping. 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

#+BEGIN_EXAMPLE
  ATDT2755550143
#+END_EXAMPLE

Next, issue a ping of death to the provided server IP

#+BEGIN_SRC sh
  ping -v 0x2b2b2b415448290d 93.184.216.34
#+END_SRC

Now the ground station should be disconnected so it is available for us
to dial.

#+BEGIN_EXAMPLE
  +++ATH0
  ATDT45845550142
#+END_EXAMPLE

We get a login prompt for SATNET

#+BEGIN_EXAMPLE
  *   *   . *  *    * .   *   * . *   *  .  
      .  *   *   .    * .    . *      .  *  
   *   +------------------------------+     
     . |            SATNET            |   * 
       +------------------------------+ .   
    .  |    UNAUTHORIZED ACCESS IS    |     
       |     STRICTLY PROHIBITED      |     
  .    +------------------------------+   . 
           .             .                  
                                    .       
                                            
  Setting up - this will take a while...

  LOGIN
  Username: 
#+END_EXAMPLE

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
[[https://www.itu.int/rec/T-REC-V.21-198811-I/en][ITU V.21 standard]]
which uses dual-channel Frequency Shift Keying at 300 bits/second. We
can use [[https://github.com/kamalmostafa/minimodem][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.

#+BEGIN_SRC sh
  minimodem -8 -S 980 -M 1180 -f recording.wav 300
  minimodem -8 -S 1650 -M 1850 -f recording.wav 300
#+END_SRC

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:

#+BEGIN_EXAMPLE
  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!}
#+END_EXAMPLE

It starts with =7eff=, which is characteristic of
[[https://en.wikipedia.org/wiki/Point-to-Point_Protocol][Point-to-Point
Protocol]]. We can decode the packets with
[[https://github.com/secdev/scapy][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.

#+BEGIN_SRC python
  def decode(ch):
      buf2 = b""
      esc = False

      for x in ch:
          if x == 0x7e:
              if buf2 != b"\xFF" and buf2 != b"":
                  PPP(buf2).show()
              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:
          PPP(buf2).show()
#+END_SRC

(This code is really awful CTF code, please ignore the 200 awful
spaghetti things I'm doing in this snippet.)

Now we can see what the packets mean. In particular, we spot these ones:

#+BEGIN_EXAMPLE
  ###[ HDLC ]### 
    address   = 0xff
    control   = 0x3
  ###[ PPP Link Layer ]### 
       proto     = Link Control Protocol
  ###[ PPP Link Control Protocol ]### 
          code      = Configure-Ack
          id        = 0x2
          len       = 28
          \options   \
  .....
           |###[ PPP LCP Option ]### 
           |  type      = Authentication-protocol
           |  len       = 5
           |  auth_protocol= Challenge-response authentication protocol
           |  algorithm = MS-CHAP
  .....

  ###[ PPP Link Layer ]### 
    proto     = Challenge Handshake Authentication Protocol
  ###[ PPP Challenge Handshake Authentication Protocol ]### 
       code      = Response
       id        = 0x0
       len       = 67
       value_size= 49
       value     = 0000000000000000000000000000000000000000000000006c2e3af0f2f7760
  2e9831310b56924f3428b05ad60c7a2b401
       optional_name= 'rocketman2674'
#+END_EXAMPLE

and

#+BEGIN_EXAMPLE
  ###[ PPP Link Layer ]### 
    proto     = Challenge Handshake Authentication Protocol
  ###[ PPP Challenge Handshake Authentication Protocol ]### 
       code      = Challenge
       id        = 0x0
       len       = 26
       value_size= 8
       value     = 12810ab88c7f1c74
       optional_name= 'GRNDSTTNA8F6C'

  ###[ PPP Link Layer ]### 
    proto     = Challenge Handshake Authentication Protocol
  ###[ PPP Challenge Handshake Authentication Protocol ]### 
       code      = Success
       id        = 0x0
       len       = 4
       data      = ''
#+END_EXAMPLE

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
[[https://www.openwall.com/john/][John-the-Ripper]] compatible hash like

#+BEGIN_EXAMPLE
  username:$NETNTLM$challenge$hash

  rocketman2674:$NETNTLM$12810ab88c7f1c74$6c2e3af0f2f77602e9831310b56924f3428b05ad60c7a2b4
#+END_EXAMPLE

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.

#+BEGIN_EXAMPLE
  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
#+END_EXAMPLE

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
   :PROPERTIES:
   :CUSTOM_ID: resources-and-other-writeups-2
   :END:

- [[https://en.wikipedia.org/wiki/Hayes_command_set]]
- [[https://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling]]
- [[https://github.com/kamalmostafa/minimodem]]
- [[https://en.wikipedia.org/wiki/Point-to-Point_Protocol]]
- [[https://tools.ietf.org/html/rfc2433]]
- [[https://www.openwall.com/john/]]

\newpage
* Phasors to Stun
  :PROPERTIES:
  :CUSTOM_ID: phasors-to-stun
  :END:

*Category:* Ground Segment *Points (final):* 62 *Solves:* 71

#+BEGIN_QUOTE
  Demodulate the data from an SDR capture and you will find a flag. It
  is a wav file, but that doesn't mean its audio data.
#+END_QUOTE

** Write-up
   :PROPERTIES:
   :CUSTOM_ID: write-up-2
   :END:

by [[https://awoo.systems][haskal]]

The provided WAV file contains a signal that looks like this:

[[file:signal.png]]

This looks suspiciously like Phase Shift Keying (PSK) and it's a very
clean signal (this is also hinted at by the challenge name). We can use
[[https://github.com/jopohl/urh][Universal Radio Hacker]] to demod this
with very little effort.

[[file:urh1.png]]

Select PSK modulation, then click "Autodetect parameters". Then move to
Analysis:

[[file:urh2.png]]

We discovered that the signal is NRZI (non-return-to-zero inverted)
coded, and after selecting this in URH the flag is decoded in the data
view.

** Resources and other writeups
   :PROPERTIES:
   :CUSTOM_ID: resources-and-other-writeups-3
   :END:

- [[https://github.com/jopohl/urh]]
- [[https://en.wikipedia.org/wiki/Phase-shift_keying]]
- [[https://en.wikipedia.org/wiki/Non-return-to-zero#NRZI]]

\newpage
** Leaky Crypto
   :PROPERTIES:
   :CUSTOM_ID: leaky-crypto
   :END:

Many optimized implementations of AES utilize lookup tables to combine
all steps of each round of the algorithm (SubBytes, ShiftRows,
MixColumns, AddKey) into a single operation. For some X (the plaintext
or the result from the previous round) and some K (the round key), they
are split bytewise and the XOR product of each respective byte pair is
used as the index into a lookup table. During the first round of AES, X
is the plaintext of the message, and K is the original message key.
Accordingly, given some known plaintext, leaking the index into the
lookup table for a particular character leaks the corresponding key
byte. There are four lookup tables which are used in each iteration of
AES (besides the last round) and which is used is determined by the
index of the byte MOD 4. We utilized
[[http://www.jbonneau.com/doc/BM06-CHES-aes_cache_timing.pdf][this
paper]] as a reference for both our understanding of AES and the attack
we will detail below.

Many CPUs cache RAM accesses so as to speed up subsequent accesses to
the same address. This is done because accessing RAM is quite slow, and
accessing cache is quite fast. This behavior would imply that on systems
which implement such caching methods, there is a correlation between the
amount of time it takes to encrypt a particular plaintext and the
occurrences of repeated values of a plaintext byte XORd with a key byte.
Accordingly, for every i, j, pi ⊕ pj in a family (with i, j being byte
indexes, p being the plaintext, and families corresponding to which
lookup table is being used), we calculate the average time to encrypt
such a message over all messages. We then determine if for any pair of
characters pi, pj there is a statistically significant shorter
encryption time compared to the average. If so, we can conclude that i ⊕
ki = pj ⊕ kj => pi ⊕ pj = ki ⊕ kj. From this information, we gain a set
of redundant system of equations relating different key bytes at
different indexes with each other. It is important to note that in order
for this attack to work, we must know at least one key byte in each
family in order to actually solve each system of equations.
Additionally, due to how cache works, this attack only leaks the most
significant q bits (q being related to the number of items in a cache
line). Once the set of possible partial keys (accounting for the
ambiguity in the least significant bits of each derived byte) has been
obtained by the above method, an attacker may brute force the remaining
unknown key bytes.

In the case of Leaky Crypto, a set of 100,000 plaintexts and
corresponding encryption times is provided along with the first six
bytes of the encryption key. We ran an analyzer program[^1] against
these plaintexts to obtain the probable correlation between different
indexes in the key with respect to the XOR product of those bytes with
plaintext bytes. Per the above, the plaintexts and timing data provided
enough information to derive the systems of equations which may be used
to solve for key bytes, and the first 6 bytes of the key provided enough
information to actually solve said systems of equations. Given the
ambiguity of the low bits of each derived key byte, we obtained 214
partial keys with three unknown bytes each. Thus, we reduced the problem
of guessing 2128 bits to guessing only 238 bits. We fed our derived
partial keys into [[https://github.com/pgarba/Hulk][Hulk]] to brute
force the remaining bytes for each candidate partial key. After 30
minutes had passed, we successfully brute forced the key.

#+BEGIN_SRC python
      from itertools import combinations
      import matplotlib.pyplot as plt
      import numpy as np

      def find_outliers(corpus, num_samps, i, j):
          idxs = corpus[i][j].argsort()[:num_samps]
          return idxs
      
      def guess_bytes(corpus, known_keybytes, num_samps, avg):
          candidates = []
          for base in range(4):
              family = [base, base + 4, base + 8, base + 12]
              for combo in combinations(family, 2):
                  i,j = combo
                  guesses = find_outliers(corpus, num_samps, i, j)
                  guesses2 = []
                  for guess in guesses:
                      cnt = corpus[i][j][guess]
                      if cnt-avg < -10:
                          guesses2.append((i, j, guess, cnt-avg))
                      print(i, j, guess, cnt - avg)
                  candidates.append(tuple(guesses2))
          print(candidates)
              
      if __name__ == '__main__':
          known_keybytes = bytes.fromhex("64c7072487f2")
          secret_data = "c1a5fe7beb2c70bfab98926627dcff8b9671edc52441....."
      
          data = set()
          with open("test.txt", "r") as fp:
              for line in fp:
                  pt, timing = line.strip().split(',')
                  pt = bytes.fromhex(pt)
                  timing = int(timing)
                  data.add((pt, timing))
              
          tavg = sum((d[1] for d in data)) / len(data)
          print("tavg: %d" % tavg)
      
          known_tly = np.zeros((16, 16, 256))
      
          for base in range(4):
              print("Building corpus for family %d" % base)
              family = [base, base + 4, base + 8, base + 12]
              for combo in combinations(family, 2):
                  times = np.zeros(256)
                  counts = np.zeros(256)
                  i,j = combo
                  print("Working on %d, %d" % (i, j))
                  for d in data:
                      n = d[0][i] ^ d[0][j]
                      c = d[1]
                      times[n] += c
                      counts[n] += 1
                  for c in range(256):
                      cnorm = times[c] / counts[c]
                      known_tly[i][j][c] = cnorm
                      known_tly[j][i][c] = cnorm
                  
          guess_bytes(known_tly, known_keybytes, 4, tavg)
#+END_SRC

\newpage
* Bytes Away!
  :PROPERTIES:
  :CUSTOM_ID: bytes-away
  :END:

*Category:* Satellite Bus *Points (final):* 223 *Solves:* 11

#+BEGIN_QUOTE
  We have an encrypted telemetry link from one of our satellites but we
  seem to have lost the encryption key. Thankfully we can still send
  unencrypted commands using our Cosmos interface (included). I've also
  included the last version of kit_to.so that was updated to the
  satellite. Can you help us restore communication with the satellite so
  we can see what error "flag" is being transmitted?
#+END_QUOTE

** Write-up
   :PROPERTIES:
   :CUSTOM_ID: write-up-3
   :END:

by [[https://awoo.systems][haskal]]

Two files are provided for this challenge, one contains the =kit_to.so=
and the other contains a full [[https://cosmosrb.com/][COSMOS]]
directory tree for accessing the virtual satellite, which can be booted
up with the provided netcat endpoint. COSMOS is an open-source command
and control framework for satellites using
[[https://cfs.gsfc.nasa.gov/][NASA's Core Flight System]]. The provided
COSMOS directory contains everything we need to interact with the
virtual satellite, and the =kit_to.so= is part of the code that runs
onboard the actual satellite. Booting up COSMOS is enormously
complicated, so Docker can be used to automate the setup. We adapted the
Ball Aerospace COSMOS Docker image, and created a script to configure
COSMOS to connect to the CTF's satellite instance automatically by
writing the configuration file at
=cosmos/config/tools/cmd_tlm_server/cmd_tlm_server.txt=. When COSMOS is
successfully connected to the CTF instance it looks like this (no themes
were installed in the Docker container so it looks like Windows 95, I'm
so sorry,)

[[file:COSMOS.png]]

COSMOS can be used to send commands with the Command Sender, and we can
send for example a command for ENABLE_TELEMETRY, which causes the
satellite to start sending telemetry. However these are encrypted, so
COSMOS cannot understand them.

[[file:COSMOS_enable_telemetry.png]]

We also discover another present subsystem called =MM=, which allows for
reading and writing arbitrary memory on the satellite (how useful!) as
well as interacting with memory by symbols (extremely useful!).

[[file:COSMOS_MM.png]]

The provided =kit_to.so= contains the code used by the satellite to
transmit telemetry to COSMOS. We used
[[https://ghidra-sre.org/][Ghidra]] to analyze the binary (which
helpfully includes symbols and debugging information, and that makes our
lives way easier for this problem). We discovered that it uses AES CBC
with a key and IV retrieved with external functions =get_key= and
=get_iv= that are not present in the binary. However, these are stored
in known locations in memory, which means it would be possible to read
the AES key and IV using the PEEK_MEM command in COSMOS and then decrypt
the telemetry packets, but there's an easier way. The code contains a
function =KIT_TO_SendFlagPkt= which (as you might guess) sends the flag
via encrypted telemetry, and this also writes the flag as an
intermediate value to a known memory location. PIE is enabled for this
binary, however since the PEEK_MEM command allows looking up memory by
symbol name the address randomization is very trivially bypassed.

[[file:ghidra.png]]

Inspecting the structure of =KitToFlagPkt= shows that the flag is
located at offset 12 and is (up to) 200 bytes long. We created a Ruby
script in the COSMOS Script Runner to execute PEEK_MEM commands for each
byte in the flag range, based on the command COSMOS outputs to the
console when running the command manually in the GUI. Note that in order
for the function =KIT_TO_SendFlagPkt= to be called at all, we must first
run the ENABLE_TELEMETRY command even though we're not going to look at
any actual telemetry.

#+BEGIN_SRC ruby
  12.upto(212) { |off|
    offset = off
    cmd("MM PEEK_MEM with CCSDS_STREAMID 6280, CCSDS_SEQUENCE 49152, CCSDS_LENGTH 73, "
        + "CCSDS_FUNCCODE 2, CCSDS_CHECKSUM 0, DATA_SIZE 8, MEM_TYPE 1, PAD_16 0, "
        + "ADDR_OFFSET #{offset}, ADDR_SYMBOL_NAME 'KitToFlagPkt'")
  }
#+END_SRC

This directly prints the flag to the console, simply decode the hex to
get the flag value.

** Resources and other writeups
   :PROPERTIES:
   :CUSTOM_ID: resources-and-other-writeups-4
   :END:

- [[https://cosmosrb.com/]]
- [[https://cfs.gsfc.nasa.gov/]]
- [[https://ghidra-sre.org/]]

\newpage
* Magic Bus
  :PROPERTIES:
  :CUSTOM_ID: magic-bus
  :END:

*Category*: Satellite Bus *Points (final)*: 91 *Solves*: 44 (this number
taunts me)

/Important note:/ Team BLAHAJ did not solve this problem until after the
competition, and it did not count toward our final point total.

#+BEGIN_QUOTE
  There's a very busy bus we've tapped a port onto, surely there is some
  juicy information hidden in the device memory... somewhere...
#+END_QUOTE

** Write-up
   :PROPERTIES:
   :CUSTOM_ID: write-up-4
   :END:

by [[https://qtp2t.club/][hazel (=arcetera=)]]

*I hate this problem. I hate this problem. I hate this problem. I hate
this problem. I literally hate this problem so much. This problem made
me cry. I have literally no words to describe the exact extent to which
this problem has driven me insane. This problem taunts me in my sleep.
This problem taunts me while I am awake. The extent to which I despise
this problem is beyond words. I hate this. I hate whoever made this. I
want to burn this problem to the ground. This problem has achieved
active sentience and holds malice against me and the rest of my team
specifically. Had the competition not ended, this problem would hold the
rest of the world hostage.*

Furthermore, much of this writeup is /failed/ attempts at a solution.
Other writeups may be more useful at determining success, despite our
team eventually finding a solution.

...anyway...

When netcatting into the server, a series of hex bytes appears. A
cursory analysis of these bytes reveals that all packets start with =^=
and end with =.=, aside from lines starting with byte CA. Decoding the
data beginning with byte CA reveals some 🧃🧃🧃. This output has =\xca\x00=
stripped:

#+BEGIN_EXAMPLE
  b'\xb2M*\xf9H\xacyvQ}\xd4\xf2\xa0\xcd\xc9Juicy Data 03\x00M\xae@\x9a\xd89\xe2\x85\xb2Y\xd6/-\xc9\xd0\xfb\x92\xd2\xc4Y\xaa[ B\xc6\xb5'
#+END_EXAMPLE

I prefer water though. #WaterDrinkers

While I was asleep, the rest of the team managed to reverse the protocol
to a decent extent. Namely, the format for strings beginning with
=:\x00\x00>= and =:\x00\x00?= and ending with =?= is: - =\0x000000= (6
bytes) - @ or ? - A (7 bytes) - @ (2 bytes) - @ or ? - =\xc1= (3 bytes)

An example:

#+BEGIN_EXAMPLE
  b'\x00\x00\x008\x94S@\xc8.@A\x01:\xa0\xc0i\x11\xa1@|.@\xc1\x9b\x1c\xe6?'
  : 00:00:00 > 38:94:53:40:c8:2e @ 1:3a:a0:c0:69:11:a1 @ 7c:2e @ 9b:1c:e6 ?
#+END_EXAMPLE

The following Python code decodes this packet structure:

#+BEGIN_SRC python
  def to_hex(b):
      return ':'.join(hex(x)[2:] for x in b)

  def decode_pkt(b):
      if len(b) == 0:
          return
      if b[0] == 0xCA:
          pass # raw data?
      elif b[0] == ord(':'):
          if b[3] == ord('>') or b[3] == ord('?'): # > or ?
              field1 = to_hex(b[7:13])  # 6 bytes
              field1end = chr(b[13])    #
              field2 = to_hex(b[15:22]) # 7 bytes
              if b[22] != ord('@'):
                  print('b[22] should be @ but is {}'.format(chr(b[22])))
              field3 = to_hex(b[23:25])
              field3end = chr(b[25])
              c1 = b[26]
              field4 = to_hex(b[27:30])
              if b[30] != ord('?'):
                  print('b[30] is not ?')
              print(': 00:00:00 > {} {} {} @ {} {} {} ?'.format(field1, field1end, field2,
                    field3, field3end, field4))
      elif b[0] == ord(';'):
          print('delimiter') # end of previous packet?
      else:
          print(b[0])
          print('unknown data')
      print('\n')
#+END_SRC

Noting a delay between packets led me to derive the following packet
structure: - START packet, which is equal to the preceding END packet -
ONCE call, which occurs prior to a... - ONCE packet - JUICY DATA - END
call - END packet, which is equal to the next START packet

This proved to be incorrect, but more on that later.

The following code differentiates between these packets from the netcat,
where the variable =rawn= is the raw byte string:

#+BEGIN_SRC python
  start = True
  while True:
      r.recvuntil('^')
      raw = r.recvuntil('.')
      rawn = bytes([94]) + raw
      print(rawn)
      v = raw.decode().split('+')
      del v[-1]
      h = bytes([int(i, 16) for i in v])
      if h == b';\x00\x00?':
          print("ONCE CALL")
      elif h == b';\x00\x00>':
          print("END CALL")
      elif h.startswith(b':\x00\x00?'):
          print(f"ONCE: {h[4:]}")
      elif h.startswith(b':\x00\x00>'):
          # notable delay between start and end each time
          if start:
              print(f"START: {h[4:]}")
              start = False
          else:
              print("INJECTING")
              r.send(inj)
              print(f"END: {h[4:]}")
              start = True
      elif h.startswith(b'\xca'):
          print(f"JUICE: {h}")
      else:
          print(f"???: {h}")
      
      sys.stdout.flush()
#+END_SRC

I then noticed that post-text the string
=\x00R\x01\x1e{\x81G\x00\xc9\x9d\xe3\xe7\xc2#6= had the characters ={=
and =6= at the same point as =flag{oscar39616kilo=, which would
correspond to a flag. I graphed this and tried to find a function (or
multiple) modeling a relation here, but with R2 being something like
0.39 for every relation I tried, this was extremely unlikely.

We then tried reading the data sequentially from the buffer, from Juicy
Data 00 to 04. Here's the entire string:

#+BEGIN_EXAMPLE
  00000000: 4a75 6963 7920 4461 7461 2030 3000 c8f7  Juicy Data 00...
  00000010: eb15 963d 6b70 5cc9 2c5e d5cf 5c31 9919  ...=kp\.,^..\1..
  00000020: 779a c6a9 0865 8d55 926a 372c 00ff 23eb  w....e.U.j7,..#.
  00000030: 14b9 297f 2985 4856 e31d 2558 58be 59c6  ..).).HV..%XX.Y.
  00000040: 4a75 6963 7920 4461 7461 2030 3100 5201  Juicy Data 01.R.
  00000050: 1e7b 8147 00c9 9de3 e7c2 2336 817c fcd9  .{.G......#6.|..
  00000060: 9b6b 3a1f 68f0 35ce dd77 35ca dc87 ccfa  .k:.h.5..w5.....
  00000070: 024d 4102 16df e5fd a108 3322 842f fc1f  .MA.......3"./..
  00000080: 4a75 6963 7920 4461 7461 2030 3200 c08f  Juicy Data 02...
  00000090: e702 91fd e177 fb82 7f2e a504 5ea1 23f9  .....w......^.#.
  000000a0: d762 fcfd d5cd 00c0 d4ce 8661 6847 f14f  .b.........ahG.O
  000000b0: 4982 4d2a f948 ac79 7651 7dd4 f2a0 cdc9  I.M*.H.yvQ}.....
  000000c0: 4a75 6963 7920 4461 7461 2030 3300 4dae  Juicy Data 03.M.
  000000d0: 409a d839 e285 b259 d62f 2dc9 d0fb 92d2  @..9...Y./-.....
  000000e0: c459 aa5b 2042 c6b5 6193 b3c6 5001 7590  .Y.[ B..a...P.u.
  000000f0: 9b4d ca7e d27c d7a9 ac04 727c ff04 4ec4  .M.~.|....r|..N.
  00000100: 4a75 6963 7920 4461 7461 2030 3400 5a83  Juicy Data 04.Z.
  00000110: 2524 01f8 a0d8 a14c dc13 c8dc 1717 a075  %$.....L.......u
  00000120: 10bf f24b a525 e81e 0c4b e8f3            ...K.%...K..
#+END_EXAMPLE

Unfortunately, nothing meaningful was derived from this. There are a ={=
and =}= with bytes between them, but they aren't flag length.

I noticed that injecting instructions performed something, but I didn't
think it did anything notable. I injected various data at various
points, but I never managed to break out at the previous region of
memory... which is where gashapwn's /incredible/ work came in.

If the packet =^3b+00+00+00.= is sent, the bus /stops sending data/,
which is decidedly confirmation that the server accepts data. Each of
the following injects has the same effect:

#+BEGIN_EXAMPLE
  ^3b+00+00+30+.
  ^3b+00+00+31+.
  ^3b+00+00+32+.
  ^3b+00+00+33+.
  ^3b+00+00+34+.
  ^3b+00+00+34+.
  ^3b+00+00+35+.
  ^3b+00+00+36+.
  ^3b+00+00+37+.
#+END_EXAMPLE

In practice, only this last packet is needed to shut down the server.
Anything of the form of =^3b+00+00+XX+.= where XX<38 shuts it down, but
only 37 enables dump mode. This can probably be done with a fuzzer. Why
has God abandoned us? What accursed malfunction did we do to deserve
this fate?

If you send the packet
=^ca+00+44+79+20+44+61+74+61+20+30+31+00+52+01+1e+7b+81+47+00+.....+87+cc+.=,
the same packet is sent back. This means that the juice packets
deliminated with =\xca= are actually instructions.

By playing with the packet, the format appears to go: - Byte 0: CA -
Byte 1-2: Memory offset - Byte 3-end: Size of memory to return

...so if we ask for a really large chunk of data, we can get a dump.
With the inject:

#+BEGIN_EXAMPLE
  b"^3b+00+00+37+."
  b"^ca+00+00+00+00+00+00+00+00+00+00+00+00+00+00+00+00+00+00+00+00+00+00+00+00+....+00+00+."
#+END_EXAMPLE

we can query for everything in memory. And we did.

*** Full code
    :PROPERTIES:
    :CUSTOM_ID: full-code-3
    :END:

#+BEGIN_SRC python
  #!/usr/bin/env python3
  import time
  import sys

  from pwnlib import tubes

  TICKET = 'THE_TICKET'
  r = tubes.remote.remote('bus.satellitesabove.me', 5041)
  r.send(TICKET+'\n')
  time.sleep(0.5)
  r.recvuntil('Ticket please:\n', drop=True)

  def to_hex(b):
      return ':'.join(hex(x)[2:] for x in b)

  def decode_pkt(b):
      if len(b) == 0:
          return
      if b[0] == 0xCA:
          pass # raw data?
      elif b[0] == ord(':'):
          if b[3] == ord('>') or b[3] == ord('?'): # > or ?
              field1 = to_hex(b[7:13])  # 6 bytes
              field1end = chr(b[13])    #
              field2 = to_hex(b[15:22]) # 7 bytes
              if b[22] != ord('@'):
                  print('b[22] should be @ but is {}'.format(chr(b[22])))
              field3 = to_hex(b[23:25])
              field3end = chr(b[25])
              c1 = b[26]
              field4 = to_hex(b[27:30])
              if b[30] != ord('?'):
                  print('b[30] is not ?')
              print(': 00:00:00 > {} {} {} @ {} {} {} ?'.format(field1, field1end, field2,
                    field3, field3end, field4))
      elif b[0] == ord(';'):
          print('delimiter') # end of previous packet?
      else:
          print(b[0])
          print('unknown data')
      print('\n')

  start = True
  inj = b"^3b+00+00+37+."
  inj2 = b"^ca+" + (b"00+" * 512) + b"."

  dont = False
  inj2_b = False

  print("Injection: " + inj.decode("utf-8"))

  while True:
      r.recvuntil('^')
      raw = r.recvuntil('.')
      rawn = bytes([94]) + raw
      print(rawn)
      v = raw.decode().split('+')
      del v[-1]
      h = bytes([int(i, 16) for i in v])
      if h == b';\x00\x00?':
          print("ONCE CALL")
      elif h == b';\x00\x00>':
          print("END CALL")
      elif h.startswith(b':\x00\x00?'):
          print(f"ONCE: {h[4:].hex()}")
      elif h.startswith(b'\x3b\x00\x00\x37'):
          print("SHUT DOWN SUCCESSFUL")
          dont = True
          inj2_b = True
          print("INJECTING AGAIN")
          r.send(inj2)
      elif h.startswith(b':\x00\x00>'):
          # notable delay between start and end each time
          if start:
              print(f"START: {h[4:].hex()}")
              start = False
          elif inj2_b == False:
              print("INJECTING")
              r.send(inj)
              print(f"END: {h[4:].hex()}")
              start = True
          else:
              print("INJECTING AGAIN")
              r.send(inj2)
              print(f"END: {h[4:].hex()}")
              start = True
      elif h.startswith(b'\xca'):
          print(f"JUICE: {h}")
      else:
          dont = True
          print(f"???: {h.hex()}")

      if not dont:
          decode_pkt(h)
      dont = False
      sys.stdout.flush()
#+END_SRC

Run it:

#+BEGIN_EXAMPLE
   λ has-writeup/satellite-bus/magic-bus python magic-bus.py

  ....

  JUICE: b'.....v\xaf\xe88\x856Mflag{oscar39616kilo:GCxmhORYa65Y0PmRtFmlFSBmnvImEiWg.....'
#+END_EXAMPLE

Hey look, a flag!

I hate this problem so much. At the time of me writing this, it's 1:30
AM and I'm sitting in my kitchen on my Lenovo(tm) ThinkPad(tm) T440(tm).
I genuinely don't know how this got so many solves. I hate this.
Goodnight.

** Resources and other writeups
   :PROPERTIES:
   :CUSTOM_ID: resources-and-other-writeups-5
   :END:

- God I wish there was any

\newpage
* Good Plan? Great Plan!
  :PROPERTIES:
  :CUSTOM_ID: good-plan-great-plan
  :END:

*Category*: Space and Things *Points (final)*: 77 *Solves*: 54

#+BEGIN_QUOTE
  Help the Launchdotcom team perform a mission on their satellite to
  take a picture of a specific location on the ground. No hacking here,
  just good old fashion mission planning!
#+END_QUOTE

#+BEGIN_QUOTE
  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:

  #+BEGIN_EXAMPLE
    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
  #+END_EXAMPLE

  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.
#+END_QUOTE

** Write-up
   :PROPERTIES:
   :CUSTOM_ID: write-up-5
   :END:

by [[https://qtp2t.club/][hazel (=arcetera=)]]

In the time range [2020-04-22 00:00:00, 2020-04-23 23:59:00], the USA
224 satellite as given in the TLE above reaches the Iranian space port
twice: - Around 09:28 on 2020-04-22 - Around 09:50 on 2020-04-23 This
was found in GPredict after loading in the TLE given in the netcat.

Additionally, the USA 224 reaches the ground station in Alaska twice: -
Around 10:47 on 2020-04-22 - Around 11:10 on 2020-04-23

The rest of the strategy is pretty much just to use trial and error: -
Image for as long as possible until the battery runs dry or we're out of
range - Downlink for as long as possible until the battery runs dry or
we're out of range - Desaturate the wheels about an hour before they
exceed maximum velocity

*** Full plan
    :PROPERTIES:
    :CUSTOM_ID: full-plan
    :END:

#+BEGIN_EXAMPLE
  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:50:00Z wheel_desaturate
  2020-04-22T11:30:00Z sun_point
  2020-04-23T08:50:00Z wheel_desaturate
  2020-04-23T09:30:00Z sun_point
  2020-04-23T09:50:00Z imaging
  2020-04-23T09:56:00Z sun_point
  2020-04-23T11:10:00Z data_downlink
  2020-04-23T11:14:00Z sun_point
  2020-04-23T22:00:00Z wheel_desaturate
#+END_EXAMPLE

** Resources and other writeups
   :PROPERTIES:
   :CUSTOM_ID: resources-and-other-writeups-6
   :END:

- [[http://gpredict.oz9aec.net/]]
- [[https://en.wikipedia.org/wiki/Two-line_element_set]]

\newpage
