三道CTF题目学习h3c路由器&stm32裸机&Infineon车机固件逆向

三道CTF题目学习h3c路由器&stm32裸机&Infineon车机固件逆向

2024L3hCTF hhhc

考点:h3c路由器固件逆向

题目描述

1
Remember the "hillst0ne" challenge in L3HCTF2021?L3HSec also has an MSR3610 router deployed since 2021. Now we have decided to upgrade to a newer model, but we couldn't find the PPPoE password. Could you locate it in the existing configuration?

出题人把h3c路由器的pppoe密码忘了,将其通过路由器配置信息恢复密码的经历出成了题目,还挺有趣的。

没有给固件,只给了startup.cfg文件,里面有路由器的配置信息,如下。要求还原PPPoE协议身份验证中admin用户的密码。

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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
[L3HSEC-ROUTER-1]show current-configuration
#
version 7.1.064, Release 0821P16
#
sysname L3HSEC-ROUTER-1
#
wlan global-configuration
#
security-zone intra-zone default permit
#
dhcp enable
dhcp server always-broadcast
#
dns proxy enable
#
system-working-mode standard
password-recovery enable
#
vlan 1
#
dhcp server ip-pool lan1
gateway-list 192.168.0.1
network 192.168.0.0 mask 255.255.254.0
address range 192.168.1.2 192.168.1.254
dns-list 192.168.0.1
#
controller Cellular0/0
#
interface Dialer0
ppp chap password cipher $c$3$TKYJXT4RmMIvPHQX+5Ehf9oD3kjskIur3PGJfR/7fEyqfbx0K0DAokR0pd3rsRbWR5t9Cr3xSbYoPdogCg==
ppp chap user hustpppoe114514
ppp pap local-user hustpppoe114514 password cipher $c$3$3PbDU2m2/6Neiiz9iO+i641UKjafFMvrfphBc3fmrZ+9Q2TZu3g5l2Hlg1gJWO6ZQLJ4S+r85qU8EQpqQQ==
dialer bundle enable
dialer-group 2
dialer timer idle 0
dialer timer autodial 5
ip address ppp-negotiate
nat outbound
#
interface NULL0
#
interface GigabitEthernet0/0
port link-mode route
description LAN-interface
ip address 192.168.0.1 255.255.254.0
tcp mss 1280
#
interface GigabitEthernet0/1
port link-mode route
#
interface GigabitEthernet0/1.3647
vlan-type dot1q vid 3647
pppoe-client dial-bundle-number 0
#
interface GigabitEthernet0/2
port link-mode route
combo enable copper
#
interface GigabitEthernet0/3
port link-mode route
combo enable copper
#
interface GigabitEthernet0/4
port link-mode route
#
interface GigabitEthernet0/5
port link-mode route
#
scheduler logfile size 16
#
line class console
user-role network-admin
#
line class tty
user-role network-operator
#
line class vty
user-role network-operator
#
line con 0
user-role network-admin
#
line vty 0 63
authentication-mode scheme
user-role network-operator
#
performance-management
#
password-control enable
undo password-control aging enable
undo password-control history enable
password-control length 6
password-control login-attempt 3 exceed lock-time 10
password-control update-interval 0
password-control login idle-time 0
#
domain system
#
domain default enable system
#
role name level-0
description Predefined level-0 role
#
role name level-1
description Predefined level-1 role
#
role name level-2
description Predefined level-2 role
#
role name level-3
description Predefined level-3 role
#
role name level-4
description Predefined level-4 role
#
role name level-5
description Predefined level-5 role
#
role name level-6
description Predefined level-6 role
#
role name level-7
description Predefined level-7 role
#
role name level-8
description Predefined level-8 role
#
role name level-9
description Predefined level-9 role
#
role name level-10
description Predefined level-10 role
#
role name level-11
description Predefined level-11 role
#
role name level-12
description Predefined level-12 role
#
role name level-13
description Predefined level-13 role
#
role name level-14
description Predefined level-14 role
#
user-group system
#
local-user admin class manage
service-type telnet http
authorization-attribute user-role network-admin
#
ip http enable
web new-style
#
wlan ap-group default-group
vlan 1
#
return
[L3HSEC-ROUTER-1]

关键信息

  • 设备型号
1
MSR3610
  • 固件版本
1
version 7.1.064, Release 0821P16
  • PPPoE密码的密文,这里chap和pap对应同一个密码
1
2
3
ppp chap password cipher $c$3$TKYJXT4RmMIvPHQX+5Ehf9oD3kjskIur3PGJfR/7fEyqfbx0K0DAokR0pd3rsRbWR5t9Cr3xSbYoPdogCg==
ppp chap user hustpppoe114514
ppp pap local-user hustpppoe114514 password cipher $c$3$3PbDU2m2/6Neiiz9iO+i641UKjafFMvrfphBc3fmrZ+9Q2TZu3g5l2Hlg1gJWO6ZQLJ4S+r85qU8EQpqQQ==

两种解题思路

  • 由于PPPoE协议的密码明文传输,可以仿真运行任意h3c路由器后导入配置,抓包查看密码

  • 获取该版本固件对加密进行逆向分析

思路1 - 仿真抓包

参考PPP 会话验证:PAP和CHAP有啥区别?两张神图总结完!可知PPPoE采用CS架构,有PAP和CHAP两种身份验证方式,CHAP的用户名密码传输时使用md5加密,PAP的用户名密码在从client向server传输时是明文,因此可以仿真 -> 导入PAP配置 -> 抓包 -> 获取密码。

