Skip to content

Latest commit

 

History

History
713 lines (561 loc) · 26.3 KB

6. Organizers_Hack-A-Sat 3 quals writeups.md

File metadata and controls

713 lines (561 loc) · 26.3 KB

Hack-A-Sat 3 quals writeups

Team Organizers

The Only Good Bug is a Dead Bug

Small Hashes Anyways (73 pts)

Micro hashes for micro blaze ¯\_(ツ)_/¯

The challenge provides a "microblaze-linux.tar" which contains all the tools and libraries required to run linux usermode microblaze programs. This is used for this and all following microblaze challenges. The main challenge is a 14KB microblaze binary named small_hashes_anyways.

Solution

Running the binary with some input shows that it complains that the input is suppost to be 112 characters long:

$ QEMU_LD_PREFIX=~/microblaze-linux LD_LIBRARY_PATH=~/microblaze-linux/lib qemu-microblaze ./small_hashes_anyways
small hashes anyways:
potato
wrong length wanted 112 got 6

When running it with the right length input the program complains about the first index mismatch.

$ QEMU_LD_PREFIX=~/microblaze-linux LD_LIBRARY_PATH=~/microblaze-linux/lib qemu-microblaze ./small_hashes_anyways
small hashes anyways:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
mismatch 1 wanted 1993550816 got 3554254475

From this the solution is easy as we can bruteforce character per character using qemu:

import subprocess, os
import string

customenv = os.environ.copy()

# microblaze-linux in user home folder
customenv["QEMU_LD_PREFIX"] = os.path.expanduser("~")+"/microblaze-linux/"
customenv["LD_LIBRARY_PATH"] = os.path.expanduser("~")+"/microblaze-linux/lib/"

# binary tells us that it wants 112 bytes input
inp = [0x20]*112

