我是谁?

大家好,我叫 Jonathan,现在18岁,是一名打算机科学和数学专业的学生,对漏洞研究感兴趣,也是微软 MSRC-IL 的一名安全研究员。
我也打 CTF 比赛,所在团队是 Perfect Blue。

高中生一年从0到0day的秘密 休闲娱乐

我在去年才开始打仗安全研究,这次演讲的第一部分紧张说一下我在一年中都学了什么以及是如何学的;第二部分说一下我是如何从 ChakraCore 中找到第一个 0day 漏洞(JIT类型稠浊漏洞)的。
可能你刚刚打仗安全领域,只知道一些根本的编程知识,但可能你也能听懂。
虽然我会讲很多代码,但还好,不是太繁芜。
末了,我们看下演示。
好的,我们开始吧。

知识和实践准备

我为什么要研究漏洞?对我而言,漏洞就像是一个谜,非常具有寻衅性的东西。
我们必须找到开拓职员未考虑到的一些毛病。
至少对我而言这很具有寻衅性,也非常故意思。
我以为挖漏洞是个很棒的事情。

那么,什么是漏洞呢?关于漏洞的定义很多。
当你想理解某件事的时候,你就会去维基百科上搜索。
上面有很多不同的阐明,而且有一些看起来很古怪,比如有一个阐明是这么说的,“某资产无法抵御威胁攻击动作的可能性”。
我不懂它到底说的是什么。
我的理解是,漏洞便是程序中涌现的各种毛病,你可以用来改变程序的掌握流。
这便是我对漏洞的理解,但定义没有见告我如何找到一个漏洞。

那么,我们怎么才能找到漏洞呢?我当时开始找漏洞时有一些编程根本,我不是写程序最精良的开拓,也便是“还行”的水平,知道C措辞、汇编措辞,学了一些操作系统内部知识,理解操作系统是会如何运作的,能读一些 python 代码。
以是我虽然并不是最精良的开拓,但我具备一些干系知识。
比如,我从一本很不错的希伯来语书中理解了一些 C++ 措辞的知识,以便我真正地开展漏洞研究。

下一步,我理解了一些漏洞的根本知识,如一些根本的漏洞类型像范例的堆缓冲溢出问题、整数溢出等。
之后我就开始通过仿照练习开始实践自己所学的知识。
仿照练习是可以在线下办理的安全寻衅,这些寻衅问题包括找到漏洞并利用它。
最开始的时候我败得一塌糊涂,但随着韶光的推移,我认为失落败了也没紧要,由于我看理解决方案、write-up,我知道了如何写办理方案,如何办理问题。
以是最开始失落败没紧要,由于我们每个人都是这样过来的。
之后我开始参加 CTF 比赛。

CTF 比赛哀求团队作战,这便是我找到团队成员的办法,我们通过 CTF 相识,然后一起打比赛。
有时候我们输得很惨,有时候我们打得不错进了决赛,还一起漫游天下,由于有时候赛本家儿办方会支付差旅费,哈哈。
我们去了很多很酷的地方,真的很爽。

我认为 CTF 比赛是进入安全领域的一个很好的办法。

之后我决定“潜入深水区”。
一旦你理解了根本知识之后,不要在“浅水区”勾留太长的韶光,这一点很主要,我们要给自己一些寻衅。
刚开始我害怕失落败,但是随着韶光的流逝,我想明白了,失落败也没什么恐怖的。
失落败后我能找到办理问题的技能。
以是我认为这一点很主要:不症结怕失落败,纵然失落败了,我们也能从别人的办理方案中学到东西。

LiveOverflow 发的一条推文说得很好:学到根本后,尽快去做自己不懂的更难的事;不断地打仗自己不懂的事情,然后重新来看自以为懂但实际上不懂的事情;然后从各种资源理解信息,学习从多个角度和方向一步步办理问题。