下载H3C网络设备模拟器-HCL 笔者的计网实验课程是H3C的讲师上的,所以课内用过这个工具

放两个MSR3620路由器,开启并连接GE0/0

1

PPPoE客户端和服务端配置参考 H3C PPPoE配置手册

这里不需要进行IP相关配置,将PPPoE服务绑定在GE0/0物理接口直连即可。同时只采用PAP身份验证,所以CHAP的信息不用配置。

配置client,每5秒自动拨号一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
system-view
interface Dialer0 #配置一个拨号接口,通常用于 PPPoE 连接
ppp pap local-user hustpppoe114514 password cipher $c$3$3PbDU2m2/6Neiiz9iO+i641UKjafFMvrfphBc3fmrZ+9Q2TZu3g5l2Hlg1gJWO6ZQLJ4S+r85qU8EQpqQQ== #指定拨号的认证方式为PPP并设置用户名和密码
dialer bundle enable #开启共享DDR(Dial-on-Demand Routing,按需拨号路由)
dialer-group 2 #应用特定的拨号组(组号为2)进行流量过滤或控制,通常与特定的 ACL(访问控制列表)配合使用。
dialer timer idle 0 #将空闲断线计时器设置为 0,意味着接口不会因为没有流量而自动断开
dialer timer autodial 5 #如果连接断开或没有建立连接,每隔 5 秒自动尝试重新拨号
ip address ppp-negotiate #配置接口从 PPPoE 服务器动态协商获得 IP 地址
nat outbound #启用出站流量的 NAT 功能,使内部网络可以通过该接口访问公网
exit
interface GigabitEthernet0/0 #进入物理接口GigabitEthernet0/0的配置
port link-mode route #将端口模式设置为路由模式,表示该接口将进行数据包转发,而不是数据交换(常用于 WAN 接口)
pppoe-client dial-bundle-number 0 #将物理接口与拨号接口(dial bundle 0)关联,以启动 PPPoE 连接
exit

配置server

1
2
3
4
5
6
7
8
9
10
11
system-view
interface virtual-template 1 #配置虚拟模板接口 1,用于 PPPoE 服务器端的接口模板
ppp authentication-mode pap domain dm1 #设置 PPP 的认证模式为 PAP,并指定认证域为 dm1
quit
interface gigabitethernet 0/0 #进入GE0/0物理接口配置
pppoe-server bind virtual-template 1 #将物理接口绑定到虚拟模板接口 1,使 GigabitEthernet 0/0 能够作为 PPPoE 服务器的服务接口
quit
local-user hustpppoe114514 class network #配置一个本地用户,用户名为 hustpppoe114514,并将用户类型设为 network
password cipher $c$3$3PbDU2m2/6Neiiz9iO+i641UKjafFMvrfphBc3fmrZ+9Q2TZu3g5l2Hlg1gJWO6ZQLJ4S+r85qU8EQpqQQ== #配置用户的密码
service-type ppp #将用户的服务类型设置为ppp
quit

右键连线开启抓包,开启wireshark得到密码

2

1
L3HCTF{pPp0e_mus7_s@v3_pla1ntex7_pswdDddD}

思路2 - 逆向固件

获取固件

华为/h3c的固件需要授权账号才能下载,所以只能通过其它渠道获取固件。

该版本固件似乎应用于一系列路由器,我们只要找到其中一种即可

3

找到MER8300的该版本固件

4

下载后解压得到固件MER8300-CMW710-R0821P16.ipe

固件解包

ipe是H3C自研的固件打包格式,如果不按照打包格式进行解包,直接binwalk只能获取一堆零散的文件

如果有H3C设备,可以参考该文章通过install命令解包ipe固件

github上有ipe固件解包脚本 - H3C-IPE-Unarchiver,但是只能提取出固件的各个分区文件,还要继续分析分区文件

5

除了system分区,其余分区内容都很少,包括PPPoE相关程序在内的大部分内容位于system分区文件中

通过不断的对比分析,发现对于各个分区文件,有如下的通用格式

其中有一些未知的数据,但是最关键的文件名、文件大小和文件本身都有了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0x188c 分区整体头部
-------------------------
0x68 unknown_struct
0xE0 filename
0x4 file_size
0x4 unknown_var
0x4 padding(FF FF FF FF)
文件本身,大小取决于file_size
-------------------------
0x68 unknown_struct
0xE0 filename
0x4 file_size
0x4 unknown
0x4 padding(FF FF FF FF)
文件本身,大小取决于file_size
-------------------------
0x68 unknown_struct
0xE0 filename
0x4 file_size
0x4 unknown
0x4 padding(FF FF FF FF)
文件本身,大小取决于file_size
-------------------------
............

这里可以开发一个提取的脚本作为对原本项目的补充,但是笔者最近比较忙,先开个坑日后再填

同样对比分析其它分区文件可知文件系统位于mpu.cpio.xz中,提取出该文件

6

另外,LianSecurity的IOT分析工具Shambles可以一键解包出该固件的文件系统并自动分析固件信息和潜在漏洞,非常好用,如图

7

定位加解密代码

逆向分析

密文是base64,我们搜索包含base64码表的文件,根据文件名判断加密可能存在于/lib/libcrypto.so.1.1.1.185/lib/liblauth.so.1.1.1.602/lib/libencrypt.so.0.0.0.14

8

最终在/lib/libencrypt.so.0.0.0.14找到了符合加解密特征的函数PASSWORD_EncryptPASSWORD_Decrypt

