SECCON CTF 2019 Crypto Writeup(+3) Crypto道場八[13/100]

どうも。duckです。
今回はCrypto全部解きました。及第点といったところでしょう。
本戦への壁は厚いっすね。くそう。



1.coffee_break
2.ZKPay
3.Crazy Repetition of Codes



1.coffee_break

貼るけど一応元ファイルもおいとく。
file:encrypt.py
Dropbox - encrypt.py_b7d6c2e28d7f4eee9db8673b5c82191e54d1fea1 - Simplify your life
暗号文は問題文に書いてあった。
cipher.txt

FyRyZNBO2MG6ncd3hEkC/yeYKUseI/CxYoZiIeV2fe/Jmtwx+WbWmU1gtMX9m905

encrypt.py

import sys
from Crypto.Cipher import AES
import base64


def encrypt(key, text):
    s = ''
    for i in range(len(text)):
        s += chr((((ord(text[i]) - 0x20) + (ord(key[i % len(key)]) - 0x20)) % (0x7e - 0x20 + 1)) + 0x20)
    return s


key1 = "SECCON"
key2 = "seccon2019"
text = sys.argv[1]

enc1 = encrypt(key1, text)
cipher = AES.new(key2 + chr(0x00) * (16 - (len(key2) % 16)), AES.MODE_ECB)
p = 16 - (len(enc1) % 16)
enc2 = cipher.encrypt(enc1 + chr(p) * p)
print(base64.b64encode(enc2).decode('ascii'))

自作encrypt関数とAESで暗号化してますがAESのkeyがすでにわかっているのでAESはないも同然。Base64は暗号化ですらない。一瞬でデコードできる。
よって、自作encrypt関数を復号する関数を書くだけでよい。

数式書いてた紙をなくしたので、珍しくコードを貼っておく。なんかちょっとしたmodだった気がする。

def decrypt(key,enc):
    dec = ''
    for i in range(len(enc)):
        num = ord(enc[i]) + 32 - ord(key[i % len(key)])
        if 126 > num > 33:
            dec += chr(num)
        elif 126 > num + 95 > 33:
            dec += chr(num+95)
    return dec

アスキーコードで文字を表してるのが33~126くらいだったと記憶している。
だから上のような書き方だと思われる。たぶん。しかもこれなんか不備があった気がするけど、答え出たから無視した気がする。

from Crypto.Cipher import AES
import base64

key1 = "SECCON"
key2 = "seccon2019"
C = "FyRyZNBO2MG6ncd3hEkC/yeYKUseI/CxYoZiIeV2fe/Jmtwx+WbWmU1gtMX9m905"

enc2 = base64.b64decode(C)
cipher = AES.new(key2 + chr(0x00) * (16 - (len(key2) % 16)), AES.MODE_ECB)
enc1 = cipher.decrypt(enc2)
ans = decrypt(key1,enc1)

print(ans)

これで答えが出るはず。たぶん。



2.ZKPay

QRコードを偽造してお金持ちになる問題。ただし、その分誰かが借金を背負う。過酷な世界。
URLにアクセスして、Registerしたあとログインする。

home画面はこんな感じ。

f:id:falconctf:20191020154251p:plain
home画面
1000000$むしり取ればフラッグが見れるっぽい。最初はrootが500くれてるだけの画面だったが、いろいろいじってたら今はこんな感じになってる。

send画面。

f:id:falconctf:20191020154458p:plain
send画面
人にお金を送るためのQRコードを発行できる。ただし持ってるお金以上送ろうとすると怒られる。

