Insomni'hack Teaser 2019|Drinks

#Insomni'hack

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-gnupggpgによる暗号化を行っているので少し調べてみる(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

カテゴリ一覧