Insomni'hack Teaser 2019唯一のCrypto問だったらしい。Insomni'hackは難しいという印象。あと髑髏が怖い
desctiption
Use this API to gift drink vouchers to yourself or your friends!
Vouchers are encrypted and you can only redeem them if you know the passphrase.
Because it is important to stay hydrated, here is the passphrase for water: WATER_2019.
Beers are for l33t h4x0rs only.
APIサーバが用意されていて、ビール用のパスフレーズを当てればいいらしい。かつそれはl33t h4x0rsで構成される
ソースコードも提供されていたらしい。
generateEcryptedVoucherで好きな文字列 + "||" + couponCodeを暗号化できる
暗号化された文字列と好きなpassphraseを与えてredeemEncryptedVoucherで復号できる
復号したcouponCodeがbeerのcouponCodeなら勝ち(あるいは別の方法でbeerのcouponCodeを求めても勝ち)
from flask import Flask,request,abort import gnupg import time app = Flask(__name__) gpg = gnupg.GPG(gnupghome="/tmp/gpg") couponCodes = { "water": "WATER_2019", "beer" : "█████████████████████████████████" # REDACTED } @app.route("/generateEncryptedVoucher", methods=['POST']) def generateEncryptedVoucher(): content = request.json (recipientName,drink) = (content['recipientName'],content['drink']) encryptedVoucher = str(gpg.encrypt( "%s||%s" % (recipientName,couponCodes[drink]), recipients = None, symmetric = True, passphrase = couponCodes[drink] )).replace("PGP MESSAGE","DRINK VOUCHER") return encryptedVoucher @app.route("/redeemEncryptedVoucher", methods=['POST']) def redeemEncryptedVoucher(): content = request.json (encryptedVoucher,passphrase) = (content['encryptedVoucher'],content['passphrase']) # Reluctantly go to the fridge... time.sleep(15) decryptedVoucher = str(gpg.decrypt( encryptedVoucher.replace("DRINK VOUCHER","PGP MESSAGE"), passphrase = passphrase )) (recipientName,couponCode) = decryptedVoucher.split("||") if couponCode == couponCodes["water"]: return "Here is some fresh water for %s\n" % recipientName elif couponCode == couponCodes["beer"]: return "Congrats %s! The flag is INS{%s}\n" % (recipientName, couponCode) else: abort(500) if __name__ == "__main__": app.run(host='0.0.0.0')
ソースコードではpython-gnupgでgpgによる暗号化を行っているので少し調べてみる(python-gnupgで複数のライブラリがヒットするが pypiに乗っているやつを信じた https://bitbucket.org/vinay.sajip/python-gnupg )。まずキーワード引数のgnupghomeだが、ここにPGP鍵などを保存するという設定(GPGでは他人の公開鍵をインポートして一箇所に置き、どこからでも呼び出せるようにしている)。
ソースコード中のpython-gnupgの使われ方はgpg.encryptとgpg.decryptだが、encyptでは受け取った文字列A(recipientsName)とドリンク名(waterかbeer)を結合した文字列を暗号化している。オプション引数が recipients=None, symmetric=True, passphrase=couponCode
になっている。ドキュメント (https://pythonhosted.org/gnupg/gnupg.html#gnupg.GPG.encrypt )によればsymmetricをTrueにしたときはrecipientsはNoneで良くて、このときは対称性暗号のCAST5を用いると書いてあるが、実際にはgpgのデフォルトの暗号化方式を使用しているだけであって、今ではAES256が用いられている。
怪しいのは暗号化の部分で recipeintsName + "||" + couponCodes drink としているところだけど、例えば recipeintsNameを"hoge||piyo"みたいにしてもdecrypt+split時にはcouponCodeが["piyo", couponCode]
になるだけで特に嬉しくはない
とりあえず適当なrecipientsNameを与えてwaterを注文してみる
import requests URL = "http://localhost:5000/" def gen(recipientName, drink): r = requests.post(URL + "generateEncryptedVoucher", json={ "recipientName": recipientName, "drink": drink, }) return r print(gen("hoge", "water").text)
結果はこんな感じ
-----BEGIN DRINK VOUCHER----- jA0ECQMCaC6Nx8qXRb3/0kUBD0AUujBnQeYlKdwmtuGtLMx51nY4Qhq8oXuV7sHI Oe9cB4cje+ql4+wCSEAB/OVMB85sdJHFshrBZ55Rr3D6kgHfadA= =Mi/J -----END DRINK VOUCHER-----
pgpdumpというツールでこのメッセージの詳細を見れるらしいので見てみる。(当然DRINK VOUCHERはPGP MESSAGEに置き換えている)
Old: Symmetric-Key Encrypted Session Key Packet(tag 3)(13 bytes) New version(4) Sym alg - AES with 256-bit key(sym 9) Iterated and salted string-to-key(s2k 3): Hash alg - SHA1(hash 2) Salt - 68 2e 8d c7 ca 97 45 bd Count - 65011712(coded count 255) New: Symmetrically Encrypted and MDC Packet(tag 18)(69 bytes) Ver 1 Encrypted data [sym alg is specified in sym-key encrypted session key] (plain text + MDC SHA1(20 bytes))
ちなみに gpg --decrypt
として暗号化に用いた鍵を入力するとちゃんと復号もできた
---
ここで手詰まりになって死んでいたんですが、他のwriteup(https://inshallhack.org/drinks_insomnihack_teaser_2019/ )をみるとどうやらgpgが暗号化前に圧縮を行っているのを利用するらしい。GnuPGのmanページ (https://www.gnupg.org/documentation/manpage.html#text-2-5 )によると-zというオプションがあり、デフォルトではLevel6のzlib圧縮が用いられている。なんかこういうのの攻撃ってあったきがする。圧縮サイドチャネル攻撃っていうらしい。
このPDF( https://www.ekoparty.org/archive/2012/CRIME_ekoparty2012.pdf ) の12枚目に書いてある。どうやら同じ方法が適用できそうな感じ。
最終的に色んなwriteupを見ながらこれに落ち着いたけど本当は G1MME_B33R_PLZ_1M_S0_V3RY_TH1RSTY なのに ||G1MME_B33RY_の時点で間違えていてうまく行かないなぁ。そもそも c * 5 ってやってるのがかなり筋悪な気はするけどたしかにこうしないと無限に候補が生成されるんだよな……。
import requests from oldknife import * URL = "http://localhost:5000/" def gen(recipientName, drink): r = requests.post(URL + "generateEncryptedVoucher", json={ "recipientName": recipientName, "drink": drink, }) return r flag = "||G1MME_B" while True: xs = [] for i, c in enumerate(flag_chars): r = gen(flag + c * 5, "beer").text xs.append((flag + c, len(r))) candidates = [] xs = sorted(xs, key=lambda x: x[1]) for x in xs: if x[1] == xs[0][1]: candidates.append(x[0]) else: break print(candidates) flag = input().strip()
https://inshallhack.org/drinks_insomnihack_teaser_2019/
https://github.com/newjam/insomnihack-teaser-ctf-2019/tree/master/drinks