漫谈漏洞挖掘

聊聊漏洞分析、漏洞利用和漏洞挖掘。

前言

说到安全就不能不说漏洞,而说到漏洞就不可避免地会说到三座大山:

  • 漏洞分析
  • 漏洞利用
  • 漏洞挖掘

从笔者个人的感觉上来看,这三者尽管通常水乳交融、相互依赖,但难度是不尽相同的。本文就这三者分别谈谈自己的经验和想法。

漏洞分析

漏洞分析相对简单,通常公开的漏洞中就有一两句话描述了漏洞的成因,自己拉代码下来看也就能了解个大概。对于一些自己发现的bug,从崩溃日志中一般也能比较轻松地进行复现和调试。尽管有的bug排查起来相对繁琐,但总归是可以一步步减少范围锁定最终目标的。因此,网上对于漏洞分析的文章很多,一方面分析起来有迹可循,另一方面分析的漏洞也不一定是自己的“原创”漏洞,素材来源更加广泛。

漏洞分析虽然简单,却是每个安全研究人员的必经之路。就像练武中的扎马步、站梅花桩一样,是日积月累的基本功。之前研究内核时有段时间热衷于写漏洞分析的文章,后来随着日渐熟练,写文章记录的速度已经远远跟不上分析的进度,所以现在往往懒得动笔了。

基本功必不可少,但扎马步扎得再稳也不表示你能独步武林。有大佬曾经说过,如果他想的话,可以一天写好几篇分析文章还不带重样的。毕竟,漏洞分析的目的是为了学习、吸收、转化,以史为鉴,最终形成自己独到的理解。

漏洞利用

漏洞利用就相对复杂一点,尤其是对于二进制漏洞,成功的利用需要精巧的内存布局,因此需要对程序涉及到的数据结构要相当的了解。而且并不是所有漏洞都能转换为有效利用的,一般比较容易编写利用的漏洞,我们称之为品相好。对于品相不好的漏洞,我更喜欢将其称之为bug。当然也有人认为 bug 至少造成了程序崩溃,所以可以算一个DoS(拒绝服务)漏洞。

当然漏洞能否利用其实也是和人有关。对于复杂的系统,你认为无法利用的漏洞,大佬就能以一种你没想到的方式利用成功。比如安卓CVE-2019-2025(水滴)漏洞,属于Binder中的一个条件竞争,竞争窗口只有几条汇编指令。漏洞品相相当不好,连CVSS给出的可利用分数(Exploitability Score)也只有1.8分,但360的大佬们也通过玩弄调度器进行稳定利用提权了。

因此,关于漏洞利用的文章也就少了很多。一方面处于负责任披露安全问题的考虑,安全研究人员不会给出完整的利用细节,以免脚本小子滥用;另一方面对于公开的利用,你也总不能跟着写一篇文章灌水,毕竟利用思路很多时候是因人而异的,过于雷同就难免有炒冷饭的嫌疑,除非有一些独到的思考补充,或者有新的利用思路。

很多时候漏洞利用的文章看着看着就变成了漏洞利用分析的文章,这也说明了漏洞利用难度颇高,能独立写出原创利用并进行分享的人不多。就我的感觉而言,漏洞利用更像是另一种形式的软件开发,首先通过漏洞构造原语,然后通过原语实现最终的利用程序。

漏洞挖掘

漏洞挖掘可以说是安全研究人员向往的高地之一,不管你分析了多少漏洞,写了多少利用,如果你没有自己挖掘出过原创的漏洞,那你的安全研究生涯就是不完整的。但是漏洞挖掘这事儿并不是确定性的。漏洞分析只要有漏洞肯定能分析清楚,只不过是时间问题;漏洞利用只要不是明显的无法利用,那至少也存在利用成功的可能性。

漏洞挖掘则不然,即便你盯着某个应用使劲挖,也不能保证有结果,说不定对方根本就没有能触发的漏洞。都说世上没有绝对安全的系统,但是相对安全的系统一抓一大把,至少在出问题之前,你是不知道的。

我们能看到各种安全会议中介绍各种新发现的漏洞和问题,网上也有很多相关的文章,但更多是炫技式分享(show-off),很少有分享怎么挖洞的。因此笔者就先抛砖引玉,谈谈自己的想法。

自从 AFL 横空出世之后,当今漏洞挖掘言必称 Fuzzing,仿佛这是李云龙他娘的意大利炮,不管三七二十一先来上一发就能轰出几个 0day。当然,我不是说 Fuzz 不好,只不过凡事都有其诞生和得以应用的环境。

Fuzzing 即模糊测试,在早期是 QA 测试中的一项黑盒测试技术,通过随机变异的输入来测试程序的鲁棒性。在程序的崩溃被用于漏洞利用后,也就一跃成为一种漏洞挖掘方法。随机变异输入的效率相对低下,可能变异了半天连 main 函数里第一个 if 都没有跳进去。AFL 率先提出并实现了根据路径/覆盖率等反馈来进行输入的变异,从而大大提升了测试用例的有效性。

AFL

