TokyoWesterns CTF 2018 mixed-cipher 復習
Writeupとか見ながら復習したので、その解説。 参考にしたWriteupはhttps://github.com/GabiTulba/Tokyo-Westerns-2018-Mixed-Cipher-Crypto-Write-up/blob/master/README.mdのやつ。
問題自体の解説はしないでその中で使われていた手法、特にLSB Decryption Oracle Attackの解説をメインに書く。
mixed-cipher
問題で渡されるスクリプト
from Crypto.PublicKey import RSA from Crypto.Cipher import AES from Crypto.Util.number import long_to_bytes import random import signal import os import sys sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) privkey = RSA.generate(1024) pubkey = privkey.publickey() flag = open('./flag').read().strip() aeskey = os.urandom(16) BLOCK_SIZE = 16 def pad(s): n = 16 - len(s)%16 return s + chr(n)*n def unpad(s): n = ord(s[-1]) return s[:-n] def aes_encrypt(s): iv = long_to_bytes(random.getrandbits(BLOCK_SIZE*8), 16) aes = AES.new(aeskey, AES.MODE_CBC, iv) return iv + aes.encrypt(pad(s)) def aes_decrypt(s): iv = s[:BLOCK_SIZE] aes = AES.new(aeskey, AES.MODE_CBC, iv) return unpad(aes.decrypt(s[BLOCK_SIZE:])) def bulldozer(s): s = bytearray(s) print('Bulldozer is coming!') for idx in range(len(s) - 1): s[idx] = '#' return str(s) def encrypt(): p = raw_input('input plain text: ').strip() print('RSA: {}'.format(pubkey.encrypt(p, 0)[0].encode('hex'))) print('AES: {}'.format(aes_encrypt(p).encode('hex'))) def decrypt(): c = raw_input('input hexencoded cipher text: ').strip().decode('hex') print('RSA: {}'.format(bulldozer(privkey.decrypt(c)).encode('hex'))) def print_flag(): print('here is encrypted flag :)') p = flag print('another bulldozer is coming!') print(('#'*BLOCK_SIZE+aes_encrypt(p)[BLOCK_SIZE:]).encode('hex')) def print_key(): print('here is encrypted key :)') p = aeskey c = pubkey.encrypt(p, 0)[0] print(c.encode('hex')) signal.alarm(300) while True: print("""Welcome to mixed cipher :) I heard bulldozer is on this channel, be careful! 1: encrypt 2: decrypt 3: get encrypted flag 4: get encrypted key""") n = int(raw_input()) menu = { 1: encrypt, 2: decrypt, 3: print_flag, 4: print_key, } if n not in menu: print('bye :)') exit() menu[n]()
簡単に動作を説明すると、CBCモードのAESと1024bitのRSAを使って暗号化とかする感じ。 ただし、bulldozerが来て復号化したメッセージなどはほとんど取得できないようになっている。 おそらく、このbulldozerで復号化したメッセージの下位1バイトが取得できるところからLSB Decryption Oracle Attackを発想できるんだと思う。
解読手順 概略
Writeupで示されていた手順だと、だいたい以下のようになる。
- 暗号文と平文が入手できることとgcdを利用してNを入手 (2~4回の試行)
- Nを使って、暗号化されたASE Keyの下位1バイトが分かるので、RSA LSB oracle AttackでAES Keyを入手
- PythonのrandomモジュールのメルセンヌツイスターをクラックしてIVを入手
- 鍵とIVが分かるのでAESのフラグを復号する
print_key
によってRSAで暗号化されたAES Keyを手に入れ、decrypt
でそれを復号することができる。
ただし、bulldozer
で下位1バイトしか分からないようにされてしまう。
ここでRSAに対する攻撃のひとつであるLSB Decryption Oracle Attackが使える。
この攻撃は任意の暗号文を復号した結果の下位1ビットが分かれば、元の平文を復元することができるというものである、詳しくは後で。
これを利用してAES Keyを復号できる。
これでprint_flag
が解読できるかというとそうでもない。
print_flag
では先頭にあるIVがbulldozer
で潰されるため、鍵があっても復号できないようになっている。
そのためこのIVを取得する必要があるが、一見すると手に入らなそうに思える。
しかし、疑似乱数を推測することでこれを突破することができる。
IVの生成に使っているPythonのrandomモジュールはメルセンヌツイスターというアルゴリズムで疑似乱数を生成しているが、モジュール内部の状態であるPRNGを取得できるために予測が可能になってしまう。
推測のためのツール、randcrackがある。
推測といっても何度か(Writeupでは156回)乱数を取得する必要がある。
これで、AESの鍵とIVが入手できたのでprint_flag
によって出力されるAESで暗号化されたフラグを解読することができるようになった。
RSAの解読
LSB Decryption Oracle Attack
この攻撃は、任意の暗号文の平文の下位1ビットが分かれば平文を求めることができてしまうというものである。 だいたい以下の条件を満していれば使える。
- 任意の暗号文を復号でき、その結果の下位1ビットが分かる
- 公開鍵(N, e)が分かっている
この問題ではNも分かっていないので、その取得方法もこのあとで解説する。 ここではNは分かっているものとして話を進める。 eはPyCryptoのモジュールのデフォルト値0x10001が使われている。 普通RSAのeには0x10001が使われるので、分からなくてもこれを仮定して良い気はする。 eが極端に大きかったり小さかったりするとそれはそれで攻撃可能になりかねないので注意する必要がある。
求めたい平文を、求めたい平文を暗号化した暗号文は、公開鍵をととする。 まずを復号させる。 このとき、とはであるため、ではが成立する。 よって、を復号するとが得られる。
の値をとすると、と表すことができる(は整数)。
また、であり(がより大きくなるとが一意に定まらず解読不可能になるので、普通はそうならないようにする)、であるため、であるはず。
// TODO: ここの解釈は微妙、もう少しちゃんとした証明を探す。
要は、なので、のときはとなるというだけである。
ありえるのはだけとなる。
ここで、の偶奇(下位1ビット)に着目する。 のは、素数と素数の積なので奇数のはず(偶数なら素数2が選ばれているのでNから秘密鍵を求められる)。 の偶奇は前提条件により復号した結果から分かる。 また、は偶数であるので、によりの偶奇が分かることになる。 もし、つまり復号した結果が奇数(下位ビット1)なら、も奇数である。 なのでということになる。 もしが偶数(下位ビット0)なら、となる。
のとき、なのでということになる。
のとき、なのでということになる。
// FIXME: 等号が間違ってる気がする
結果を簡潔にまとめると、の復号した結果の下位ビットが
- 0なら
- 1なら
となる。 つまり、下位1ビットから平文の範囲をの半分に限定することができることになる。 も同じようにさらに半分にでき、でさらに半分にできる。 これをせいぜい鍵長のビット数回繰り返せば平文を定めることができる。
のときについて
同じような議論で、に限定できる。 のときの範囲と合わせて考えて、さらに半分にすることができる。
Nの特定
LSB Decryption Oracle Attackで、任意の暗号文を解読した結果の下位1ビットを知ることができると平文全体を求めることができると分かった。 しかし、この問題では肝心のNが分からないようになっている。 そこでまずNを特定する必要がある。
任意の平文の暗号文を入手できると十分な確度で推測することができる。 これにはgcdを利用する。
暗号化処理をと表す。 ある平文を用意し、暗号文を取得する。 暗号化処理により、となる。 によりが取得できる。
このとき、同士のgcdを計算することでが取得できる。 の値によっては失敗しそうだが、のビット長を利用することで確度を上げられる。 だいたい2つから4つくらいのgcdで大丈夫らしい。
AESの解読
AESの概略
RSAよりは浸透してないだろうと思うので、簡単に説明。 共通鍵暗号なので、暗号化と復号に使う鍵は同じものになる。 簡単に言えば、鍵から乱数列を作ってその乱数列を使い1ブロックずつを暗号化していく。
モードというものがあり、暗号化のされ方や強度に影響が出る。 この問題で使われているのはCBCモードというもので、前に暗号化されたブロックのデータが次のブロックの暗号化に使われるモードになる。 これにより、一部だけ解読したり改ざんしたりしにくくなる。 前のブロックを使うため、一番最初のブロックだけは初期化ベクトルという特別なブロックを前のブロックとして使う必要がある。 復号するためにはこの初期化ベクトルも必要になる。 この初期化ベクトルは暗号文と同時に送信される(そのはずだけど微妙に憶えてない)。
解読方法
RSAの解読することにより、print_key
からAESの鍵を取得することができた。
本来なら鍵され分かれば共通鍵暗号のAESは復号できるはずだが、IV(初期化ベクトル)をbulldozer
に潰されてしまっているため復号できない。
そのため、IVをどうにかして取得する必要がある。
問題の処理ではIVをrandom.getrandbits(BLOCK_SIZE*8)
で生成している。
このrandom
では、疑似乱数の生成にメルセンヌツイスターというアルゴリズムを使用している。
乱数の実装はたいていこのメルセンヌツイスターになってるはず、多分。
モジュールなので当然内部の初期状態などは分かっているため、これを解析して次の乱数を予想できるらしい。
randcrackでそれが可能、方法についても記載されているがまだ調べてない。
ともかく、これにより鍵とIVが手に入ったためprint_flag
を解読することができ、フラグをゲットできる。
スクリプト
writeupの方のコードは汚なくて読みにくいので、できればあとで書きたい。