for curIndex in range(112):
    # bruteforce for all ascii characters that one would expect in the flag
    for c in string.ascii_letters + string.digits + string.punctuation:
        inp[curIndex] = ord(c)
        # run in qemu
        proc = subprocess.Popen(["qemu-microblaze", "./small_hashes_anyways"], env=customenv, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
        
        # give it the hash
        proc.stdin.write(bytes(inp)+b"\n")
        proc.stdin.close()
        while proc.returncode is None:
            proc.poll()
            
        # small hashes anyways:
        # mismatch 1 wanted 1993550816 got 3916222277
        stin = proc.stdout.read().decode("utf-8")
        res = stin.split("\n")[1].split(" ")
        
        # if it's a digit then the hash was wrong, otherwise the input was correct
        if res[1].isdigit():
            wrongIndex = int(res[1])-1
        else:
            print(stin)
            break
        
        # found correct character
        if wrongIndex != curIndex:
            break

    print(bytes(inp[:(curIndex+1)]).decode("utf-8"))

Alternatively we might notice that the binary complained about the wrong hash of A... being 3554254475 = 0xd3d99e8b which is equal to zlib.crc32(b'A')

A quick look into the binary confirms that it is hashing using crc32:

Looking for the hash table it is compared against

With this it is possible to calculate the flag way faster

import zlib
import string

data = [
	0x76d32be0, 0x367af3ad, 0x40e61be5, 0xd1f4eb9a,
	0x05dec988, 0x92d83a22, 0xb9fb6643, 0x07db550f,
	0xfbdcfd40, 0x79280e29, 0x2f193ef9, 0x49493403,
	0x6af603b6, 0x4fba6e21, 0xc897819e, 0x7aada759,
	0xe7cdead0, 0x74597500, 0xefae232f, 0xda807c83,
	0x9bb128bc, 0xc4f591e0, 0xcd1c9972, 0xaac83ab6,
	0x1812e53d, 0xf27578db, 0xe528a105, 0xf5e58686,
	0x87f22c24, 0x51569866, 0x77590c7e, 0x09ce7f57,
	0x79da1cab, 0x8f132e93, 0x02edea54, 0x71bc0213,
	0x827622b3, 0xdfedf391, 0x9cd9812a, 0xb0432bd3,
	0xe56a9756, 0xf43eb5b6, 0x3f4e5218, 0xe6537823,
	0x595a041f, 0x5e88c97a, 0x3c520668, 0x135fa020,
	0x59af08c7, 0x18e1820f, 0xfcae038e, 0x05f39360,
	0x3c097d32, 0xa254127f, 0x2119cad7, 0xed96f09a,
	0x93eb031d, 0xee92e05f, 0x763db900, 0xd3afa332,
	0x1bdd60a8, 0x92c6398b, 0xba4d9a4c, 0xfdd7de90,
	0x11f3b6f2, 0xc47fd37e, 0xdaabad73, 0xe9b664e8,
	0x39e56fd6, 0xeb38b920, 0xe6870ec8, 0xe38ae66c,
	0xc0568c3b, 0x657fe53a, 0xefbf05bf, 0x683d668d,
	0xd9b10dca, 0xa8b10428, 0x767b9ae4, 0x31179f05,
	0x4ae1d8ae, 0xc424c110, 0xc71ce605, 0xf5c7b2c1,
	0x41fed7a2, 0xc3421a06, 0xf4189d3b, 0xf29972a3,
	0xe5284d0f, 0x3a5f13f3, 0xa852ea36, 0x5e79c194,
	0x7a3b4919, 0xe7cd7c3e, 0xe1e63f14, 0x27eccde5,
	0xa84f59e8, 0xea726210, 0x99ec49a8, 0xc39a0898,
	0xca7632f2, 0x19a92a33, 0xd4adf3b8, 0x41dfbde3,
	0x38967709, 0x0f370533, 0x29d994ed, 0xb340c9ca,
	0xfe6831cf, 0x69f7c0bf, 0x95d9d732, 0x34f46a5b
]

# binary tells us that it wants 112 bytes input
inp = [0x20]*112

for curIndex in range(112):
    # bruteforce for all ascii characters that one would expect in the flag
    for c in string.ascii_letters + string.digits + string.punctuation:
        inp[curIndex] = ord(c)
        # character matches
        if (zlib.crc32(bytes(inp[:(curIndex+1)]))&0xffffffff)==data[curIndex]:
            break
    print(bytes(inp[:(curIndex+1)]).decode("utf-8"))

It's a wrap (43 pts)

  • Run the provided pyc with the right python version
  • input 72,65,67,75,65,83,65,84,51, the ascii representation of HACKASAT3
  • it spits out -701,140,773,-726,149,791,-654,134,721
  • get the flag from the socket connection

Writeup in a screenshot

Ominous Etude (106 pts)

we didn't start the fire

you'll want microblaze-linux.tar.bz2 from "small hashes anyways"

Ominous Etude is a 11.6MB microblaze binary that asks for a number and outputs a positive or negative response.

Solution

Running it shows that the program wants a number from us.

$ QEMU_LD_PREFIX=~/microblaze-linux LD_LIBRARY_PATH=~/microblaze-linux/lib qemu-microblaze ./ominous_etude
enter decimal number: 123
hmm… not happy :(

Compared to the first challenge we now actually need to look into the binary. For this I used a binary ninja microblaze plugin which even contains lifting features! The problem is that at least for Ominous Etude the plugin had two bugs that prevented it from being immediate useful.

Analyzing the program a bit (even with just the disassembly) shows that it converts the input string to a number and runs it through a function called quick_math(uint32_t).

Using the plugin the following wrong lifted code / decompilation is shown:

After matching this with the disassembly, the call destination, the xor immediate and the shift direction are wrong.

Fixing the shift direction was easy microblaze/microblaze.py:489: replacing

                val = il.shift_left(4, a, one, flags='carry')

with

                val = il.logical_shift_right(4, a, one, flags='carry')

The reason why the xor immedaite and call destination are wrong is for the same reason. Instead of just using the 16-bit immediate these instructions usually have in the microblaze architecture, an imm instruction is prepended. Without the imm prefix the 16-bit immediates are sign extended to 32-bit, with the imm prefix the the immediate encoded in the imm instruction provides the upper 16-bit.

Here the bug was in microblaze/arch.py:149 that the sign extended 16-bit immediate was logical or'ed with the upper 16-bit, but as they were sign extended those upper 16-bit would always be 0xffff which is wrong:

                    i=(x.b.i << 16) | (y.b.i),

with

                    i=(x.b.i << 16) | ((y.b.i)&0xFFFF),

Now the decompilation looks very good:

With this we can build a Z3 model to solve for the right input (careful with unsigned division and logical right shift here):

from z3 import *
import ctypes

# manual translation of the decompilation
def quick_maths(arg1):
    r3_15 = UDiv(UDiv((arg1 + 0x1dd9), 6) - 0x189, 0xf)
    r3_23 = r3_15 - 0x1f7 + r3_15 - 0x1f7
    r3_24 = r3_23 + r3_23
    r3_25 = r3_24 + r3_24
    r3_26 = r3_25 + r3_25
    r3_33 = (r3_26 + r3_26 + 0x249a) ^ 0x2037841a
    return LShR(((0 - r3_33) | r3_33) ^ 0xffffffff, 0x1f)&0xFF
    
arg1 = BitVec("arg1", 32)

s = Solver()

# solve for the bit to be set
s.add(quick_maths(arg1)&1 == 1)

# check if the model is sat
print(s.check())

# convert the number to a 32bit signed integer
vl = s.model()[arg1].as_long()&0xffffffff
print(ctypes.c_long(vl).value)

Testing this locally works, and we get the flag if we send it to the remote server:

$ QEMU_LD_PREFIX=~/microblaze-linux LD_LIBRARY_PATH=~/microblaze-linux/lib qemu-microblaze ./ominous_etude
enter decimal number: 1520195799
cool :)

Blazin' Etudes (304 pts)

it was always burning since the world's been turning

you'll need the microblaze-linux.tar.bz2 file from "Small Hashes Anyways"

Blazin' Etudes provides 178 (!!) different microblaze binaries that all differ in their quick_maths implementation.

Solution

All the Blazin' Etudes are like the Ominous Etude binary, but instead dynamically linked. The only difference is that they all have different quick_maths implementations (and thus different numbers needed to solve them).

To solve this we need automation!

Thankfully mdec has code for automatically getting the decompilation using Binary Ninja. In mdec it is meant to be executed headless for which a professional license is required, but instead we can also just run this in the Binary Ninja GUI Python Interpreter with exec(open(r"blazing_etudes_decompile.py").read()).

files = # list of all Blazin' Etudes binary file names

blazing_etudes_folder = # folder path to the binaries
blazing_etudes_decompilation_folder = # folder path where the decompilation will be saved to

def getDecompilation(file):
    v = open_view(blazing_etudes_folder+file)
    ds = DisassemblySettings()
    ds.set_option(DisassemblyOption.ShowAddress, False)
    ds.set_option(DisassemblyOption.WaitForIL, True)
    lv = LinearViewObject.language_representation(v, ds)
    out = []
    for f in v.functions:
        # only give decompilation for the quick_maths functions
        if not "quick_maths" in f.name: continue
        c = LinearViewCursor(lv)
        c.seek_to_address(f.highest_address)
        last = v.get_next_linear_disassembly_lines(c.duplicate())
        first = v.get_previous_linear_disassembly_lines(c)
        for line in (first + last): out.append(str(line))
    
    return '\n'.join(out)

# decompile and save
for file in files:
    dec = getDecompilation(file)
    f = open(blazing_etudes_decompilation_folder+file, "w")
    f.write(dec)
    f.close()

When trying to run it I ran into another bug in the microblaze plugin, which was just a typo and quickly fixed (microblaze/reloc.py:208)

                reloc.pcRelative = false

replaced with

                reloc.pcRelative = False

Ok, now we have 178 decompiled functions. The next step is to automatically generate a z3 model out of them. This is supprisingly easy:

from z3 import *
import ctypes

files = # list of all Blazin' Etudes binary file names
blazing_etudes_decompilation_folder = # folder path where the decompilation will be saved to

def readDecompilation(file):
    f = open(blazing_etudes_decompilation_folder+file, "r")
    data = f.read()
    f.close()
    return data
    
# overwrite functions that may be used
# thanks to how function variables work (int32_t)(...) is valid call to int32_t, so let's just define them all

def __udivsi3(a, b):
    return UDiv(a, b)
    
def uint32_t(v):
    return ZeroExt(32, v)
    
def int32_t(v):
    return SignExt(32, v)
    
def uint8_t(v):
    return ZeroExt(32, Extract(7, 0, v))
    
def int8_t(v):
    return SignExt(32, Extract(7, 0, v))
    

# all the shifts in the decompilation are unsigned
# but python has no infix logical shift right
# so let's just define one
BitVecRef.__matmul__ = lambda a, b: LShR(a, b)
        

for file in files:

    # define arg1
    arg1 = BitVec("arg1", 32)
    s = Solver()

    # split the decompilation into lines
    decompilation = readDecompilation(file).split("\n")
    
    for line in decompilation:
        # skip empty and "{" or "}" lines
        if not(" " in line): continue
        # skip the function definition
        if "quick_maths" in line: continue
        # align correctly
        line = line.strip()
        
        # strip the type information
        prefix = line[:line.index(" ")]
        
        # content with ";" removed
        content = line[line.index(" ")+1:][:-1]
        
        # replace shifts with unsigned shifts (as only unsigned shift happen)
        content = content.replace(">>", "@")
        
        # if the function returns declare our res variable
        if prefix == "return":
            content = "res = "+content
            
        # just run the modified C line as python code
        exec(content)
    
    # the first bit of the return value needs to be one
    s.add(res&1 == 1)
    
    # solver go brrrrr
    if(s.check() == sat):
        # return back signed 32bit number
        vl = s.model()[arg1].as_long()&0xffffffff
        print(file, ctypes.c_long(vl).value)
    else:
        # note whether solving failed otherwise
        print(file, "unsat")
        
print("# done")

By slightly modifying the decompiled C lines to fit the python environment, we can just interpret them and make them set the variables correctly for us. Then we use the solver to give us the correct input and it works. Note the hack of replacing >> with @ and registering it as logical shift right, as python doesn't have an infix operation for it and restructing the decompilation more would take more effort.

This actually finds a solution for all 178 binaries and generates a list like this:

alarming_shuffle 961399395
alarming_study 258436646
anxious_ball 1230989635
anxious_concerto -1378532556
anxious_jitterbug 1451269225
anxious_mixer -1456257589
anxious_polka -1831522656
baleful_concerto 485533518
...

Verifying manually shows that the values are indeed correct:

$ QEMU_LD_PREFIX=~/microblaze-linux LD_LIBRARY_PATH=~/microblaze-linux/lib qemu-microblaze ./ghastly_dance
enter decimal number: 847747155
cool :)