我认为这样做很主要。
LiveOverFlow 也有 YouTube 频道,我也在看,讲的是漏洞和安全问题。
推举大家也可以关注他。

我具备一些知识后,通过 CTF 比赛、仿照演示等不断实践、实践、再实践。
不断实践、自己办理问题很主要,由于这样你才能知道办理问题的技巧,然后再自己办理碰着的问题。
一些漏洞是有特定模式的,要理解这些模式,你就须要多看几次。
创造这些模式的一种好方法便是不断办理问题。
这也是我为什么喜好办理真实漏洞问题的缘故原由,很多网站供应不少这样的路子,比如 Project Zero 的 0day 项目,你可以读取一些漏洞信息等。

我还创造CTF 和真实环境中的漏洞之间存在很大的关联,它们同时存在于 CTF 和真实天下中。
漏洞实际上便是由 bug引起的。
你在真正动手开始进行研究时碰着的最大问题便是代码库过于弘大(因此你可能会头大)。
但实际上纵然代码框架很弘大,但漏洞还是在详细某处。
以是,不要发愁看代码库,由于漏洞很可能就恰好在你开始查看详细的某些代码时涌现。
之后你可以开始考试测验办理问题,即开始理解真正的代码库。

经由一些实践后,我在想我们如何创造漏洞呢?当我们开始真正重复办理问题、实践的过程后,我创造漏洞研究便是关于找到 bug,而我们是通过读代码实现这个目标的。
因此,审计代码很主要,由于我们想从中找到漏洞,对吧?而这须要我们真正审计代码,须要实践。
因此实践在漏洞研究中起着非常主要的浸染。
因此我们搞砸了的时候可能便是更靠近找到漏洞的时候。
因此我认为实践很主要。

那我是如何创造漏洞的?我之前说过漏洞存在模式,而模式是通过韶光的积累创造的。
但我并没有研究很长的韶光,只有短短的一年,那么我是如何创造漏洞的?实际上便是通过不断实践创造的。
像我之前说过的那样,多实践能填补开始晚的问题。
我创造漏洞里面有一些模式,比如编程缺点像整数溢出或类型稠浊问题,这些问题实际上是存在的,由于人总是会犯错的。
人类会犯错,我们并不是完美的。
下面的代码便是一个很好的例子。

第3行有一个整数溢出漏洞。
很多开拓职员都理解这些漏洞,但仍旧可能会犯错。
就像我之前说过的,人总会犯错。
以是不要畏惧在繁杂的代码框架中去审计代码。
这种类型的漏洞实际上是真实的存在于代码框架中,而不仅仅局限于 CTF 比赛,以是,只要不畏惧找漏洞的难度,你就能创造这类大略的漏洞。

CTF 和真实环境中的漏洞之间存在一个很大的不同之处。
在 CTF 比赛中,常日当你创造问题时,你就大概知道该怎么处理它、如何去利用它等。
比如,碰着溢出问题时,你须要去覆写变量或返回地址等。
以是当你在 CTF 比赛中碰着漏洞时,大多数时候你知道怎么处理它,而在真实环境中,你有的只是像 “state(状态)”这样的词,以及一些原语 (premitives),而原语是攻击者具备的一些能力,因此我们须要链接 (chain) 这些原语,做一些影响更大的事情,从而触发更大的漏洞。
我在 Chakra 中找到的便是这种漏洞。
这些便是我在开始查看 Chakra 之前所理解到的漏洞研究和安全研究知识。

Chakra 0day 干系背景知识

我们说说 JavaScript。
先说 JavaScript 引擎。
有人会说你之前没说你学过 JavaScript 啊。
我确实没说,由于我真的没学过。
JS 是一种非常可靠的措辞,当你学会一些编程措辞后,学习 JavaScript 的过程可能会更顺畅一些。
因此,做到这一点该当不会太难。

