BooFuzz入门-Vivotek摄像头栈溢出漏洞

IOT FUZZ学习ing

BooFuzz入门-Vivotek摄像头栈溢出漏洞

BooFuzz介绍

Boofuzz是一个基于生成的黑盒协议模糊测试框架,继承自Sulley,基于python语言。

BooFuzz使用

官方文档,有User Guide和API文档

环境配置

官方建议在python虚拟环境中使用boofuzz,同时python版本不能太低,具体可以看文档中的changelog

安装虚拟环境及boofuzz

1
sudo apt-get install python3-venv build-essential

在当前目录创建虚拟环境env,env是可以自定义的环境名称

1
python3 -m venv env

激活虚拟环境,激活后终端前面会标识(env)

1
source ./env/bin/activate

安装最新版的pip和setuptools再安装boofuzz包

1
2
pip install -U pip setuptools
pip install boofuzz

fuzz脚本编写

该文章实现了一个boofuzz脚本生成器,可以导入请求的文本文件生成对应的boofuzz脚本

下面是boofuzz的基本步骤以及一些不充分的解释,要熟练掌握boofuzz还得是结合api文档读源码,下面贴一张网上找的源码结构图

1

创建Session会话

1
2
3
4
#示例
session = Session(
target=Target(
connection=SocketConnection("127.0.0.1", 8080, proto='tcp')))

构造请求

静态协议定义api

以下面代码为例,定义了部分http请求

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
s_initialize(name="Request") 
with s_block("Request-Line"):
# LINE 1
s_static("POST", name="Method")
s_delim(" ", name='space-1')
s_string("/fromLogin", name='Request-URI') # variation
s_delim(" ", name='space-2')
s_static('HTTP/1.1', name='HTTP-Version')
s_static("\r\n")

# LINE 2
s_static("Host", name="Host")
s_static(": ")
s_static("192.168.10.1", name="ip")
s_static("\r\n")

# LINE 3
s_static('Content-Length')
s_static(': ')
s_size('data', output_format='ascii', fuzzable=True) # size的值根据data部分的长度自动进行计算,同时对该字段进行fuzz
s_static('\r\n')
# 对应http请求数据
with s_block('data'):
s_static('login_name=&curTime=1581845487827&setLang=&setNoAutoLang=&login_n=admin&login_pass=')
s_string('123456', max_len=1024) # 需要变异,且最大长度为1024
s_static('&languageSel=1')

s_initialize

s_static生成Static primitives,是固定的,在模糊测试时不会突变

s_delim,s_string是动态的,不指定fuzzable = false的情况下在模糊测试时会发生突变

s_size与第一个name参数对应的block块绑定,以ascii的形式返回绑定的块的大小

最终定义的消息如下,对/fromLogin以及前后的空格,login_pass=后的123456字段进行了fuzz

1
2
3
4
5
POST /fromLogin HTTP/1.1
Host: 192.168.10.1
Content-Length: 103

login_name=&curTime=1581845487827&setLang=&setNoAutoLang=&login_n=admin&login_pass=123456&languageSel=1

链接请求,构建状态图

初始的Session中是没有请求的,需要手动将之前定义的请求按照一定的先后顺序链接起来,如果只有单个请求需要fuzz,那么直接添加即可。同时也可以在两个请求之间注册回调函数,这个回调函数会在状态转移时被调用,可以来收集返回包信息等。

例如以下代码

1
2
3
4
session.connect(s_get('login'))		# 默认前置节点为root
session.connect(s_get('login'), s_get('setsysemailsettings'), callback=add_auth_callback)
session.connect(s_get('login'),s_get('setsyslogsettings'), callback=add_auth_callback)
session.connect(s_get('login'),s_get('setschedulesettings'), callback=add_auth_callback)

对应的协议树如下

2

在发送完login请求完成登陆后,才能发送后面的几个请求,同时在login后添加了回调函数

开始fuzz

传入一个请求的name的话就会只fuzz这个请求,不传默认按建立的状态图去遍历着fuzz,fuzz时可以访问 http://127.0.0.1:26000/查看统计数据

1
session.fuzz()

添加对目标设备的监控和重启机制等

监视器用来检测目标是否崩溃,是否正常运行,收集目标程序返回包的信息,启动、停止和重新启动目标等。

Boofuzz 提供三种监视器:ProcessMonitor、NetworkMonitor、CallbackMonitor,它们都基于BaseMonitor,都具备BaseMonitor的基础功能。

一般可以在session中添加回调函数,回调函数有下面三种

  • pre_send_callbacks – 在每个模糊请求之前都会调用已注册的方法。 默认值:None。
  • post_test_case_callbacks – 在每个模糊测试用例之后都会调用已注册的方法。 默认值:None。
  • post_start_target_callbacks – 方法将在目标启动或重新启动后调用, 比如说,通过进程监视器。

BooFuzz练习-Vivotek摄像头固件栈溢出漏洞