Providing the remote service with the numbers for the binaries it asks for gives us the flag.

Rocinante Strikes Back

Once Unop a Djikstar (131 pts)

From the challenge name, we can already assume we're dealing with some kind of (weighted) pathfinding problem. Connecting to the tcp connection quickly confirms that. Splitting up the work to be done, one player can start implementing Dijkstra's algorithm, another can implement reading the graph from the csv files, and a last one can browse through the binary decompilation/disassembly a bit to find any details we might not be able to guess offhand. Steps one and two go smoothly, with the exception of not being entirely sure whether the graph is directed or not; the first attempts against the tcp connection fail however. We indeed failed to guess a detail, the reverse engineer informs us, the edges have an additional weight multiplier depending on the "type" of destination node: some truncated mathematical constants that depend on the remainder of the last digit of the satellite name, modulo 3. Implementing this on top of the csv reading we already have now gives us a correct shortest path, and subsequently a flag is obtained from the tcp connection.

Solution code

from heapq import *

def dijkstra(start, to, edges):
    par = {}
    best = {start: 0}
    q = [(0, start)]
    while q:
        curcost, cur = heappop(q)
        if cur == to:
            break
        if curcost > best[cur]: continue
        for neigh, c in edges[cur].items():
            cc = c + curcost
            if neigh not in best or cc < best[neigh]:
                best[neigh] = cc
                heappush(q, (cc, neigh))
                par[neigh] = cur

    assert cur == to
    path = [to]
    while cur in par:
        cur = par[cur]
        path.append(cur)
    return best[to], path[::-1]