JavaScript 引擎卖力运行开拓编写的 JS 代码。
它由很多部分组成,对我们而言最主要的是 JIT 编译器,它的浸染是当有些函数变得很热门被调用很多次时,它会把这个函数编译成机器代码来改进它的性能。
JIT 编译器还卖力优化代码。
它具有很多针对代码的假设,它不肯望这些假设崩溃。
我们随后会讲讲 JIT 编译器中涌现的干系漏洞问题。

来理解下 JavaScript 的根本知识,它是一种动态输入措辞,可读性尚可。
JS 工具具有“原型”,用于从其它工具中继续各种功能,它在漏洞创造过程中很主要。
它可通过 _proto_ 属性变动工具的原型进行修正。
Proxy 是可用于重新定义根本操作的工具。
我们可以通过这些根本操作,将调用限定在 getter 和 setter 等函数中。

来看下 ChakraCore。
JavaScript 具有数组,而 ChakraCore 具有类型数组。
我们来看看第一种类型 JavascriptNativeIntArray。
它用于存储整数,每个元素具有四个字节。
(举例: varint_arr=[1])其余一种类型是 JavascriptNativeFloatArray,它用于存储浮点数,和C措辞不同,它的每个元素具有8个字节(举例 varfloat_arr=[13.37])。
JavascriptArray 用于存储工具(紧张是指针),每个元素具有8个字节(举例: varobject_arr=[{}])。

我们来看下如何转换这些类型。

个中末了一种 (也便是 aray2._proto_=array1 中的 array1 直接转换为 JavascriptArray) 转换在 JavaScript 引擎很少见,但对付我们本日讲的主题很主要。
当我们有两个数组,并将个中一个设置为其余一个的原型,那么充当原型的这个数组就会被直接转换为 JavascriptArray。
这一点我们稍后再着重讲。

我们再来看看这些数组在内存中是什么样的。
举个例子:

我们来看看实际在内存中,当调试如下样本代码时,可以看到我们刚才提到的字段状态 (vararr=[0xaaaaaa,0x31377];)。

红框部分是 JavascriptArray 属性,我们能看到数组的初始值,也便是 ArrayFlags 的值。
绿框部分是实际的片段属性,它有长度、大小。
蓝框是片段的内存布局(包括元素,下图的地址是 pArr->head)。
图中右下角我们定义了两个片段。
那么什么是 ArrayFlags ?它们是提示数组的某些东西的一些 flag。
在这个案例中,它被定义为一个列举类型。
我们感兴趣的字段是 JavascriptArray 的 arrayFlags 字段 HasNoMissingValues(如下图)。

在我们的例子中,被我们定义为 ArrayFlags 的 InitialArrayValue 实际上由两个不同的 flag 组成:一个是 ObjectArrayFlagsTag flag,它和我们讲的内容不主要就不讲了;我们将重点看第二个 HasNoMissingValues flag,它解释数组并不存在缺失落的值,也便是说数组中不存在任何洞 (holes)。
那么,“洞”是什么意思?我们创建一个数组,元素之间有一些值。
在 ChakraCore 案例中,它有三个元素,但中间的一个元素是缺失落的。

放在这里的值,它们在内存中表示为这些常数。
我举这个例子是由于这样我们更随意马虎地能在内存布局中创造它们。

像这里(如上图)就存在一个“洞”,它并没有开启 HasNoMissingValues,也便是说数组中存在洞,数组中确实存在缺失落的值。
这看似很合理,但当我们查看内存布局时,我们会创造一些奇怪之处。
我们来看下这些所谓的“缺失落的值”是如何在内存中表示的。

这(上图)是片段的内存转储,赤色部分是数组的元素。
我们看到 deadbeef deadbeef ,但在中间即“缺失落的值”(“洞”)的位置,我们看到了一些 Magic 常数。
0xfff80002fff80002是从哪里来的?这些常数代表的是“缺失落的值”或者说数组中的“洞”彷佛能说得通。
但如之前所述,我们已经知道有一些东西能代表“缺失落的值”,便是没有 HasNoMissingValues flag。
而现在我们彷佛创造了其余一种表示方法,便是数组的内容(上面提到的 Magic 常量)。

