Draytek vigo3910 工业路由器固件解密及其CVE-2024-23721漏洞分析
DrayTek Vigor 3910 简介
DrayTek Vigor 3910 是一款面向企业的高性能 Multi-WAN 安全路由器,定位 10Gb 级 VPN 集中器。采用 1.2 GHz 四核处理器,配备 2 个 10G SFP+、2 个 2.5G 及 8 个千兆接口,最高支持 8 条 WAN 链路。NAT 吞吐量达 9 Gbps,IPsec VPN 吞吐量 3 Gbps,可承载 500 条 VPN 隧道。基于 DrayOS 固件,支持多 WAN 负载均衡、防火墙、QoS、VLAN、高可用等企业级功能。
安全漏洞现状
在固件版本 4.3.2.6 及之前,Vigor 3910 存在大量安全缺陷, CVE-2024-23721就因为鉴权不当导致可以匹配绕过。本文将以 4.3.2.5 固件为目标,对 CVE-2024-23721等系列典型漏洞进行逆向分析与完整复现。
固件解密
由于Vigor 3910系列的固件在4.x.x.x系列就已经显示加密了,因此我们可以通过分析过渡版本来查看其是否存在审计程序所需要的秘钥
我们 先看固件下载地址https://fw.draytek.com.tw/在这个里面ctrl+f搜索

点击去可以看到

由于v3.9.7.2刚好是v3.x到v4.x的中间版本,那么我们可以选择这个版本去分析看其是否存在解密v4.x所需要的秘钥。当然如果不存在的话那么大概率这个秘钥可能需要我们购买设备想办法从内存中dump了或者联系厂商看是否能取得未进行加密前的设备,或者考虑其他加密方法。
接下来我们下载完成然后拷贝过来后发现binwalk -Me 竟然也不能解开v3.9.7.2的固件,
binwalk -E v3910_3972.all
使用该命令查看两个版本之间不同的熵值。
v3.9.7.2

v4.3.2.5

从熵值上来看v3.9.7.2这个版本的固件应该是没有进行加密的,接下来考虑可能是压缩问题
从智玩宇宙大佬的文章参考到,可能是binwalk可能对lz4压缩不太支持,所以导致文件系统解包失败。那么我们手动对binwalk进行功能添加。
# Ubuntu系统 添加lz4压缩工具
apt install liblz4-tool
# 加入lz4的压缩支持(binwalk安装路径/binwalk/config/extract.conf,如果找不到用find命令查找->sudo find / -name "extract.conf" 2>/dev/null)
^lz4 compressed data:lz4:lz4 -d '%e' '%e.bin'

找到之后然后vim把这一行添加进去即可

然后再次解包就可以成功获得完整的文件系统了。然后由于过渡版本升级到新版本大概率是需要在升级的时候对上传的加密固件先进行解密然后再替换的。那么我们先看看固件升级的逻辑,通过参考从CVE到DrayTek Vigor系列企业路由器固件分析 - 智能硬件个人技术分享这篇大佬的博客了解到在/etc/runcommand/fwupload这个文件夹下面的这个脚本记录了如何进行固件版本更新的

在vim中命令行模式下输入/再输入字符串进行enc,denc,dec这种字符串搜索到以下关键线索

然后可以看到chacha20应该是核心相关的解密二进制文件,随后我们在解包文件中全局搜索这个二进制文件并通过IDA打开它,同时这个chacha20这个二进制文件名字我们大概率可以猜到所用到的加密算法应该也是chacha20。下面是关于这个算法的介绍
(9 封私信 / 80 条消息) chacha20加密算法笔记 - 知乎
通过分析这个二进制文件的main函数可以确认秘钥应该为
key = b"0DraytekKd5Eason3DraytekKd5Eason" # J to E

然后nonce可以通过直接查看新版固件的二进制直接看到

再确认该镜像的加密起始位置

通过这个大概可以知道加密的镜像为: 0x034A1A00。随后将 enc_Image 部分拆出,编写解密代码进行解密
dd if=v3910_4325.all of=enc_Image skip=243 count=55187968 bs=1
最终编写出以下解密这个脚本
from Crypto.Cipher import ChaCha20
def do_decrypt(enc_Image):
nonce = b"UODAjyXZOzH0"
with open(enc_Image, "rb") as f:
enc_data = f.read()
key = b"0DraytekKd5Eason3DraytekKd5Eason" # J to E
cipher = ChaCha20.new(key=key, nonce=nonce)
dec_data = cipher.decrypt(enc_data)
with open(enc_Image + "_decrypted.bin", "wb") as f:
f.write(dec_data)
print("Done!")
if __name__ == "__main__":
filename = "enc_Image.bin"
do_decrypt(filename)
最后得到以下文件系统

