VolgaCTF 2024 Qualifier Writeup

この大会は2024/3/31 0:00(JST)~2024/4/1 0:00(JST)に開催されました。
今回もチームで参戦。結果は295点で257チーム中100位でした。
自分で解けた問題をWriteupとして書いておきます。

PQ (crypto)

Nを素因数分解する。

N = 11492900508587989954531440332263108922084741581974222102472118047768380430540353920289746766137088552446667728704705825767564907436992191013818658710263387461050109924538094327413211095570408951831804511312664061115780934181616871945218328272318074413219648490794574757471025686956697500105383560452507555043577475707379430674941097523563439495291391376433292863780301364692520578080638180885760050174506019604723296382744280564071968270446660250234587067991312783613649361373626181634408704506324790573495089018767820926445333763821104985493381745364201010694528200379210559415841578043670235152463040761395093466757
  * 11492900508587989954531440332263108922084741581974222102472118047768380430540353920289746766137088552446667728704705825767564907436992191013818658710263387461050109924538094327413211095570408951831804511312664061115780934181616871945218328272318074413219648490794574757471025686956697500105383560452507555043583332241787496934511124508164581479242699938954664986778778089174462752116916848703843649899098070668669678312274875686923774909987821333120312320645501581685293243247702323898532381890355435523179413889042894904950054113266062322984191234608909924029167449813744492456171898301189923726970620918920525920821

あとは通常通り、RSA暗号の復号を行う。

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

with open('output', 'r') as f:
    params = f.read().splitlines()

N = int(params[0])
c = bytes_to_long(eval(params[1]))
e = 65537

p = 11492900508587989954531440332263108922084741581974222102472118047768380430540353920289746766137088552446667728704705825767564907436992191013818658710263387461050109924538094327413211095570408951831804511312664061115780934181616871945218328272318074413219648490794574757471025686956697500105383560452507555043577475707379430674941097523563439495291391376433292863780301364692520578080638180885760050174506019604723296382744280564071968270446660250234587067991312783613649361373626181634408704506324790573495089018767820926445333763821104985493381745364201010694528200379210559415841578043670235152463040761395093466757
q = 11492900508587989954531440332263108922084741581974222102472118047768380430540353920289746766137088552446667728704705825767564907436992191013818658710263387461050109924538094327413211095570408951831804511312664061115780934181616871945218328272318074413219648490794574757471025686956697500105383560452507555043583332241787496934511124508164581479242699938954664986778778089174462752116916848703843649899098070668669678312274875686923774909987821333120312320645501581685293243247702323898532381890355435523179413889042894904950054113266062322984191234608909924029167449813744492456171898301189923726970620918920525920821
assert N == p * q

phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, N)
flag = long_to_bytes(m).decode()
print(flag)
VolgaCTF{0x8922165e83fa29b582f6a252cb337a05}

QR (crypto)

フラグ入りのQRコードに対する処理の概要は以下の通り。

・generator = LFSR(register, branches)
 ・generator.register = register(未知)
 ・generator.branches = branches(未知)
 ・generator.n: registerの長さ
・image: QRコード画像オブジェクト
・w: QRコード画像の幅
・h: QRコード画像の高さ
・new_image: 新しい画像オブジェクト
・pixels: QRコード画像ピクセルデータ
・各ピクセルについて以下を実行
 ・pixel: 白の場合1、黒の場合0
 ・next_bit = generator.next_bit()
 ・encrypted = pixel ^ next_bit
 ・new_imageにencrypted * 255を設定

画像のサイズから2x2で1つのセルを表していると推測できる。元の画像の左上から右に0000000000000011になっているはず。
16個分のregisterを割り出すと以下のようになる。

[1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1]

registerの長さを総当たりで考え、推測する。

・長さ1の場合、register = [1]、branches = [1]
 ret = 1
 new = 0
 new ^= 1 = 1
 register = [1] -> NG

・長さ2の場合、register = [0, 1]、branches = [1]
 ret = 1
 new = 0
 new ^= 0 = 0
 register = [0, 0]
 ret = 0
 new = 0
 new ^= 0 = 0
  :
 0しか出てこない -> NG
 ※長さ3~6も同様

・長さ7の場合、register = [1, 0, 0, 0, 0, 0, 1]
 -> branches = [1, 3, 4, 5, 7] -> OK

とりあえずこの前提で復号してみる。

#!/usr/bin/env python3
from PIL import Image

class LFSR:
    def __init__(self, register, branches):
        self.register = register
        self.branches = branches
        self.n = len(register)

    def next_bit(self):
        ret = self.register[self.n - 1]
        new = 0
        for i in self.branches:
            new ^= self.register[i - 1]
        self.register = [new] + self.register[:-1]

        return ret

image = Image.open('qr.png')
w = image.width
h = image.height

pixels = image.load()

## research ##
register = []
for i in range(16):
    pixel = pixels[i, 0] // 255
    if i < 14:
        register.append(pixel ^ 0)
    else:
        register.append(pixel ^ 1)
register = register[::-1]
print('[+] register sequence:', register)

## decrypt ##
register = [1, 0, 0, 0, 0, 0, 1]
branches = [1, 3, 4, 5, 7]

generator = LFSR(register, branches)

org_image = Image.new(image.mode, image.size)

for y in range(h):
    for x in range(w):
        pixel = pixels[x, y] // 255
        next_bit = generator.next_bit()
        decrypted = pixel ^ next_bit

        org_image.putpixel((x, y), decrypted * 255)

org_image.save('flag_qr.png', image.format)


復号した結果、QRコードになったので、コードリーダで読み取る。

VolgaCTF{0x05d76db737ac94674907136eb1f15f02}

Feedback (misc)

アンケートに答えたら、フラグが表示された。

VolgaCTF{5b84a0a9f6ba8d171b4a5633a4457a1f}