9

正向分析

在服务程序校验客户端传来的用户名密码的过程中必然存在解密操作,因此我们将pppoe相关的服务程序作为切入点。在目录搜索ppp,如下

10

pppoecd和pppoesd是pppoe服务程序,分别负责维护和管理PPPoE连接、发现和协商PPPoE会话,但是其中并没有用户认证相关代码。

参考PPPOE和pppd的流程详解,PPPOE协议是基于PPP协议的协议,在PPPOE应用程序中并没有将PPP协议实现,PPP协议是由PPPD这个用户空间程序实现的。逆向分析图中的PPPD程序,我们可以发现PAP和CHAP两种身份认证的处理函数中都用到了PASSWORD_Decrypt函数。11

部分函数调用链如下:

  • PAP

main -> PPP_Worker_DataInit -> PPP_PACKET_Create -> ppp_packet_Create -> sub_120067D10 -> sub_120067BD8 -> PPP_PROTO_ReceivePacket -> PPP_NEGO_ProcPacket/PPP_NEGO_ProcAAAMsg -> PPP_PAP_ReceivePacket/PPP_PAP_ReceiveEvent -> sub_1200A6700 -> PASSWORD_Decrypt

  • CHAP

main -> PPP_Worker_DataInit -> PPP_PACKET_Create -> ppp_packet_Create -> sub_120067D10 -> sub_120067BD8 -> PPP_PROTO_ReceivePacket -> PPP_NEGO_ProcPacket -> PPP_CHAP_ReceivePacket -> sub_120085B60 -> PASSWORD_Decrypt

以及pppd中的LAUTH_GetUserPassword也调用了PASSWORD_Decrypt函数,该函数来自/lib/liblauth.so.1.1.1.602库文件

Q12

readelf -d查看pppd程序链接了哪些库

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
$ readelf -d pppd

Dynamic section at offset 0xce0 contains 56 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libdl.so.0]
0x0000000000000001 (NEEDED) Shared library: [libpthread.so.0]
0x0000000000000001 (NEEDED) Shared library: [libsystem.so]
0x0000000000000001 (NEEDED) Shared library: [libipbase.so]
0x0000000000000001 (NEEDED) Shared library: [libcioctl.so]
0x0000000000000001 (NEEDED) Shared library: [libdns.so]
0x0000000000000001 (NEEDED) Shared library: [libcryptoex.so]
0x0000000000000001 (NEEDED) Shared library: [libcrypto.so]
0x0000000000000001 (NEEDED) Shared library: [libencrypt.so]
0x0000000000000001 (NEEDED) Shared library: [libkevent.so]
0x0000000000000001 (NEEDED) Shared library: [libpam.so]
0x0000000000000001 (NEEDED) Shared library: [libdombasic.so]
0x0000000000000001 (NEEDED) Shared library: [libl3vpn.so]
0x0000000000000001 (NEEDED) Shared library: [libppp.so]
0x0000000000000001 (NEEDED) Shared library: [liblauth.so]
0x0000000000000001 (NEEDED) Shared library: [libbitmap.so]
0x0000000000000001 (NEEDED) Shared library: [libcli.so]
0x0000000000000001 (NEEDED) Shared library: [libddr.so]
0x0000000000000001 (NEEDED) Shared library: [libmor.so]
0x0000000000000001 (NEEDED) Shared library: [libvlan.so]
0x0000000000000001 (NEEDED) Shared library: [libaclmgr.so]
0x0000000000000001 (NEEDED) Shared library: [libif.so]
0x0000000000000001 (NEEDED) Shared library: [libtcmalloc.so]
0x0000000000000001 (NEEDED) Shared library: [libtrange.so]
0x0000000000000001 (NEEDED) Shared library: [libdhcpsr.so]
0x0000000000000001 (NEEDED) Shared library: [librtstatic.so]
0x0000000000000001 (NEEDED) Shared library: [libvsrp.so]
0x0000000000000001 (NEEDED) Shared library: [libsync.so]
0x0000000000000001 (NEEDED) Shared library: [libchannel.so]
0x0000000000000001 (NEEDED) Shared library: [libeth.so]
0x0000000000000001 (NEEDED) Shared library: [libancp.so]
0x0000000000000001 (NEEDED) Shared library: [libip6addr.so]
0x0000000000000001 (NEEDED) Shared library: [libmemalert.so]
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.0]

nm -D + grep查看哪个库中定义了PASSWORD_Decrypt函数

1
2
3
$ nm -D libencrypt.so.0.0.0.14  | grep PASSWORD_Decrypt
0000000000003728 T PASSWORD_Decrypt
0000000000003358 T PASSWORD_DecryptBin

最终定位到libencrypt.so.0.0.0.14

分析加密算法

将16字节随机数(IV)和password传入加密函数sub_2FC8

13

sub_2FC8中操作如下

  • KEY_GetKey生成AES初始密钥
  • 拼接16字节IV和password,存在data
  • 由初始密钥enc_key生成加密密钥AES_key
  • 使用IV和密钥AES_key加密data中0x10偏移处的password
  • 对data进行base64编码后在开头拼接$c$3$,加密完成并返回

14

分析KEY_GetKey函数生成AES初始密钥的过程

读取了/etc/key-data文件,并且从0x102偏移开始解析数据

15

解析了每个区块并将ID和CT_SIZE存入全局缓冲区

16

进入KEY_GetKeyData继续解析

17