这很奇怪,也引发了很多问题:数组的 flag 和数组的内容不匹配怎么办? HasNoMissingValues 设为真,那么就意味着不存在任何“洞”;但是 数组中实际上存在一个“缺失落的值”。
其余,我们在某种程度上把“数据”和“元数据”混为一谈了,由于如果把常量当作掌握流,那么我们如果能够假造它的话就很故意思了。

事实证明,我们真的能够假造这个“缺失落的值”。
这是由 @s0rryMybad 和 @lokihardt 创造的漏洞(如上图),得到了CVE 编号 CVE-2018-8505。
他们便是把我们之前看到的常量转换为浮点数数组,从而假造了“缺失落的值”,进而创造了漏洞。
缓解这个漏洞的方法有很多,可以通过不断变动这个Magic值常量或增加更多的检讨加固安全性。

我们上面讲的是如何能将这些奇怪的状态转变为我们实际上能利用的漏洞。
首先我们先来看看一些故意思的东西。
之前@s0rryMybad 和 @lokihardt 创造的漏洞是原生的浮点数数组。
显然,JavascriptArray 并不直接将浮点数组存储为真正的浮点数,而是这些值被 “boxed”,然后以常量进行 XORed (下图)。

那么问题就变成,我们能否在 JavascriptArray 中利用同样的“缺失落的值”技巧?如果能的话,那么常数会改变吗?由于我们从上面的例子看到,我们变动了值的代表办法,我们才能表示新的值。
从理论上来讲,引擎应该能改变常量,否则我们就可能表示它。

而事实上,常量并没有改变,因此我们就能表示它。
首先对其进行 boxing,然后通过之前的常量值( 0xfff80002fff80002)对常量进行 XORed( xor(magic,FlatTag_Value))。
这样,我们得到的常量还是原始值,因此值便是原始值。
当你 XORed 三个元素,个中两个元素是相同的,这样做是不许可的,它会给你原始的值。
但如果我们让个中的一个的值是 0xFFFcull<<48,那么我们就能返回 Magic 值表示值。

而正是在这里,我找到了漏洞。
我们依赖的是 JavaScript 引擎的根本知识,而boxing 便是我们最先会学到的东西。
我们利用 boxing 的想法,利用这种该当不会被利用的状态找到了漏洞。

那么我们是如何把这种奇怪的状态转变为漏洞的?先来看看什么是 JIT 类型稠浊漏洞。
我们现在常见的 JIT 漏洞是类型稠浊。
JIT 类型稠浊实际上是两种类型的稠浊,是指因 JIT 做出了缺点的假设而发生的漏洞,最常见的是发生“Side-Effect”,也便是发生了一些 JIT 并未意识到的事情。
例如,JITed 函数调用函数 foo(),变动了某些工具比如是数组的类型,而 JITed 函数并不知道转换已发生,利用了数组之前的类型,从而导致 JITed 代码中涌现类型稠浊情形,从而可能导致 RCE 漏洞的发生。
举个例子:(如下图)

我如何创造 Chakra 0day?

Loki 和 S0rryMybad 创造 Array.prototype.concat 具有一个故意思的代码路径,它同时考虑了 HasNoMissingValues 和数组元素的值,而两人成功让 HasNoMissingValues 和数组值不匹配。
他们成功在数组中假造一个“缺失落的值(以下称为 buggy)”后,以下代码就会触发一个故意思的流:

之后,我们看到如下 if 语句:

首先我们来看函数 ConcatArgs。
这里的 aItem 便是假造的数组,也便是 buggy。
我们想让 isFillFromPrototypes 返回假值,如果 HasNoMissingVlues 已设置如下。

