digital filters writeup
This commit is contained in:
parent
e7e4956916
commit
56568d82b8
|
@ -0,0 +1,141 @@
|
||||||
|
# Attitude Adjustment
|
||||||
|
|
||||||
|
**Category**: Astronomy, Astrophysics, Astrometry, Astrodynamics, AAAA
|
||||||
|
**Points (final)**: 69 points
|
||||||
|
**Solves**: 62
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
> 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.
|
||||||
|
|
||||||
|
## Writeup
|
||||||
|
by [erin (`barzamin`)](https://imer.in).
|
||||||
|
|
||||||
|
As part of this challenge, we're given a file `src.tar.gz` by the scoreboard. This contains Octave code (humorously, almost MATLAB-compatible, but uses incompatible `endfunction` tokens) 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:
|
||||||
|
```{.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
|
||||||
|
```
|
||||||
|
|
||||||
|
Note `err_thresh` is defined upscript as
|
||||||
|
```{.matlab}
|
||||||
|
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):
|
||||||
|
```{.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
|
||||||
|
```
|
||||||
|
|
||||||
|
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:
|
||||||
|
```{.matlab}
|
||||||
|
% Get Observations
|
||||||
|
q_att = startracker(actual);
|
||||||
|
```
|
||||||
|
|
||||||
|
Looking inside `startracker()`, we see that it
|
||||||
|
The commented-out code 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):
|
||||||
|
```{.matplotlib}
|
||||||
|
% 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:
|
||||||
|
```{.python}
|
||||||
|
sep = ','
|
||||||
|
r = remote('filter.satellitesabove.me', 5014)
|
||||||
|
r.clean()
|
||||||
|
r.send('ticket{foxtrot78531papa:GLliqPNOiBYZl7OwLiZCOx2yfdmbyO6cdgrfcNRC8iMPcJgy0YQ_H1kBeWTloVB_-w}\n')
|
||||||
|
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):
|
||||||
|
```{.python}
|
||||||
|
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
|
||||||
|
```{.python include=meh.py}
|
||||||
|
```
|
|
@ -0,0 +1,50 @@
|
||||||
|
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('ticket{foxtrot78531papa:GLliqPNOiBYZl7OwLiZCOx2yfdmbyO6cdgrfcNRC8iMPcJgy0YQ_H1kBeWTloVB_-w}\n')
|
||||||
|
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')
|
Loading…
Reference in New Issue