对密钥数据也进行AES-CTR解密

18

解密密钥数据用的初始密钥是由Key_data[2:102]数据以如下方式生成的哈希

19

总结:key_data的0x2-0x102是用于生成初始密钥的数据,0x102偏移开始数据分为一个个区块,每个区块结构如下

  • 1-2字节为区块ID
  • 3-4字节为加密后AES密钥的密文长度
  • 5-20字节是IV
  • AES密钥的密文(CT)

20

解密密码

先获取这里的初始密钥

21

如图,用的是第4区块的数据,结构如下

1
2
3
4
5
6
7
8
ID
00 04
CT_SIZE
00 34
IV
9B 33 AE DE 8E 29 B2 2A 9E D1 8C 1D CD A7 32 58
CT
DF BF 4E 75 AD D5 29 2B 54 78 BE 47 89 04 14 8A 34 7B F4 FD EC FC 7A EE 87 AF 83 C6 2E 3B 0B 26 42 2F 13 48 07 0B 44 65 AD A8 CA 0F F4 8D 96 10 84 68 7B 6A

生成解密密钥数据用的密钥

1
2
3
4
5
6
7
8
9
10
from hashlib import sha512

fp = open("key-data",'rb')
data = fp.read()[0x2:0x102]
a = sha512()
a.update(data[0x40:0x80])
a.update(data[0x0:0x40])
a.update(data[0x80:])
print(a.digest()[:0x20].hex())
#ecc679703bb2daf7c09a941cb992dcdd03150e0f67ed9b32a548d8624add9c07

获得解密PASSWORD用的密钥数据

22

密钥数据格式如下

1
2
3
4
5
6
7
8
IV_SIZE
0010
KEY_SIZE
0020
IV
c695c466f32e90d0fb12ed31c5c72265
KEY
a2e6658865746b4b954f0bd37fd1ece03b1acb47fbc543ec32d35987b20b6866

对CHAP的PASSWORD的密文解BSAE64(不知道为什么不能解密PAP的数据,可能因为PAP传输明文不需要解密?)

1
2
3
4
5
#CHAP
IV
4c a6 09 5d 3e 11 98 c2 2f 3c 74 17 fb 91 21 7f
CT
da 03 de 48 ec 90 8b ab dc f1 89 7d 1f fb 7c 4c aa 7d bc 74 2b 40 c0 a2 44 74 a5 dd eb b1 16 d6 47 9b 7d 0a bd f1 49 b6 28 3d da 20 0a

23

2024SCTF uds

考点:STM32单片机裸机固件逆向、uds诊断服务

题目描述

1
2
能告诉我汽车的VIN码吗?
Can you tell me the VIN number of the car?

给了32位arm小端裸机固件uds.hex,内含uds诊断服务代码,程序源码修改自iso14229仓库,要求逆向固件得到汽车的VIN码

Intel Hex文件格式

关于hex文件

1
2
以*.hex为后缀的文件我们称之为HEX文件。hex是intel规定的标准,hex的全称是Intel HEX,此类文件通常用于传输将被存于ROM或EEPROM中的程序和数据。是由一行行符合Intel HEX文件格式的文本所构成的ASCII文本文件。
这种文件格式主要用于保存单片机固件。

以本题的uds.hex为例

1
2
3
4
5
6
7
8
9
10
11
:020000040800F2   
:10000000B0490020AD020008A7241008BD03000875
......
:104EE000D1421052015890D0030090D00302180113
:020000040810E2
:10000000202A04DB203A00FA02F1002070479140D8
......
:104EB00061206C6172676572206275666665720A50
:044EC00000000000EE
:040000050800029954
:00000001FF
  • :020000040800F2是拓展线性地址记录,指定了地址空间的高16位为0x0800
  • :020000040810E2也是拓展线性地址记录,指定从此地址空间的高16位为0x0810
  • :104EB00061206C6172676572206275666665720A50表示将0x10字节的数据00 61 20 6C 61 72 67 65 72 20 62 75 66 66 65 72 0A 50存放在(线性拓展地址 << 4 + 0x4EB0)地址的存储空间
  • :040000050800029954是启动线性地址记录,表示程序起始执行地址为0x08000298+1,根据arm规范,这里的末位的1是thumb指示位,所以实际起始地址为0x08000298,采用16位的thumb指令集。
  • 其余语句都是类似:104EB00061206C6172676572206275666665720A50的数据长度 + 地址 + 数据

hex和bin文件转换有如下几种方式

  • objcopy工具(gnulinux自带无需安装)
1
2
objcopy -I ihex -O binary input.hex output.bin  #hex -> bin
objcopy -I binary -O ihex input.bin output.hex #bin -> hex
1
2
3
sudo apt install srecord #安装srecord包
srec_cat uds.hex -intel -offset -0x08000000 -o uds.bin -binary #hex -> bin
srec_cat uds.bin -binary -offset -0x08000000 -o uds.hex -intel #bin -> hex
  • Hexview

    Vector公司的产品CANape中的一个小工具,官方渠道是到官网下载CANape包找出Hexview。有好人单独分享了Hexview工具

srec工具转换得到的bin文件很大,ida打开直接卡死,所以建议用objcopy和HexView

如何正确反编译

rom基址是0x08000000,程序入口在0x08000298,使用16位的thumb指令集,ram基址根据代码判断为0x20000000

包括VIN密文在内的很多变量存储在ram中,需要进行设置,否则会如图所示爆红

24

