
1. 项目概述一个潜伏三十年的“定时炸弹”最近安全圈里有个事儿挺火的一个叫Libpng的开源图像库被曝出来一个高危漏洞而且这个漏洞的“潜伏期”长得吓人——足足有30年。这意味着什么意味着从1990年代中后期开始所有使用了这个库的软件都可能埋下了一个可以被远程代码执行的“定时炸弹”。Libpng是什么它是用来处理PNG便携式网络图形格式图像文件的标准库几乎无处不在。从你的浏览器、办公软件、图片查看器到服务器上的图像处理服务、嵌入式设备只要涉及到PNG图片的显示或处理十有八九背后就是Libpng在干活。所以这个漏洞的影响范围说“数百万系统”可能都保守了应该是数以亿计的设备。这个漏洞的编号是CVE-2024-38624CVSS评分高达9.8属于严重级别。攻击者只需要构造一张特殊的PNG图片当受害者的软件去解析这张图片时就可能触发漏洞导致攻击者能够在受害者的系统上执行任意代码。想象一下你只是点开了一个论坛里的“搞笑图片”或者收到了一个带图片附件的邮件甚至只是浏览了一个嵌入了恶意图片的网页你的电脑控制权就可能易主。这种攻击方式隐蔽性强成本极低是攻击者最青睐的“武器”之一。我之所以花时间深入研究这个漏洞不仅仅是因为它的严重性和广泛性更因为它是一个典型的“遗产代码”安全问题。一个存在了三十年的漏洞直到今天才被发现这背后反映出的开源软件供应链安全、代码审计的长期性以及我们对于基础组件的“信任危机”都值得每一个开发者和安全从业者深思。接下来我会带你彻底拆解这个漏洞的原理、复现过程、影响范围并给出详尽的修复和缓解方案。无论你是负责系统安全的工程师还是日常开发中会用到图像处理的程序员这篇文章都能帮你理解风险所在并采取行动。2. 漏洞核心原理深度剖析要理解这个漏洞我们得先回到PNG图片的格式和Libpng库的工作原理上。PNG文件并不是一堆像素数据的简单堆砌它是由一系列被称为“数据块”Chunks的结构化数据组成的。每个数据块都有特定的类型和功能比如IHDR块存放图像宽高、色彩深度等头信息IDAT块存放压缩后的图像数据IEND是结束块。2.1 PNG数据块与PLTE调色板块这次漏洞的核心围绕着一个不那么起眼但至关重要的数据块PLTE调色板块。对于索引彩色Indexed-ColorPNG图像它并不直接存储每个像素的RGB值而是存储一个颜色列表调色板图像数据IDAT里存放的则是这个调色板的索引值。PLTE块的结构很简单它包含一个连续的RGB三元组序列。每个三元组代表一种颜色分别是一个字节的红色R、绿色G、蓝色B分量。在Libpng中当解析到PLTE块时库会为这个调色板分配内存。这里就涉及到第一个关键点内存分配的大小。分配的大小取决于PLTE块中声明的颜色条目数。这个数量是从块数据长度计算出来的chunk_length / 3。因为每个颜色占3个字节。2.2 漏洞触发点整数溢出与缓冲区溢出漏洞的根源在于一个经典的整数溢出问题进而导致了堆缓冲区溢出。问题出在Libpng处理PLTE块和与之关联的tRNS透明度块的代码逻辑中。tRNS块用于为索引彩色图像提供透明度信息。它可以为调色板中的某些颜色指定一个Alpha透明度值。在Libpng的解析函数例如png_handle_PLTE和png_handle_tRNS中存在类似下面逻辑的代码此为概念性还原非实际源码// 假设 png_ptr-num_palette 是PLTE块中颜色的数量 int num_trans png_ptr-num_trans; // tRNS块中定义的透明度条目数 png_colorp palette png_ptr-palette; // 指向PLTE调色板的指针 // ... 一些处理 ... // 关键的危险操作将透明度数据复制到内部结构 if (png_ptr-num_trans 0) { // 这里可能缺乏严格的边界检查 memcpy(png_ptr-trans_alpha, trans_data, png_ptr-num_trans); }或者在分配内存时计算所需总大小的代码可能出现溢出// 概念性示例计算分配调色板和相关数据所需的总内存 size_t palette_size png_ptr-num_palette * sizeof(png_color); size_t trans_size num_trans; // 假设每个透明度占1字节 size_t total_size palette_size trans_size; // 如果 png_ptr-num_palette 非常大导致 palette_size 溢出 // 例如在32位系统上如果 num_palette 为 0x55555556 sizeof(png_color)3 // 那么 palette_size 0x55555556 * 3 0x100000002 截断为32位后变成 0x00000002 // 最终 total_size 会变得异常小导致后续分配的内存缓冲区远小于实际需要。 png_voidp buffer png_malloc(png_ptr, total_size); // 分配过小的内存 // ... 后续向buffer中拷贝palette和trans数据时就会发生堆溢出漏洞的精妙也是险恶之处在于攻击者可以通过精心构造PNG文件中的PLTE和tRNS块的长度字段使得num_palette或num_trans等变量被赋予一个极大的值。这个值在进行乘法运算计算内存大小或作为内存拷贝的长度参数时会发生整数溢出。整数溢出后计算出的内存大小会变得很小导致库只分配了一小块内存。然而后续的数据拷贝操作却依然按照原始的巨大数据量进行数据就像决堤的洪水一样淹没了分配缓冲区之后的内存区域这就是堆缓冲区溢出。注意现代Libpng代码经过多年加固直接这样简单的溢出可能已被修复。CVE-2024-38624更可能涉及一些边界条件、特定状态机下的路径或者对某些“不可能”情况的检查缺失。例如可能在处理某些“非索引彩色模式却包含PLTE/tRNS块”的畸形文件时状态变量不一致导致越界读写。但其核心威胁模型依然是通过畸形PNG数据触发边界错误覆盖关键内存数据。2.3 从溢出到代码执行单纯的缓冲区溢出可能造成程序崩溃拒绝服务。但攻击者的目标是代码执行Remote Code Execution, RCE。如何实现内存布局操控堆风水攻击者通过多次加载、释放PNG图像可以一定程度上影响Libpng及其应用程序的堆内存布局。目的是让Libpng为PLTE分配的内存缓冲区紧挨着一些重要的数据结构。关键数据覆盖溢出的数据会被精心构造。攻击者的目标不是用随机数据填满内存而是覆盖特定的对象或指针。例如函数指针覆盖存储在堆上的回调函数指针、虚函数表vtable指针等。当程序后续调用这个函数时就会跳转到攻击者控制的内存地址。返回地址虽然堆溢出通常不直接覆盖栈上的返回地址但如果溢出的缓冲区足够大或者程序存在某些特定的堆栈对象也有可能影响执行流。重要标志/长度字段覆盖相邻数据结构的长度字段可以制造出二次漏洞如越界读/写进一步扩大控制范围。利用内存保护机制现代操作系统有数据执行保护DEP/NX、地址空间布局随机化ASLR。攻击者需要结合信息泄露漏洞例如利用Libpng或其他库的另一个漏洞先泄露内存地址来绕过ASLR或者使用面向返回的编程ROP技术在现有代码片段gadgets中拼接出恶意逻辑以绕过DEP。虽然利用过程复杂但在漏洞细节公开后安全研究人员通常会快速开发出针对特定环境如特定Linux发行版、特定浏览器版本的稳定利用代码Exploit。对于攻击者而言一旦有了可用的Exploit攻击就变成了“流水线作业”。3. 漏洞影响范围与严重性评估这个漏洞的影响绝不仅仅是理论上的。我们可以从以下几个维度来评估它的冲击波3.1 受影响的软件生态Libpng是一个底层依赖库其影响是树状的、传递的。直接依赖任何直接链接了Libpng库的应用程序。这包括图像处理软件GIMP, ImageMagick, GraphicsMagick, IrfanView, XnView等。网页浏览器Chrome, Firefox, Safari, Edge及其使用的底层引擎如Blink/WebKit的历史版本或特定组件。现代浏览器可能使用自研或高度定制的解析器但许多衍生项目或嵌入式组件仍可能受影响。办公软件Microsoft Office, LibreOffice, Apache OpenOffice用于插入和显示PNG图片。操作系统组件Windows、Linux、macOS的图片预览功能、壁纸设置、图标渲染等系统模块。编程语言库Python的PIL/Pillow库、Java的ImageIO、C#的System.Drawing等其底层可能调用Libpng。游戏引擎与多媒体框架SDL, SFML, FFmpeg等。间接依赖这是更可怕的部分。一个软件可能不直接使用Libpng但它依赖的另一个库如ImageMagick使用了Libpng。这意味着即使你检查自己的代码没有调用Libpng只要你的供应链里有一环用了你就可能被波及。常见的Web应用框架、文档处理服务、云原生应用都可能中招。3.2 受影响的设备与系统服务器Web服务器处理用户上传的图片、邮件服务器解析邮件附件、文档转换服务、云存储服务。一旦被利用攻击者可能获得服务器权限窃取数据、植入后门、发起进一步攻击。桌面电脑与工作站通过恶意网站、钓鱼邮件、即时通讯文件传播。移动设备Android和iOS系统中大量应用处理PNG图片系统组件也可能受影响。物联网IoT与嵌入式设备路由器、摄像头、打印机、智能电视等设备中的固件如果使用了旧版Libpng几乎无法通过常规方式更新风险极高且持久。工业控制系统ICS与操作技术OT环境这些环境更新周期极长系统老旧一旦存在漏洞后果可能是灾难性的。3.3 漏洞的独特严重性超长潜伏期30年未被发现说明代码审计的深度和广度存在巨大挑战。许多“古董级”代码路径可能只在解析极其畸形或罕见的文件时才会触发常规测试和模糊测试难以覆盖。触发简单载荷隐蔽攻击载体就是一张图片。图片可以轻松通过所有内容过滤系统上传到网站、嵌入邮件、在社交平台分享。防火墙、IDS/IPS很难区分一张正常图片和恶意图片。供应链攻击的完美入口攻击者不需要直接攻击最终目标。他们可以攻击一个流行的开源软件包维护者提交带有“问题”的PNG样例文件或者将恶意图片放在流量巨大的图床/CDN上等待自动化的图片处理服务如缩略图生成去触发漏洞。这被称为“投毒攻击”防不胜防。4. 漏洞复现与环境搭建警告漏洞复现仅限于在你自己完全控制的、隔离的法律实验室环境如虚拟机、沙箱中进行。任何对他人系统的测试都是非法的。以下内容仅供安全研究与学习之用。要真正理解一个漏洞没有什么比自己动手复现一遍更深刻的了。下面我将搭建一个受控环境来模拟这个漏洞的触发。4.1 实验环境准备我们选择一个经典的靶标一个使用了易受攻击版本Libpng的简单图片查看器程序。操作系统使用一台Ubuntu 20.04 LTS的虚拟机。为什么选它因为它系统库版本适中且易于安装旧版软件包。安装易受攻击的Libpng我们需要一个包含CVE-2024-38624漏洞的旧版本。假设漏洞在Libpng 1.6.40中被修复具体版本需根据官方公告那么我们就安装1.6.39或更早的版本。# 首先尝试从源代码编译旧版本 wget https://download.sourceforge.net/libpng/libpng-1.6.39.tar.gz tar -xzf libpng-1.6.39.tar.gz cd libpng-1.6.39 ./configure --prefix/usr/local/libpng-1.6.39 make sudo make install编译一个测试程序编写一个最简单的C程序使用这个旧版Libpng来解析PNG文件。// test_png.c #include stdio.h #include stdlib.h #include png.h void read_png(const char* filename) { FILE *fp fopen(filename, rb); if (!fp) { perror(File open failed); return; } png_structp png_ptr png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (!png_ptr) { fclose(fp); return; } png_infop info_ptr png_create_info_struct(png_ptr); if (!info_ptr) { png_destroy_read_struct(png_ptr, NULL, NULL); fclose(fp); return; } if (setjmp(png_jmpbuf(png_ptr))) { png_destroy_read_struct(png_ptr, info_ptr, NULL); fclose(fp); printf(Error during PNG read\n); return; } png_init_io(png_ptr, fp); png_read_info(png_ptr, info_ptr); printf(Successfully read PNG info.\n); // 这里可以读取更多信息... png_destroy_read_struct(png_ptr, info_ptr, NULL); fclose(fp); } int main(int argc, char* argv[]) { if (argc ! 2) { printf(Usage: %s png_file\n, argv[0]); return 1; } read_png(argv[1]); return 0; }编译时链接我们安装的旧版库gcc -o test_png test_png.c -I/usr/local/libpng-1.6.39/include -L/usr/local/libpng-1.6.39/lib -lpng -lz -lm export LD_LIBRARY_PATH/usr/local/libpng-1.6.39/lib:$LD_LIBRARY_PATH4.2 构造恶意PNG样本这是复现的关键。我们不需要从零开始写一个PNG文件可以借助Python的struct等模块来修改一个正常PNG文件或者使用专业的模糊测试工具生成崩溃样本。方法一基于已知POC修改如果漏洞细节POC已公开我们可能会找到一个能导致程序崩溃的PNG文件。我们可以用十六进制编辑器如xxd,010 Editor或Python脚本分析这个文件重点查看其PLTE和tRNS块的结构。例如一个畸形的PLTE块可能长这样十六进制示意0000: 50 4c 54 45 PLTE签名 0004: ff ff ff ff 块长度一个巨大的值0xFFFFFFFF 0008: ... PLTE数据颜色列表长度本应是 块长度但这里可能很短或畸形 ... 74 52 4e 53 tRNS签名 ... ... tRNS数据其长度可能与巨大的PLTE长度计算产生交互错误方法二使用模糊测试生成我们可以使用像libfuzzer、afl这样的模糊测试工具针对我们编译的test_png程序进行测试工具会自动生成大量畸形输入试图触发崩溃。# 使用afl编译插桩版本的程序 afl-gcc -o test_png_afl test_png.c -I/usr/local/libpng-1.6.39/include -L/usr/local/libpng-1.6.39/lib -lpng -lz -lm # 准备一个种子PNG文件然后运行模糊测试 afl-fuzz -i input_seeds/ -o output_findings/ -- ./test_png_afl 模糊测试运行一段时间后通常能在output_findings/crashes/目录下找到导致程序崩溃如段错误SIGSEGV的PNG样本。这些样本很可能就触发了我们想要的漏洞。4.3 触发与分析崩溃运行测试用我们的test_png程序加载恶意PNG样本。./test_png malicious.png观察现象程序很可能直接段错误崩溃Segmentation fault。这是缓冲区溢出破坏内存的典型表现。使用调试器分析用GDB加载程序复现崩溃查看崩溃时的寄存器状态、堆栈回溯和内存内容。gdb ./test_png (gdb) run malicious.png (gdb) bt # 查看堆栈回溯崩溃点很可能在libpng内部的memcpy、malloc或相关函数中 (gdb) info registers # 查看寄存器关注指令指针RIP/EIP和栈指针RSP/ESP (gdb) x/20x $rsp # 查看栈内存内容可能能看到溢出数据通过分析我们可以确认崩溃是否发生在Libpng代码中以及是否是由于越界写造成的。例如如果backtrace指向png_handle_PLTE或png_handle_tRNS函数并且崩溃指令是mov到某个由malloc返回的地址之外那就基本坐实了堆溢出。实操心得在复现这类历史漏洞时最大的挑战往往是构建一个恰好包含漏洞的旧版本环境。开源项目的Git仓库是你的好朋友。你可以直接检出漏洞修复前的一个提交commit进行编译。这样能最精确地复现漏洞。命令类似git clone https://github.com/glennrp/libpng.git cd libpng git checkout vulnerable-commit-hash。5. 漏洞修复方案与缓解措施知道漏洞怎么来的更要知道怎么堵上。对于不同角色的读者应对策略不同。5.1 对于系统管理员与运维人员你的首要任务是盘点与升级。全面资产清查使用软件成分分析SCA工具或系统包管理器扫描你的服务器、容器镜像、应用程序列出所有使用Libpng的软件及其版本。命令示例# Linux下查找链接了libpng的进程和文件 lsof | grep libpng find / -name \*libpng*\ -type f 2/dev/null dpkg -l | grep libpng # Debian/Ubuntu rpm -qa | grep libpng # RHEL/CentOS/Fedora立即升级将Libpng库升级到已修复该漏洞的最新版本。请务必查阅Libpng官方安全公告如 SourceForge 或 GitHub 确认修复版本号例如1.6.41或更高。# 基于包管理器的升级 sudo apt update sudo apt install --only-upgrade libpng16-16 # Ubuntu sudo yum update libpng # RHEL/CentOS 7 sudo dnf update libpng # RHEL/CentOS 8/Fedora注意升级系统库后可能需要重启依赖它的服务如Web服务器、图形服务才能生效。容器与云原生环境更新所有基础镜像Base Image重建并重新部署所有相关的容器。检查Helm Charts、Kubernetes YAML文件中是否锁定了旧版本镜像。缓解措施如果无法立即升级网络层过滤在WAFWeb应用防火墙或反向代理如Nginx中可以尝试拦截包含异常大PLTE或tRNS块例如块长度超过一个合理阈值如1024个颜色条目的PNG文件上传或访问。但这属于临时缓解可能被绕过。沙箱与隔离确保处理用户上传图片的服务运行在权限最小化的沙箱环境中如单独的容器、虚拟机或使用Seccomp/AppArmor限制其系统调用。入侵检测在IDS/IPS规则中添加针对此漏洞利用模式的检测特征如果后续有公开的Exploit特征码。5.2 对于开发者你的责任是确保你的项目及其依赖链安全。更新项目依赖直接依赖如果你在C/C项目中直接链接Libpng更新你的构建脚本指向新版本库。间接依赖检查你的语言包管理器。对于Python项目确保pillow库更新到最新其底层会更新libpng。对于Java项目检查图像处理库如Apache Commons Imaging的版本。使用npm auditNode.js、cargo auditRust、safety checkPython等工具扫描依赖。构建时安全静态链接考虑将Libpng静态链接到你的应用程序中这样你可以严格控制其版本避免受系统库版本碎片化的影响。但要注意这会增大二进制文件体积并且你需要自己跟踪安全更新。编译器加固在编译你的应用程序和Libpng时启用所有安全编译选项如-fstack-protector-strong,-D_FORTIFY_SOURCE2,-fPIE -pie,-Wl,-z,now,-z,relro。这些选项不能防止漏洞发生但能极大增加漏洞利用的难度。安全编码与输入验证即使使用了最新库在你的应用层对用户上传的图片文件也应进行严格的验证检查文件头、魔术字、文件大小限制、图像尺寸限制。对于PNG可以简单解析其IHDR块确保宽高在合理范围内。使用沙箱机制处理不可信图片。例如在服务器端可以使用一个独立、资源受限的微服务或Lambda函数来执行图片处理任务即使该进程崩溃也不会影响主服务。5.3 对于个人用户你的行动相对简单但至关重要。更新操作系统和软件立即为你的电脑、手机安装所有可用的系统更新和安全补丁。操作系统厂商微软、苹果、各大Linux发行版会在获悉漏洞后第一时间为自家系统集成的Libpng发布更新。更新应用程序特别是浏览器Chrome、Firefox、办公软件、图片查看器等直接处理图片的软件确保它们更新到最新版本。现代浏览器通常有自动更新功能请保持开启。保持安全意识不要随意点击来源不明的图片链接尤其是邮件、即时通讯工具中收到的。对于必须打开的图片可以尝试先上传到谷歌相册、Imgur等大型在线图片服务它们的后端通常有强大的安全清洗能力。6. 从Libpng漏洞看开源软件供应链安全CVE-2024-38624不仅仅是一个技术漏洞它更像一面镜子映照出整个开源软件生态在安全上的系统性挑战。1. 遗产代码的“暗债”开源项目往往由志愿者维护许多核心代码写于数十年前。当时的开发者安全意识、编程规范与今天不可同日而语。这些“遗产代码”就像一座老房子的承重墙没人敢轻易动但里面可能早已布满蚁穴。审计这些代码耗时耗力且缺乏商业激励。2. 依赖关系的“黑洞”现代软件建立在层层叠叠的开源依赖之上。你的一个简单项目通过依赖树可能间接引入成百上千个开源包。你根本不知道也无力全面审计它们。Libpng这样的基础库就是这棵依赖树深埋地下的根须之一它一摇晃整片森林都可能颤抖。这就是所谓的“供应链攻击”攻击一个广泛使用的底层库就能打击无数上游应用。3. 漏洞发现的“随机性”这个漏洞30年未被发现说明传统代码审计和测试的局限性。近年来兴起的模糊测试Fuzzing是发现此类深藏漏洞的利器。Google的OSS-Fuzz项目就持续对Libpng等重要开源项目进行模糊测试发现了无数漏洞。企业和社区应加大对这类自动化安全测试的投入。4. 修复与部署的“长尾效应”即使Libpng官方立刻发布了修复补丁补丁传递到最终用户设备上可能还需要数月甚至数年时间。Linux发行版要打包、测试软件厂商要集成、发布新版本企业IT部门要评估、部署嵌入式设备厂商可能已停止支持最终用户可能从不更新软件。这个漫长的时间窗口就是攻击者的黄金机会。作为从业者我们可以做什么拥抱SBOM软件物料清单像管理实物供应链一样管理你的软件供应链。要求你的软件供应商提供SBOM清楚列出所有组件及其版本。这样在出现类似Libpng漏洞时你能快速定位受影响的产品。实施漏洞预警与快速响应订阅CVE公告如NVD、关注你所用关键项目的安全邮件列表。建立内部流程确保在收到高危漏洞通知后能快速评估影响、测试补丁并部署。贡献与支持如果你有能力为你所依赖的关键开源项目贡献代码尤其是安全修复和测试用例。如果没时间写代码考虑通过Open Collective、GitHub Sponsors等方式进行资金捐赠支持维护者的工作。设计安全架构假设底层组件总会出问题。在设计系统时采用最小权限原则、纵深防御、故障隔离如微服务、不可变基础设施等架构限制单个漏洞的影响范围。Libpng的这个30年老洞给我们敲响了一记沉重的警钟。它提醒我们在享受开源软件带来的巨大便利和创新的同时我们必须共同承担起维护其安全与可持续性的责任。安全不是某个团队或某个环节的事它是贯穿软件生命周期需要开发者、维护者、分发者和最终用户共同参与的持续过程。下一次当另一个“沉睡的巨人”醒来时希望我们已准备得更加充分。