【CTF对抗-Polarisctf招新赛-UDS】此文章归类为:CTF对抗。
| 报文类型 | 字节0(高4bit) | 字节0(低4bit) | 字节1 | 字节2 | 字节3-7 | 描述 |
|---|---|---|---|---|---|---|
| 单帧(SF) | 0000 | DLC(0~7) | DATA | - | - | 单帧传输,数据长度 ≤ 7字节 |
| 第一帧(FF) | 0001 | 长度高4bit | 长度低8bit | DATA | - | 多帧起始帧,表示总长度(8~4095字节) |
| 连续帧(CF) | 0010 | SN(0~F循环) | DATA | - | - | 后续数据帧,按序号递增 |
| 流控帧(FC) | 0011 | FS(流控状态) | BS(块大小) | STmin(最小间隔) | - | 接收方控制发送节奏 |
| 字段 | 取值 | 含义 | 说明 |
|---|---|---|---|
| FS(Flow Status) | 0x00 | CTS(Continue To Send) | 允许发送方继续发送 |
| 0x01 | WT(Wait) | 暂停发送,等待下一个 FC | |
| 0x02 | OVFLW(Overflow) | 接收方缓存溢出,终止通信 | |
| BS(Block Size) | 0x00 | 无限制 | 可连续发送所有 CF |
| 0x01~0xFF | 分块发送 | 每发送 BS 个 CF 需等待新 FC | |
| STmin(Separation Time) | 0x00~0x7F | 0~127 ms | 连续帧最小发送间隔 |
| 0xF1~0xF9 | 100~900 μs | 高精度时间(微秒级) |
| SID | 主要作用 | 子功能(中文) | 发送格式 | 是否需要27认证 | 积极响应格式 | 消极响应格式 |
|---|---|---|---|---|---|---|
| 0x10 | 切换诊断会话 | 01:默认会话;02:编程会话;03:扩展会话 | 10 xx |
否 | 50 xx |
7F 10 NRC |
| 0x11 | ECU 复位 | 01:硬复位;02:钥匙开关复位;03:软复位 | 11 xx |
否 / 是 | 51 xx |
7F 11 NRC |
| 0x22 | 读取 ECU 数据 | 读取指定数据标识符 | 22 DID;如获取 PIN 码:22 F1 90 |
否 | 62 DID DATA;62 F1 90 PIN码 |
7F 22 NRC |
| 0x27 | 安全访问认证 | 01:请求 Seed;02:发送 Key | 27 xx [KEY] |
— | 67 xx [Seed] |
7F 27 NRC |
| 0x28 | 控制 ECU 通信 | 00:开启收发;01:只接收不发送;02:只发送不接收;03:关闭收发 | 28 xx xx |
是 | 68 xx xx |
7F 28 NRC |
| 0x31 | 控制 ECU 内部例程 | 01:启动例程;02:停止例程;03:获取例程结果 | 31 xx RID [DATA] |
是 | 71 xx RID [DATA] |
7F 31 NRC |
| 0x34 | 请求下载 | 准备刷写下载 | 34 DF AL [ADDR] [LEN] |
是 | 74 xx [len] |
7F 34 NRC |
| 0x36 | 传输刷写数据 | 分块传输数据 | 36 BS [DATA] |
是 | 76 BS |
7F 36 NRC |
| 0x2E | 写 ECU 数据 | 写入指定数据标识符 | 2E DID DATA;2E F1 90 PIN码 |
是 | 6E DID;2E F1 90 |
7F 2E NRC |
| 0x3E | 维持当前诊断会话 | 00:需要 ECU 回复;80:不需要 ECU 回复 | 3E xx |
否 | 7E 00 |
7F 3E NRC |
| NRC | 名称 | 含义 | 常见触发场景 |
|---|---|---|---|
0x10 |
General Reject | 通用拒绝 | ECU内部错误或未分类异常 |
0x11 |
Service Not Supported | 服务不支持 | SID 不存在 |
0x12 |
Sub-function Not Supported | 子功能不支持 | 子功能值错误 |
0x13 |
Incorrect Message Length Or Invalid Format | 报文长度/格式错误 | 参数长度不对 |
0x14 |
Response Too Long | 响应过长 | 超过ISO-TP限制 |
0x21 |
Busy Repeat Request | ECU忙 | 需要重试 |
0x22 |
Conditions Not Correct | 条件不满足 | 会话/状态不对 |
0x24 |
Request Sequence Error | 请求顺序错误 | 27流程错误 |
0x25 |
No Response From Sub-net Component | 子节点无响应 | 网关/子ECU问题 |
0x26 |
Failure Prevents Execution Of Requested Action | 执行条件失败 | ECU内部状态异常 |
0x31 |
Request Out Of Range | 请求超范围 | DID/RID不存在 |
0x33 |
Security Access Denied | 安全访问拒绝 | 未解锁或权限不足 |
0x35 |
Invalid Key | Key错误 | 27认证失败 |
0x36 |
Exceeded Number Of Attempts | 超过尝试次数 | 防爆破触发 |
0x37 |
Required Time Delay Not Expired | 冷却时间未到 | 需等待 |
0x70 |
Upload Download Not Accepted | 下载/上传被拒绝 | 34/35失败 |
0x71 |
Transfer Data Suspended | 传输中断 | 36异常 |
0x72 |
General Programming Failure | 编程失败 | 刷写失败 |
0x73 |
Wrong Block Sequence Counter | 块序号错误 | 36顺序错误 |
0x78 |
Request Correctly Received – Response Pending | 请求已接收,处理中 | ECU仍在处理 |
0x7E |
Sub-function Not Supported In Active Session | 当前会话不支持子功能 | 会话错误 |
0x7F |
Service Not Supported In Active Session | 当前会话不支持该服务 | 服务需在特定会话 |
[000.000] 7E0 8 02 10 03 00 00 00 00 00
[000.012] 7E8 8 06 50 03 00 32 01 F4 00
[000.105] 7E0 8 02 27 01 00 00 00 00 00
[000.118] 7E8 8 06 67 01 12 34 56 78 00
[000.220] 7E0 8 06 27 02 9A BC DE F0 00
[000.233] 7E8 8 02 67 02 00 00 00 00 00
[000.340] 7E0 8 03 22 F1 90 00 00 00 00
[000.355] 7E8 8 10 14 62 F1 90 4C 56 44
[000.362] 7E0 8 30 00 0A 00 00 00 00 00
[000.373] 7E8 8 21 43 42 31 32 33 34 35
[000.385] 7E8 8 22 36 37 38 39 30 31 32
[000.500] 7E0 8 03 22 F1 87 00 00 00 00
[000.514] 7E8 8 07 62 F1 87 31 2E 30 35
[000.620] 7E0 8 04 31 01 02 03 00 00 00
[000.635] 7E8 8 04 71 01 02 03 00 00 00
[000.760] 7E0 8 10 0B 34 00 44 00 00 10
[000.772] 7E0 8 21 00 00 00 10 00 00 00
[000.790] 7E8 8 04 74 20 00 08 00 00 00
[000.910] 7E0 8 10 0A 36 01 AA BB CC DD
[000.922] 7E0 8 21 EE FF 11 22 33 00 00
[000.940] 7E8 8 02 76 01 00 00 00 00 00
[001.050] 7E0 8 10 0A 36 02 44 55 66 77
[001.062] 7E0 8 21 88 99 AA BB CC 00 00
[001.078] 7E8 8 02 76 02 00 00 00 00 00
[001.190] 7E0 8 01 37 00 00 00 00 00 00
[001.205] 7E8 8 01 77 00 00 00 00 00 00
[001.320] 7E0 8 02 11 01 00 00 00 00 00
[001.335] 7E8 8 02 51 01 00 00 00 00 00
[002.000] 7E0 8 06 2E F1 90 41 42 43 00
[002.014] 7E8 8 03 7F 2E 33 00 00 00 00
[002.120] 7E0 8 03 22 F1 FF 00 00 00 00
[002.133] 7E8 8 03 7F 22 31 00 00 00 00
[002.250] 7E0 8 02 31 01 00 00 00 00 00
[002.264] 7E8 8 03 7F 31 7F 00 00 00 00
| 字段 | 值 |
|---|---|
| 时间戳 | 000.340 |
| CAN ID | 0x7E0 |
| DLC | 8 |
| 数据区 | 03 22 F1 90 00 00 00 00 |
| 高 4 bit | 类型 | 含义 |
|---|---|---|
0x0 |
SF | 单帧 |
0x1 |
FF | 首帧 |
0x2 |
CF | 连续帧 |
0x3 |
FC | 流控帧 |
| 类型 | 代表报文 |
|---|---|
| SF | 02 10 03 ... |
| FF | 10 14 62 F1 90 4C 56 44 |
| FC | 30 00 0A 00 00 00 00 00 |
| CF | 21 ... / 22 ... |
| 字段 | 长度 | 说明 |
|---|---|---|
| SID | 1 字节 | 服务标识 |
| Sub-function | 1 字节 | 子功能,可选 |
| DID / RID | 2 字节 | 数据标识符 / 例程标识符 |
| 参数区 | 可变 | 服务参数 |
| 正响应 SID | 1 字节 | 原 SID + 0x40 |
| 否定响应 | 7F |
否定响应固定头 |
送分题,仅模拟了 UDS 诊断中 27 服务的使用。
def generate_seed():
return random.randint(0, 0xFFFFFFFF)
def calculate_key(seed):
key = seed ^ 0xA5A5A5A5
key = ((key << 3) | (key >> 29)) & 0xFFFFFFFF
key = (key + 0x12345678) & 0xFFFFFFFF
return key
题目已经给出算法且给出使用方法,无论是人工手动计算或者使用 AI 写交互脚本都可以,学习为主。
plus 更偏向 UDS 实战,同时也添加了实战中存在的限制,如空闲时间为 3 秒、防爆破、防抓包以及更高级的算法等级策略。
本题给了两个附件:BLE 的流量包和 CAN 的日志。本次还是主要以介绍 UDS 服务为主。
ble.pcapng 是一个 BLE 蓝牙流量包,这里我们可以使用 tshark 提取流量包中的有用数据。
# 1. 看协议分布
tshark -r ble.pcapng -q -z io,phs
# 2. 看看 ATT 里有哪些操作
tshark -r ble.pcapng -Y "btatt" -T fields -e btatt.opcode.method | uniq -c
# 3. 所有写操作的 handle 列出来
tshark -r ble.pcapng -Y "btatt.opcode.method==0x12" -T fields -e btatt.handle | uniq -c
# 4. 验证这个 handle 上到底写了什么并导出
tshark -r ble.pcapng -Y "btatt.handle==0x0021 && btatt.value" -T fields -e btatt.value >> 1.txt
示例分析:
1. 看协议分布
tshark -r ble.pcapng -q -z io,phs
===================================================================
Protocol Hierarchy Statistics
Filter:
nordic_ble frames:21836 bytes:672885
btle frames:21836 bytes:672885
_ws.malformed frames:193 bytes:13642
btmesh frames:1 bytes:225
_ws.malformed frames:1 bytes:225
btl2cap frames:492 bytes:23395
btatt frames:488 bytes:23246
_ws.unreassembled frames:1 bytes:94
===================================================================
重点看有没有这些:
- btle
- btl2cap
- btatt
如果有 btatt,说明里面有 GATT/ATT 层数据,值得继续看。
2. 看看 ATT 里有哪些操作
tshark -r ble.pcapng -Y "btatt" -T fields -e btatt.opcode.method | uniq -c
……………………
1 0x01,0x08
1 0x0a
2 0x0b
2 0x0a
2 0x0b
354 0x12
发现存在 0x12,在 ATT 里,0x12 对应写操作,说明这次数据传输主要是在写某个特征值。
3. 所有写操作的 handle 列出来
tshark -r ble.pcapng -Y "btatt.opcode.method==0x12" -T fields -e btatt.handle | uniq -c
354 0x0021
4. 验证这个 handle 上到底写了什么并导出
tshark -r ble.pcapng -Y "btatt.handle==0x0021 && btatt.value" -T fields -e btatt.value >> 1.txt
至此,我们流量包已经分析完了,那么我们提取出来的值有什么用呢?
打开 1.txt,不难发现开头和结尾是特殊字符,分别代表着私有协议的起始控制帧和结束控制帧。