还有一些爆红的变量,并不影响关键代码的逆向,不处理也没关系。对0x58024410这个地址进行信息搜集可以判断这是AHB总线的地址,猜测出题人用的设备应该是STM32,CPU型号是Cortex-M系列,指令集为ARMv7-M兼容16位thumb

25

反编译intel hex格式

ida能自动解析Intel hex格式,这意味着hex文件中包含的信息都不需要再手动设置,但是hex中没有ram信息,所以ram段要我们自己创建,ram基址需要自己判断。

拖进32位ida选择arm Little Endian打开uds.hex

26

Edit -> Segments -> Create segment,添加ram段

27

创建ram段后其它变量正常了,vin变量还是地址形式,右键set call type转换参数类型

28

重命名为vin

29

反编译bin格式

对于bin格式,参考该文章,ROM段、RAM段都要手动创建,还需要手动到程序入口点或reset handler切换至16位thumb。

hex.bin拖进32位ida,处理器选arm Little-endian打开,跳出memory映射结构设置界面,按下图设置,然后OK

30

打开后先看到ROM的首地址,对于STM32固件,这里存放了中断向量表(也可以设置到ram中),CPU上电后就靠中断向量表来完成初始化工作,如图:

31

stm32手册中可以看到中断向量表中各个项的含义,这里只展示部分,第一项是复位后栈顶sp的初始值,第二项是reset handler,后面都是不同中断服务的函数地址,用于触发对应中断时查表跳转执行。

32

关于reset handler:CPU上电后先设置SP,再将PC设置为reset handler跳转执行。然后先执行SystemInit设置系统时钟,再调用__main函数,也就是上文从hex文件解析出来的程序入口点。

33

这里reset handler的值是0x80002AD(0x80002AC+1),最后一位按照arm规范,是thumb指示位,1表示目标地址采用thumb16指令集。

所以我们按G跳转到0x80002AC,再按Alt+G,把段寄存器T的值设置为1,表示使用16位的thumb指令集,再按c就可以反编译了

34

反编译reset handler后大量函数也被自动分析出来了,但是并不全,有大量函数被遗漏了。我们大片选中ROM中的数据(图中箭头指的部分)直接按c全部反编译即可

35

分析代码,获取VIN

从CTF做题的角度看,思路是Findcrypt找到tea特征,查看交叉引用追溯到sub_80043E8函数,找到如下解密VIN的逻辑

36

但既然是复现,为了加深理解,我们对照源码梳理程序的逻辑,以下是从源码提取出的相关代码

/src/server.c

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
static uint8_t EmitEvent(UDSServer_t *srv, UDSServerEvent_t evt, void *data) {
if (srv->fn) {
return srv->fn(srv, evt, data);
} else {
UDS_DBG_PRINT("Unhandled UDSServerEvent %d, srv.fn not installed!\n", evt);
return kGeneralReject;
}
}
static inline uint8_t NegativeResponse(UDSReq_t *r, uint8_t response_code) {
r->send_buf[0] = 0x7F;
r->send_buf[1] = r->recv_buf[0];
r->send_buf[2] = response_code;
r->send_len = UDS_NEG_RESP_LEN;
return response_code;
}

static uint8_t _0x27_SecurityAccess(UDSServer_t *srv, UDSReq_t *r) {
uint8_t subFunction = r->recv_buf[1];
uint8_t response = kPositiveResponse;

if (UDSSecurityAccessLevelIsReserved(subFunction)) {
return NegativeResponse(r, kIncorrectMessageLengthOrInvalidFormat);
}

if (!UDSTimeAfter(UDSMillis(), srv->sec_access_boot_delay_timer)) {
return NegativeResponse(r, kRequiredTimeDelayNotExpired);
}

if (!(UDSTimeAfter(UDSMillis(), srv->sec_access_auth_fail_timer))) {
return NegativeResponse(r, kExceedNumberOfAttempts);
}

r->send_buf[0] = UDS_RESPONSE_SID_OF(kSID_SECURITY_ACCESS);
r->send_buf[1] = subFunction;
r->send_len = UDS_0X27_RESP_BASE_LEN;

// Even: sendKey
if (0 == subFunction % 2) {
uint8_t requestedLevel = subFunction - 1;
UDSSecAccessValidateKeyArgs_t args = {
.level = requestedLevel,
.key = &r->recv_buf[UDS_0X27_REQ_BASE_LEN],
.len = r->recv_len - UDS_0X27_REQ_BASE_LEN,
};

response = EmitEvent(srv, UDS_SRV_EVT_SecAccessValidateKey, &args);

if (kPositiveResponse != response) {
srv->sec_access_auth_fail_timer =
UDSMillis() + UDS_SERVER_0x27_BRUTE_FORCE_MITIGATION_AUTH_FAIL_DELAY_MS;
return NegativeResponse(r, response);
}

// "requestSeed = 0x01" identifies a fixed relationship between
// "requestSeed = 0x01" and "sendKey = 0x02"
// "requestSeed = 0x03" identifies a fixed relationship between
// "requestSeed = 0x03" and "sendKey = 0x04"
srv->securityLevel = requestedLevel;
r->send_len = UDS_0X27_RESP_BASE_LEN;
return kPositiveResponse;
}

// Odd: requestSeed
else {
/* If a server supports security, but the requested security level is already unlocked when
a SecurityAccess ‘requestSeed’ message is received, that server shall respond with a
SecurityAccess ‘requestSeed’ positive response message service with a seed value equal to
zero (0). The server shall never send an all zero seed for a given security level that is
currently locked. The client shall use this method to determine if a server is locked for a
particular security level by checking for a non-zero seed.
*/
if (subFunction == srv->securityLevel) {
// Table 52 sends a response of length 2. Use a preprocessor define if this needs
// customizing by the user.
const uint8_t already_unlocked[] = {0x00, 0x00};
return safe_copy(srv, already_unlocked, sizeof(already_unlocked));
} else {
UDSSecAccessRequestSeedArgs_t args = {
.level = subFunction,
.dataRecord = &r->recv_buf[UDS_0X27_REQ_BASE_LEN],
.len = r->recv_len - UDS_0X27_REQ_BASE_LEN,
.copySeed = safe_copy,
};

response = EmitEvent(srv, UDS_SRV_EVT_SecAccessRequestSeed, &args);

if (kPositiveResponse != response) {
return NegativeResponse(r, response);
}

if (r->send_len <= UDS_0X27_RESP_BASE_LEN) { // no data was copied
return NegativeResponse(r, kGeneralProgrammingFailure);
}
return kPositiveResponse;
}
}
return NegativeResponse(r, kGeneralProgrammingFailure);
}