自从 2013 年 AFL 提出以来,各类 Fuzzer 百花齐放,在学术上有了爆发性的论文增长;开源社区有 honggfuzzlibFuzzer 等优秀的项目;在工程上有 OSS-Fuzz 利用庞大机器集群进行持续测试的应用和 syzkaller 这种年均发现几千内核漏洞的工具等。

从产出的漏洞数量来看,Fuzzing 作为一种漏洞挖掘方法可谓一骑绝尘。当时,随着时间的推移,通用模糊测试策略已经越来越难发现新的漏洞,要么你有独特的测试语料,要么你有领先的运算资源。因此,一部分人就从通用 Fuzzer 转回到了针对特定目标变异的 Fuzzer(即 Structure Aware Fuzzer)。也就是说,只针对目标程序接受的数据类型进行特定变异,比如针对 PDF 文件格式每个字段变异等等。

别忘了,AFL 这种通用 Fuzzer 的出现,就是为了实现一次编码,到处 Fuzz 的目的;而针对不同目标去写 Fuzzer,显然有违初衷。另外,如果目标接受的是已知格式的输入还好,对于未知格式,还需要自己去分析和理解各个字段含义。在阅读代码和理解代码逻辑的过程中,目标程序的潜在问题很可能已经出现在你眼前了,再去编写一个不能复用的 Fuzzer,显得有些多此一举。这种发现漏洞的方法也就是下节所说的——代码审计。

代码审计,俗称看代码。有的人用 SourceInsight 看,有的人用 VIM 看,但不管怎么样还是用眼睛看。既然大家都长了两只眼睛,为什么有的人就能一个月看出十几个高危,有的人就只看了个寂寞?

看代码也是有方法的。虽然我比较想听听 @oldfresher 是怎么看代码的,但他似乎不太愿意分享,所以我只能说说自己的方法。

要挖漏洞首先要对漏洞进行分类,大致可以分为下面三种:

  • 设计漏洞
  • 实现漏洞
  • 操作漏洞

所谓设计漏洞就是软件设计过程中就存在逻辑问题,比如 WiFi 的 WEP,设计的问题往往是比较严重的,而且修复周期长,所幸这类问题不是太多;实现漏洞就是我们常见的软件漏洞,内存溢出、UAF、条件竞争等都算在里面;操作漏洞和实现漏洞往往类似,只不过更多是指配置错误而产生的漏洞,比如 nginx 配置错误导致的目录穿越问题就属于一种操作漏洞。

明确漏洞类型后也不是马上开始看代码,而是先进行初步的攻击面分析,也就是常说的威胁建模。主要分为下面几步:

  • 信息收集: 收集所有相关的资料,尤其是设计文档或者帮助手册等
  • 应用架构建模: 列举应用所含的组件以及状态机,注意各个组件之间隐含的信任关系
  • 威胁鉴定: 在各个流程中列举潜在的攻击点,并分别标记危害等级
  • 审计计划: 按优先级对实现的审计计划进行排序

代码审计听起来是个让人退缩任务,因为你面对的是一堆你不熟悉的代码,而且要求你快速地建立起自己的理解而且找到其中最深层的秘密(安全漏洞)。通常你不会有足够的时间审计项目的每一行代码,因此这其中的一个重点就是懂得如何分配精力,在最可能出现安全问题的代码中达到满意的审计覆盖率。

成功的代码审计过程一定是务实、灵活且面向结果的。虽然讲究方法,但并不意味着根据别人的方法就能成功。与很多人的认知不同,代码审计其实是一件需要创造力的技能。那位说了,看别人代码要什么创造力?要在应用中找到漏洞,审计者需要将自己代入作者的思维中理解代码,同时还需要跳出作者的思维,洞察到原作者没有预料到的可能性;这种技能与知识相对,像是骑车、游泳、弹琴一样,是需要通过学习和练习去掌握的,而一旦掌握就像本能的一部分难以忘记。当然代码审计也有知识的部分,比如需要了解各种漏洞类型和场景等。

这些都这只是代码审计中的冰山一角,篇幅原因无法面面俱到。比如代码审计策略的抉择,是深度优先还是广度优先(看到某个函数调用是否需要追进去看);以及使用现代的静态分析工具来辅助审计,比如 Fority、CodeQL 等,后续有时间再单独进行介绍吧。

后记

漏洞分析是技术,漏洞利用是艺术,漏洞挖掘是法术。关于漏洞利用和漏洞挖掘哪个“技术含量”更高,还存在一定争议,但漏洞分析的基础地位毫无疑问。技术可以学,艺术可以练,法术呢?其实同样也可以通过练习提高自己的 漏洞挖掘水平。推荐一个忘了是什么地方的议题中提供的方法(也许是 CCC):

  1. 找到公开的漏洞通告,根据标题的内容自己去相关模块中审计看是否能发现该漏洞
  2. 如果发现不了,就回头接着看漏洞通告的细节,反思自己的审计方法
  3. 不断重复,直到让自己可以认为找漏洞只是时间问题而不是能力问题

任何行业都没有捷径,你看到的所谓魔法,很可能只是某人在某些事情上付出了你意想不到的时间。最后借用一句谚语作为结尾吧:

Ever tried. Ever failed. No matter.

Try again. Fail again. Fail better.

共勉。