def w(name):
    if name in ["ShippyMcShipFace", "Honolulu"]:
        return 1999.9
    else:
        return [2.718, 3.141, 4.04][int(name[-1]) % 3]

import csv, collections

paths = []

source_to_dict = collections.defaultdict(lambda: collections.defaultdict(lambda: float("inf")))

filenames = ["gateways.csv","sats.csv","users.csv"]

for name in filenames:
    with open(name) as csv_file:
        csv_reader = csv.reader(csv_file, delimiter=',')
        for row in csv_reader:
            if row[0] == "Source":
                continue
            source = row[0]
            to = row[1]
            if source == "Honolulu":
                source,to = to,source
            distance = float(row[2])
            source_to_dict[source][to] = min(w(to) * distance, source_to_dict[source][to])
            # source_to_dict[to][source] = min(w(source) * distance, source_to_dict[to][source])


print(r := dijkstra("ShippyMcShipFace", "Honolulu", source_to_dict))
cost, p = r

print(", ".join(x.split("-", 1)[1] for x in p[1:-1]))

Revenge of the Space Math

Crosslink (58 pts.)

Who am i?

Using netcat, you might run: nc crosslinks.satellitesabove.me 5300

Solution

The name crosslink and the description did not tell us much. After taking a look at the following data gotten from the server on connecting we researched TLE.