/src/uds.h

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
enum UDSServerEvent {
UDS_SRV_EVT_DiagSessCtrl, // UDSDiagSessCtrlArgs_t *
UDS_SRV_EVT_EcuReset, // UDSECUResetArgs_t *
UDS_SRV_EVT_ReadDataByIdent, // UDSRDBIArgs_t *
UDS_SRV_EVT_ReadMemByAddr, // UDSReadMemByAddrArgs_t *
UDS_SRV_EVT_CommCtrl, // UDSCommCtrlArgs_t *
UDS_SRV_EVT_SecAccessRequestSeed, // UDSSecAccessRequestSeedArgs_t *
UDS_SRV_EVT_SecAccessValidateKey, // UDSSecAccessValidateKeyArgs_t *
UDS_SRV_EVT_WriteDataByIdent, // UDSWDBIArgs_t *
UDS_SRV_EVT_RoutineCtrl, // UDSRoutineCtrlArgs_t*
UDS_SRV_EVT_RequestDownload, // UDSRequestDownloadArgs_t*
UDS_SRV_EVT_RequestUpload, // UDSRequestUploadArgs_t *
UDS_SRV_EVT_TransferData, // UDSTransferDataArgs_t *
UDS_SRV_EVT_RequestTransferExit, // UDSRequestTransferExitArgs_t *
UDS_SRV_EVT_SessionTimeout, // NULL
UDS_SRV_EVT_DoScheduledReset, // enum UDSEcuResetType *
UDS_SRV_EVT_Err, // UDSErr_t *
UDS_EVT_IDLE,
UDS_EVT_RESP_RECV,
};

enum UDSSecurityAccessType {
kRequestSeed = 0x01,
kSendKey = 0x02,
};

/src/server.h

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
typedef struct UDSServer {
UDSTpHandle_t *tp;
uint8_t (*fn)(struct UDSServer *srv, UDSServerEvent_t event, const void *arg);

/**
* @brief \~chinese 服务器时间参数(毫秒) \~ Server time constants (milliseconds) \~
*/
uint16_t p2_ms; // Default P2_server_max timing supported by the server for
// the activated diagnostic session.
uint32_t p2_star_ms; // Enhanced (NRC 0x78) P2_server_max supported by the
// server for the activated diagnostic session.
uint16_t s3_ms; // Session timeout

uint8_t ecuResetScheduled; // nonzero indicates that an ECUReset has been scheduled
uint32_t ecuResetTimer; // for delaying resetting until a response
// has been sent to the client
uint32_t p2_timer; // for rate limiting server responses
uint32_t s3_session_timeout_timer; // indicates that diagnostic session has timed out
uint32_t sec_access_auth_fail_timer; // brute-force hardening: rate limit security access
// requests
uint32_t sec_access_boot_delay_timer; // brute-force hardening: restrict security access until
// timer expires

/**
* @brief UDS-1-2013: Table 407 - 0x36 TransferData Supported negative
* response codes requires that the server keep track of whether the
* transfer is active
*/
bool xferIsActive;
// UDS-1-2013: 14.4.2.3, Table 404: The blockSequenceCounter parameter
// value starts at 0x01
uint8_t xferBlockSequenceCounter;
size_t xferTotalBytes; // total transfer size in bytes requested by the client
size_t xferByteCounter; // total number of bytes transferred
size_t xferBlockLength; // block length (convenience for the TransferData API)

uint8_t sessionType; // diagnostic session type (0x10)
uint8_t securityLevel; // SecurityAccess (0x27) level

bool RCRRP; // set to true when user fn returns 0x78 and false otherwise
bool requestInProgress; // set to true when a request has been processed but the response has
// not yet been sent

// UDS-1 2013 defines the following conditions under which the server does not
// process incoming requests:
// - not ready to receive (Table A.1 0x78)
// - not accepting request messages and not sending responses (9.3.1)
//
// when this variable is set to true, incoming ISO-TP data will not be processed.
bool notReadyToReceive;

UDSReq_t r;
} UDSServer_t;

