Featured image of post CVE-2025-10779 DCS-935L摄像头栈溢出漏洞分析

CVE-2025-10779 DCS-935L摄像头栈溢出漏洞分析

跟着教程学习基础

这篇文章主要是参考二进制安全 摄像头0day漏洞挖掘入门第一课_哔哩哔哩_bilibili进行学习,在这个过程中纪录的学习笔记

初步分析

找漏洞之前首先要通过FOFA查询用户量,查询语法app="设备型号" && after="2026-01-01",用户量越多的设备漏洞价值越高

下载固件,解压得到一个二进制文件,使用binwalk进行解包

1
binwalk -e DCS-935L_A1_FW_1.10.01_20161128_r4156.bin

得到一个文件夹,其中包含如下文件,固件分析一般在squashfs-root下进行

下一步需要查找web服务相关的程序,关键词如html、.php、.aspx、favicon.icon、web、http等和web相关的关键词。这里查到有一个名称是web的文件夹,里面有favicon.ico和一些.asp文件,推测这就是目标文件夹

web目录下有一个httpd文件,以d为后缀说明这是http服务的服务端。还有一个文件夹名称是cgi_bin,推测设备采用的是cgi协议。cgi的特点就是将用户传入的参数设置为环境变量,然后用getenv()来获取环境变量

cgi-bin中可以看到hnap文件夹,hnap是一个D-Link、TP-Link等厂商设备中常见的Web协议。文件夹中包含hnap_service,这就是这次的分析目标

一般情况在分析这类小型固件时,由于文件数量较少且逆向方便,可以逐个程序进行分析

逆向分析

寻找溢出点

用ida打开目标文件,自动识别架构是mips大端序,主界面可以按空格切换流程图与汇编。这里首先在左侧找到main函数,然后f5进行反编译。注意到伪代码中多次引用了getenv函数,这也说明了这个代码确实和cgi有关

这次的目标是栈溢出漏洞,所以全局搜索相关函数sprintf和strcpy,然后按x查看调用情况

strcpy(a,b)意思就是用strcpy函数将b的内容复制到a中。因此在strcpy(栈上有限字符串,用户输入)中,如果用户输入字符串的长度超过栈可容纳上限,超出部分也一同被复制,这就导致了溢出

挨个查看调用,通过伪代码寻找有可能出现栈溢出的位置。注意到sub_402280函数中有这样的一句strcpy(v28, v12);,查看定义char v28[256]; // [sp+64h] [-40Ch] BYREF,v12 = getenv("HNAP_AUTH");,分别是256字节大小的字符串和用户输入,刚好符合栈溢出可能产生的条件。这时如果HNAP_AUTH大于了256字节,就会产生溢出

发现溢出点后,可以对其进行标记,也就是将本地变量名改成便于识别的形式,右键重命名。我这里分别改了函数名和记录用户输入的变量名

判断溢出条件

对目标函数按x来到调用处,发现是main函数,在调用这一行打一个断点,随后分析其中的if判断逻辑,找出进入这一行的条件。这部分一共有4个if:

  • if ( !v7 ):

    当v7=0时会进入第一层调用目标函数的逻辑,v7 = strcmp(v4, "POST");说明条件是v4=POST,而v3 = getenv("REQUEST_METHOD"); v4 = v3;,最终得知用户需要传入"POST"作为REQUEST_METHOD参数从而进入正确的分支

  • if ( v9 ):

    当v9=1时会进入正确逻辑,v9 = getenv("SOAP_ACTION");,也就是传入'1'作为SOAP_ACTION的值

  • if ( v5 ):

    当v5=0时会进入正确逻辑,v5 = *v9 == 0;,这部分是验证v9有内容,无需特别分析

  • if ( v11 >= 0x20000 ):

    当v11<0x20000时会进入正确逻辑,v10 = getenv("CONTENT_LENGTH"); v11 = atoi(v10);,atoi函数的功能是将字符串转换为整数,也就是传入的CONTENT_LENGTH转换成整数后需要小于0x20000

重新回到DANGER_ITEM中,分析如何来到产生栈溢出的这一行:

  • if ( strcmp(a2, "GetMultipleHNAPs") ):

    进入下一步需要strcmp(a2, "GetMultipleHNAPs") 返回1,也就是a2不等于"GetMultipleHNAPs"

  • if ( strcmp(a2, "PushDCHEvent") && strcmp(a2, "PushDCHEventList") && strcmp(a2, "Login") && strcmp(a2, "DoAction") ):

    a2不等于"PushDCHEvent"、"PushDCHEventList"、"Login"、"DoAction"

  • if ( !v13 || !*v13 || !DANGER_IVAR || !*DANGER_IVAR ):

    DANGER_IVAR = getenv("HNAP_AUTH"); v13 = getenv("COOKIE");,也就是需要COOKIE和HNAP_AUTH都存在,才可以进行到下一步。同时由于snprintf(v28, 0x80u, "%s", v15); v16 = strstr(v28, "uid=");,需要在COOKIE中加上"uid="