isFillFromPrototyps 检讨数组只有一个片段,也便是通过检讨下一个头部片段是否为空,没有“缺失落的值”。
它确保长度匹配,也便是数组的长度和片段的长度相等。
因此这个片段便是数组中的唯一一个片段。
这是它做的第一个检讨。
它做的第二个检讨是 flag 将 HasNoMissingValues 设为真,这一点可被轻松绕过。
这样我们就能让 isFillFromPrototypes() 返回假值,然后进入 if 语句。

通过 isFillFromPrototypes() 检讨后,我们看到如下的 else 语句,由于我们的数组并非原生数组。

如下图, srcArray 便是我们创建的虚假的“缺失落的值”数组(也便是我们说的 buggy)。
首先让数组本身进行迭代,当且只当没有找到所有的元素时,才在数组的原型上进行迭代。
Enumerator 会列举数组中的所有的元素。

我们看下它是如何实现的。

通过 ArrayElementEnumerator 迭代源数组,如果值是“缺失落的值( ==0xfff80002fff80002)”,则会跳过该元素。
这里发生了什么呢?便是我们每次进入 while 循环时,如果创造是“缺失落的值”,则会跳过它。
迭代器会跳过缺失落的值,以是它的计数和数组中的元素数目不一致。

还有一个函数也很故意思。

它做的第一件事便是在原型 (prototype) 链之间循环。
我们之前说过,原型可以是继续功能的工具。
那么我们可以自己创建一个原型、其余一个原型,从而假造一个原型链。
这个函数首先进行循环原型链,然后调用带有 prototype 参数的这个具有很长名字的函数。
这个原型是一个 JavaScriptArray,然后我们对其进行循环。

以是, ForEachOwnMissingArrayIndexOfObject 为原型链中的每个原型调用了 EnsureNonNativeArray。

我们来快速回顾一下。
如果我们创建一个带有假造的 MissingValue 的数字,但设置了 HasNoMissingValue flag,那么我们就能得到来自 Array.prototype.concat() 的故意思的代码流。
它会循环假造数组的原型链,并担保这个链中的每个原型都是一个 Non-native 数组(也便是 JavascriptArray)。
记住,如果某些工具是其余一个工具的直接原型,那么这个原型就被转换为一个 JavascriptArray。
以是,从理论上来讲,如果我们的原型是一个原生数组,那么我们就能将其转换为 JavascriptArray,而 JIT 并不知道这一点,这和我们之前阐明过的“平常的” Side-Effect JIT 漏洞类似。

幸运的是,已经存在造成这一后果的技能了!
我们可以利用代理限定 GetPrototype() 调用,但如果我们编写函数的话,它会被检测为 side-effect。
Object.prototype.valueOf 不会产生 Side-Effect,这是 Lokihardt 利用的已知的技能。
我们来看个例子。

要利用这个漏洞,我们首先假造了一个 DataView 工具,从而能让我们任意读/写。
我们的利用代码基于 Pwn.js 库,这是一个很好的库,但我们须要修复一下才能利用。
我们通过一种已知技能泄露了一个栈地址,因而能够通过从 ThreadContext 读取一些数据而获取栈指针。
之后,我们 ROP 并规复我们的覆写,就能得到合法的进程连续。

我们本来打算在 Edge 浏览器上实际实行我们的代码,我们在沙箱环境下实行了代码,它不许可我们弹出打算器等东西。
我们无法进行演示。
于是我们编译了 Linux 版本的 Chakra,在 Linux 上进行了演示。
在 Linux 上利用该漏洞也是类似的。

