国家太空安全是国家安全在空间领域的表现。随着太空技术在政治、经济、军事、文化等各个领域的应用不断增加,太空已经成为国家赖以生存与发展的命脉之一,凝聚着巨大的国家利益,太空安全的重要性日益凸显[1]。而在信息化时代,太空安全与信息安全紧密地结合在一起。
2020年9月4日,美国白宫发布了首份针对太空网络空间安全的指令——《航天政策第5号令》,其为美国首个关于卫星和相关系统网络安全的综合性政策,标志着美国对太空网络安全的重视程度达到新的高度。在此背景下,美国自2020年起,连续两年举办太空信息安全大赛“黑掉卫星(Hack-A-Sat)”,在《Hack-A-Sat太空信息安全挑战赛深度解析》一书中有详细介绍,本文介绍了Hack-A-Sat黑掉卫星挑战赛的
寻找阿波罗导航计算机中被修改的PI(apollo_gcm)这道赛题的解题过程。这里是解法二。
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…)
从上述题目介绍可知,这道题目需要比较旧的知识,与阿波罗导航计算机(Apollo Guidance Computer,AGC)相关,并且要用到DSKY。DSKY是AGC的输入/输出,类似于现代计算机的显示器、键盘。
主办方给出了一个链接地址,使用netcat打开该链接后,会获得一段提示信息,如下:
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 Commanche055 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 172.17.0.1:19008
What is the floating point value of PI?:
通过分析,主要给出如下信息:
要求参赛者找到当前PI的值。
这个挑战题的代码位于apollo目录下,查看challenge、solver目录下的Dockerfile,发现其中用到的是python:3.7-slim,为了加快题目的编译进度,在apollo目录下新建一个文件sources.list,内容如下:
deb https://mirrors.aliyun.com/debian/ bullseye main non-free contrib
deb-src https://mirrors.aliyun.com/debian/ bullseye main non-free contrib
deb https://mirrors.aliyun.com/debian-security/ bullseye-security main
deb-src https://mirrors.aliyun.com/debian-security/ bullseye-security main
deb https://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib
deb-src https://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib
deb https://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib
deb-src https://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib
将sources.list复制到apollo、challenge、solver目录下,修改challenge、solver目录下的Dockerfile,在所有的FROM python:3.7-slim下方添加:
ADD sources.list /etc/apt/sources.list
打开终端,进入apollo所在目录,执行命令:
sudo make build
此时如果使用make test命令进行测试,等待30~60秒,会出现如图7-10所示的结果。可以发现测试中,找到的PI值由两个八进制数组成(AGC采用八进制表示各种数据),具体解释后面会有介绍,找到PI值后,题目给出了flag值。
图7-10 apollo挑战题的测试结果
阿波罗计划是美国在1961年—1972年组织实施的一系列载人登月飞行任务,其目的是实现载人登月飞行和人对月球的实地考察,为载人行星飞行和探测进行技术准备。它是世界航天史上具有划时代意义的一项成就。阿波罗计划始于1961年5月,至1972年12月第6次登月成功结束,历时约11年,耗资255亿美元。阿波罗号飞船由指挥舱、服务舱和登月舱3部分组成。
(1)指挥舱:是宇航员在飞行中生活和工作的座舱,也是全飞船的控制中心。指挥舱为圆锥形,高3.2m,重约6吨。指挥舱分前舱、宇航员舱和后舱3部分。前舱内放置着陆部件、回收设备和姿态控制发动机等。宇航员舱为密封舱,存有供宇航员生活14天的必需品和救生设备。后舱内装有10台姿态控制发动机,各种仪器和贮箱,姿态控制、制导导航系统,以及船载计算机和无线电分系统等。
(2)服务舱:其前端与指挥舱对接,后端有推进系统主发动机喷管。舱体为圆筒形,高6.7m,直径4m,重约25吨。主发动机用于轨道转移和变轨机动。姿态控制系统由16台火箭发动机组成,用于飞船与第三级火箭分离、登月舱与指挥舱对接和指挥舱与服务舱分离等。
(3)登月舱:由下降级和上升级组成,地面起飞时重14.7吨,宽4.3m,最大高度约7m。其中下降级由着陆发动机、4根着陆架和4个仪器舱组成,上升级是登月舱主体。宇航员完成月面活动后驾驶上升级返回环月轨道与指挥舱会合。上升级由宇航员座舱、返回发动机、推进剂贮箱、仪器舱和控制系统组成。宇航员座舱可容纳2名宇航员,舱内设有导航、控制、通信、生命保障和电源等设备。
AGC是阿波罗计划中的主要船载计算机,使用在所有的登月任务中。指挥舱和登月舱都有AGC,但是两者运行的软件不同。AGC及其软件是在麻省理工学院仪器实验室(现在称为德雷珀实验室)开发的。性能参数如下:
题目中使用的是VirtualAGC。VirtualAGC是AGC爱好者制作的一个AGC模拟器,是开源软件,可以运行AGC上的程序。此外,AGC普遍使用的是八进制,本书采用Python的写法,数字前加“0o”表示八进制数,还有一种表示方法,就是数字加一个下标“8”。
线存储器是一种只读存储器(ROM)。利用磁环改变导线上电压的状态,如果导线穿过磁环,导线上的电压就会发生改变。系统检测到这种改变后,就会把这条导线上的数据解释为1,如果导线没有穿过磁环,那么导线上的电压不发生改变,系统就会把这条导线上的数据解释为0。线存储器如图7-11所示。
(a)
(b)
(c)
图7-11 线存储器
AGC上的ROM是以Bank组织的,每个bank为1024字,每个字为15bit,每个Bank中的字的地址是从0o2000(对应十进制1024)开始的,所以给出一个数据的Bank号及Bank中的地址address,可以计算实际地址,方法为:
Bank×20008 + address - 20008
例如,第0o27个Bank中的地址0o3355,对应的实际地址为:
278×20008 + 33558 - 20008 = 573558
DSKY类似于现代的显示器和键盘,但是那时候的显示器和键盘比较简单,如图7-12、图7-13所示。可以发现,上半部分是两个显示屏,下半部分是一个键盘,可以用于输入。
图7-12 DSKY
图7-13 飞船舱内操作面板,其中中间偏左有DSKY
为了更加清晰地了解DSKY,这里以VirtualAGC中实现的yaDSKY(yet another DSKY)为例进行介绍,其界面如图7-14所示。yaDSKY就是DSKY的模拟器,其界面和功能是完全一致的。
先介绍上半部分的显示屏,需要关注的是右半边,都是使用7段数码管来实现的,第二行有一个VERB,下方对应两个7段数码管,第二行还有一个NOUN,下方也对应两个7段数码管,接下来是三行连续的显示,每一行都是5个7段数码管,而且每一行最前方有一个类似加号的显示,显示的是正、负。
再介绍下半部分的键盘,需要关注的是,最左边一个按键名称是VERB,另一个是NOUN,与上半部分的显示刚好对应。在最右边有一个按键名称是ENTR,应该就是类似于现代键盘的回车键。
图7-14 yaDSKY的界面
这里就涉及DSKY的操作方法了,DSKY采用动词VERB(简称V)+名词NOUN(简称N)的方式进行控制操作,其中V、N的部分取值及其作用如图7-15所示。
(a)V的取值
(b)N的取值
图7-15 DSKY的V、N的部分取值(续)
注意到其中V的取值最下面的0o27,可以用来显示存储器中的数据,所以使用DSKY查询存储器特定地址的方式为:依次输入V27N02E,然后会发现DSKY上27、02两个数字会闪,此时输入57355,按ENTR键,即可得到地址为57355的数字,并在下面3行的第1行显示。此时再次按ENTR键,又可以输入一个地址,再按ENTR键,就会显示存储器中这个新地址存储的数据,如图7-16所示。
图7-16 使用DSKY读取ROM指定地址存储的数据
AGC中字有15位,还带1个奇偶校验位,但是这个奇偶校验位只供硬件使用,软件访问不了。一个字采用MSB的方式,最高位是第15位,最低位是第1位,如图7-17所示,最后一个P是奇偶校验位。
图7-17 AGC中字的格式
1)单精度浮点数(Single-Precision,SP)的格式
SP使用一个15位的字表示,第15位是符号位,为1表示负数,为0表示正数。第14~第1位构成SP的小数部分。如果是正数,那么SP的值就是:
如果是负数,那么SP的值就是:
比如:
2)双精度浮点数(Double-Precision,DP)的格式
为了提高精度,使用两个连续的15位的字表示一个DP。前一个字称为字1,后一个字称为字0,字1的第14位~第1位表示较高的有效位,字0的第14位~1位表示较低的有效位,并且字1的第15位是符号位,如图7-18所示。
图7-18 DP格式
一般而言,这两个字的第15位是一致的,但是也有不一致的情况,这里只考虑一致的情况。如果是正数,那么DP的值就是:
如果是负数,那么DP的值就是:
现在,回头检查一下前文在进行测试时,显示的PI/16的结果,如图7-10所示,为0o6413 0o11416,这是一个DP,按照DP的定义,其对应的十进制数为:
这个就是PI/16的值,将其乘以16,得到PI的值为3.26103293895721435546875,可见确实是偏了一点。
解法二与解法一的思路是一致的,都是寻找到ROM中存储数据0o37777的位置,然后将其前两个位置的数据读出,即PI/16的值。但是,解法二不使用yaDSKY,而是通过分析DSKY的原理,编写程序模拟DSKY与AGC的交互过程,读取AGC中ROM的数据。
AGC的I/O(Input/Output,输入/输出)使用4字节的数据包,格式如图7-26所示,最左边是MSB,最右边是LSB。其中ppppppp代表不同的通道,一共有128个通道,与本挑战题相关的通道如表7-1所示。ddddddddddddddd表示15位数据。
图7-26 AGC I/O数据包的格式
表7-1 与本挑战题相关的通道
通 道 号 | 作 用 |
输出通道0o10(八进制) | 用于驱动7段数码管的显示 |
输入通道0o15(八进制) | 用于得到DSKY的按键信息 |
DSKY有19个按键,每个按键使用5位编码,如表7-2所示。按键的编码信息会存储在AGC I/O的最低5位中。
表7-2 DSKY的按键编码
按 键 | 二进制编码 | 十进制编码 | 八进制编码 |
0 | 10000 | 16 | 20 |
1 | 00001 | 1 | 1 |
续表
按 键 | 二进制编码 | 十进制编码 | 八进制编码 |
2 | 00010 | 2 | 2 |
3 | 00011 | 3 | 3 |
4 | 00100 | 4 | 4 |
5 | 00101 | 5 | 5 |
6 | 00110 | 6 | 6 |
7 | 00111 | 7 | 7 |
8 | 01000 | 8 | 10 |
9 | 01001 | 9 | 11 |
VERB | 10001 | 17 | 21 |
RSET | 10010 | 18 | 22 |
KEY REL | 11001 | 25 | 31 |
+ | 11010 | 26 | 32 |
- | 11011 | 27 | 33 |
ENTR | 11100 | 28 | 34 |
CLR | 11110 | 30 | 36 |
NOUN | 11111 | 31 | 37 |
PRO | 当通道号是0o32(八进制)时,第14位为1,表示PRO按键按下,本挑战题的解析过程使用不到PRO按键,读者无须关注 |
为了理解AGC是如何驱动7段数码管显示的,需要首先了解DSKY上7段数码管的编号约定,如图7-27所示。DSKY I/O中通道号如果是0o10,那么表示驱动7段数码管的显示,此时15位数据的定义如图7-28所示。可以发现分为了4部分:
其中RLYWD取不同的值时的作用如表7-3所示。例如,当RLYWD为1011时,会驱动显示M1、M2两个7段数码管,其中M1显示的数字存储在DSPH,M2显示的数字存储在DSPL;当RLYWD为0001时,会驱动显示编号为34、35的两个7段数码管,其中编号为34的数码管显示的数字存储在DSPH,编号为35的数码管显示的数字存储在DSPL,另外,此时DSPC位控制编号为3-的符号位的显示。7段数码管的显示与DSPH、DSPL的值的对应关系如表7-4所示。
图7-27 DSKY上7段数码管的编号
图7-28 当DSKY I/O中通道号为驱动7段数码管显示时的15位数据的定义
表7-3 当DSKY I/O为驱动7段数码管显示时,15位数据位的含义
第15位~12位 RLYWD | 第11位 DSPC | 第10位~6位 DSPH | 第5位~1位 DSPL |
1011 | M1 | M2 | |
1010 | FLASH | V1 | V2 |
1001 | N1 | N2 | |
1000 | UPACT | 11 | |
0111 | 1+ | 12 | 13 |
0110 | 1- | 14 | 15 |
0101 | 2+ | 21 | 22 |
0100 | 2- | 23 | 24 |
0011 | 25 | 31 | |
0010 | 3+ | 32 | 33 |
0001 | 3- | 34 | 35 |
表7-4 7段数码管的显示DSPH、DSPL的值与的对应关系
DSPH或者DSPL的值 | 7段数码管显示 |
00000(对应十进制数0) | 空 |
10101(对应十进制数21) | 0 |
00011(对应十进制数3) | 1 |
11001(对应十进制数25) | 2 |
11011(对应十进制数27) | 3 |
01111(对应十进制数15) | 4 |
11110(对应十进制数30) | 5 |
11100(对应十进制数28) | 6 |
10011(对应十进制数19) | 7 |
11101(对应十进制数29) | 8 |
11111(对应十进制数31) | 9 |
有了上述基础知识,就可以编写代码实现了,主要代码如下:
# 按照表7-4,定义数字与7段数码管显示的对应,用一个数组实现
numLookup = {
0 : " ",
3 : "1",
15: "4",
19: "7",
21: "0",
25: "2",
27: "3",
28: "6",
29: "8",
30: "5",
31: "9"
}
# 按照表7-2,定义按键与编码的对应,也用一个数组实现
keys = {
'0': 16,
'1': 1,
'2': 2,
'3': 3,
'4': 4,
'5': 5,
'6': 6,
'7': 7,
'8': 8,
'9': 9,
'e': 28,
'v': 17,
'n': 31,
'c': 30,
}
# 按照图7-28的格式,定义两个函数,分别用于组装形成AGC I/O数据包、分析AGC I/O数据包
# 第一个函数:组装形成AGC I/O数据包,输入的参数是通道号、数据
def FormIoPacket(chan, val):
if (chan < 0 or chan > 0x1ff):
return None
if (val < 0 or val > 0x7fff):
return None
return struct.pack("BBBB",
0xFF & ( chan >> 3),
0xFF & ( 0x40 | ((chan << 3) & 0x38) | ((val >> 12) & 0x7) ),
0xFF & ( 0x80 | ((val >> 6) & 0x3F)),
0xFF & ( 0xc0 | (val & 0x3F) )
)
# 第二个函数:分析AGC I/O数据包,输出通道号、数据
def ParseIoPacket(bs):
channel = ((bs[0] & 0x1F) << 3) | ((bs[1] >> 3) & 7)
value = ((bs[1] << 12) & 0x7000) | ((bs[2] << 6) & 0xFC0) | (bs[3] & 0x3F)
ubit = (0x20 & bs[0])
return channel,value,ubit
# 按照表7-2的格式,定义SendKey函数,用于发送键盘按键信息,注意,其中的通道号是0o15
def SendKey(keyCode):
return FormIoPacket(0o15, keyCode)
# 定义显示面板对应的7段数码管,分别是:
# 2个VERB、2个NOUN
# 第1行5个数码管
# 第2行5个数码管
# 第3行5个数码管
Verb = [0] * 2
Noun = [0] * 2
R1 = [0] * 5
R2 = [0] * 5
R3 = [0] * 5
# 定义一个显示函数,通过传递来的编码,查找numLookup数组,得到要显示的对应数字
def formatNums(nums):
out = ""
for num in nums:
out += numLookup[num]
return out
# 下面两个函数参考表7-3,依据RLYWD的值,确定那个数码管显示数字,将相应的数字赋值过去
def doLeft(digit, vL):
if vL == 0:
pass
elif digit == 0x3800:
R1[1] = vL
elif digit == 0x3000:
R1[3] = vL
elif digit == 0x1000:
R3[1] = vL
elif digit == 0x800:
R3[3] = vL
def doRight(digit, vR):
if vR == 0:
pass
elif digit == 0x4000:
R1[0] = vR
elif digit == 0x3800:
R1[2] = vR
elif digit == 0x3000:
R1[4] = vR
elif digit == 0x1800:
R3[0] = vR
elif digit == 0x1000:
R3[2] = vR
elif digit == 0x800:
R3[4] = vR
# 定义一个处理AGC I/O数据包的函数,注意其中只处理通道号为0o10的数据包,参考表7-1可知,这个
# 通道是驱动7段数码管显示的通道def HandlePacket(bs):
while len(bs) > 4:
chan, val, ubit = ParseIoPacket(bs[:4])
bs = bs[4:]
if (chan != 0o10):
continue
digit = 0x7800 & val # 取出RLYWD的值
vL = (val >> 5) & 0x1F # 取出DSPH的值
vR = val & 0x1F # 取出DSPL的值
doLeft(digit, vL) # 调用前面定义的两个函数,确定要显示的数字
doRight(digit, vR)
return bs
......
if __name__ == "__main__":
Host = os.getenv("HOST", "localhost")
Port = int(os.getenv("PORT", 31450))
Ticket = os.getenv("TICKET", "")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((Host, Port))
if len(Ticket):
sock.recv(128)
sock.send((Ticket + "\n").encode("utf-8"))
for line in sock.recv(2048).split(b'\n'):
if b"listening" in line:
Host2,Port2 = line.decode('utf-8').split(" ")[-1].split(":")
print(line.decode('utf-8'))
time.sleep(5)
Port2 = int(Port2)
print("Connecting to:",Host2,Port2)
sock2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock2.connect((Host2, Port2))
lock = threading.Lock()
reader = threading.Thread(target=readLoop, args=(sock2,lock))
reader.start()
# 发送读取ROM的指令
for b in "v27n02":
sock2.send(SendKey(keys[b]))
time.sleep(0.1)
time.sleep(1)
# 开始搜索
startCount = 45
nums = []
for ii in range(0,20):
sock2.send(SendKey(keys['e']))
time.sleep(0.9)
# 读取的地址是从0o57355开始的20个位置
command = "573" + oct(startCount + ii)[2:]
for idx,b in enumerate(command):
sock2.send(SendKey(keys[b]))
while True:
time.sleep(0.5)
lock.acquire()
get = R3[idx]
lock.release()
if numLookup[get] == b:
break
sock2.send(SendKey(keys['e']))
time.sleep(0.9)
while True:
try:
lock.acquire()
num = (int(formatNums(R1),8))
lock.release()
except:
continue
break
# 若读取到了0o37777,则停止往下读取
if num == 0o37777:
break
else:
nums.append(num)
running = False
hi,lo = nums[-2:] # 取出数据0o37777对应存储地址的前两个地址存储的数字
print(oct(hi), oct(lo))
reader.join()
hi_b = bin(hi)[2:]
hi_b = '0' * ( 14-len(hi_b) ) + hi_b
lo_b = bin(lo)[2:]
lo_b = '0' * ( 14-len(lo_b) ) + lo_b
bits = hi_b + lo_b
# 将数据0o37777对应存储地址的前两个地址存储的数字,按照AGC中DP的解读,计算其对应的浮点数
value = 0.0
for idx, bit in enumerate(bits):
if bit == '1':
value += 2.0**(-1 - idx)
# 上面计算得到的DP乘以16,就是PI的值
sock.send(bytes("{:1.09f}\n".format(value * 16), 'utf-8'))
for line in sock.recv(1024).split(b'\n'):
print(line.decode('utf-8'))
sys.stdout.flush()
sock.close()
sock2.close()