模拟执行
然后下一步就是固件模拟执行了,这部分我参考的是Draytek Vigor 3910 | Catalpa's Site这篇大佬的文章。解包出来的V4.3.2.5的固件下的firmware 目录下的启动脚本run.sh中qemu的启动参数中有-dtb DrayTek。那边大概率这个设备或许在开发的时候就考虑过模拟的功能

因此这里我们选择Draytek 提供设备的 GPL 代码,下载 3910 型号的 GPL 代码分析确定,开发者为 QEMU 添加了一些新功能,用来支持 drayos 运行,所以我们需要编译这份 GPL 代码。解压缩之后执行以下命令
./configure --enable-kvm --enable-debug --target-list=aarch64-softmmu
make
然后找到这个二进制文件

、
再执行这个命令复制到解包出来的文件的firmware目录下,然后新建一个network.sh文件并写入以下内容
#!/bin/bash
iflan=ens38
ifwan=ens39
mylanip="192.168.1.2"
brctl delbr br-lan
brctl delbr br-wan
ip link add br-lan type bridge
ip tuntap add qemu-lan mode tap
brctl addif br-lan $iflan
brctl addif br-lan qemu-lan
ip addr flush dev $iflan
ifconfig br-lan $mylanip
ifconfig br-lan up
ifconfig qemu-lan up
ifconfig $iflan up
ip link add br-wan type bridge
ip tuntap add qemu-wan mode tap
brctl addif br-wan $ifwan
brctl addif br-wan qemu-wan
ip addr flush dev $ifwan
ifconfig br-lan $mylanip
ifconfig br-wan up
ifconfig qemu-wan up
ifconfig $ifwan up
brctl show
#for speed test
ethtool -K $iflan gro off
ethtool -K $iflan gso off
ethtool -K $ifwan gro off
ethtool -K $ifwan gso off
ethtool -K qemu-lan gro off
ethtool -K qemu-lan gso off
ethtool -K qemu-wan gro off
ethtool -K qemu-wan gso off
#for telnet from linux to drayos 192.168.1.1
ethtool -K br-lan tx off
然后再修改firmware目录下的run.sh文件为下面内容
#!/bin/bash
# 1. do "fw_setenv purelinux 1" first , then reboot
# 2. do setup_qemu_linux.sh (default P3 as WAN, P4 as LAN, for both 1Gbps connection only)
# 3. remember to recover to normal mode by "fw_setenv purelinux 0"
rangen() {
printf "%02x" `shuf -i 1-255 -n 1`
}
rangen1() {
printf "%x" `shuf -i 1-15 -n 1`
}
wan_mac(){
idx=$1
printf "%02x\n" $((0x${C}+0x$idx)) | tail -c 3 # 3 = 2 digit + 1 terminating character
}
A=$(rangen); B=$(rangen); C=$(rangen);
LAN_MAC="00:1d:aa:${A}:${B}:${C}"
if [ ! -p serial0 ]; then
mkfifo serial0
fi
if [ ! -p serial1 ]; then
mkfifo serial1
fi
platform_path="./platform"
echo "x86" > $platform_path
enable_kvm_path="./enable_kvm"
echo "kvm" > $enable_kvm_path
cfg_path="./magic_file"
echo "GCI_SKIP" > gci_magic
mkdir -p ../data/uffs
touch ../data/uffs/v3910_ram_flash.bin
uffs_flash="../data/uffs/v3910_ram_flash.bin"
echo "1" > memsize
(sleep 20 && ethtool -K qemu-lan tx off) &
model="./model"
echo "3" > ./model
rm -rf ./app && mkdir -p ./app/gci
GCI_PATH="./app/gci"
GCI_FAIL="./app/gci_exp_fail"
GDEF_FILE="$GCI_PATH/draycfg.def"
GEXP_FLAG="$GCI_PATH/EXP_FLAG"
GEXP_FILE="$GCI_PATH/draycfg.exp"
GDEF_FILE_ADDR="0x4de0000"
GEXP_FLAG_ADDR="0x55e0000"
GEXP_FILE_ADDR="0x55e0010"
echo "0#" > $GEXP_FLAG
echo "19831026" > $GEXP_FILE
echo "GCI_SKIP" > $GDEF_FILE
SHM_SIZE=16777216
./qemu-system-aarch64 -M virt,gic_version=3 -cpu cortex-a57 -m 1024 -L ../usr/share/qemu \
-kernel ./vqemu/sohod64.bin $serial_option -dtb DrayTek \
-nographic $gdb_serial_option $gdb_remote_option \
-device virtio-net-pci,netdev=network-lan,mac=${LAN_MAC} \
-netdev tap,id=network-lan,ifname=qemu-lan,script=no,downscript=no \
-device virtio-net-pci,netdev=network-wan,mac=00:1d:aa:${A}:${B}:$(wan_mac 1) \
-netdev tap,id=network-wan,ifname=qemu-wan,script=no,downscript=no \
-device virtio-serial-pci -chardev pipe,id=ch0,path=serial0 \
-device virtserialport,chardev=ch0,name=serial0 \
-device loader,file=$platform_path,addr=0x25fff0 \
-device loader,file=$cfg_path,addr=0x260000 \
-device loader,file=$uffs_flash,addr=0x00be0000 \
-device loader,file=$enable_kvm_path,addr=0x25ffe0 \
-device loader,file=memsize,addr=0x25ff67 \
-device loader,file=$model,addr=0x25ff69 \
-device loader,file=$GDEF_FILE,addr=$GDEF_FILE_ADDR \
-device loader,file=$GEXP_FLAG,addr=$GEXP_FLAG_ADDR \
-device loader,file=$GEXP_FILE,addr=$GEXP_FILE_ADDR \
-device nec-usb-xhci,id=usb \
-device ivshmem-plain,memdev=hostmem \
-object memory-backend-file,size=${SHM_SIZE},share,mem-path=/dev/shm/ivshmem,id=hostmem

