2024 年の 1 月に開催されていた Insomni’hack CTF に 0nePadding で参加していました。
早々に撤退して Knight CTF に参加していたので 3 問しか解いていないのですが、最終順位 120 位でした。
全体的に難易度が高めだったのかもしれません。
復習を兼ねて Writeup を書いていきます。
frown(Rev)
How good is your Tetris? Connect, win, and reveal the flag!
この問題はコンテスト中は非想定解っぽい guess で解いてしまったので、Writeup では想定解でやっていきます。
この問題では問題バイナリは与えられず、問題サーバに SSH 接続可能な情報のみが与えられます。
与えられたサーバに SSH 接続するとテトリスゲームが起動します。
しばらく適当にプレイしてくいくとスコアがいくらか加算されたタイミングで、Frida INFO] Listening on 127.0.0.1 TCP port 27042
というメッセージが表示されます。
また、さらにスコアが加算されていくと、暗号化したような Flag 文字列がコンソールに表示されるようになります。
ここで、Flag を取得するために Frida Server への接続を試みます。
まず、Frida サーバが Listen しているとみられる 27042 ポートについては、当然外部からはアクセスすることができません。
Frida Server にポートフォワードしてアプリケーションのプロセスを特定する
しかし、今回は SSH のクレデンシャルが与えられているため、SSH トンネリングを構成してローカルの Frida Server に接続することができます。
そのため、まずは以下のコマンドでポートフォワーディングを設定しながら、テトリスを再プレイします。
sudo ssh -L 27042:127.0.0.1:27042 user@frown.insomnihack.ch -p 24
Frida Server が起動したところで、frida-ps -H localhost
コマンドを実行し、解析対象の PID と名前を特定します。
pip install frida frida-tools
frida-ps -H localhost
Frida Server 経由でアプリケーションのバイナリを取得する
公式の Writeup を読むと、Frida Server 経由で問題アプリケーションのバイナリを取得できるとの記載がありました。
参考:frown/solution at master · leonjza/frown
まず、特定した名前を使用して frida -H localhost Gadget
コマンドで Frida Server にアタッチします。
続いて、Process.mainModule
を実行してプロセスの実行モジュールの情報を取得します。
参考:JavaScript API | Frida • A world-class dynamic instrumentation toolkit
ここから、実行アプリケーションのパスが /usr/local/bin/tetris
であることを特定できます。
これで、Frida を使用してリモートサーバ上の特定のプロセス(Gadget) にアタッチし、そのプロセスのコンテキストでファイルをダウンロードすることができます。
node -v #v16.13.1
cd solution/frida/agent/
npm i
python3 -m solution getfile "/usr/local/bin/tetris"
以下は、公式の Solver を一部カスタマイズしたものです。
パスを指定して getfile を呼び出すことで fs.readFileSync(p).toString('base64')
によってバイナリを Base64 暗号化して取得することができます。
import fs from "fs";
import http from "http";
rpc.exports = {
dir: (p: string) => fs.readdirSync(p),
binpath: () => Process.mainModule.path,
getfile: (p: string) => fs.readFileSync(p).toString('base64'),
modules: () => Process.enumerateModules(),
watchoffset: (a: number) => {
const offset = Process.mainModule.base.add(ptr(a));
send(`watching func at ${a}. offset="${offset}"`);
Interceptor.attach(offset, {
onEnter(args) {
send(`watchfunc: ${a} called. arg[0]="${args[0]}"`);
}
});
},
watchlibs: () => {
Interceptor.attach(Module.getExportByName(null, "dlopen"), {
onEnter(args) {
const path = args[0].readUtf8String();
const flags = args[1].toInt32();
send(`dlopen() path="${path}", flags="${flags}"`);
}
});
Interceptor.attach(Module.getExportByName(null, "dlclose"), {
onEnter(args) {
const handle = args[0];
send(`dlclose() handle="${handle}"`);
}
});
},
blockdlclose: () => {
Interceptor.replace(Module.getExportByName(null, "dlclose"), new NativeCallback((handle) => {
return 0;
}, 'int', ['pointer']));
},
pinscore: () => {
const tetris_refresh = Process.mainModule.base.add(0x00002dc8);
Interceptor.attach(tetris_refresh, {
onEnter(args) {
const tetris_t = args[0];
const score_t = tetris_t.add(Process.pointerSize * 14);
const score_ptr = score_t.add(4 * 4);
send(`tetris_t=${tetris_t}, score_t=${score_t}, score=${score_ptr.readInt()}`);
score_ptr.writeInt(9179);
}
});
},
flagkey: (key: number) => {
const m = Module.load("libttyris.so");
const flag_key_ptr = m.getExportByName("flag_key");
const flag_key = new NativeFunction(flag_key_ptr, 'void', ['int', 'pointer', 'int']);
const flag_len = 100;
const flag = Memory.alloc(flag_len);
flag_key(key, flag, flag_len);
const flag_value = flag.readUtf8String();
return flag_value;
},
watchcurl: () => {
const curl_ptr = Process.mainModule.base.add(0x00001d2f);
Interceptor.attach(curl_ptr, {
onEnter(args) {
send(`curl->() arg0="${args[0].readUtf8String()}" arg1=${args[1].readUtf8String()}`);
this.response = args[2];
},
onLeave(retval) {
send(`curl<-() arg3="${this.response.readUtf8String()}"`);
}
});
},
usecurl: (key: number) => {
const curl_ptr = Process.mainModule.base.add(0x00001d2f);
const curl = new NativeFunction(curl_ptr, 'void', ['pointer', 'pointer', 'pointer']);
const response = Memory.alloc(100);
const url = Memory.allocUtf8String("http://frown-service/");
const key_ptr = Memory.allocUtf8String(key.toString());
curl(url, key_ptr, response);
return response.readUtf8String();
},
sendkey: (key: number) => {
return new Promise((resolve, reject) => {
const opts: http.RequestOptions = {
hostname: 'frown-service',
port: 80,
path: '/',
method: 'POST',
headers: {
'Content-Type': 'text/plain',
'content-length': key.toString().length
}
};
const req = http.request(opts, (res) => {
let body = '';
res.on('data', (chunk) => {
body += chunk;
});
res.on('end', () => {
resolve(body);
});
});
req.on('error', (error) => {
reject(error);
});
req.write(key);
req.end();
});
},
exec: (c: string) => {
const popen_ptr = Module.getExportByName(null, "popen");
const fgets_ptr = Module.getExportByName(null, "fgets");
const pclose_ptr = Module.getExportByName(null, "pclose");
const popen = new NativeFunction(popen_ptr, 'pointer', ['pointer', 'pointer']);
const fgets = new NativeFunction(fgets_ptr, 'pointer', ['pointer', 'int', 'pointer']);
const pclose = new NativeFunction(pclose_ptr, 'int', ['pointer']);
const command = Memory.allocUtf8String(c);
const mode = Memory.allocUtf8String("r");
const output_size = Process.pointerSize * 80;
const output = Memory.alloc(output_size);
const pipe = popen(command, mode);
fgets(output, output_size, pipe);
pclose(pipe);
return output.readUtf8String();
}
};
ここで取得したバイナリを実行すると、リモートサーバと同じようにテトリスゲームをプレイすることができます。
バイナリを解析して Flag を取得する
ダウンロードしたバイナリを Ghidra で解析したところ、libttyris.so
というライブラリをロードして flag_key 関数を取得しているようです。
ここまで試したところでサーバが落ちてしまいこれ以上試せませんでしたが、同じように libttyris.so
をダウンロードして解析すると埋め込まれている値から正しい Flag を特定できたようです。
まとめ
復習している途中で問題サーバが公開終了してしまい投げやりな感じになってしまいましたが、Frida を勉強するモチベーションは高まりました。