TLE
SATELLITE 106
1 41917U 17003A   22026.58239212  .00000113  00000+0  33171-4 0  9990
2 41917  86.3954  38.2127 0002381  96.6462 263.5004 14.34219916263505
SATELLITE 103
1 41918U 17003B   22026.56970277  .00000104  00000+0  30203-4 0  9995
2 41918  86.3954  38.1230 0001839  94.2026 265.9380 14.34218174263527
SATELLITE 109
1 41919U 17003C   22026.57604279  .00000109  00000+0  31738-4 0  9992
2 41919  86.3955  38.1532 0002080  92.4859 267.6575 14.34219432263496
SATELLITE 102
1 41920U 17003D   22026.54433320  .00000116  00000+0  34497-4 0  9991
2 41920  86.3954  38.1243 0002055  85.0909 275.0521 14.34219041263571
SATELLITE 105
1 41921U 17003E   22026.44520069  .00000134  00000+0  40813-4 0  9997
2 41921  86.3986   6.5810 0002351 101.7455 258.4004 14.34225962265412
SATELLITE 104
1 41922U 17003F   22026.55701779  .00000120  00000+0  35887-4 0  9999
2 41922  86.3955  38.1374 0002255  98.3272 261.8179 14.34218567263564
SATELLITE 114
1 41923U 17003G   22026.56336572  .00000112  00000+0  32913-4 0  9994
2 41923  86.3954  38.1230 0002447  88.5611 271.5865 14.34215786263544
SATELLITE 108
1 41924U 17003H   22026.45211254  .00000138  00000+0  42244-4 0  9998
2 41924  86.3980   6.5603 0002392  93.4127 266.7342 14.34218234265384
SATELLITE 112
1 41925U 17003J   22026.55067372  .00000113  00000+0  33292-4 0  9996
2 41925  86.3950  38.0455 0002289  83.5757 276.5699 14.34219327263567
...
Measurements
Observations SATELLITE 123
 Time, Range, Range Rate