(注:末了, Jonathan 成功地在 Linux 版本的 Chakra 上进行了演示。
接着掌声雷动。

希望我的演讲能给想进入安全领域的人带来一些帮助,同时也给只想听技能部分的不雅观众带来一些帮助。

感激大家。

大家怎么说

大一的学生挖掘到代价颇大(天府杯8万美金褒奖)的漏洞,我以为这学生很厉害,膜拜之。
从文章可以看出该同学根本踏实、功底深厚,对调试、内存布局、编译、C++、漏洞类型、代码审计等知识的节制具有一定的深度,以是能够从0到0 day且成功利用漏洞只须要1年的韶光。
Chakra 的漏洞挖掘难点挺大,该同学的研究思路、研究方法对付漏洞挖掘的学习具有很好的帮助。

看到Magic Value实在算是很熟习的,在5月份的时候最初看到lokihardt公开的第一个漏洞的时候,很惊异!
由于这个漏洞并不是传统的通过操作回调过程的办法造成的类型稠浊,而是通过一个分外的值。
第一韶光剖析后,很明显的便是代码和数据没有区分。

此时想到三个问题

1.Magic Value是什么?

2.这是一个新的类型稠浊漏洞的转化点。

3.为什么引擎中要定义一个Magic Value?

研究过patch之后,创造对StElem指令这块做了检讨。
也便是(作者提到的利用原生浮点数组)无法直接通过赋值的这种办法在Array中天生Magic Value。

Jacobi 的演讲中也是这样的思路,他举了一个例子解释Magic Value在内存中的样子。
初步认识了Magic Value,然后通过其他的办法去假造一个Magic Value绕过之前补丁的检讨,作者通过concat的办法进行了一种实现,创造了一个新的0day。

但我大胆预测concat是他多次考试测验成功的一个方法,这个问题可以抽象成如何天生一个新的包含Magic Value的数组,且不通过直接赋值的办法。
类似copywithin,concat,push…等等方法。
作者在布局PoC时提到比较多的技巧,比如Object.prototype.valueOf 不会产生 Side-Effect。
这都建立在他长期第一韶光对这个领域知识的积累上。

这个中的严谨踏实的求真、勤于思考的积累、和清晰完善的思路,都值得去学习。

比较人家的大一,我大一在玩泥巴。

他从验证地方动手找漏洞蛮有借鉴意义的,只要验证少了就可以有问题。

比较找到程序中的毛病,一步步打破限定将其转化为利用是更难的过程,关键是不轻易放弃。

文章中有一段话感触很深,学到根本后,尽快去做自己不懂的更难的事;不断地打仗自己不懂的事情,然后重新来看自以为懂但实际上不懂的事情。
从0到0day最主要的是鼓起勇气去寻衅自己以为的不可能。
梦想多晚都不算晚。

潜入深水区,分开舒适区,保持初入的学习劲头,通过新的知识,重新核阅与加强以往的知识,得到进步;不害怕发愁看代码框架,不断的实践,互换与学习;这种学习研究的思路,对付各个方向都是共通的,要学习实践这种精神,夯实根本,积极参与,努力提升。

去世磕到底。

实在很多学习的方法都大同小异,大家都是知道的,Jonathan在一年的韶光里能取得这样的成绩,我以为他的实行力更值得我们学习,确定目标之后就尽情投入,这很让人钦佩。

从0到0day最主要的是鼓起勇气去寻衅自己以为的不可能。
在有了一定得根本后,不能因循守旧,对更深层次的知识望而生畏。
要保持学习的激情亲切和动力,不断进步,勇往直前。

即便只有根本知识,也敢于去欢迎各种寻衅,须要莫大的勇气,直面失落败,长于利用失落败,从失落败中学到东西,这种心态和学习方法很值得我们初学者去学习借鉴。

安全涵盖的方面很广,在理解这个专业后,专注自己感兴趣的一方面往里钻。
这个学生便是想办法把一件事做到极致,再加上自己本身稳定的根本功底,直到自己佩服自己的地步。
对付Jonathan 从安全根本为0,到找到edge浏览器0day漏洞只利用了一年韶光,对付我来说,是一种勉励,向他学习。

欢迎在留言区分享你的意见~

原文链接

https://media.ccc.de/v/35c3-9657-from_zero_to_zero_day

本文作者:360代码卫士,转载请注明来自FreeBuf.COM