10
2
Fork 0
has-writeup/aaaa/digital-filters-meh
Erin Moon cb7a567f44 lint for header consistency 2020-05-28 00:26:14 -05:00
..
README.md lint for header consistency 2020-05-28 00:26:14 -05:00
meh.py cleanup 2020-05-26 07:07:21 -04:00

README.md

Digital Filters, Meh

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

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?

Given files: src.tar.gz.

Write-up

by 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:

% 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

Note err_thresh is defined upscript as

err_thresh = 1*pi/180. ;   % 1 Degree error max

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):

% 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

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:

% Get Observations
q_att = startracker(actual);

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):

% 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

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:

%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

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:

sep = ','
r = remote('filter.satellitesabove.me', 5014)
r.clean()
r.send('THE_TICKET')
time.sleep(0.1)

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

def adversary(true_pose):
    lie = 0.25

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

    return R.from_euler('xyz',euler)

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

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')

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

Full code