/test/test_server_0x27_security_access.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
uint8_t fn(UDSServer_t *srv, UDSServerEvent_t ev, const void *arg) {
switch (ev) {
case UDS_SRV_EVT_SecAccessRequestSeed: {
UDSSecAccessRequestSeedArgs_t *r = (UDSSecAccessRequestSeedArgs_t *)arg;
const uint8_t seed[] = {0x36, 0x57};
TEST_INT_NE(r->level, srv->securityLevel);
return r->copySeed(srv, seed, sizeof(seed));
break;
}
case UDS_SRV_EVT_SecAccessValidateKey: {
UDSSecAccessValidateKeyArgs_t *r = (UDSSecAccessValidateKeyArgs_t *)arg;
const uint8_t expected_key[] = {0xC9, 0xA9};
if (memcmp(r->key, expected_key, sizeof(expected_key))) {
return kSecurityAccessDenied;
} else {
return kPositiveResponse;
}
break;
}
default:
assert(0);
}
return kPositiveResponse;
}

调用链如下,最后的fn函数就是上文通过FindCrypt找到的VIN加密逻辑所在函数,作者自定义了该函数,uds.h中可以看到case6对应case UDS_SRV_EVT_SecAccessRequestSeed

1
2
3
4
_0x27_SecurityAccess -> Odd: requestSeed -> EmitEvent(srv, UDS_SRV_EVT_SecAccessRequestSeed, &args); -> 
EmitEvent -> srv->fn(srv, evt, data) ->
typedef struct UDSServer {... uint8_t (*fn)(struct UDSServer *srv, UDSServerEvent_t event, const void *arg); ...}
-> uint8_t fn(UDSServer_t *srv, UDSServerEvent_t ev, const void *arg)

同时,由于fn函数不是直接被调用,而是通过结构体传入_0x27_SecurityAccess进行调用,我们很难通过静态分析定位到加密逻辑所在的fn函数,如图

37

定位到fn的加密逻辑后就好办了,都是标准加密

38

先转换端序,再对(a1+3)进行标准tea解密,最后校验解密后的(a1+3)等于seed,那么tea加密seed就能得到(a1+3)

39

VIN在内存中是加密的状态,rc4函数以(a3+1)为key,采用标准rc4解密VIN

40

找到可疑数据

41

查看交叉引用发现在start函数的下一个函数(这里设置为main)中调用0x810004c处理0x8004EC8的数据生成了VIN密文

42

43

直接复制密文生成函数,填入参数运行得到VIN密文14 a6 91 fe b9 d7 41 af 82 cc 4e e9 47 47 28 4f d1

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
#include <stdio.h>
#include "defs.h"
unsigned char data[1000] = { 0x1,0x13,0x2,0x96,0x88,0x0,0x12,0xb0,0x14,0xa6,0x91,0xfe,0xb9,0xd7,0x41,0xaf,0x82,0xcc,0x4e,0xe9,0x47,0x47,0x28,0x4f,0xd1,0x42,0x10,0x52,0x1,0x58,0x90,0xd0,0x3,0x0,0x90,0xd0,0x3,0x2,0x18,0x1 };

unsigned char encvin[1000] = { 0 };
int __fastcall initfunc(char* from_addr, _BYTE* to_addr, int start_index)
{
_BYTE* v3; // r4
unsigned int v4; // r2
unsigned int v5; // t1
int v6; // r3
int v7; // t1
unsigned int v8; // r2
unsigned int v9; // t1
char v10; // t1

v3 = &to_addr[start_index];
do
{
v5 = *from_addr++;
v4 = v5;
v6 = v5 & 0xF;
if ((v5 & 0xF) == 0)
{
v7 = (unsigned __int8)*from_addr++;
v6 = v7;
}
v8 = v4 >> 4;
if (!v8)
{
v9 = (unsigned __int8)*from_addr++;
v8 = v9;
}
while (--v6)
{
v10 = (unsigned __int8)*from_addr++;
*to_addr++ = v10;
}
while (--v8)
*to_addr++ = 0;
} while (to_addr < v3);
return 0;
}

int main() {

initfunc(data, encvin, 0x194);
return 0;
}

最后seed转换端序,tea加密seed后再次转换端序得到Key,使用Key对密文rc4解密

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
import struct
from Crypto.Cipher import ARC4

key = [0x00000123, 0x00004567, 0x000089AB, 0x0000CDEF]

DELTA = 0x9E3779B9


def tea_enc(v0, v1, key):
_sum = 0
for i in range(32):
_sum += 0x9E3779B9
_sum &= 0xFFFFFFFF
v0 += (key[0] + 16 * v1) ^ (v1 + _sum) ^ (key[1] + (v1 >> 5))
v0 &= 0xFFFFFFFF
v1 += (key[2] + 16 * v0) ^ (v0 + _sum) ^ (key[3] + (v0 >> 5))
v1 &= 0xFFFFFFFF
return v0, v1


b = struct.pack('<2I', 0x44332211, 0x88776655)
v0, v1 = struct.unpack('>2I', b)

v0, v1 = tea_enc(v0, v1, key)
b = struct.pack('>2I', v0, v1)

arc4 = ARC4.new(b)
a = arc4.encrypt(bytes([0x14, 0xA6, 0x91, 0xFE, 0xB9, 0xD7, 0x41,
0xAF, 0x82, 0xCC, 0x4E, 0xE9, 0x47, 0x47, 0x28, 0x4F, 0xD1]))
print(a)
#W0L000043MB541337

2024SCTF easymcu

题目描述

1
小明从车机主控中提取出来固件 mcu.s19文件。车机主控的PCBA如图PCBA.jpg。用户使用串口助手xcom输入一串flag后,在串口助手中返回了一些数据,如图xcom.jpg所示。求flag

PCBA.jpg

