2021 年 09 月 18 日, 一篇博客公开了一个影响海康威视 IPC/NVR 多个型号设备的未授权代码执行安全风险,编号为 CVE-2021-36260,本文此漏洞进行复现分析。
获取固件
根据披露信息,去海康威视欧洲区域网站下载设备固件,由于此漏洞影响不同型号设备,我们选择 DS-2CD1x10 系列进行测试,点此下载固件。
拿到固件后 binwalk 直接解包,得到一个 linux rootfs,以及一个 cramfs 分区数据。(PS:某些版本的公开固件可能是加密的,需要读取 flash 芯片数据)
尽管能够成功解开固件,但是在解包的文件系统中没有找到类似 httpd 等关键程序。在 cramfs 分区中存在一些可执行文件,其中一个叫做 davinci_bak 的文件既不是可执行程序,也不是配置文件,且通过 binwalk 查看熵值发现文件似乎经过加密。
搜索 davinci_bak 在 daemon_fsp_app 文件中找到相关信息,此程序从 /dev/hikio 设备文件中读取密钥,然后利用 OpenSSL 对 davinci_bak 进行 AES 解密,猜测此文件内包含主要逻辑。
串口调试
从本地固件中无法得到 AES 密钥信息,所以我们要尝试从真机上提取主程序。
拆解机器,在主板上找到了预留的 UART 调试接口,焊接处理之后即可使用 FT232 调试。
接好线路将串口工具接入电脑,设置波特率为 115200,上电开机看到串口有数据输出。
等待系统正常启动,发现输出了命令提示符,但是许多 linux 命令都无法使用,ps 看到目前我们操作的是 /bin/psh 程序,此程序为海康威视编写的 protect-shell,禁用了大部分系统功能。
获取 root shell
为了方便后续的操作,我们需要获取完整的 shell,通过在网络上检索信息,找到 ipcamtalk 论坛一篇帖子,其中提到了可以通过修改 u-boot 启动参数的方式来获取完整 shell。
重启设备,当串口提示 “Hit Ctrl+u to stop autoboot:” 时按下 Ctrl + u 进入 u-boot 命令行,输入 help 看到帮助信息。
用 “setenv bootargs console=ttyS0,115200 root=/dev/ram0 mem=61M dbg=9 debug single” 命令来修改 bootargs 环境变量,“saveenv” 保存修改,“reset” 重启设备,等待设备启动后即可得到完整的 shell。
不过由于我们修改了启动进程,导致目前系统还未初始化,其中的关键程序没有启动,可以执行以下命令序列完成设备初始化并保留 root shell。
1 2 3 4 5
| rm /bin/psh echo "#!/bin/bash" > /bin/psh echo "/bin/bash" >> /bin/psh chmod +x /bin/psh # 将 psh 替换为完整的 bash ./linuxrc # 执行系统初始化脚本
|
挂载 nfs
设备自带的 busybox 经过大幅度裁剪,缺少 curl、wget 等向设备传输文件的功能,导致后续测试困难。我们考虑用 nfs 实现文件传输,在摄像头上利用 mount 命令挂载外部设备的 nfs 文件系统到本地。
首先准备一份完整的 busybox,在本地开启一个 nfs 服务器,设备上执行以下命令
1 2 3 4
| mkdir /tmp/hik busybox mount -o nolock -t nfs 192.168.0.160:/home/iot/Desktop/hik /tmp/hik chmod 777 /tmp/hik/busybox-armv5l /tmp/hik/busybox-armv5l telnetd -l/bin/sh -p31337 &
|
执行后即可连接设备 31337 端口得到 shell,防止串口输出的其它信息干扰分析。
补丁对比
在 /home/process 目录下找到解密完成的 davinci 程序,将它拷贝回 nfs 目录
1
| cp /home/process/davinci /tmp/hik
|
这样在 nfs 服务器端就能下载到此程序。
同样的操作我们解包出修复前和修复后的两个 davinci 程序,用 bindiff 进行补丁对比,得到的结果中涉及一个叫做 hwif_is_all_language_character_legal 的函数,两个版本函数如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| int __fastcall hwif_is_all_language_character_legal(char *filename, int a2) { int v3; size_t filename_len; size_t v5; char *v6; char *filtered_filename; char *tmp_buf; size_t i; int v10; char v11; bool v12; size_t v13; size_t v14;
v3 = a2; if ( !filename ) goto LABEL_5; if ( !a2 ) { dev_debug_v1(2, 48, "basefun_lib/string/string_deal.c"); return v3; } filename_len = strlen(filename); v5 = filename_len; if ( filename_len != v3 || (v6 = calloc(1u, filename_len + 1), (filtered_filename = v6) == 0) ) { LABEL_5: dev_debug_v1(2, 48, "basefun_lib/string/string_deal.c"); return -1; } tmp_buf = v6; for ( i = 0; i < v5; ++i ) { v10 = filename[i]; v11 = filename[i]; v12 = v10 == '`'; if ( v10 != '`' ) v12 = v10 == ';'; if ( !v12 ) *tmp_buf++ = v11; } v3 = 0; *tmp_buf = 0; memset(filename, 0, v5); v13 = strlen(filtered_filename); if ( v13 >= v5 ) v14 = v5; else v14 = v13; memcpy(filename, filtered_filename, v14); free(filtered_filename); return v3; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| int __fastcall hwif_is_all_language_character_legal(int a1, unsigned int a2) { unsigned int i; unsigned __int8 v3; bool v4; int result;
if ( a1 ) { for ( i = 0; i < a2; ++i ) { v3 = *(a1 + i); v4 = v3 - 48 > 9; if ( v3 - 48 > 9 ) v4 = v3 - 97 > 25; if ( v4 && v3 - 65 > 25 && v3 != 45 && v3 != 95 ) goto LABEL_11; } result = 0; } else { dev_debug_v1( 1, 38, "hardwareif/r2/unihardwareif.c", 402, "hwif_is_all_language_character_legal", "p_str is NULL.\n"); LABEL_11: result = -1; } return result; }
|
旧版本只检查了字符串中是否包含 `;,而新版中限制字符串是纯数字/字母组成。
漏洞分析
通过交叉引用 hwif_is_all_language_character_legal 函数定位到漏洞的具体位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| if ( hwif_is_all_language_character_legal(a1, v3) ) goto LABEL_13; snprintf(s, 0x1Fu, "%s%s", "/home/webLib/doc/i18n/", a1); if ( !check_is_file_exist(s) ) { dev_debug_v1(5, 38, "hardwareif/r2/unihardwareif.c"); return 0; } memset(s, 0, 0x20u); snprintf(s, 0x1Fu, "/dav/IElang.tar %s.tar.gz", a1); memset(v5, 0, sizeof(v5)); snprintf( v5, 0xFFu, "tar -xf %s -C %s; tar -zxf %s%s.tar.gz -C %s", s, "/home/webLib/doc/i18n/", "/home/webLib/doc/i18n/", a1, "/home/webLib/doc/i18n/"); if ( system(v5) < 0 ) { }
|
我们可以看到参数 a1 经过 hwif_is_all_language_character_legal 过滤之后会将其作为文件名检查目标文件是否存在,如果不存在则将 a1 拼接到 tar 命令中直接带入 system 函数执行,显然存在命令注入。