默认账号密码是admin/admin然后登录进后台
漏洞复现分析
这里选择的是对CVE-2024-23721这个经典漏洞进行复现分析。3910 设备底层运行的是 Linux 系统,在系统中使用 qemu 模拟启动 sohod64.bin,该文件是 Draytek 自行实现的 DrayOS 系统,业务逻辑基本集中在此文件中。我们可以在/firmware/vqemu目录下找到该文件然后载入IDA

好家伙这里可以看到总共有3w个函数,这里就很有必要可以借助一下AI进行分析了。最终在AI的辅助分析下可以找到sub_40153890这个函数是出来请求接受参数的关键函数之一。然后我们接着往下看
这里大概在287~291这几行识别请求头来区分这些是HEAD类型的请求还是GET,POST类型的

sub_406514D8这个函数我们大概率可以猜测他应该是从V1这个结构体获取参数,因此我们给他重命名为get_var。然后它会接着往下走

最后走到358行这里sub_4015F640这个函数大概率就是处理POST请求的具体业务,我们再继续跟进。可以看到如下图的22行这个关键函数就是处理不同POST对应URL如何处理的请求

继续跟进这个sub_40156840函数

可以看到在138行如果URL以/Draytek开头并且结尾是.exp 字符时会返回 0x1b67。那么我们继续回到sub_4015F640这个函数我们看它这个返回值对应的是什么

最终在657行这里找到对应的处理方法。然后跟进这里的sub_4010E084函数几,进入这个函数,在142行这里可以看到这里会检查v22也就是传递给这个函数的第一个参数和/auth_check.cgi进行strncmp比较v10是len,这里显然是可以通过的

所以v26会被置0,我们继续往下追到193行

继续追踪这个sub_4010CE00函数
来到这个函数的250行左右可以看到这里判断了URL 中是否包含 /images/,/weblogin.htm等这些内容,一般这种页面也是不需要鉴权就可以访问的页面只一,如果是则来到LABEL_121对应的位置


这个if条件判断里面执行了什么无所谓,这个重点关注的是他在这里最后会返回1
回到上层函数可以看到这里会返回1,然后在上层函数的99行这里也会返回1.

然后就会进入这个函数


这个函数大概意思就是获取sFormAuthStr=这个变量的值然后再与内存中保存的数据比较。类似于那种stok,token,等那种凭证数据进行比较,不过有意思的是这种数据在无用户登录的情况下一般是为空的。最后到这里就绕过登录成功执行了cfg_gci_export_script这个导出配置的脚本行为。URL构造如下
POST /Draytek.exp/images/?sFormAuthStr= HTTP/1.1
Host: 192.168.1.1
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 65
chk_encryptcfg=170
参考文章
从CVE到DrayTek Vigor系列企业路由器固件分析 - 智能硬件个人技术分享
Draytek3910 固件解密及漏洞分析 - IOTsec-Zone