汽车安全 | 9分钟
2025 “招商铸盾” 智能网联汽车攻防赛Writeup
十月 9, 2025
CAN UPX 仿射密码

之前的之江铸网有个车联网,没来得及参加,也没流出附件和wp,这次通过狼组的附件做一下,当然还是通过wp学习

不过这个WP为什么都是常规CTF题的啊,iov只有附件没有wp

2025“招商铸盾”智能网联汽车攻防赛是由招商局检测车辆技术研究院有限公司主办的面向智能网联汽车领域的安全攻防活动。

MISC

日志审计-8-19

可能是让找哪个恶意IP登录了Admin吧,根据格式,搜索Accepted password for root from就俩IP有这个一个是root,一个是admin用户,那个IP就是恶意,不过狼组居然提取所有IP扔微步里,没想到。微步普通用户好像不能上传ioc判断是否恶意了。

另一个数独题没附件。

Crypto

这是古典密码

1YWlweHs4MjNqNTZwMzdhcDkycDkzcGQ0ZzdhZDZhMHAwMXAyMX0=

base64解开是个仿射密码,仿射密码是根据函数加密的:

$$ E(x)=(ax+b)\bmod{m} $$
  • am 互质
  • m 是字母的数目

解密函数是:

$$ D(x)=a^{-1}(x-b)\bmod{m} $$

$a^{−1}$是 a 在 $\mathbb{Z}_m$ 群的乘法逆元

注意:x 是字母的次序,以 0 为开始

这里使用爆破,把每种可能都输出,狼组的脚本很好,不用任何库,26位直接遍历逆元,虽然没有验证互质。但是很简洁。

用我的思路,我还是会用库的:

python
 1import gmpy2
 2# 解密函数
 3def affine_decrypt(ciphertext,a,b):
 4    plaintext=''
 5    inver_a=gmpy2.invert(a,26)
 6    for char in ciphertext:
 7        if char.isalpha():
 8            # 获取字母次序
 9            char_num = ord(char) - ord('a')
10            decrypted_char_num = (inver_a * (char_num - b)) % 26
11            # 得到密文字母次序,要加上a的ascii才能得到此密文字母的ascii
12            decrypted_char = chr(decrypted_char_num + ord('a'))
13            plaintext += decrypted_char
14        else:
15            plaintext += char
16    
17    return plaintext
18
19def brute_force(ciphertext):
20    for a in range(1, 26):
21        for b in range(26):
22            if gmpy2.gcd(a,26)==1:
23                plaintext = affine_decrypt(ciphertext, a, b)
24                print(f"参数:a={a},b={b} 解密:{plaintext}")
25                # 输出太多还要搜索,这里设置如果是flag开头直接停止函数
26                if plaintext[:4]=="flag":
27                    return 0
28            else:
29                continue
30
31ciphertext = 'aipx{823j56p37ap92p93pd4g7ad6a0p01p21}'
32brute_force(ciphertext)

不同在于,添加了互质校验,检测到flag开头直接停止,不用再搜索。如果是大小写字母加数字的,m实际上就从26变成了 26+26+10=62

Re

gogogo

Go写的,而且UPX标识不全。所以需要补全,然后脱壳。

补全后,可以手动脱壳,也可以用upx脱壳。

这个知识点在 无名侠在看雪的《IDA特训营》课程学过

缺少了字母P,ASCII是80,十六进制是50,补上两个就行,然后upx脱壳:

cmd
1upx -d test.exe

IDA打开就是Go的典型入口点:_rt0_amd64_windows,不过没什么用,看main函数:main.main

