niteCTF 2022 Writeup

この大会は2022/12/24 1:00(JST)~2022/12/26 1:00(JST)に開催されました。
今回もチームで参戦。結果は350点で492チーム中123位でした。
自分で解けた問題をWriteupとして書いておきます。

4dm1n_c0ntR0L (Web)

Usernameに以下を入力して、Submitすると、ログインできる。

' or 1=1 -- - 

Get flagボタンだけあるので、押してみると、フラグがポップアップ画面に表示された。

nitectf{w3nT_1nT0_Th3_s3rV3r}

itsybitsyrsa (Cryptography)

3つのファイルに分散されているが、それぞれRSA暗号のパラメータが書いてある。nが非常に大きく、eが小さいので、Low Public-Exponent Attackで復号する。

#!/usr/bin/env python3
from Crypto.Util.number import *
import gmpy2

with open('e_value', 'r') as f:
    e = int(f.readline().rstrip().split(' ')[-1])

with open('c_value', 'r') as f:
    c = int(f.readline().rstrip().split(' ')[-1])

m = gmpy2.iroot(c, e)[0]
flag = long_to_bytes(m).decode()
print(flag)
nitectf{rsa_can_be_very_adaptable}

Basically, I locked up (Cryptography)

暗号・復号のスクリプトが提供されている。この暗号の処理概要は以下の通り。

・password: 8バイトの文字列(未知)
・plaintextにはb"HiDeteXT"が含まれている。
・add_spice関数: 左シフト
・ciphertext: plaintextの各文字について、add_spice(c) ^ ord(password[i % len(password)])のバイト文字の連結
 →ファイル書き込み

暗号の各位置から8バイトを復号した際にb"HiDeteXT"になることを前提にpasswordを求めることをブルートフォースで行う。

#!/usr/bin/env python3
add_spice = lambda b: 0xff & ((b << 1) | (b >> 7))
remove_spice = lambda b: 0xff & ((b >> 1) | (b << 7))

def decrypt(ciphertext, password):
    plaintext = bytes(remove_spice(c ^ ord(password[i % len(password)]))
        for i, c in enumerate(ciphertext))
    return plaintext

def is_printable(s):
    for c in s:
        if c < 32 or c > 126:
            if c != 10:
                return False
    return True

with open('Important_encrypted', 'rb') as f:
    ciphertext = f.read()

known = b'HiDeteXT'

with open('Important_encrypted', 'rb') as f:
    ciphertext = f.read()

for i in range(len(ciphertext) - 8 + 1):
    password = [''] * 8
    for j in range(8):
        password[(i + j) % 8] = chr(add_spice(known[j]) ^ ciphertext[i + j])
    plaintext = decrypt(ciphertext, password)
    if is_printable(plaintext):
        plaintext = plaintext.decode()
        print(plaintext)
        break

実行結果は以下の通り。

Oh, You Searching for HiDeteXT ??

NITE{BrUT3fORceD_y0uR_wAy_iN}
NITE{BrUT3fORceD_y0uR_wAy_iN}

Shoo-in (Cryptography)

サーバの処理概要は以下の通り。

・fn: "firstnames.py"のファイルオブジェクト
・ln: "lastnames.py"のファイルオブジェクト
・fp: "primes.py"のファイルオブジェクト
・fn_content: fnの行ごと(改行あり)の配列
・ln_content: lnの行ごと(改行あり)の配列
・prime_arr: fpの行ごと(改行あり)の配列
・N: firstnames.pyとlastnames.pyの行数の少ない方の行数(=1300)
・seed, a, b: N未満のランダム整数
・lcg = RNG(seed, a, b, N)
 ・lcg.seed = seed
 ・lcg.a = a
 ・lcg.b = b
 ・lcg.p = N