这时我们已经大致判断出危险函数被调用并产生栈溢出的条件:

  • REQUEST_METHOD="POST"
  • SOAP_ACTION="1"
  • atoi(CONTENT_LENGTH)<0x20000
  • COOKIE="uid="
  • HNAP_AUTH="超出256字节的字符串"

动态调试

初步调试

为了验证程序执行是否和刚刚的分析结果相符,这里就需要进行动态调试

cgi程序也就是一个编译好的c语言文件,可以开启cgi服务后在网页上执行,也能直接执行。如果通过网页执行的方式,实现栈溢出就需要构造一个http请求,比较麻烦,而这里可以通过export设置好环境变量,然后直接执行程序,通过设置好的环境变量传入参数,同样能达到调试的目的

mips架构的程序无法在x86下直接运行,因此首先通过qemu进行模拟。来到固件根目录,输入下面的命令将qemu复制到固件根目录,为了让程序的依赖更少出错,将根目录设置成解包后的文件目录。这样就能实现对原本设备的完整模拟

1
2
cp /usr/bin/qemu-mips-static ./ 
sudo chroot . ./bin/sh 

用qemu对目标程序进行模拟,发现没有输出直接退出,说明成功运行

1
./qemu-mips-static /web/cgi-bin/hnap/hnap_service

尝试修改第一个参数,可以看到输出HNAP REQUEST后退出

1
export REQUEST_METHOD="POST"

修改全部所需参数,发现程序停滞了,随意输入一个字符后出现段错误

1
2
3
export SOAP_ACTION="1"
export CONTENT_LENGTH="1"
export COOKIE="uid="

这时需要启动一个gdb调试端口进行排查。在程序中有这一句fread(v25, 1u, v11, stdin);意思是程序从stdin中读取CONTENT_LENGTH个字节。创建stdin.txt,其中写入任意字符作为stdin,输入下面的命令后终端暂停,等待ida连接

1
./qemu-mips-static -g 1234 /web/cgi-bin/hnap/hnap_service < /stdin.txt

新开一个终端,通过ifconfig查看ip地址为192.168.78.134

打开ida,选择debugger开启gdb调试,然后选择process options,填入刚刚的ip和端口

按f9运行,确认弹窗后来到这个界面说明成功接入调试器

我先前在程序中打过断点,进入后按一次f9,程序执行后停在第一个断点处,然后按f5就可以查看伪代码,暗紫色就是程序停住的地方,分析完一句便按f9继续

错误排查

程序运行到v12 = fopen("/tmp/.hnap_in.xml", "wb");时产生了异常。这一句是创建.hnap_in.xml,但前提条件是父目录存在,可是检查固件目录后发现根本没有tmp文件夹,这可能由于正常启动会自动创建这个目录,而这里直接开始调试,跳过了创建步骤。因此手动进行创建,然后继续调试

重新运行后成功进入了DANGER_ITEM,但依旧报错,定位到了这一句v6 = ixmlGetElementByTag(v5, "soap:Body");,意思是在v5对应的xml中查找soap:Body

向上追溯,发现这里的xml也同时是上一个报错中的.hnap_in.xml,因此重点分析目标函数被调用前的这一段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
v10 = getenv("CONTENT_LENGTH");
v11 = atoi(v10); //将CONTENT_LENGTH转换成整数
result = -1;
if ( v11 >= 0x20000 )
   return result;
fread(v25, 1u, v11, stdin); //从stdin中读取v11个字节到v25中
fprintf(stderr, "%s\n", v25);
v12 = fopen("/tmp/.hnap_in.xml", "wb"); //创建/tmp/.hnap_in.xml
fwrite(v25, 1u, v11, v12); //将v25中的内容写入刚刚创建的xml
fclose(v12);
memset(v25, 0, sizeof(v25));
v13 = DANGER_ITEM((int)"/tmp/.hnap_in.xml", (char *)v26, (int)v27); //目标函数接收这个文件路径

这样报错的原因便明了了:CONTENT_LENGTH的值为1,那么xml文件也只读入了一个字节,根本无法找到soap:Body,最终导致报错

解决方法是在stdin中写入正确的soap:Body,以及将CONTENT_LENGTH的值改成对应的字节数。我这里找了一个模板,又将CONTENT_LENGTH改成了127

1
2
3
4
5
<soap:Body>
  <m:GetPrice xmlns:m="http://www.w3schools.com/prices">
    <m:Item>Apples</m:Item>
  </m:GetPrice>
</soap:Body>

重新调试后可以看到成功来到产生溢出这一句

返回如下

验证

成功溢出

开启调试

1
./qemu-mips-static -g 1234 /web/cgi-bin/hnap/hnap_service < /stdin.txt

运行到DANGER_IVAR被赋值后,终端输出了HNAP_AUTH的值

最终来到strcpy(v28, DANGER_IVAR);,成功产生了栈溢出

错误修改