もらったQRコードをここで読んでみた。(QRコードをパソコンで読み取る(インストール不要)

username=egg&amount=100&proof=MI0bWQyatoUpSAwexg5YfHsb16bVb/HvGO9S73dOnlUiMSAwjDWHNyfVBJu4LboVjYNRHX7Amd268Hn42CJCz9ACog8wCjDxUF86bhqo/gNRrX2hVEmmr3TVD5y/oyWcV0qhdI9sCgfs4LBHdYEZ88Y2t9mQ+y8reD9wKBg50HvHYLs+wgwZMSAwEqtINttKiFkBf1z0oRDZVNzlDMHIcNBmpWPqsspe5hYwCjChQs4pLc1BI+RcZEcwnlIVXKMg3DtLCpmb2ae68wVpFjAgMICbAKUpCUTEkvPqCgAyEn495kmmuIHHwvAMOcgoBBckMAowc88QQ3fzC/fScQ/bYjgWatbrwSv6oC5b7FKjGVe8UgkxCjCFM8gQREFJNGRpfKk0oxjHDid21FI+kyBx8grtsf0vKjAK&hash=c728621fcb2fc4fffb72f4ce548a5d04b8a7424199f022d0377c2f99e76564d9

これをrecieve画面で読み込むとお金をゲットできる。

f:id:falconctf:20191020155001p:plain
rcieve画面

ユーザーの初期の手持ちは500しかなく、ユーザーをたくさん作って送り付けてもいいが、ちまちまやってられないので、借金を背負ってもらってゲームを終わらせる。

username=egg&amount=-1000000&proof=MI0bWQyatoUpSAwexg5YfHsb16bVb/HvGO9S73dOnlUiMSAwjDWHNyfVBJu4LboVjYNRHX7Amd268Hn42CJCz9ACog8wCjDxUF86bhqo/gNRrX2hVEmmr3TVD5y/oyWcV0qhdI9sCgfs4LBHdYEZ88Y2t9mQ+y8reD9wKBg50HvHYLs+wgwZMSAwEqtINttKiFkBf1z0oRDZVNzlDMHIcNBmpWPqsspe5hYwCjChQs4pLc1BI+RcZEcwnlIVXKMg3DtLCpmb2ae68wVpFjAgMICbAKUpCUTEkvPqCgAyEn495kmmuIHHwvAMOcgoBBckMAowc88QQ3fzC/fScQ/bYjgWatbrwSv6oC5b7FKjGVe8UgkxCjCFM8gQREFJNGRpfKk0oxjHDid21FI+kyBx8grtsf0vKjAK&hash=c728621fcb2fc4fffb72f4ce548a5d04b8a7424199f022d0377c2f99e76564d9

amount=-1000000となるQRコードはsendからだと作れない仕様なので、自分で作ってreceiveに流せばおけ。

認証に使われってるっぽいproofとhashを復元するのかと思って無駄に時間食った。つらい。



3.Crazy Repetition of Codes

code:

import os
from Crypto.Cipher import AES

def crc32(crc, data):
  crc = 0xFFFFFFFF ^ crc
  for c in data:
    crc = crc ^ ord(c)
    for i in range(8):
      crc = (crc >> 1) ^ (0xEDB88320 * (crc & 1))
  return 0xFFFFFFFF ^ crc

key = b""

crc = 0
for i in range(int("1" * 10000)):
  crc = crc32(crc, "TSG")
assert(crc == 0xb09bc54f)
key += crc.to_bytes(4, byteorder='big')

crc = 0
for i in range(int("1" * 10000)):
  crc = crc32(crc, "is")
key += crc.to_bytes(4, byteorder='big')

crc = 0
for i in range(int("1" * 10000)):
  crc = crc32(crc, "here")
key += crc.to_bytes(4, byteorder='big')

crc = 0
for i in range(int("1" * 10000)):
  crc = crc32(crc, "at")
key += crc.to_bytes(4, byteorder='big')

crc = 0
for i in range(int("1" * 10000)):
  crc = crc32(crc, "SECCON")
key += crc.to_bytes(4, byteorder='big')

crc = 0
for i in range(int("1" * 10000)):
  crc = crc32(crc, "CTF!")
key += crc.to_bytes(4, byteorder='big')

flag = os.environ['FLAG']
assert(len(flag) == 32)

aes = AES.new(key, AES.MODE_ECB)
encoded = aes.encrypt(flag)
assert(encoded.hex() == '79833173d435b6c5d8aa08f790d6b0dc8c4ef525823d4ebdb0b4a8f2090ac81e')

なかなかにクレイジー

range(int("1" * 10000))

とくにここ。111111111111 \cdots 111(10000個並んでる。)回も crc32を使って計算している。
方針は単純。どっかでループすると思われるので、それを特定する。ランダム要素がないので最悪でもcrc32を2^{32}回ぶん回せばループが発覚する。鳩ノ巣原理。
あとは111111111111 \cdots111 \bmod loopを計算して、ループの先だけ計算すればよし。

2^{32}回ぶん回すのも頭おかしいと思ったが、ほとんどbit演算だったので意外に行けた。高速化するためにC++で書きなおしたけど。
一応アルゴリズムごと高速化できるアイデアもあったが、実装しなくてもいけたのでお蔵入りした。

こつこつcrcを計算したらあとは以下のコードで復号できた。

import os
from Crypto.Cipher import AES


key = b""

crc = 0xb09bc54f
key += crc.to_bytes(4, byteorder='big')

crc = 3836056187
key += crc.to_bytes(4, byteorder='big')

crc = 2369777541
key += crc.to_bytes(4, byteorder='big')

crc = 3007692607
key += crc.to_bytes(4, byteorder='big')

crc = 1526093488
key += crc.to_bytes(4, byteorder='big')

crc = 3679021396
key += crc.to_bytes(4, byteorder='big')

text='79833173d435b6c5d8aa08f790d6b0dc8c4ef525823d4ebdb0b4a8f2090ac81e'
bin=binascii.unhexlify(text)

aes = AES.new(key, AES.MODE_ECB)
flag = aes.decrypt(bin)
print(flag)



眠すぎて何を書いてるかもよくわからない。お休み。