内容简介:如果你查看最近关于浏览器安全性和JavaScript引擎安全性的所有文章,你很容易产生一种错觉,即在现代JavaScript实现中,只能在即时(just-in-time, JIT)编译器中寻找未被发现的漏洞。JavaScript引擎是一个专门处理JavaScript脚本的虚拟机,一般会附带在网页浏览器之中,该引擎的结构非常复杂、代码非常混乱(仅在3月份,仅V8就提交了然而,在研究web浏览器等常见的攻击目标时,许多研究人员的关注点很可能会习惯性的被公开披露的漏洞所主导。我想在发现漏洞这件事上,造成这样的原因
如果你查看最近关于浏览器安全性和JavaScript引擎安全性的所有文章,你很容易产生一种错觉,即在现代JavaScript实现中,只能在即时(just-in-time, JIT)编译器中寻找未被发现的漏洞。JavaScript引擎是一个专门处理JavaScript脚本的虚拟机,一般会附带在网页浏览器之中,该引擎的结构非常复杂、代码非常混乱(仅在3月份,仅V8就提交了 500次 有问题的代码),另外 公开披露的漏洞的数量 似乎不断地在增加,这些都表明现代JavaScript就是所有攻击的初始源头。
然而,在研究web浏览器等常见的攻击目标时,许多研究人员的关注点很可能会习惯性的被公开披露的漏洞所主导。我想在发现漏洞这件事上,造成这样的原因主要有两方面:
一方面,在开始寻找一个从未被发现的新漏洞时,这种参考已有成果的方法可以让研究人员很好地快速了解代码库中可能容易出错的区域。毫无疑问,有些研究范围(JavaScript引擎)比其他领域更复杂,技术要求更高,因此为了快速得出成果,许多人就愿意在既定成果中打转转,不做思考。
另一方面,人们常常忘记代码库的其他部分(目前不太受公众关注)也可能提供一些有趣的攻击面和漏洞,不应该被轻易忽略。
如果研究人员的目标是寻找生命周期已经超过几周甚至几个月的漏洞,那以上这种跟风的方法是没有什么问题的。不过在寻找新出现甚至还未在公开出版物中提及的漏洞时,以上方法就没有用了。
在这篇文章里,我们描述我们是如何成功地找到一个在v8引擎中的漏洞(CVE-2019-5790)?除此之外,我还会介绍一下如何专注于一个看起来不像是有攻击面的架构的。
JavaScript管道
下面给出了JavaScript管道中涉及的不同阶段的高级描述,以便对可能的攻击表面提供非常粗略的概述。如果你想了解更多,请 点此 。
AST Bytecode +-------------+ +--------+ +-------------+ +--------------+ | JavaScript |-->| Parser |--->| Interpreter |------->| JIT Compiler |----+ | source code | | | | (Ignition) | | (TurboFan) | | +-------------+ +--------+ +-------------+ +--------------+ | Assembly | | code | +---------+ | +-------------->| Runtime |<--------+ Bytecode +---------+
以下描述将重点介绍V8,但类似的概念也适用于其他引擎。
分析JavaScript引擎漏洞的第一步是解析JavaScript源代码,目的就是将源代码转换为抽象语法树(AST)表示。即使是这样一个看似简单的任务,例如从字符流中扫描已知令牌的文本,在V8等现代JavaScript引擎中也得到了高度的 速度优化 和 持续改进 。这正是第一阶段,这一阶段是我们能够确定漏洞的基础,下文将加以说明。
构建AST之后,它被转换为自定义字节码,然后由解释器或JIT编译器使用。V8使用 Ignition 作为它的解释器,2016年谷歌V8 JavaScript引擎引入新解释器Ignition,旨在减少内存消耗。字节码要么由寄存器设备直接执行,要么传递给JIT编译器。在JavaScript管道这个阶段,我们已经有了进行了一些优化,因此潜在的漏洞也可能会出现。
当一个函数在interpeter中执行了一定次数后,它被令牌为“hot”,并将由JIT编译器编译为设备代码。V8使用 TurboFan 作为它的JIT编译器,从整体架构上Ignition与TurboFan在一起配合工作有许多的好处。例如,不需要在手工编写高效的程序集处理Ignition生成的字节码,而是使用TurboFan中间件来处理,并让TurboFan为V8所支持的平台进行优化和生成最终的执行代码。目前,这个阶段是一个非常复杂的过程, 已经成为了大量漏洞的来源之处 。
与JavaScript管道同时使用的还有垃圾收集器,它允许 程序员 不必显式地管理内存。尽管这减少了大量的漏洞,比如内存泄漏,但是它也会导致 一些有趣的漏洞 。因为当需要分配的内存空间不再使用的时候,JVM将调用垃圾回收机制来回收内存空间。
JavaScript解 析
实现解析器的代码可以在src/parser/V8源代码树中找到:
+-----------+ +---------->| PreParser | | tokens +-----------+ | | | v +-------+ +--------+ +---------+ +--------+ | Blink |------>| Stream |------->| Scanner |------->| Parser | +-------+ +--------+ +---------+ +--------+ ASCII UTF-16 tokens | | AST v +----------+ +----------+ | TurboFan |<-----------| Ignition | +----------+ +----------+ bytecode
解析JavaScript源代码的第一步是扫描令牌的文本。Scanner类使用输入并生成由解析器使用的令牌对象。UTF16CharacterStream类用作文本输入流的抽象,为扫描器提供UTF-16格式的令牌,并抽象出从网络接收到的不同的JavaScript编码格式。然后,Parser类根据所使用的令牌生成最终的AST。
LiteralBuffer整数溢出(CVE-2019-5790)
以下漏洞是由研究人员DimitriFourny ( @DimitriFourny )发现的,并于2018年12月13日报告给谷歌的。它已在Chrome版本73.0.3683.75中修复,可以在这里找到相应的 漏洞 跟踪信息。
Scanner::Scan方法通过调用Scanner::ScanSingleToken来查找数据流中的下一个非空白令牌。根据遇到的令牌,它会实现一些特殊情况来适当地处理它们。例如,单字符令牌(如大括号、大括号或分号)只会返回,而其他令牌则会消耗数据流中的更多字符。
比如TOKEN::String令牌,它是为引用字符返回的。如果遇到此令牌,将调用Scanner::ScanString方法。该方法在循环中调用Scanner::AddLiteralChar,直到找到结束引号字符。
Scanner::AddLiteralChar方法调用Scanner::LiteralBuffer::AddChar,如果初始引号字符后跟两个字节的字符,它最后会调用Scanner::LiteralBuffer::AddTwoByteChar。
void Scanner::LiteralBuffer::AddTwoByteChar(uc32 code_unit) { DCHECK(!is_one_byte()); if (position_ >= backing_store_.length()) ExpandBuffer(); if (code_unit <= static_cast(unibrow::Utf16::kMaxNonSurrogateCharCode)) { *reinterpret_cast<uint16_t*>(&backing_store_[position_]) = code_unit; position_ += kUC16Size; } else { *reinterpret_cast<uint16_t*>(&backing_store_[position_]) = unibrow::Utf16::LeadSurrogate(code_unit); position_ += kUC16Size; if (position_ >= backing_store_.length()) ExpandBuffer(); *reinterpret_cast<uint16_t*>(&backing_store_[position_]) = unibrow::Utf16::TrailSurrogate(code_unit); position_ += kUC16Size; } }
backing_store_byte向量缓冲已经扫描的字符串部分,会根据需要动态调整大小。如果Scanner::LiteralBuffer::AddTwoByteChar方法检测到向量需要扩大,它会调用Scanner::LiteralBuffer::ExpandBuffer,它会分配一个更大的缓冲区,然后将旧缓冲区中的字节复制到新缓冲区中。
void Scanner::LiteralBuffer::ExpandBuffer() { Vector new_store = Vector::New(NewCapacity(kInitialCapacity)); MemCopy(new_store.start(), backing_store_.start(), position_); backing_store_.Dispose(); backing_store_ = new_store; }
Scanner::LiteralBuffer::NewCapacity方法用于计算新向量的大小:
int Scanner::LiteralBuffer::NewCapacity(int min_capacity) { int capacity = Max(min_capacity, backing_store_.length()); int new_capacity = Min(capacity * kGrowthFactory, capacity + kMaxGrowth); return new_capacity; }
我们可以通过改变初始引号字符后面的字符数来控制backing_store_.length()。巨大的JavaScript字符串会导致巨大的容量值,这会使表达式capacity * kGrowthFactory溢出,因此new_capacity将被设置为比前一个容量更小的值。因此,下一个MemCopy调用将向向量写入比先前分配的字节更多的字节,从而导致堆内存损坏。
以下简单的概念验证就可以触发漏洞:
let s = String.fromCharCode(0x4141).repeat(0x10000001) + "A"; s = "'"+s+"'"; eval(s);
这个漏洞看起来非常明显,阅读以上代码便可以轻而易举地发现。但是如果它们被模糊处理,则可能很难被发现,因为它需要大约20 GB的内存,而且在典型的台式机上触发它需要相当长的时间。
总结
人云亦云的做法存在于各个行业,当要探索的漏洞是一个被很多人研究过的目标时,即使环境比较复杂,你也可能会发现许多漏洞。尽管如此,像web浏览器这样的大型攻击目标,还是有很多未被发现的漏洞,要想找到这些未被发现的漏洞,就得找到新的攻击面。
以上所述就是小编给大家介绍的《如何在v8引擎中找到未被发现的攻击面(以CVE-2019-5790为例)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 如何找到APT攻击的“脉门”?
- 手机蹭WiFi被黑客攻击,终于找到原因了,专家告诉你如何解决?
- 利害攸关:区块链治理如何在Cryptopia的黑客攻击中找到出路
- 找到思聪王
- React发展历程中找到问题
- 如何找到适合自己的研发模式?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。