那么其次,中间的内容是什么呢?
不难发现每行为 18 个字节,很符合一个固件分片的特征:2 字节索引 + 16 字节数据。那么,我们可以尝试修复这个固件。
from pathlib import Path
lines = [x.strip() for x in Path("1.txt").read_text().splitlines() if x.strip()]
frags = {}
for s in lines:
if len(s) != 36:
continue
idx = int.from_bytes(bytes.fromhex(s[:4]), "little")
data = bytes.fromhex(s[4:])
frags[idx] = data
out = b"".join(frags[i] for i in range(min(frags), max(frags) + 1))
Path("firmware_rebuilt.bin").write_bytes(out)
print(len(out))
拿到固件后,我们依然没办法分析内容。这里我们通过固件的数据不难发现,这个固件被加密了。
那么,我们该如何解密这个固件?首先分析另一个附件,也就是 can.asc。
在流量的末尾处我们会发现很明显的诊断记录:
接着,我们按照数据结构划分,不难分析出在使用 22 F1 90 命令时,返回了 62 F1 90 加上一串数据,这明显意味着积极响应。根据数据结构对内容进行删除并解密:
可以得到:
key-is-Polaris_ctf_2026
得到了 key,刚好 16 字节,敏感的人已经可以猜测到是 AES 加密了,此时对固件进行解密。
openssl enc -aes-128-ecb -d -nopad -K 506f6c617269735f6374665f32303236 -in firmware_rebuilt.bin -out firmware_decrypted_aes128_ecb.bin
这样,我们就可以得到一个可分析的 AARCH64 结构固件了。
分析过程不再赘述,无论是通过 AI 解析固件,还是模拟执行固件,都是可以的。
拿到了固件,就差最后一步:获取 flag。
这里我给出了 8 个诊断服务,我们需要理解其使用方法和排除干扰项,这里直接给到使用步骤。
1、切换到扩展会话
10 03
2、过 level 1
27 01
27 02
3、过 level 5
27 05
27 06
4、触发漏洞
22 F1 99
5、读取 flag
31 01 13 37
这里因为设置了会话空闲时间,导致在无操作时会返回到默认会话(10 01),从而无法执行其他服务。那么我们写一个交互代码即可。那么我们该如何得到可用的参数呢?这里我们可用通过观察服务的错误码来判断这个服务是否支持这个参数。

