文章

2025 “招商铸盾” 智能网联汽车攻防赛Writeup

2025 “招商铸盾” 智能网联汽车攻防赛Writeup

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

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

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

MISC

日志审计-8-19

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

另一个数独题没附件。

Crypto

这是古典密码

1
YWlweHs4MjNqNTZwMzdhcDkycDkzcGQ0ZzdhZDZhMHAwMXAyMX0=

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位直接遍历逆元,虽然没有验证互质。但是很简洁。

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

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
import gmpy2
# 解密函数
def affine_decrypt(ciphertext,a,b):
    plaintext=''
    inver_a=gmpy2.invert(a,26)
    for char in ciphertext:
        if char.isalpha():
            # 获取字母次序
            char_num = ord(char) - ord('a')
            decrypted_char_num = (inver_a * (char_num - b)) % 26
            # 得到密文字母次序,要加上a的ascii才能得到此密文字母的ascii
            decrypted_char = chr(decrypted_char_num + ord('a'))
            plaintext += decrypted_char
        else:
            plaintext += char
    
    return plaintext

def brute_force(ciphertext):
    for a in range(1, 26):
        for b in range(26):
            if gmpy2.gcd(a,26)==1:
                plaintext = affine_decrypt(ciphertext, a, b)
                print(f"参数:a={a},b={b} 解密:{plaintext}")
                # 输出太多还要搜索,这里设置如果是flag开头直接停止函数
                if plaintext[:4]=="flag":
                    return 0
            else:
                continue

ciphertext = 'aipx{823j56p37ap92p93pd4g7ad6a0p01p21}'
brute_force(ciphertext)

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

Re

gogogo

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

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

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

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

upx -d test.exe

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

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
// main.main
void __fastcall main_main()
{
  string *v0; // rcx
  __int64 v1; // rax
  __int64 r1; // rdx
  __int64 r0; // rbx
  retval_44A060 v4; // kr00_16
  retval_449800 v5; // kr10_16
  char *ptr; // rdi
  __int64 v7; // [rsp+0h] [rbp-68h]
  __int64 v8; // [rsp+8h] [rbp-60h]
  __int64 v9; // [rsp+10h] [rbp-58h]
  string *p_string; // [rsp+18h] [rbp-50h]
  _QWORD v11[2]; // [rsp+20h] [rbp-48h] BYREF
  _QWORD v12[2]; // [rsp+30h] [rbp-38h] BYREF
  _QWORD v13[2]; // [rsp+40h] [rbp-28h] BYREF
  _QWORD v14[2]; // [rsp+50h] [rbp-18h] BYREF

  p_string = (string *)runtime_newobject(&RTYPE_string);
  p_string->ptr = 0;
  v14[0] = &RTYPE__ptr_string;
  v14[1] = p_string;
  fmt_Fscanf(go_itab__os_File_io_Reader, os_Stdin, "%s", 2, v14, 1, 1);
  v0 = p_string;
  if ( p_string->len == 32 )
  {
    v1 = 0;
    r1 = 0;
    r0 = 0;
    while ( v1 < 32 )
    {
      ptr = v0->ptr;
      if ( v1 >= v0->len )
        runtime_panicIndex(v1, r0, v0->len, ptr);
      v7 = r1;
      v9 = r0;
      v8 = v1 + 1;
      v4 = runtime_intstring(0, (unsigned __int8)((v1 + 1) ^ ptr[v1]));
      v5 = runtime_concatstring2(0, v9, v7, v4._r0, v4._r1);
      v0 = p_string;
      r1 = v5._r1;
      r0 = v5._r0;
      v1 = v8;
    }
    if ( r1 == 31 && (unsigned __int8)runtime_memequal(r0, "66a<254?=??>=>k$u! r-&t,/,(x~+z", 31) )
    {
      v12[0] = &RTYPE_string;
      v12[1] = &off_4DBE00;
      fmt_Fprintln(go_itab__os_File_io_Writer, os_Stdout, v12, 1, 1);
    }
    else
    {
      v11[0] = &RTYPE_string;
      v11[1] = &off_4DBE10;
      fmt_Fprintln(go_itab__os_File_io_Writer, os_Stdout, v11, 1, 1);
    }
  }
  else
  {
    v13[0] = &RTYPE_string;
    v13[1] = &off_4DBDF0;
    fmt_Fprintln(go_itab__os_File_io_Writer, os_Stdout, v13, 1, 1);
  }
}

逻辑:

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

反过来就行,代码:

1
2
3
4
5
s = '66a<254?=??>=>k$u! r-&t,/,(x~+z'
result = ''
for i in range(len(s)):
    result += chr((i+1) ^ ord(s[i]))
print(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还是占了半壁江山。

本文由作者按照 CC BY 4.0 进行授权