Erin Moon 71583185a7 | ||
---|---|---|
.. | ||
README.md | ||
double-precision.png | ||
double-precision.svg | ||
dsky.png |
README.md
1201 Alarm
Category: Space and Things Points (final): 197 points Solves: 14
Step right up, here's one pulled straight from the history books. See if you can DSKY your way through this challenge! (Thank goodness VirtualAGC is a thing…)
Writeup
by erin (barzamin
).
Note: I went into this challenge without knowing much about the Apollo Guidance Computer beyond the existence of a fanatic preservation community and the fact that it used a weird verb-noun interface. Thanks to this challenge, utterly unrelated to any extant satellites I know of, I've gained the ability to read core rope memory words off of an Apollo 11 guidance computer, word-by-word from the DSKY. Useful, right?
Connecting to the challenge, we get some flavor text and a problem, as well as an IP/port to connect to a virtual Apollo Guidance Computer:
λ ~
» nc apollo.satellitesabove.me 5024
Ticket please:
THE_TICKET
The rope memory in the Apollo Guidance Computer experienced an unintended 'tangle' just
prior to launch. While Buzz Aldrin was messing around with the docking radar and making Neil
nervous; he noticed the value of PI was slightly off but wasn’t exactly sure by how much. It
seems that it was changed to something slightly off 3.14 although still 3 point something.
The Comanche055 software on the AGC stored the value of PI under the name "PI/16", and
although it has always been stored in a list of constants, the exact number of constants in
that memory region has changed with time.
Help Buzz tell ground control the floating point value PI by connecting your DSKY to the
AGC Commanche055 instance that is listening at 3.15.213.229:18364
What is the floating point value of PI?:
The Apollo Guidance Computer (AGC) used a head module called the DSKY as its user-faciing interface. The VirtualAGC suite has tools for simulating the AGC as well as controlling it with a tool called yaDSKY. Since yaDSKY communicates over a TCP socket, it's reasonable to assume the address given can be connected to with yaDSKY, which will let us control the remote, simulated AGC.
I downloaded and built the VirtualAGC suite, and threw together a little script (start_dsky.py
) to grab a new challenge and connect to it:
from pwn import *
import time
TICKET = 'THE_TICKET'
r = remote('apollo.satellitesabove.me', 5024)
time.sleep(0.1)
r.clean()
r.send(TICKET+'\n')
r.readuntil('listening at ')
[ip, port] = r.readuntil('\n').decode().strip().split(':')
os.spawnl(os.P_NOWAIT, cmd := f'./yaDSKY2 --ip={ip} --port={port}')
log.info(cmd)
r.interactive()
Unfortunately, apollo.satellitesabove.me
is now down and I can't provide screenshots of my exact solvepath. However, I can vaguely illustrate with a DSKY screenshot.
The real DSKY looks much cooler; the fake DSKY looks like this.
The important thing to know about the DKSY is that it uses a weird verb-noun UI. Effectively, to do anything in the running program, you press VERB and type two digits to select an action, NOUN and two digits to select that action's target, and hit ENTR. Depending on the noun, you might have to key in some additional information and press enter again.
By grepping the source code in the Comanche055
directory of the VirtualAGC repo, I found the location of the PI/16
constant: the TIME_OF_FREE_FALL
file, near the end in a table of constants:
060455,000703: 27,3355 06220 37553 PI/16 2DEC 3.141592653 B-4
The important things here are the address 27,3355
(bank 27, address 3355 octal), and the values 06220
and 37553
, the raw octal words making up the double-precision representation of \pi/16
. The AGC's memory is split into banks of 1024 words; each word is fifteen bits long. Address 27,3355
thus means "address 3355_8
in bank $27_8$"; the addressing for each bank starts at 2000_8
, so this address is actually the $3355_8-2000_8 = 1355_8$th word in bank 27. Given the problem description, I assumed that the constant we're looking for (something close to the true value of $\pi$) would be around this location.
I immediately started looking for verbs in the Comanche055 default program which could read memory, and found 'V27 DISPLAY FIXED MEMORY' in verb tables for Apollo 11, which seems like it should be able to read the read-only ("fixed") rope memory. Looking for a noun to use with this, I found N02 SPECIFY MACHINE ADDRESS (WHOLE)
. I didn't initially understand how to use this, but I googled V27N02E
(the shorthand for pressing verb, 27, noun, 02, and hitting enter), and realized that the VirtualAGC website frontpage shows how to do this; effectively, after keying V27N02E
, you just enter an octal "machine address" and hit enter again; the machine address shows up on the third line of the DSKY and the word's value shows up on the first line in octal.
Machine addresses are just bank*1024 + word
according to the VirtualAGC site; we can compute 27,3355
's machine address easily as 27_8 \times 1024 + 3355_8 = 57355_8
. Keying a read for 57355_8
and advancing with V15E
(verb 15, INCREMENT MACHINE ADDRESS), we see the following values:
27,3355: 01333
27,3356: 00075
Time for a digression! How does the AGC represent fractional values? Thanks to VirtualAGC's assembler manual, I don't have to trawl NASA PDFs. A single precision number is just 1's complement with the sign in the MSB; the magnitude is thought of as a fraction out of the maximum expressible magnitude. Locations storing numbers thus carry metadata in the assembler indicating the scaling required to fit the number into [-1,1]
and properly manipulate them doing when fixed-point math.
Double precision has the following layout:
The first word's value bits are higher-significance; the second word's are lower. Signs (sn2
, sn1
) are independent and can cancel for some awful reason, which thankfully wasn't relevant in this CTF. I threw some (miserable) code together using the Python library bitstrings
to decode these, testing on the original PI/16
values:
from bitstring import Bits, BitArray
TRUE_WORDS = [0o06220, 0o37553]
def sp(bits):
sign = -1 if bits[0] else +1
data = bits[1:].uint
return sign * data/((1<<14) -1)
def dp(bits):
sign1 = -1 if bits[0] else +1
sign2 = -1 if bits[15] else +1
data1 = bits[1:15]
data2 = bits[16:]
if sign1 == -1:
data1 = ~data1
if sign2 == -1:
data2 = ~data2
d1 = (data1+Bits('0b00000000000000')).uint
d2 = data2.uint
return (d1*sign1+d2*sign2)/((1<<28) - 1)
print(dp(sum(BitArray(uint=w, length=15) for w in TRUE_WORDS))*16)
This prints out 3.1415926931112734
, which is close enough to the 3.141592653
given in the listing for government NASA CTF work.
However, the values of 1333_8
, 75_8
we found where PI/16
is supposed to be decode to 0.71387
: not at all what we want. Moreover, it looks like we've been visited by a haxor at these addresses.
Scanning further through core rope memory (with V15E
to increment and repeated presses of E
to increment further), I eventually stumbled upon two addresses which held different words every time I got a contest instance. They also happened to have magnitude similar to the original words for PI/16
. Unfortunately, the contest is now down, and I don't remember the exact address, but the words I found right before I got the flag were 7440_8, 2122_8
; let's try decoding them!
print(dp(sum(BitArray(uint=w, length=15) for w in [0o7440, 0o2122]))*16)
This gives 3.781315936823621
; pasting this into the contest, we got the flag.
Resources and other writeups
- https://www.ibiblio.org/apollo/listings/Comanche051/TIME_OF_FREE_FALL.agc.html#50492F3136
- https://www.ibiblio.org/apollo/CMC_data_cards_15_Fabrizio_Bernardini.pdf
- https://www.ibiblio.org/apollo/index.html#Playing_with_Colossus_
- https://www.ibiblio.org/apollo/Documents/Apollo15_Colossus3_CMC_Data_Cards.pdf
- https://bitstring.readthedocs.io/