・gen=lcg.gen()
・以下10回繰り返し
 ・iが0の場合
  ・name1: "[firstnamesのリストのa番目] [lastnamesのリストのb番目]"
 ・iが0以外の場合
  ・name1: "[firstnamesのリストのnext(gen)番目] [lastnamesのリストのnext(gen)番目]"
 ・name2: "[firstnamesのリストのnext(gen)番目] [lastnamesのリストのnext(gen)番目]"
 ・name1, name2を表示
 ・winner = next(gen) % 2
 ・inp: 入力(1または2)
 ・winnerがinpと一致しない場合、終了
 ・winnerがinpと一致する場合
  ・iが9より小さい場合、正解メッセージを表示
  ・iが9の場合
   ・key=generate_keys()
    ・p: prime_arr[next(gen)]
    ・q: prime_arr[next(gen)]
    ・n = p * q
    ・g = n + 1
    ・l = (p - 1) * (q - 1)
    ・mu = gmpy2.invert(l, n)
    ・n, g, l, muを返却
   ・pallier_encrypt(key, int.from_bytes(flag, "big"), next(gen))の結果を表示
    ・(pow(g, m, n**2) * pow(next(gen), n, n**2) % n**2を返却
    ※m: フラグの数値化

最初のname1からa, bはわかる。さらにseedをブルートフォースし、name2になる条件からseedを求める。あとはnext(gen)の値がわかるので、各パラメータを算出できる。p, qがわかるので、Paillier暗号の暗号文を復号することができる。

#!/usr/bin/env python3
import socket
from Crypto.Util.number import GCD, inverse

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

class RNG:
    def __init__ (self, seed, a, b, p):
        self.seed = seed
        self.a = a
        self.b = b
        self.p = p

    def gen(self):
        out = self.seed
        while True:
            out = (self.a * out + self.b) % self.p
            self.a += 1
            self.b += 1
            self.p += 1
            yield out

def getPrime():
    prime = int(prime_arr[next(gen)].strip())
    return prime

def L(u, n):
    return (u - 1) // n

def pallier_decrypt(c):
    p = getPrime()
    q = getPrime()
    n = p * q
    g = n + 1
    l = (p - 1) * (q - 1)
    _lambda = l // GCD(p - 1, q - 1)
    m = L(pow(c, _lambda, n**2), n) * inverse(L(pow(g, _lambda, n**2), n), n) % n
    return m

fn = open(r"firstnames.py", 'r')
ln = open(r"lastnames.py", 'r')
fp = open (r"primes.py", 'r')
fn_content = fn.readlines()
ln_content = ln.readlines()
prime_arr = fp.readlines()

N = (min(len(fn_content), len(ln_content)))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('34.90.236.228', 1337))

data = recvuntil(s, b'\n').rstrip()
print(data)
data = recvuntil(s, b'\n').rstrip()
print(data)
name1 = data.split('\t')[0]
name2 = data.split('\t')[-1]

a = fn_content.index(name1.split(' ')[0] + '\n')
b = ln_content.index(name1.split(' ')[1] + '\n')
next1 = fn_content.index(name2.split(' ')[0] + '\n')
next2 = ln_content.index(name2.split(' ')[1] + '\n')

found = False
for seed in range(N):
    c1 = (a * seed + b) % N
    if c1 == next1:
        if ((a + 1) * c1 + (b + 1)) % (N + 1) == next2:
            found = True
            break

assert found

lcg = RNG(seed, a, b, N)
gen = lcg.gen()
next(gen)
next(gen)
winner = next(gen) % 2
if winner == 0:
    winner = 2

data = recvuntil(s, b'\n').rstrip()
print(data)
print(winner)
s.sendall(str(winner).encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
data = recvuntil(s, b'\n').rstrip()
print(data)

for i in range(1, 10):
    for _ in range(4):
        next(gen)
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    winner = next(gen) % 2
    if winner == 0:
        winner = 2
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    print(winner)
    s.sendall(str(winner).encode() + b'\n')
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    data = recvuntil(s, b'\n').rstrip()
    print(data)

data = recvuntil(s, b'\n').rstrip()
print(data)
c = int(data)
m = pallier_decrypt(c)
flag = m.to_bytes((m.bit_length() + 7) // 8, "big").decode()
print(flag)

実行結果は以下の通り。

== proof-of-work: disabled ==
Andre Reese     vs      Blaine Harmon
Choose the winner: 1 or 2
1
That's correct, here is the next round

Heath Francis   vs      Shiloh Heath
Choose the winner: 1 or 2
2
That's correct, here is the next round

Kash Avery      vs      Nikhil Lopez
Choose the winner: 1 or 2
1
That's correct, here is the next round

Margaret Schneider      vs      Bethany Selene
Choose the winner: 1 or 2
2
That's correct, here is the next round

Todd Estes      vs      Adrien Frye
Choose the winner: 1 or 2
1
That's correct, here is the next round

Rodrigo Moore   vs      Cadence Kent
Choose the winner: 1 or 2
2
That's correct, here is the next round

Jesse Beard     vs      Rodrigo Tobias
Choose the winner: 1 or 2
1
That's correct, here is the next round

Abigail Aryn    vs      Alberto Rodgers
Choose the winner: 1 or 2
2
That's correct, here is the next round

Jonathan Woods  vs      Daisy Hollyn
Choose the winner: 1 or 2
1
That's correct, here is the next round

Brice Schroeder vs      Daisy Werner
Choose the winner: 1 or 2
1
Congratulations! you made it
Can you decode this secret message?
9247144570829358924841240967295610831251025340080430839001135462876063348979941518647185296879931786766958032992008977123505926544277369888568600621632355851505450281378555399544202335817356532297136697333575081814361295223072969220560307669233249687870270873089007634771695172315068511805588870393530505136577345386776211456988815465939654026602528062989553270554332170428745622557131002272007524816246732049457089239722213074314397985724894219764450273744264907017723444907563274142874472362285638387792413075013020704951426410641101658781288819482936333414155207342080191537216025570791495740491524447242675986564
niteCTF{n0T_sO_R@nd0m}
niteCTF{n0T_sO_R@nd0m}