这里我们可以对比NRC错误码标判断,当服务不支持这个参数时,返回的时0x12,也就是子服务不支持此功能;当输入正确的值时,会返回0x33,也就是没有经过27服务认证,22服务也是同理。所以,我们只需要对这些值进行爆破即可。
(正常来说可以使用 3E 服务进行会话维持,但是出题人比较菜,3E 写了没作用,不过不影响做题,给大家磕了。)
最后脚本如下:
from unicorn import *
from unicorn.arm64_const import *
import struct, socket, time
with open('dec_ECB_sorted.bin', 'rb') as f:
fw = bytearray(f.read())
PROC = b'Name:\tfirmware\nState:\tS\nTgid:\t1234\nPid:\t1234\nPPid:\t1\nTracerPid:\t0\nUid:\t1000\t1000\t1000\t1000\n'
poff = [0]
def hsys(mu, intno, _):
if intno != 2:
return
n = mu.reg_read(UC_ARM64_REG_X8)
a0 = mu.reg_read(UC_ARM64_REG_X0)
a1 = mu.reg_read(UC_ARM64_REG_X1)
a2 = mu.reg_read(UC_ARM64_REG_X2)
if n == 172:
mu.reg_write(UC_ARM64_REG_X0, 1234)
elif n == 56:
poff[0] = 0
mu.reg_write(UC_ARM64_REG_X0, 42)
elif n == 63:
rem = PROC[poff[0]:]
tr = min(len(rem), a2)
mu.mem_write(a1, rem[:tr])
poff[0] += tr
mu.reg_write(UC_ARM64_REG_X0, tr)
elif n == 57:
mu.reg_write(UC_ARM64_REG_X0, 0)
elif n == 113:
mu.mem_write(a1, struct.pack('<QQ', 100, 1000))
mu.reg_write(UC_ARM64_REG_X0, 0)
elif n == 117:
mu.reg_write(UC_ARM64_REG_X0, 0)
elif n == 93:
mu.emu_stop()
else:
mu.reg_write(UC_ARM64_REG_X0, 0)
# Pre-init emulator
mu_g = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
mu_g.mem_map(0, 0x10000)
mu_g.mem_map(0x80000, 0x10000)
mu_g.mem_map(0x100000, 0x10000)
mu_g.mem_write(0, bytes(fw))
mu_g.mem_write(len(fw), b'\x00' * (0x2000 - len(fw)))
mu_g.mem_write(0x15F0, b'\x00' * 16)
mu_g.hook_add(UC_HOOK_INTR, hsys)
sp = 0x8FE00
mu_g.reg_write(UC_ARM64_REG_SP, sp)
mu_g.reg_write(UC_ARM64_REG_LR, 0x3000)
poff[0] = 0
try:
mu_g.emu_start(0x0000, 0x3000, timeout=60000000, count=20000000)
except:
pass
init_mem = bytes(mu_g.mem_read(0, 0x2000))
def calc(seed_hex):
poff[0] = 0
mu = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
mu.mem_map(0, 0x10000)
mu.mem_map(0x80000, 0x10000)
mu.mem_map(0x100000, 0x10000)
mu.mem_write(0, init_mem)
mu.mem_write(0x2000, b'\x00' * (0x10000 - 0x2000))
mu.hook_add(UC_HOOK_INTR, hsys)
sa, oa = 0x100000, 0x100100
mu.mem_write(sa, bytes.fromhex(seed_hex))
mu.mem_write(oa, b'\x00' * 16)
mu.reg_write(UC_ARM64_REG_SP, sp)
mu.reg_write(UC_ARM64_REG_X0, sa)
mu.reg_write(UC_ARM64_REG_X1, oa)
mu.reg_write(UC_ARM64_REG_LR, 0x3000)
try:
mu.emu_start(0x0E70, 0x3000, timeout=120000000, count=50000000)
except:
pass
return bytes(mu.mem_read(oa, 16))
def l1key(seed):
k = seed ^ 0xA5A5A5A5
k = ((k << 3) | (k >> 29)) & 0xFFFFFFFF
return (k + 0x12345678) & 0xFFFFFFFF
def hb(d):
return ' '.join('{:02X}'.format(b) for b in d)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('nc1.ctfplus.cn', 36541))
s.settimeout(5)
def rx():
d = b''
s.settimeout(0.3)
while True:
try:
c = s.recv(4096)
except:
break
if not c:
break
d += c
return d
def tx(c):
s.sendall((c + '\n').encode())
time.sleep(0.08)
return rx().decode('utf-8', errors='replace').strip().replace('UDS> ', '').strip()
rx()
# L1
tx('10 03')
r = tx('27 01')
h = r.split()
sd = int(''.join(h[2:6]), 16)
k = l1key(sd)
kk = '{:08X}'.format(k)
tx('27 02 {} {} {} {}'.format(kk[0:2], kk[2:4], kk[4:6], kk[6:8]))
# L5
tx('10 02')
r = tx('27 05')
h = r.split()
seed = ''.join(h[2:6])
key16 = calc(seed)
r = tx('27 06 ' + hb(key16))
print(f'L5: {r}')
if '67 06' in r:
print('*** L5 UNLOCKED ***')
# Read specific DIDs fast
for did in [
'F1 90', 'F1 91', 'F1 92', 'F1 93', 'F1 94', 'F1 95', 'F1 96', 'F1 97',
'F1 98', 'F1 99', 'F1 9A', 'F1 9B', 'F1 9C', 'F1 9D', 'F1 9E', 'F1 9F',
'F1 A0', 'F1 A1', 'F1 A2', 'F1 A3', 'F1 A4', 'F1 A5'
]:
r2 = tx('22 ' + did)
if r2 and not r2.startswith('7F'):
parts = r2.split()
hd = [x for x in parts[3:] if len(x) == 2]
asc = ''.join(chr(int(x, 16)) for x in hd if 32 <= int(x, 16) < 127)
print(f' {did}: raw={r2}')
if asc:
print(f' ascii={asc}')
# Try write + read
print('\nTrying WriteDataByIdentifier...')
print(tx('2E F1 99 DE AD BE EF'))
print(tx('22 F1 99'))
# RoutineControl - get flag (routine 0x1337)
print('\nRoutineControl...')
for cmd in ['31 01 13 37', '31 02 13 37', '31 03 13 37']:
r3 = tx(cmd)
print(f'{cmd}: {r3}')
if r3 and not r3.startswith('7F'):
parts = r3.split()
hd = [x for x in parts if len(x) == 2]
asc = ''.join(chr(int(x, 16)) for x in hd if 32 <= int(x, 16) < 127)
print(f' ASCII: {asc}')
# Try reading in extended session after L5
print('\nSwitch to extended...')
tx('10 03')
r2 = tx('27 01')
h2 = r2.split()
if h2[0] == '67':
sd2 = int(''.join(h2[2:6]), 16)
k2 = l1key(sd2)
kk2 = '{:08X}'.format(k2)
tx('27 02 {} {} {} {}'.format(kk2[0:2], kk2[2:4], kk2[4:6], kk2[6:8]))
for did in ['F1 90', 'F1 98', 'F1 99', 'F1 9A']:
r2 = tx('22 ' + did)
if r2 and not r2.startswith('7F'):
parts = r2.split()
hd = [x for x in parts[3:] if len(x) == 2]
asc = ''.join(chr(int(x, 16)) for x in hd if 32 <= int(x, 16) < 127)
print(f' {did}: {asc if asc else r2}')
else:
print(f'FAILED: {r}')
s.close()
更多【CTF对抗-Polarisctf招新赛-UDS】相关视频教程:www.yxfzedu.com