PCBA

xcom.jpg

xcom

通过CPU的丝印得知该固件指令集是32位TriCore以及相关参数

44

Motorola Hex文件格式

类似intel hex的烧录文件格式,由Motorola制定,也叫S-Record。该格式文件的后缀有**.s19, .s28, .s37, .s, .s1, .s2, .s3, .sx, .srec, .mot**等。这里直接用hexview查看和转换,比命令行工具直观且方便多了。

如图,直接帮我们分析各个区块的基址和大小。

45

导出为bin文件或其它格式

46

其实不转换也没关系,ida和ghidra都能识别intel hex和motorola hex,转换成bin反而麻烦,需要手动设置rom基址。

反编译

tricore指令集的反编译需要使用ghidra,因为ida只能反汇编,无法反编译看伪c。

mcu.19用ghidra打开,设置好文件格式和指令集如下,然后用CodeBrower分析,打开后没有分析出函数,Ctrl + A再按D自动反编译所有机器码。

47

分析代码,解密数据

官方wp给出了两种思路:

1、从波特率入手。题目给的XCOM图片里显示波特率是115200,在串口初始化函数UART init的配置数据中包含了波特率。我们用ida或ghidra的search功能在固件中搜索波特率标量,追踪交叉引用可以找到UART串口初始化函数,继而找到该串口的通信加密逻辑。

2.作者用了标准AES加密,可以通过加密特征定位到加密函数。

显然第1种思路更科学,第2种思路偏”CTF”,我们采用第一种思路进行分析。

ida搜索标量 : Search->Immediate value或Sequence of bytes

48

ghidra搜索标量

49

由于ida不能看伪c,我们用ghidra分析,找到波特率常量位置

50

交叉引用到UART_init再到主函数,一般while里就是MCU循环处理的部分。

51

进入MCU循环函数

52

循环函数操作:获取串口输入 -> AES加密 -> 对加密后的每一位进行位运算(循环左移、异或下一位、取反) -> 在串口输出加密数据

53

AES_KEY

54

算法分析完成,写脚本解密,这里直接贴官方脚本了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from Crypto.Cipher import AES
def ror(val, shifts):
return ((val >> shifts) & 0xff) | ((val << (8 - shifts)) & 0xFF)
def decrypt_aes(ciphertext, key):
cipher = AES.new(bytes(key), AES.MODE_ECB)
decrypted = cipher.decrypt(bytes(ciphertext))
return decrypted
hex_array=[0x63, 0xd4, 0xdd, 0x72, 0xb0, 0x8c, 0xae, 0x31, 0x8c, 0x33, 0x03, 0x22, 0x03, 0x1c, 0xe4, 0xd3, 0xc3, 0xe3, 0x54, 0xb2, 0x1d, 0xeb, 0xeb, 0x9d, 0x45, 0xb1, 0xbe, 0x86, 0xcd, 0xe9, 0x93, 0xd8]
print(len(hex_array))
for i in range(len(hex_array) - 1, -1, -1):
val = (~hex_array[i]) & 0xff
val ^= hex_array[(i + 1) % len(hex_array)]
val = ror(val, 3)
hex_array[i] = val
key = [0x2e,0x35,0x7d,0x6a,0xed,0x44,0xf3,0x4d,0xad,0xb9,0x11,0x34,0x13,0xea,0x32,0x4e]
decrypted_data = decrypt_aes(hex_array, key)
decrypted_string = ''.join(chr(x) for x in decrypted_data)
print("解密后的数据:")
print(decrypted_string)
#Flag: SCTF{Wlc_t0_the_wd_oF_IOT_s3cur}

总结

  • 一般来说异构程序的反编译用ghidra,但笔者感觉还是ida顺手,9.0的IDA已经支持MIPS等普通的异构了。当然更冷门的指令集架构比如英飞凌的TriCore还是得用ghidra
  • 逆向固件打包算法以及固件中的算法时,结合数据HEX对数据结构进行猜测非常重要,这些需要经验、直觉以及冷静的观察
  • mcu裸机固件逆向思路:获取芯片型号查datasheet -> 获取指令集架构 -> 获取memory的映射结构,包括:rom基址、程序入口地址、ram基址、外设接口地址、各类总线地址等信息 -> 在反汇编工具中创建正确的内存映射并反编译
  • 对于bin、hex、s19等mcu烧录文件的格式转换,可以使用objcopy、SRecord等命令行工具,但是更推荐使用专业工具Hexview
  • ida和ghidra具有强大的搜索功能,除了字符串,还能搜索指令、标量、字节序列等

Reference

2024 L3HCTF writeup by Arr3stY0u

2024 L3HCTF Reverse WP by WM

SCTF 2024 writeup by Arr3stY0u

2024SCTF 官方wp

Spirit战队 2024 SCTF wp

[星盟] SCTF 2024 Writeup

[Clang裁缝店] SCTF 2020 Password Lock Plus 入门STM32逆向

[吾爱破解] 采用IDA Pro 分析878UVII radio对讲机固件初步研究

芯片,SOC和MCU区别;裸机和带系统

MCU 单片机固件逆向分析入门

固件逆向的通用技巧

ISO14229-uds源码


三道CTF题目学习h3c路由器&stm32裸机&Infineon车机固件逆向
https://lkliki.github.io/2024/11/06/从2024L3hCTF&&SCTF三道题学习h3c路由器&stm32裸机&Infineon车机固件逆向/
作者
0P1N
发布于
2024年11月6日
许可协议