参考

解包固件

1
binwalk -Me CC8160-VVTK-0100d.flash.pkg

文件结构解析

使用tree命令查看文件结构

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
_CC8160-VVTK-0100d.flash.pkg.extracted$ tree -d -L 5
.
├── _31.extracted
│ ├── boot
│ ├── defconf
│ │ └── _CC8160.tar.bz2.extracted
│ │ └── _0.extracted
│ │ └── etc
│ ├── dtb
│ ├── _kernel.img.extracted
│ │ └── _5FB4.extracted
│ │ ├── cpio-root
│ │ │ ├── dev
│ │ │ └── root
│ │ ├── dev
│ │ └── root
│ ├── nandspl
│ └── _rootfs.img.extracted
│ └── squashfs-root
│ ├── bin
│ ├── dev
│ │ ├── misc
│ │ ├── net
│ │ ├── pts
│ │ ├── shm
│ │ └── snd
│ ├── drivers
│ ├── etc
│ │ ├── default
│ │ ├── init.d
│ │ └── udhcpc
│ ├── home
│ ├── lib
│ │ └── iptables
│ ├── mnt
│ │ ├── auto
│ │ ├── flash
│ │ ├── flash2
│ │ └── ramdisk
│ ├── proc
│ ├── root
│ ├── sbin
│ ├── sys
│ ├── tmpfs
│ │ ├── CF
│ │ ├── samba
│ │ ├── tmp
│ │ └── var
│ ├── usr
│ │ ├── bin
│ │ ├── lib
│ │ ├── local
│ │ ├── sbin
│ │ └── share
│ └── www
│ ├── colorpicker
│ ├── css
│ ├── include
│ ├── pic
│ └── setup
├── _BD6E8E.extracted
├── _D48788.extracted
└── _DC7909.extracted
└── setup
└── vadppkg
├── _genetec-vadp-1-0-2-7.tar.gz.extracted
│ └── _0.extracted
└── _stratocast-1-1-1-3.tar.gz.extracted
└── _0.extracted

大部分内容都在_31.extracted文件夹中,其中boot文件夹中是u-boot的主程序;dtb文件夹中是内核的设备树文件;kernel.img.extracted是提取后的内核镜像文件,没东西;nandspl中没有提取出文件,但是nandspl本身是针对nandflash的二级程序加载器(secondary program loader),也是u-boot的一部分,在u-boot主程序前执行,进行最基本的硬件初始化,比如关闭中断,内存初始化,设置堆栈等。这几个文件可以总结为以下流程:

ROM code -> u-boot spl -> uboot主程序 ->加载kernel。

关于u-boot spl,具体可以看这里

然后就是最重要的rootfs.img.extracted,这是提取后的文件系统。

提取的文件系统中etc目录下很多文件都是软链接,链接到了defconf目录下的etc文件夹中,可以用’’ls -l’’命令查看.

寻找服务程序

linux启动流程,大致为

init程序(linuxrc) -> busybox -> /etc/inittab -> /etc/init.d/rcS

所以进入文件系统找到”**/etc/init.d/rcS**”开机自启文件,在下面代码中用run-parts命令遍历执行了”/etc/rcS.d“目录下的所有可执行脚本。

这里注释标注的是Start daemons,即启动守护进程。守护进程是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或循环等待处理某些事件的发生。很多服务进程如httpd都是守护进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Apply patch brfore rcS.d
echo "check apply_patch_before"
if [ -f /mnt/flash2/patch/apply_patch_before ]; then
chmod 777 /mnt/flash2/patch/apply_patch_before
/mnt/flash2/patch/apply_patch_before
fi

# Start daemons
echo "run-parts -a start /etc/rcS.d"
run-parts -a start /etc/rcS.d

# Apply patch after rcS.d
echo "check apply_patch_after"
if [ -f /mnt/flash2/patch/apply_patch_after ]; then
chmod 777 /mnt/flash2/patch/apply_patch_after
/mnt/flash2/patch/apply_patch_after
fi

logger "init-rcS end"
exit 0

rcS.d在/etc中是个软链接,指向”../mnt/flash/etc/rcS.d”,在提取出的文件中位于”31.extracted/defconf/_CC8160.tar.bz2.extracted/_0.extracted/etc/“,该目录下的文件都被软链接到了/etc。

进入rcS.d,里面都是开机自启服务进程的执行脚本,发现执行脚本均为软连接文件,连接至”etc/init.d/“下的对应启动脚本文件,在启动脚本文件中可以找到可执行二进制程序的真正位置。

以httpd服务为例,启动过程如下:

/etc/init.d/rcS -> /etc/rcS.d/S31httpd -> /etc/init.d/httpd -> /usr/sbin/httpd

寻找漏洞点

结合对httpd程序的逆向,可以判断这里用的是boa二次开发的web服务器

ida中查找字符串Content-Length即可,这里会将Content-Length字段后从:到\n全都复制到栈上的dest变量,没有限制长度,导致了栈溢出。

