凌晨两点,溢出漏洞我盯着屏幕上的溢出漏洞十六进制转储,突然意识到问题出在哪儿了。溢出漏洞一个整数溢出,溢出漏洞就这么简单——但又不那么简单。溢出漏洞
那是溢出漏洞某个老牌路由器固件里的UPnP服务,处理端口映射请求时,溢出漏洞把用户提供的溢出漏洞端口号直接塞进了一个16位的short变量里。攻击者发一个65536,溢出漏洞short存不下,溢出漏洞溢出成0,溢出漏洞程序接着往下走,溢出漏洞把这个0当成内部管理端口的溢出漏洞映射请求处理了。就这么绕过了所有访问控制,溢出漏洞直接打开了防火墙的溢出漏洞一个口子。
这漏洞藏了七年。七年啊,够一个婴儿上完小学了。这七年里,这款路由器在全球卖出了上千万台,企业、银行、甚至一些政府机构都在用。没人发现那个小小的short变量在默默溢出,像水龙头没关紧,水滴一滴一滴地漏,没人注意。
我翻出厂商的补丁公告,他们修复的方式也很粗暴——把short改成了int。就这么一行代码的改动,结束了七年的隐患。可问题是,谁去检查那些还没被打补丁的设备?谁去检查那些已经被攻破的设备里,攻击者留下了什么?
溢出漏洞的本质,其实是一种信任。程序员信任数据不会越界,信任用户不会输入极端值,信任系统边界足够坚固。这种信任在大多数时候是合理的——毕竟没人会故意给年龄字段填负数。但互联网上,偏偏就有人专门找这种信任的缝隙,一点一点往里撬。
我见过最离谱的溢出案例,是某个智能门锁的固件。它的用户ID字段是32位的,但存储时被截断成8位。结果就是,只要构造一个特定的ID值,让它在截断后等于管理员的ID,就能直接开锁。攻击者不需要密码,不需要指纹,只需要知道这个数学把戏。
那门锁装在某个高档小区的每一户门上。厂商知道漏洞后,选择了静默推送固件更新,没发任何安全公告——怕影响品牌形象。可那些没联网的门锁呢?那些永远停留在漏洞版本的设备呢?它们就像城市里随处可见的、没上锁的消防通道,平时看着没事,真出了事,就是逃命的路被堵死了。
写代码的人常说,内存安全是个系统工程。可现实是,工程总有疏漏,而疏漏总会被有心人发现。这不是悲观,是事实。你看,从1988年的Morris蠕虫利用gets()溢出,到现在都快四十年了,C语言里那些不安全的函数还在被大量使用。我们发明了Rust,发明了Go,可存量代码的包袱太重,重到所有人都在继续用胶带修补旧管道。
那天晚上,我最终没提交那个漏洞报告。不是不想交,是发现厂商的漏洞反馈邮箱早就被垃圾邮件塞满了,根本没人看。我只好把分析过程整理成长文,发在了技术论坛上。三天后,有人回复说,他们公司用的正是那款路由器,已经中招了,内网被挖矿程序占满了CPU。
那个溢出漏洞,就像房间里的一根漏水的水管。你听到滴水声,但总想着“应该没事吧”,直到某天天花板塌下来,才发现水早就把承重结构泡烂了。技术上的修复从来不是最难的,难的是让人相信——那滴水声,真的意味着什么。