go
 1// main.main
 2void __fastcall main_main()
 3{
 4  string *v0; // rcx
 5  __int64 v1; // rax
 6  __int64 r1; // rdx
 7  __int64 r0; // rbx
 8  retval_44A060 v4; // kr00_16
 9  retval_449800 v5; // kr10_16
10  char *ptr; // rdi
11  __int64 v7; // [rsp+0h] [rbp-68h]
12  __int64 v8; // [rsp+8h] [rbp-60h]
13  __int64 v9; // [rsp+10h] [rbp-58h]
14  string *p_string; // [rsp+18h] [rbp-50h]
15  _QWORD v11[2]; // [rsp+20h] [rbp-48h] BYREF
16  _QWORD v12[2]; // [rsp+30h] [rbp-38h] BYREF
17  _QWORD v13[2]; // [rsp+40h] [rbp-28h] BYREF
18  _QWORD v14[2]; // [rsp+50h] [rbp-18h] BYREF
19
20  p_string = (string *)runtime_newobject(&RTYPE_string);
21  p_string->ptr = 0;
22  v14[0] = &RTYPE__ptr_string;
23  v14[1] = p_string;
24  fmt_Fscanf(go_itab__os_File_io_Reader, os_Stdin, "%s", 2, v14, 1, 1);
25  v0 = p_string;
26  if ( p_string->len == 32 )
27  {
28    v1 = 0;
29    r1 = 0;
30    r0 = 0;
31    while ( v1 < 32 )
32    {
33      ptr = v0->ptr;
34      if ( v1 >= v0->len )
35        runtime_panicIndex(v1, r0, v0->len, ptr);
36      v7 = r1;
37      v9 = r0;
38      v8 = v1 + 1;
39      v4 = runtime_intstring(0, (unsigned __int8)((v1 + 1) ^ ptr[v1]));
40      v5 = runtime_concatstring2(0, v9, v7, v4._r0, v4._r1);
41      v0 = p_string;
42      r1 = v5._r1;
43      r0 = v5._r0;
44      v1 = v8;
45    }
46    if ( r1 == 31 && (unsigned __int8)runtime_memequal(r0, "66a<254?=??>=>k$u! r-&t,/,(x~+z", 31) )
47    {
48      v12[0] = &RTYPE_string;
49      v12[1] = &off_4DBE00;
50      fmt_Fprintln(go_itab__os_File_io_Writer, os_Stdout, v12, 1, 1);
51    }
52    else
53    {
54      v11[0] = &RTYPE_string;
55      v11[1] = &off_4DBE10;
56      fmt_Fprintln(go_itab__os_File_io_Writer, os_Stdout, v11, 1, 1);
57    }
58  }
59  else
60  {
61    v13[0] = &RTYPE_string;
62    v13[1] = &off_4DBDF0;
63    fmt_Fprintln(go_itab__os_File_io_Writer, os_Stdout, v13, 1, 1);
64  }
65}

逻辑:

  • 程序从标准输入读取一个字符串
  • 如果字符串长度为31,则对每个字符进行处理: (i+1) ^ char[i]
  • 然后将处理结果与字符串 66a<254?=??>=>k$u! r-&t,/,(x~+z 进行比较

反过来就行,代码:

python
1s = '66a<254?=??>=>k$u! r-&t,/,(x~+z'
2result = ''
3for i in range(len(s)):
4    result += chr((i+1) ^ ord(s[i]))
5print(result)

输出:74b87337454200d4d33f80c4663dc5e

狼组的做法是,把字符串全部按ASCII转16进制。然后跟索引(01020304 ······)异或,也能得到答案。

IOV

iov-vin交互

jadx-gui打开,看到了源代码,它通过蓝牙协议发送VIN,首先java去除空格,然后native层的encrypt函数加密发送

流量包里是加密数据,过滤蓝牙协议用btl2cap,往下翻到有条流量带有data:33303137323834663265643930333831343331316336386435313439626635633132

所以要去逆向native层的encrypt函数。

IDA看到是好像是RC4的改版,还没学到。

iov-CAN-RE

candump的流量,经典

0x733在跟0x73B交互,貌似多帧传输,

小结

web没容器,MISC少附件,Re还行,Pwn不会

给的另外4个附件,三个iov,一个AI对抗样本检测的,没有wp。

不像之前的VSEC或者其他人题目都是车相关的,国内的汽车安全CTF貌似传统CTF还是占了半壁江山。