3

固件仿真

查看漏洞程序信息

1
2
3
4
5
6
7
8
$ file httpd
httpd: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
$ checksec httpd
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)

httpd的配置文件是/etc/conf.d/boa/boa.conf,conf.d是软链接的,这里失效了,所以要替换软链接,将31.extracted/defconf/_CC8160.tar.bz2.extracted/_0.extracted/etc中的文件复制到31.extracted/_rootfs.img.extracted/squashfs-root/etc,选择全部替换

然后用户态qemu模拟运行httpd程序,尝试运行报错,无/dev/null

1
2
3
$ cp /usr/bin/qemu-arm-static Path_to/squashfs-root
$ sudo chroot ./squashfs-root/ /qemu-arm-static /usr/sbin/httpd
src/boa.c:284 (main) - can't open /dev/null: No such file or directory

解决

1
touch ./dev/null

继续运行,虽然输出Success,但是并没有对应的进程

1
2
3
4
5
$ sudo chroot ./squashfs-root/ /qemu-arm-static /usr/sbin/httpd -c /etc/conf.d/boa -d 
sendto() error 20
[debug]add server push uri 3 video3.mjpg
[debug]add server push uri 4 video4.mjpg
gethostbyname:: Success

用ida打开httpd,查看gethostbyname字符串交叉索引定位到函数。先获取hostname,再解析成ip。先./etc/hosts解析再dns解析,所以这里改hosts文件中127.0.0.1对应的hostname

4

先hostname获取主机名,再把hosts文件中的Network-Camera改成主机名

在/etc/init.d/httpd中找到httpd的启动参数,运行服务

1
2
3
4
5
6
7
8
9
$ sudo chroot ./ /qemu-arm-static /usr/sbin/httpd -c /etc/conf.d/boa -d  
sendto() error 20
[debug]add server push uri 3 video3.mjpg
[debug]add server push uri 4 video4.mjpg
[debug] after ini, server_push_uri[0] is /video3.mjpg
[debug] after ini, server_push_uri[1] is /video4.mjpg
fopen pid file: Not a directory
[01/Nov/2023:04:13:00 +0000] boa: server version 1.32.1.10(Boa/0.94.14rc21)
[01/Nov/2023:04:13:00 +0000] boa: starting server pid=4978, port 80

访问127.0.0.1发现服务正常,运行成功

进行fuzz

脚本来源

只对Content-Length字段进行fuzz

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
from boofuzz import*
IP = "127.0.0.1"
PORT = 80

def check_response(target,fuzz_data_logger,session,*args,**kwargs):
fuzz_data_logger.log_info("Checking for response ...")
try:
response = target.recv(512)
except:
fuzz_data_logger.log_fail("Unable to connect ...")
return
if not response:
fuzz_data_logger.log_fail("Empty response ...")
target.close()
return
fuzz_data_logger.log_info("Start checking ...\n"+response.decode())
target.close()
return

def main():
session = Session(
target=Target(connection=SocketConnection(IP,PORT,proto="tcp"),
),
post_test_case_callbacks=[check_response],
)
s_initialize(name="FUZZ")
with s_block("Request-Line"):
# Method
s_group("Method",["POST"])
s_delim(" ",fuzzable=False)
s_string("/cgi-bin/admin/upgrade.cgi ",fuzzable=False,name="URI")
s_static("HTTP/1.1",name="Version")
s_static("\r\n",name="CRLF")
# Host
s_static("Host")
s_delim(": ",fuzzable=False)
s_string("127.0.0.1",fuzzable=False,name="IP")
s_static("\r\n")
# Connection
s_static("Connection")
s_delim(": ",fuzzable=False)
s_string("Close",fuzzable=False,name="Active")
s_static("\r\n")
# Content-Length
s_static("Content-Length")
s_delim(": ", fuzzable=False)
s_string("65", fuzzable=True)
# End
s_static("\r\n")
s_static("\r\n")

session.connect(s_get("FUZZ"))
session.fuzz()

if __name__=="__main__":
main()

可以看到在Test Case 1中没有返回,程序已经崩溃

5

脚本还在运行时,可以访问localhost:26000查看本次fuzz相关信息(触发crash的case,case详细信息等)

6

reference

boofuzz github

boofuzz手册

[csdn] BooFuzz协议漏洞挖掘入门教程与使用心得

[csdn] IoT 设备网络协议模糊测试工具boofuzz实战

[知乎] BooFuzz实战 Vivotek的Fuzzing 栈溢出

常见嵌入式Web服务器CGI处理功能简要分析 (aslr.io)

IOT固件安全all in one - 固件仿真


BooFuzz入门-Vivotek摄像头栈溢出漏洞
https://lkliki.github.io/2023/10/30/BooFuzz入门-Vivotek摄像头栈溢出漏洞/
作者
0P1N
发布于
2023年10月30日
许可协议