本月都在埋头加班,没搞什么有趣的东西,就分享一篇 Cloudflare 捅的篓子吧 XD

Google 有个叫 Project Zero 的安全团队,成员 Tavis Ormandy 某天做一个语料库项目,突然发现 Google 缓存数据里出现了完整的 https 请求。排查发现这些请求都来自于 Cloudflare。Cloudflare 是业界闻名的 CDN,通过 DNS 解释的原理工作,不需要修改页面的资源地址,而是将代理资源的域名,解释到靠近访客的 Cloudflare 服务器上,以此达到加速效果。Tavis 怀疑这次事故跟 Cloudflare 的 ScrapeShield 产品有关。ScrapeShield 是一个页面改写的产品,用来在网页里混入不可见的随机字符串,以跟踪爬虫的来源;对爬虫改写正文里的 email,以防被批量爬 Email 地址发垃圾邮件。(参见 Tavis Ormandy 的 报告

Cloudflare 接到报告后,用了 47 分钟关掉三个特性,7 个小时后修复了漏洞。( 参见 Cloudflare 的报告 )报告非常长,我这里简单说一下。Cloudflare 使用了 Ragel 来处理 html 改写的任务,Ragel 通过一般的正则表达式描述有限状态机,并将其编译成 C 代码执行。因业务发展,Cloudflare 决定用新的 cf-html 来取代 Ragel。Ragel 和 cf-html 最终都是生成 nginx 的 C 代码模块,编译到 nginx 里面去。基于 Ragel 的分词器就有这个 bug,换 cf-html 潜在改变了 buffer 的用法,引发了泄漏。罪魁祸首是以下这段代码:

/* generated code */
if ( ++p == pe )
    goto _test_eof;

因为在某些情形下,p 有可能越过 pe,导致了内存越界读取。生成这段 C 代码的 Ragel 代码如下:

script_consume_attr := ((unquoted_attr_char)* :>> (space|'/'|'>'))
>{ ddctx("script consume_attr"); }
@{ fhold; fgoto script_tag_parse; }
$lerr{ dd("script consume_attr failed");
       fgoto script_consume_attr; };

有意思的是,Cloudflare 在正文里提示用 ++p >= pe 就可以避免这个错误,但强调这个不是 Ragel 的问题(即使这句代码是 Ragel 生成的)。Ragel 的作者则在 评论吐槽说 ,Cloudflare 的报告毁掉了他的生意。而 Google 的大牛则花了整个周末帮忙清理 Google 的 cache,验证 Cloudflare 的漏洞是否已堵上,顺便发现了一款流行密码管理器明文传输密码(1password 已否认,剩下的 Lastpass、Enpass 不知道会不会表态呢?)随后,Ragel 的作者写了 一篇文章 ,解释了 Ragel 的正确用法。

Ragel 是一门描述状态机的语言,fgoto 语句用于跳转到状态机中的其他子状态。Ragel 允许用户定义 Action,在各种情形下执行。Error Action 是正常处理流程无法继续时执行的 Action。它们一般在两种情形下执行:1. 错误发生在当前字符 2. 状态还没到达最终状态,而输入已经结束。如果错误发生在情形 1,是否 fhold 来回退指针无关紧要。但是情形 2 里,因为输入已经结束,这时候必须回退。现有的 Ragel 代码需要检查自己对情形 2 的处理。