#!/usr/bin/env python3 import random, sys from binascii import hexlify, unhexlify from ecdsa import SigningKey, NIST192p from flag import flag secret_multiplier = random.getrandbits(101) def menu(): menu = [exit, signMessage, verifyMessage, getFlag, sys.exit] print(""" bakflip&sons Signature Scheme 1) Sign Message 2) Verify Signature 3) Get Flag 4) Exit [ecdsa@cryptolab]# """, end = "") choice = int(input()) menu[choice]() def signMessage(): print(""" Sign Message Service - courtsy of bakflip&sons """) message = input("Enter a message to sign: ").encode() if message == b'please_give_me_the_flag': print("\n\t:Coughs: This ain't that easy as Verifier1") sys.exit() secret_mask = int(input("Now insert a really stupid value here: ")) secret = secret_multiplier ^ secret_mask signingKey = SigningKey.from_secret_exponent(secret) signature = signingKey.sign(message) print("Signature: ", hexlify(signature).decode()) def verifyMessage(): raise( NotImplementedError( "Geez! We are working round the clock to get this Beetle fixed." ) ) def getFlag(): print(""" BeetleBountyProgram - by bakflip&sons Wanted! Patched or Alive- $200,000 Submit a valid signature for 'please_give_me_the_flag' and claim the flag """) signingKey = SigningKey.from_secret_exponent(secret_multiplier) verifyingKey = signingKey.verifying_key try: signature = unhexlify(input("Forged Signature: ")) if verifyingKey.verify(signature, b'please_give_me_the_flag'): print(flag) except: print("Phew! that was close") if __name__=="__main__": for i in range(73): menu()
overview
73回まで、
sign
またはgetFlag
ができるsign
と を入力にとって、 とし、
NIST192p
という EllipticCurve 上で ECDSA によって へ署名するただし、平文に
please_give_me_the_flag
は渡せない
getFlag
とし、署名を検証する。
please_give_me_the_flag
という平文の検証に成功するとフラグ
考察
secret
がわかれば解けそうsign
による署名でどうにかしてsecret
を求めることができないか- 平文と
mask
を渡せることの意図は……?
- 平文と
NIST192p
を使っているので、単純な楕円曲線に対するアプローチではなさそう……?何度も署名できるので、一部分ずつ
secret
を特定する OR 何回かのリクエストの結果を組み合わせて復元する……?mesg
は固定して良さそうと による署名がそれぞれあった時なにかできることがあるか?
なので と について試してなんかする、みたいな感じか
がわかってないのでそれは難しそう……?
だめそう……
作戦1
は偶奇が分かっているので の偶奇がわからないか?
がそれぞれ偶数なら左辺は偶数になるので、右辺で が奇数なら、 の偶奇がわかりそう
73回の制限が厳しい
解法
署名時の の算出における について を考える
- r, sからP=dGを求める を使った
mask = 1
のとき が奇数であれば 、 が偶数であれば であるからmask = 0
のときの署名用の鍵 と比較して の偶奇を得られる
同様にして
mask = 1 << i
として送ると、署名用の鍵が になるので の bit目の偶奇を得られるので、これを101回やることで 全体を復元できる
が、今、73回までしかループを回せない
そこで、
mask = 0b11 << 2*i
として送ることで、一度のクエリで2bitの情報を得ることにするの該当するbitが
00
なら となるの該当するbitが
01
なら となるの該当するbitが
10
なら となるの該当するbitが
11
なら となる
from ecdsa import SigningKey, NIST192p from ecdsa.ecdsa import Signature from hashlib import sha1 from itertools import product from ptrlib import Socket mesg = b"hello" h = int.from_bytes(sha1(mesg).digest(), "big") G = NIST192p.generator sock = Socket("localhost", 19999) def oracle(mask): sock.sendlineafter("# ", "1") # sign sock.sendlineafter(": ", mesg) sock.sendlineafter(": ", str(mask)) rs = bytes.fromhex(sock.recvlineafter(": ").decode()) return int.from_bytes(rs[:len(rs)//2], "big"), int.from_bytes(rs[len(rs)//2:], "big") def recover_public_keys(r, s): return [key.point for key in Signature(r, s).recover_public_keys(h, NIST192p.generator)] r, s = oracle(0) dGs = recover_public_keys(r, s) r, s = oracle(1) d_Gs = recover_public_keys(r, s) for P, P_ in product(dGs, d_Gs): if P_ == P + G or P_ == P + -G: break else: raise Exception("a") print("[+] P = ({}, {})".format(P.x(), P.y())) d = 0 for k in range(0, 101, 2): r, s = oracle(0b11 << k) d_Gs = recover_public_keys(r, s) found = False for d_G in d_Gs: cands = [ P + (+2**k + 2**(k+1))*G, P + (-2**k + 2**(k+1))*G, P + (+2**k - 2**(k+1))*G, P + (-2**k - 2**(k+1))*G, ] try: i = cands.index(d_G) d = d | (i<<k) break except ValueError: pass else: raise Exception("b at {}".format(k)) print("[+] d = {}".format(d)) # make sign msg = b'please_give_me_the_flag' key = SigningKey.from_secret_exponent(d) signature = key.sign(msg) sock.sendlineafter("# ", "3") # getFlag sock.sendlineafter(": ", signature.hex()) print(sock.recvline())