2022-01-26T13:56:26.605050+0000, 3897.635372424908, -2.7387188248755874
2022-01-26T13:58:13.747907+0000, 3731.315241642606, -0.8387631989293013
Observations SATELLITE 126
 Time, Range, Range Rate
2022-01-26T13:58:13.747907+0000, 3055.407939151864, -8.82704425401185
2022-01-26T14:00:00.890764+0000, 2004.3661144702514, -10.045127012918103
2022-01-26T14:01:48.033621+0000, 1191.9249931017484, -3.015927203485765
2022-01-26T14:03:35.176478+0000, 1248.278943828854, 5.396945699027667
2022-01-26T14:05:22.319335+0000, 2099.2970436506976, 9.624384718138145
Observations SATELLITE 124
 Time, Range, Range Rate
2022-01-26T13:40:22.319335+0000, 3675.5924154951263, -9.356733836755243
2022-01-26T13:42:09.462193+0000, 2729.4185317713723, -6.278974006496791
2022-01-26T13:43:56.605050+0000, 2557.107325919223, 1.9097215075097105
2022-01-26T13:45:43.747907+0000, 3221.0441243996784, 10.138454377837453
Observations SATELLITE 107
 Time, Range, Range Rate
2022-01-26T13:56:26.605050+0000, 3753.7263383556096, -3.9931090180192674
2022-01-26T13:58:13.747907+0000, 3433.4097818534296, -4.230856672761411
2022-01-26T14:00:00.890764+0000, 3292.153621990009, -0.06698010058974524
2022-01-26T14:01:48.033621+0000, 3319.3565366856506, 0.7736098400126697
2022-01-26T14:03:35.176478+0000, 3561.5260605062117, 3.4931432842234424
Observations SATELLITE 132
 Time, Range, Range Rate
2022-01-26T13:56:26.605050+0000, 3863.2538726812945, -5.378948137358818
2022-01-26T13:58:13.747907+0000, 3128.5457385254094, -7.4011522342019305
2022-01-26T14:00:00.890764+0000, 2353.545661023096, -8.111694127159419
2022-01-26T14:01:48.033621+0000, 1540.0117673253546, -8.368236710908311
2022-01-26T14:03:35.176478+0000, 714.0663789750149, -8.558912772814194
2022-01-26T14:05:22.319335+0000, 234.34963706144381, 3.8892761535662084
Observations SATELLITE 135
 Time, Range, Range Rate
2022-01-26T14:00:00.890764+0000, 3733.0375624448875, -13.220847242349354
2022-01-26T14:01:48.033621+0000, 2374.022370753605, -13.040358921698527
2022-01-26T14:03:35.176478+0000, 1044.3144846502626, -11.392589981912645
2022-01-26T14:05:22.319335+0000, 716.6432234452013, 10.447294111648318
...
What satellite am I:

Two Line Elements or TLEs, or also Keplerian elements, are the inputs to the SGP4 standard mathematical model of orbits used by many programs. After first trying to parse these values manually we found TLE-tools that can directly load them from strings with only single TLEs. The goal was to determine which satellite we are based on the range-measurements to other satellites, given to us in the form of observations. Our first idea of just calculating all satellites at all observation times and which of them would see these observations turned out to work perfectly. To calculate the orbits at the given time we used poliastro. It is possible to convert the TLE objects created with TLE-tools directly to orbits in poliastro. During implementation we optimized the search to only check still possible satellites and hence wrote the following solution code.

from pwn import *
import pymap3d as pm
from astropy.time import Time
import re
from tletools import TLE
from astropy import units as u
from poliastro.bodies import Earth
from poliastro.twobody import Orbit
from poliastro.util import norm, time_range

regex_satellite = r"SATELLITE\ (\d*)\n([0-9+\ A-Z.-]*)\n([0-9+\ A-Z.-]*)\n"

