N1CTF 2020 | VSS

#Mersenne_Twister

#!/usr/bin/python3
import qrcode  # https://github.com/lincolnloop/python-qrcode
import random
import os
from PIL import Image
from flag import FLAG


def vss22_gen(img):
    m, n = img.size
    share1, share2 = Image.new("L", (2*m, 2*n)), Image.new("L", (2*m, 2*n))
    image_data = img.getdata()
    flipped_coins = [int(bit) for bit in bin(random.getrandbits(m*n))[2:].zfill(m*n)]
    for idx, pixel in enumerate(image_data):
        i, j = idx//n, idx % n
        color0 = 0 if flipped_coins[idx] else 255
        color1 = 255 if flipped_coins[idx] else 0
        if pixel:
            share1.putpixel((2*j, 2*i), color0)
            share1.putpixel((2*j, 2*i+1), color0)
            share1.putpixel((2*j+1, 2*i), color1)
            share1.putpixel((2*j+1, 2*i+1), color1)

            share2.putpixel((2*j, 2*i), color0)
            share2.putpixel((2*j, 2*i+1), color0)
            share2.putpixel((2*j+1, 2*i), color1)
            share2.putpixel((2*j+1, 2*i+1), color1)
        else:
            share1.putpixel((2*j, 2*i), color0)
            share1.putpixel((2*j, 2*i+1), color0)
            share1.putpixel((2*j+1, 2*i), color1)
            share1.putpixel((2*j+1, 2*i+1), color1)

            share2.putpixel((2*j, 2*i), color1)
            share2.putpixel((2*j, 2*i+1), color1)
            share2.putpixel((2*j+1, 2*i), color0)
            share2.putpixel((2*j+1, 2*i+1), color0)
    share1.save('share1.png')
    share2.save('share2.png')


def vss22_superposition():
    share1 = Image.open('share1.png')
    share2 = Image.open('share2.png')
    res = Image.new("L", share1.size, 255)
    share1_data = share1.getdata()
    share2_data = share2.getdata()
    res.putdata([p1 & p2 for p1, p2 in zip(share1_data, share2_data)])
    res.save('result.png')


def main():
    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_L,
        box_size=12,
        border=4,
    )
    qr.add_data(FLAG)
    qr.make(fit=True)
    img = qr.make_image(fill_color="black", back_color="white")
    vss22_gen(img._img)
    img.save('res.png')
    vss22_superposition()


if __name__ == '__main__':
    main()

QRコードの画像を random.getrandbits の生成結果でflipしているだけ。QRコードの周囲には白いマスがたくさんあるのでそこからrandbitsを647 * 32 bit 以上取得できればOK。 random.getrandbitsは rangeをprependするような挙動なので後ろから復元する。

from PIL import Image
from mt19937predictor import MT19937Predictor


img = Image.open("share2.png")

# collect bits
predict_bits = []
w, h = img.size
for y in range(0, 96, 2):
    for x in range(0, w, 2):
        p = img.getpixel((w - x - 1, h - y - 1))
        if p == 0:
            predict_bits.append(0)
        else:
            predict_bits.append(1)

# predict
bits = []
for i in range(624):
    x = "".join(str(b) for b in predict_bits[i*32:(i+1)*32][::-1])
    bits.append(int(x, 2))

predictor = MT19937Predictor()
for b in bits:
    predictor.setrandbits(b, 32)

# get flipped_coins
bs = []
for b in bits:
    bs = [int(b) for b in bin(b)[2:].zfill(32)] + bs

while len(bs) < (w * h // 4):
    bs = [int(b) for b in bin(predictor.getrandbits(32))[2:].zfill(32)] + bs

flipped_coins = bs[-(w * h // 4):]

print(flipped_coins)

# recover image
w, h = w // 2, h // 2
qr = Image.new("L", (w, h))
for i in range(len(flipped_coins)):
    x, y = i % w, i // h
    p = img.getpixel((2*x, 2*y))
    color0 = 0 if flipped_coins[i] else 255
    if p == color0:
        qr.putpixel((x, y), 255)
    else:
        qr.putpixel((x, y), 0)

qr.save("qr.png")