这里注意到产生溢出后继续执行,会产生报错,指向2B43D76C

在溢出这一句之后有这样的一句v19 = strtok(v28, " “);,也就是将字符串以空格分割,字符串中原本是空格的第一个位置会被替换成/x00,然后将前半部分的内容赋值v19。后面还有v20 = strtok(0, " “);,也就是接着上一句的位置再分割一次。为了防止报错,实现时应该在HNAP_AUTH里加上一个空格

因此最终确认产生溢出,并保证程序正常执行需要如下条件成立:

  • REQUEST_METHOD="POST"
  • SOAP_ACTION="1"
  • CONTENT_LENGTH="$(wc -c < /stdin.txt)"
  • COOKIE="uid="
  • HNAP_AUTH=超过256字节大小的字符串,其中包含一个空格
  • 固件根目录下存在/tmp目录
  • 固件根目录下有stdin.txt,里面写入构造的xml

漏洞利用

原理

常见寄存器:

pc:程序之所以会从上到下一句句执行,是因为x86架构中的ip寄存器,也是mips架构的pc寄存器中,存放了下一条指令在内存中的地址。也就是说是这个寄存器决定程序执行到了哪一步,只要能控制这个寄存器,就可以控制程序执行到的位置

sp:函数中的变量会按照顺序保存在栈上,sp寄存器就是一个指向当前栈顶的栈指针。栈从高地址向低地址增长,一般情况下两行地址差是4字节

a0、a1:an格式的寄存器是参数寄存器,用于给函数传递参数

函数调用流程:

在调用前先向栈顶压入函数的下一行地址作为返回地址,然后压入fp以及一些预留空间,随后更新sp寄存器

调用后,来到变量定义部分,程序会将变量预留在栈中,然后sp向低地址移动变量的大小,比如说int a就是移动4字节。变量的值在初始化前是混乱的,初始化后才是确定的值

将变量的值存储在栈上,也就是存储在上一步预留的空间。字符串或者数组的存储和栈增长的方向相反,是从低地址向高地址存储

函数执行到最后会进行return,也就是取出当前函数栈帧第一行保存的地址,赋值给ip寄存器,同时会进行恢复sp寄存器到上一个函数栈帧顶,以及恢复上一个函数的寄存器等操作,然后继续执行

栈溢出原理:

假如向原本预留了40字节的变量中存入100字节的值,那么在预留内存满后,多出来的部分会继续向高地址存储,最终覆盖最初存储的返回地址。继续往下执行到return后,它会取到错误的地址并赋值给ip寄存器,这个地址可能指向错误的位置,更可能在内存中不存在,程序就会直接崩溃

栈溢出的利用:

如果在溢出的输入中夹杂一个地址,让这个地址正好覆盖在返回地址的位置,而地址指向的是某个后门函数,那么函数返回后,程序执行流被劫持到后门函数,就会导致非法执行

构造可回显rce栈溢出

之前将HNAP_AUTH的值设定为多个a,那么溢出后的返回地址也会变成4个a的十六进制,这样便无法找到HNAP_AUTH中具体第几个字符覆盖了返回地址。因此这里需要采用cyclic字符串,也就是一种每四个字节都独一无二的字符串。然后通过获取返回地址的十六进制,在cyclic字符串中找到对应的四个字符,从而获取所需位置

在溢出这一句之后有这样的一句v19 = strtok(v28, " “);,也就是将字符串以空格分割,字符串中原本是空格的第一个位置会被替换成/x00。后面还有v20 = strtok(0, " “);,也就是接着上一句的位置再分割一次。为了防止报错,实现时应该在HNAP_AUTH末尾加上两个空格

由于命令行无法处理部分特殊字符,这一步采用python来实现

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

os.chroot(".")
os.chdir("/")

soap_body = b'<soap:Body><m:GetPrice xmlns:m="http://www.w3schools.com/prices"><m:Item>Apples</m:Item></m:GetPrice></soap:Body>'

REQUEST_METHOD=b"POST"
SOAP_ACTION=b"1"
CONTENT_LENGTH=str(len(soap_body)).encode("ascii")
COOKIE=b"uid="

HNAP_AUTH=cyclic(2000)+b"  aaa"
print(f'HNAP_AUTH: {HNAP_AUTH}')

subprocess.run(
    ['/qemu-mips-static', '-g', '1234', '/web/cgi-bin/hnap/hnap_service'],
    input=soap_body,
    env={
        'REQUEST_METHOD': REQUEST_METHOD,
        'SOAP_ACTION': SOAP_ACTION,
        'CONTENT_LENGTH': CONTENT_LENGTH,
        'COOKIE': COOKIE,
        'HNAP_AUTH': HNAP_AUTH
    }
)

运行程序后进行调试,可以看到提示返回值是6A61616C,也就是jaal

在字符串中成功定位

Like 0
本站已不稳定运行 天 小时 分钟
共发表文章 33 篇 ,总计 163.14 k 字
本站总访问量: 次