ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

物联网安全实战:从固件提取到AFL Fuzzing的Vivotek栈溢出漏洞挖掘全链路

物联网安全实战:从固件提取到AFL Fuzzing的Vivotek栈溢出漏洞挖掘全链路 物联网安全这门课有个特别反直觉的地方真实设备上的栈溢出往往比CTF里的pwn题更简单真正难的不是“溢出”而是“怎么把那个藏漏洞的函数找出来”。我第一次拿Vivotek的固件练手时光是把环境跑起来就折腾了三天但一旦把从固件提取到Fuzzing再到崩溃分析这条路走通一遍后面再看其他设备基本就是举一反三的事。这篇文章就是那条完整路径的记录适合有基础pwn知识但没碰过真实固件的人也适合想系统入门物联网安全的新手。我会用一个典型的Vivotek网络摄像头固件作为全程载体从选靶开始到固件提取、QEMU仿真、静态定位危险函数、AFL构造Harness最后到栈溢出崩溃的确认与验证整条链路都讲清楚并且把那些教程里不会写的坑也一并标出来。1. 选靶逻辑Vivotek固件形态与攻击面的打开方式1.1 为什么第一台设备选Vivotek而不是随便找台路由器很多人入门物联网安全第一步就卡死在“不知道拆哪个设备”。路由器、智能音箱、摄像头、门锁、PLC一大堆挑花了眼。我的建议始终是从网络摄像头开始尤其是像Vivotek这种出货量很大、生命周期很长的厂家。理由有三层。第一摄像头不像路由器那样把大量业务逻辑塞进内核和驱动它的主要逻辑集中在Web管理后台、RTSP流服务和少量后台守护进程里攻击面边界非常清晰对新手很友好。第二Vivotek的老固件在官网基本都能找到公开下载而且多数固件包没有厂商魔改的加密头用Binwalk就能直接解开省掉逆向解密那一大段痛苦。第三摄像头固件的更新频率远低于手机和路由器大量旧版本长期在线厂商有时只维护最新两三个产品分支历史漏洞留存率高。这三个条件叠在一起Vivotek几乎就是“从零开始做真实设备漏洞研究”的理想教学样本。1.2 固件整体形态画像老内核、弱缓解、纯C代码一次典型的Vivotek固件解包后你会看到一个很标准的嵌入式Linux系统uImage格式的Kernel、SquashFS或CramFS的根文件系统外加一个存放配置和运行参数的可写分区。处理器通常为32位ARM或MIPS架构内存从32MB到256MB不等。老固件的Kernel版本常年停留在2.6.x到3.x之间这意味着安全缓解机制非常有限。这里先说一个判断漏洞类型的核心经验栈溢出在物联网设备里高发原因是双重的。应用层开发大量使用C语言开发者习惯用strcpy、sprintf、strcat这类不带长度限制的函数处理字符串同时老牌嵌入式产品的编译链默认不开栈Canary即使开了也可能只在部分进程上启用。再加上ASLR在老嵌入式系统里经常因为缺少足够的熵源而形同虚设“栈溢出可被稳定利用”就成了一个非常现实的风险。我自己分析过的固件里很多二进制在checksec扫描下一片惨白检查项常见状态说明Stack Canary未启用或部分启用直接导致栈溢出可稳定控制PCNX多半启用数据段不可执行实际利用要ROPPIE通常未启用代码段基址固定Hijack更容易ASLR效果有限老内核随机源不充分这张“惨白成绩单”恰恰说明一个道理真实物联网设备的漏洞挖掘很多情况下不需要多么高深的利用技巧能发现新漏洞本身就具有价值。1.3 攻击面对比Web服务为什么是栈溢出的重灾区把固件分析到一定程度后你可以把设备的所有网络服务列一张表逐个评分。Vivotek摄像头常见攻击面大致如下服务进程默认端口协议栈溢出风险httpd80/443HTTP/HTTPS高RTSP server554RTSP/RTP中高UPnP组件1900SSDP/HTTP over UDP中云平台通信模块自定义TCP长连接私有协议中Telnet/SSH23/22远程管理低Web服务之所以成为重灾区是因为它承担了几乎所有用户输入入口URL路径、查询字符串、POST表单、HTTP Header、Cookie、JSON/XML配置。而嵌入式开发里的常见习惯是把这些输入先放进固定大小的栈缓冲区再做进一步解析。路径拼接、URL解码、Base64解码、JSON转义每一步都可能出现长度失控这就是栈溢出能活这么多年的土壤。后面所有工作本质都是在和这类模式打交道。2. 环境搭建从固件提取到QEMU仿真运行2.1 一台Ubuntu虚拟机足够不需要立刻买设备刚开始做固件研究最不需要买的就是真机。理由是学习阶段的重点是“打通流程”而不是“在真实硬件上调试”。用QEMU把固件跑起来既能观察运行行为又能方便地打断点、看内存比真机调试的效率高很多。推荐一台Ubuntu 22.04或20.04虚拟机分配4GB以上内存磁盘60GB。宿主机Windows、macOS、Linux都无所谓虚拟机的好处是快照随便打环境弄坏了回滚就行。我自己的经验是准备两套环境一套用来常规分析装好常用工具另一套保持干净专门用来跑Fuzzing任务避免测试环境互相污染。需要提前装好的核心工具如下sudo apt update sudo apt install -y binwalk unblob qemu-user qemu-system-arm \ qemu-user-static gdb-multiarch libc6-armhf-cross libc6-mipsel-cross \ build-essential python3-pip curl wget git net-tools此外还需要Ghidra或IDA做静态逆向AFL做Fuzz。Ghidra是免费的对新手没有授权门槛IDA的Hex-Rays反编译在分析复杂函数时效率更高能装就用不能装Ghidra也完全够用。这里要专门推荐一下unblob这个工具。老牌的binwalk确实能解大多数固件但近几年很多固件开始使用自定义的压缩头或嵌套结构binwalk经常只识别出一个孤零零的文件偏移后面的提取直接失败。unblob对已知文件系统的识别更全尤其对SquashFS各种变体的支持比binwalk的默认签名好很多。实际使用中我会先用binwalk快速看一眼再用unblob做实际提取两条腿走路不容易卡壳。2.2 固件获取、哈希校验与一次成功的解包固件从哪里来官方渠道最安全、最合规这也是负责任研究的底线。到Vivotek官网的下载中心按产品型号搜索固件或者去厂商维护的FTP/网盘目录里找公开的历史版本即可。下载之后建议第一时间计算哈希值并保留记录。原因有两个一是后续写分析报告时需要描述你分析的精确版本二是固件在传输过程中可能损坏一旦发现漏洞而厂商问你“你分析的是哪个文件”哈希能立刻锁定目标。sha256sum FW_VIVOTEK_xxx.tar.bz2接下来解包。以典型的固件包为例binwalk -Me FW_VIVOTEK_xxx.tar.bz2 unblob FW_VIVOTEK_xxx.tar.bz2如果内核和文件系统都能被正确识别你会得到一个类似_xxx.extracted/squashfs-root/的目录。这个目录就是提取出的根文件系统里面通常包括bin/、usr/bin/、etc/、www/、lib/等子目录。到这里固件的“壳”已经破了下一步是把它运行起来。如果遇到binwalk打了未知签名的情况先别急着下载商业软件。可以尝试手动识别用binwalk -A查看文件尾部用xxd查看文件头再对照常见文件系统的魔数。SquashFS的魔数是hsqsCramFS是CramFSJFFS2则有0x85 0x19这样的特征值。多练几次手动识别其实并不难。2.3 用chroot QEMU把目标进程跑起来的完整姿势拿到根文件系统之后第一个想做的自然是把里面的httpd跑起来看效果。但要注意直接跑通常失败因为动态链接的库路径和真机不一样。最通用的办法是用chroot切换根目录再用QEMU做指令集翻译。以ARM 32位固件为例sudo cp /usr/bin/qemu-arm-static ./squashfs-root/usr/bin/ sudo mount --bind /proc ./squashfs-root/proc sudo mount --bind /sys ./squashfs-root/sys sudo mount --bind /dev ./squashfs-root/dev sudo chroot ./squashfs-root /usr/bin/qemu-arm-static /usr/sbin/httpd这里有个高频问题chroot后进程面面相觑/etc里服务配置、/var里运行时文件、/tmp里动态生成的目录缺失都会导致启动失败。这时候不要急着怀疑自己的操作用strace跟一下最直接sudo chroot ./squashfs-root /usr/bin/qemu-arm-static \ /usr/bin/strace -f -o /tmp/httpd.strace /usr/sbin/httpdstrace会清楚告诉你进程卡在哪个文件访问或哪个系统调用上。比如缺/var/run目录就手动mkdir缺配置文件就仿照/etc里其他配置模板补一份。这个过程是固件分析里最繁琐但也最能积累经验的部分多跑几次你对嵌入式Linux运行时依赖的理解会变得非常扎实。如果chroot方式跑不起整个系统也可以直接用Firmadyne或firmware-analysis-toolkit做全系统仿真。它的原理是把Kernel、RootFS、NVRAM模拟层和QEMU组合起来模拟出一台可联网的虚拟设备。全系统仿真更贴近真实运行环境对外部网络输入的观察也更直接。3. 静态定位为什么Fuzzing之前必须知道危险函数在哪3.1 字符串和交叉引用就是第一现场很多新手直接跳到Fuzzing结果就是对着一个黑盒子瞎跑跑了几个小时也找不到崩溃。我的做法恰好相反Fuzzing之前必须先把目标二进制在静态层面过一遍至少锁定三件事程序解析哪些输入、哪些函数处理这些输入、处理过程里调用了哪些危险函数。用Ghidra或IDA加载httpd之后第一件事不是反编译而是先搜字符串。搜索这些关键词GET、POST、HTTP/1.0、QUERY_STRINGparam、passwd、username、filenamestrcpy、sprintf是危险函数但它们一般在反编译视图里看调用点.cgi、.asp、.html这类扩展名能帮你定位CGI处理器每一个字符串的交叉引用XRef都会指向一个处理函数。从字符串函数点进去你就能看到完整的处理逻辑。3.2 识别高危模式固定栈缓冲区与用户输入的相遇静态分析的目标不是通读所有代码而是找到那些“用户可控数据固定大小栈缓冲区无长度限制拷贝”的组合。下面这段伪代码风格的结构在Vivotek老固件里非常典型void handle_request(int socket) { char filename[128]; char *url get_client_url(socket); // 直接将URL拼接到固定大小缓冲区 sprintf(filename, /www%s, url); // 后续可能拼接更多参数 char cmd[256]; sprintf(cmd, log_msg %s %s, filename, getenv(QUERY_STRING)); ... }url的长度完全由远端请求决定而filename只有128字节sprintf又不做边界检查这就构成了一个教科书级的栈溢出点。实际逆向中很多点都长这样问题只是你要花多久找到它们。我习惯在反编译代码里做三遍扫描第一遍列出所有strcpy、sprintf、strcat、sscanf的调用点第二遍对每个调用点判断“源数据”是否最终来自socket recv、getenv或某个外部输入接口第三遍对可疑调用点查看目标缓冲区是栈上数组还是堆上数组。栈上数组外部输入直接进入Fuzz候选名单。3.3 手工验证一个函数QEMU单步与strace双向确认静态分析看到的调用链终究只是代码上的逻辑手工验证一次能让你对目标函数的运行行为建立直观认识。这里推荐一种轻量做法先用QEMU用户模式把httpd跑起来再故意构造一个超长请求观察它在哪个地址崩溃。python3 -c print(A*5000) | sudo chroot ./squashfs-root \ /usr/bin/qemu-arm-static -strace /usr/sbin/httpd -p 8080如果目标服务打印出奇怪的段错误或者在strace里看到recvfrom后立即崩溃说明这个路径很可能真的存在溢出。到这一步静态分析的结论就有了动态证据支持Fuzzing的优先级也自然排出来了。4. Fuzzing设计给HTTP解析函数做一个可复现的Harness4.1 为什么不直接对网络端口Fuzz不少人的直觉是既然httpd跑在80端口那就用一个网络Fuzzer直接发随机数据过去。这个方法在真实项目里效率极低原因也很简单网络协议解析是典型的“长路径”过程。数据要经过内核协议栈、套接字缓冲、HTTP解析、URL解码、CGI分发才能到达目标函数。直接发随机包大量数据会在HTTP解析框架层就被拦截能到达目标栈溢出函数的比例很低而且每发一个包都要经过完整的网络栈和套接字协商执行速度比文件输入慢几个数量级。正确做法是把目标解析函数“剥”出来写一个最小Harness从文件读取输入再调用目标解析函数。这样AFL可以直接对输入文件做变异每秒钟能执行数千次甚至数万次测试覆盖率远超网络Fuzz。4.2 最小可复现Harness的结构这里用一个模拟HTTP URL解析函数的Harness做演示。生产环境中你可以把逆向出来的目标函数体直接复制到这个框架里或者通过链接方式调用目标库函数。#include stdio.h #include stdlib.h #include string.h // 模拟目标固件中的URL处理逻辑 // 实际项目中这段函数体来自逆向结果 int parse_url_input(const unsigned char *data, unsigned int len) { char path[64]; char query[128]; unsigned int i, idx 0; // 模拟将URL path复制到固定缓冲区 // data[0..len-1] 是AFL变异的输入 for (i 0; i len; i) { if (data[i] ?) { break; } path[idx] data[i]; // 无边界检查可能溢出 } path[idx] \0; idx 0; for (; i len; i) { if (data[i] #) { break; } query[idx] data[i]; // 同样可能溢出 } query[idx] \0; return 0; } int main(int argc, char **argv) { FILE *fp; long size; unsigned char *buf; if (argc 2) { return 1; } fp fopen(argv[1], rb); if (!fp) { return 1; } fseek(fp, 0, SEEK_END); size ftell(fp); fseek(fp, 0, SEEK_SET); buf (unsigned char *)malloc(size 1); if (!buf) { fclose(fp); return 1; } fread(buf, 1, size, fp); fclose(fp); buf[size] \0; parse_url_input(buf, size); free(buf); return 0; }这个Harness的核心设计是AFL每次把变异后的输入写成文件通过argv[1]传进来程序读取文件内容作为HTTP路径parse_url_input执行真实固件中的解析逻辑。这样AFL的每次执行都直接命中目标解析函数不会浪费在协议框架层。4.3 加入AFL和初始种子开始Fuzz编译这个Harness时不要用系统自带的gcc用AFL的编译器来获得插桩支持afl-clang-fast -g -o fuzz_parse_url fuzz_parse_url.c初始种子文件也很关键。不要从空文件开始那样AFL要花很长时间才能摸索出有效结构。可以先手动构造几个正常的HTTP请求片段放到seeds/目录GET /index.htm HTTP/1.0GET /cgi-bin/admin/setparam.cgi?system_usernameadmin HTTP/1.0POST /cgi-bin/operator/setup.cgi HTTP/1.0然后就可以正式启动AFL_SKIP_CPUFREQ1 afl-fuzz -i seeds -o findings -- ./fuzz_parse_url AFL运行界面会实时显示三组关键指标execs/sec是每秒执行次数paths是发现的独特执行路径数crashes是崩溃数量。如果paths持续上升说明变异一直在生成新的执行路径这是正常状态。如果paths长期不变说明种子质量不好或解析逻辑太复杂需要重新设计Harness。4.4 无源码二进制的现实选择QEMU模式上面演示的是有源码或已还原逻辑时的做法。但更多时候你面对的是一个提取出来的ARM二进制没有源码可编译。这时候AFL的-Q模式就派上用场。它通过QEMU的插桩机制对交叉架构的二进制做覆盖引导Fuzz典型命令如下afl-fuzz -Q -i seeds -o findings -- \ ./squashfs-root/usr/bin/qemu-arm-static -L ./squashfs-root \ /usr/sbin/httpd -p 8080 不过这里必须说一句实话-Q模式的稳定性和效率普遍不如源码插桩模式且对需要多线程、多进程、复杂socket的服务支持很差。如果二进制能通过qemu-arm直接执行可以先用-Q做一轮粗筛等Fuzz出崩溃之后再用静态分析还原函数逻辑换成源码Harness做精确验证。两种方法配合比只依赖任何一种都高效。5. 崩溃分析从0x41414141反推栈溢出的完整链路5.1 还原现场让崩溃文件在不同架构上回放AFL跑出crashes/目录后目录里每个文件都是一个导致崩溃的输入样本。先不要急着看原因第一步是把崩溃稳定复现。本地Harness跑出来的崩溃直接回放./fuzz_parse_url findings/crashes/id:000000,sig:06,src:000001,op:flip1,pos:120如果只出现段错误可以进一步挂在GDB下面gdb -q ./fuzz_parse_url (gdb) run findings/crashes/id:000000,sig:06,src:000001,op:flip1,pos:120如果你是跨架构分析比如QEMU用户模式下的ARM程序可以用gdb-multiarch配合QEMU的gdbstubsudo chroot ./squashfs-root /usr/bin/qemu-arm-static -g 1234 /usr/sbin/httpdgdb-multiarch -q (gdb) target remote :1234 (gdb) continue复现成功的标志是每次执行都在同一个地址、同一种状态下崩溃。如果两次崩溃位置不一致说明程序存在时序相关状态需要在Harness里把环境初始化逻辑补齐或引入确定性输入。5.2 计算偏移用pattern确定PC覆盖点崩溃后第一件事看寄存器和栈回溯(gdb) info registers pc 0x41414141 0x41414141 r7 0x41414141 0x41414141 r11 0x41414141 0x41414141 sp 0xbefff820 0xbefff820看到pc变成0x41414141这基本就是“栈返回地址被可控数据覆盖”的经典信号。接下来要回答一个关键问题输入数据的哪个字节偏移覆盖了返回值手工数太蠢用pattern是正确办法。用pwntools或GDB插件生成循环模式from pwn import * print(cyclic(256))用这个模式替换输入里对应的数据段重新崩溃后用GDB读出pc寄存器里的四个字节再用cyclic_find反查偏移from pwn import * print(cyclic_find(0x41414141))假设结果是132那么说明从输入的第132个字节开始数据直接覆盖了栈上的返回地址。对于32位ARM这个值会因函数序言的push {r11, lr}存在而稍微前移但原理完全相同。5.3 三种常见结局不要一上来就幻想远程getshell覆盖栈返回地址在利用层面至少有三个结局难度递增。第一种只造成DoS。覆盖pc的地址没有映射进程直接崩溃但没法用。对一个正常运行的设备来说这已经算安全缺陷因为远程攻击者可以反复让Web服务崩溃造成设备脱管。第二种稳定控制PC。如果进程没有Canary且偏移可控攻击者可以把pc指到已有的代码段通常是跳转到攻击者控制的寄存器指向的内存。这一步证明了漏洞的可利用性。第三种组合绕过缓解。实际的完整利用还需要考虑NX、ASLR甚至SELinux的限制通常要配合ROP链、内存信息泄露或其他漏洞。这一步工作量往往是漏洞发现的好几倍。从“发现漏洞”的角度看能做到第二种就已经证明了栈溢出漏洞的存在。在学习阶段重点是把从崩溃输入到偏移计算这条链路走通后面利用层面的难度是另一门课。6. 剩下的真实工作修复验证与披露边界6.1 为什么说“能控制PC”不等于“能利用”写到这里必须给所有刚入坑的读者泼一盆冷水能在调试器里控制pc和能在真实网络环境里稳定利用中间隔着一条巨大的鸿沟。真实设备上的情况比调试环境复杂得多系统开启了部分缓解机制、网络数据经过多次编码或加密、目标进程发生崩溃后可能被看门狗拉起、设备会有多个实例互相联动。很多人把“崩溃了”等同于“零日漏洞”这是典型的新手思维。正确的态度是把一个崩溃稳定复现、确认根因、评估影响面再决定是否继续深入利用。真实安全研究的目标从来不是“秀操作”而是准确判断风险高低给厂商和用户一个可信的评估结果。6.2 负责任的漏洞报告流程与公开边界当你确认了一个栈溢出漏洞接下来应该进入负责任的披露流程而不是立刻发到社交媒体或漏洞论坛。通常的做法是先到厂商官网找安全联系邮箱或漏洞报告入口把问题版本、哈希、崩溃样本、崩溃地址、影响评估写清楚。报告里要给出可复现的步骤但不要把完整的利用链一并公开。厂商确认并修复后再按照双方约定的时间窗口发布公开信息。国际上也常参考CERT/CC的漏洞披露协调框架整个过程以“负责任”为底线。我做研究时的经验是报告越专业、证据越完整厂商的响应速度通常越快。只丢一个“你们的固件有漏洞”而没有哈希、没有崩溃样本、没有根因分析对方往往不知道怎么处理甚至可能把报告当成无效垃圾。至少包含以下信息报告项内容示例受影响产品Vivotek xxx Network Camera受影响版本固件版本 x.x.x.x下载哈希漏洞类型栈缓冲区溢出CWE-121触发条件发送特定长度URL到Web管理端口影响远程拒绝服务可能代码执行复现步骤附加崩溃样本和GDB输出6.3 后续路线从Vivotek延伸到更广的嵌入式世界把Vivotek这条链路完整走通之后能力模型会发生一个质变你不再害怕拿到一个陌生固件而是会用同一套方法论去应对不同厂商、不同架构、不同协议的目标。顺着这条路往下走可以朝三个方向深入。第一个方向是继续横向扩大范围把同样的Fuzzing流程应用到RTSP服务、UPnP服务、云平台私有协议上每种协议对输入分割和解析方式的差异都会带来新的挑战。第二个方向是纵向加深利用技术理解ARM和MIPS下的ROP、研究绕过缓解措施的技巧、学习从内存崩溃逐步推导完整利用链。第三个方向是研究防御侧Fuzzing不仅能发现问题也是验证补丁有效性、评估防护机制强度的重要工具很多安全团队现在正是用大规模Fuzzing来持续验证产品安全基线。我自己的体会是物联网安全这门手艺最怕的不是基础薄弱而是不敢真实动手。破解Vivotek的栈溢出不是终点而是让你知道“固件分析其实没门槛”的起点。当你跑通一次固件提取、Fuzzing、崩溃分析的完整流程后再回去看那些所谓“高深”的物联网漏洞报告会发现它们全是这套基本功的组合。
返回列表