ticket = b"ticket{india579963echo3:...}"
REM = True
context.log_level = "debug"

def to_time(t, test_sat):
    print(f"Moving to {t}")
    delta = t - test_sat.epoch
    n_state = test_sat.propagate(delta)
    return n_state

def calc_val(r1, v1, r2, v2):
    range = norm(r1 - r2)
    new_r1 = r1 + v1 * u.s
    new_r2 = r2 + v2 * u.s
    new_range = norm(new_r1 - new_r2)
    return range, new_range - range

def calc(sat_1, sat_2):
    return calc_val(sat_1.r, sat_1.v, sat_2.r, sat_2.v)

def find_sat(gotten):
    # parsing first
    matches = re.finditer(regex_satellite, gotten, re.MULTILINE)
    satellites = []
    for match in matches:
        satellites.append(TLE.from_lines(*match.groups()))
    l = gotten.split("\n")
    l_observations = l[l.index("Measurements")+1:-1]
    observations = {}
    cur_obs = []
    sat_id = l_observations[0][-3:]
    for obs in l_observations:
        if "SATELLITE" in obs:
            # new satellite
            if len(cur_obs) != 0:
                observations[sat_id] = cur_obs
                cur_obs = []
            sat_id = obs[-3:]
            continue
        if "Time" in obs:
            continue
        if "am I" in obs:
            cur_obs.append({
                "time": Time(t[:-5], format='isot', scale='utc'),
                "range": float(ran) * u.km,
                "rate": float(ran_r) * u.km
                })
            break
        t, ran, ran_r = obs.split(',')
        assert t[-5:] == "+0000"
        cur_obs.append({
            "time": Time(t[:-5], format='isot', scale='utc'),
            "range": float(ran) * u.km,
            "rate": float(ran_r) * u.km
            })
        
    named_orbits = {}
    possible_sats = [tle.name for tle in satellites]
    for tle in satellites:
        orbit = tle.to_orbit(Earth)
        named_orbits[tle.name] = orbit
    
    print(f"Starting with set of possible: {possible_sats}")
    for obsv_sat_id, obsv_set in observations.items():
        print(f"Looking for obs of {obsv_sat_id}")
        for obsv in obsv_set:
            new_orbits = {}
            t = obsv['time']
            tar_ran = obsv['range']
            tar_rate = obsv['rate']
            print(f"Calculating sats at {t}:")
            to_look_at = possible_sats + [obsv_sat_id]
            for id, orb in named_orbits.items():
                if id in to_look_at:
                    print(f"{id} ", end='')
                    new_orbits[id] = to_time(t, orb)
            # tentaive new sats
            new_possible_sats = []
            for id, new_o in new_orbits.items():
                ran, rate = calc(new_orbits[obsv_sat_id], new_o)
                #print(f"To {id}: {ran} range & {rate} rate")
                if abs(tar_ran - ran) < 500*u.km:
                    print(f"Found a match between {id} and {obsv_sat_id} with {tar_ran - ran} dist")
                    if abs(tar_rate - rate) < 10*u.km: # km because we have it wrong above, should be m/s
                        print(f"Found a match between {id} and {obsv_sat_id} with {tar_rate - rate} rate")
                        new_possible_sats.append(id)
            print(f"New possible sats: {new_possible_sats}")
            possible_sats = new_possible_sats
    #assert len(possible_sats) == 1
    print(f"Found sat ({len(possible_sats)} many): {possible_sats[0]}")
    return possible_sats[0]

def exploitStart():
    host = "crosslinks.satellitesabove.me"
    port=5300
    p = remote(host, port)
    p.readuntil("Ticket please:")
    p.sendline(ticket)
    while True:
        gotten = p.recvuntil("What satellite am I:\n").decode('utf-8')
        sat = find_sat(gotten)
        p.sendline(b"SATELLITE " + str(sat).encode('utf-8'))

if __name__ == '__main__':
    exploitStart()