Crew CTF 2022 - signsystem
TL;DR
- SECP112r1曲線を用いたECDSAで署名の生成と検証が出来、指定されたメッセージの署名を生成出来たらフラグが開示される
- 112個のメッセージと署名の組を入手出来ると、以降のnonceがこれら112個の署名で用いられたnonceの線形結合で表す事が出来る
- これを利用してNonceが同じ時のECDSAに対する攻撃と同じ要領で線形連立方程式を立てて解くと、署名鍵を導出出来るので任意のメッセージに対する署名を生成出来る
Prerequisite
- ECDSA
Writeup
問題のスクリプトは次の通り
import sys
import random
from hashlib import sha256
from Crypto.Util.number import inverse
import ecdsa
from secret import FLAG
curve = ecdsa.curves.SECP112r1
p = int(curve.curve.p())
G = curve.generator
n = int(curve.order)
class SignSystem:
def __init__(self):
self.key = ecdsa.SigningKey.generate(curve=curve)
self.nonce = [random.randint(1, n-1) for _ in range(112)]
def sign(self, msg):
e = int.from_bytes(sha256(msg).digest(), 'big') % n
h = bin(e)[2:].zfill(112)
k = sum([int(h[i])*self.nonce[i] for i in range(112)]) % n
r = int((k * G).x()) % n
s = inverse(k, n) * (e + r * self.key.privkey.secret_multiplier) % n
return (int(r), int(s))
def verify(self, msg, sig):
(r, s) = sig
e = int.from_bytes(sha256(msg).digest(), 'big')
if s == 0:
return False
w = inverse(s, n)
u1 = e*w % n
u2 = r*w % n
x1 = int((u1*G + u2*self.key.privkey.public_key.point).x()) % n
return (r % n) == x1
if __name__ == '__main__':
HDR = 'Welcome to sign system.'
print(HDR)
MENU = "1:sign\n2:verify\n3:getflag\n"
S = SignSystem()
try:
while(True):
print('')
print(MENU)
i = int(input('>> '))
if i == 1:
msghex = input('msg(hex): ')
sig = S.sign(bytes.fromhex(msghex))
print(f'signature: ({hex(sig[0])}, {hex(sig[1])})')
elif i == 2:
msghex = input('msg(hex): ')
sig0hex = input('sig[0](hex): ')
sig1hex = input('sig[1](hex): ')
result = S.verify(bytes.fromhex(msghex), (int(sig0hex, 16), int(sig1hex, 16)))
if result:
print('Verification success.')
else:
print('Verification failed.')
elif i == 3:
target = random.randint(2**511, 2**512-1)
print(f'target: {hex(target)}')
sig0hex = input('sig[0](hex): ')
sig1hex = input('sig[1](hex): ')
result = S.verify(bytes.fromhex(hex(target)[2:]), (int(sig0hex, 16), int(sig1hex, 16)))
if result:
print('OK, give you a flag.')
print(FLAG.decode())
break
else:
print('NG.')
break
except KeyboardInterrupt:
print('bye')
sys.exit(0)
except:
print('error occured')
sys.exit(-1)
次の3つのコマンドを実行出来る
- 署名: 任意のメッセージの署名を得られる
- 検証: 署名とメッセージを与えて検証出来る
- フラグ要求: ランダム生成されたメッセージの署名を与えるチャレンジが発生し、もし署名提出出来ればフラグが開示される。なお、成否に関わらずこのコマンドの実行後にプログラムは終了する。
問題のインスタンス起動時に乱数ベクトルが定義される。 (ハッシュ化済みの)メッセージをとおく。これに対してベクトルをのビット列をそのまま用いることで定義し、ECDSAに使うnonceをで生成する。
該当箇所はk = sum([int(h[i])*self.nonce[i] for i in range(112)]) % n
であり、これを見るとベクトルの内積で表すことが出来る事がわかる。
基底メッセージの取得
nonceの生成において都合が良い事にメッセージと乱数の内積を利用していることから、もし1次独立なメッセージを112個集めて基底とすれば、任意のメッセージのnonceをこの基底の線形結合で表すことが出来る。
具体的には基底となるメッセージのビット列をとし、あるメッセージのビット列をとして、この基底に対する係数ベクトルをとおくと、であるから、次のようになる。
ここでECDSAのnonceを、署名を、メッセージを、署名鍵をとおくと、の関係があったことから、と変形することで次のような行列に落とし込むことが出来る。
但し、事前に112個のビット列が一次独立なメッセージ(、ビット列は)とその署名を入手し、メッセージのビット列がであり、これを署名した結果をとする。
この式の左辺左側の行列の逆行列を両辺左側から掛ければ、が求められるので署名鍵であるが求められる。よって任意のメッセージに対して署名が生成出来るようになるので3:getFlag
コマンドで提示されたメッセージの署名を提出してフラグを得る。
Code
from pwn import remote
from hashlib import sha256
def choice(c):
sc.recvuntil(b">> ")
sc.sendline(str(c).encode())
def to_hex_bytes(msg):
hex_msg = hex(msg)[2:]
if len(hex_msg) % 2:
hex_msg = "0" + hex_msg
return hex_msg
def sign(msg):
choice(1)
sc.recvuntil(b"msg(hex): ")
hex_msg = to_hex_bytes(msg)
sc.sendline(hex_msg)
sc.recvuntil(b"signature: (")
r = int(sc.recvuntil(b",")[:-1], 16)
s = int(sc.recvuntil(b")")[:-1], 16)
return (r,s)
def hash(msg):
msg = to_hex_bytes(msg)
msg = bytes.fromhex(msg)
return int.from_bytes(sha256(msg).digest(), 'big') % order
def to_line(msg):
h = bin(hash(msg))[2:].zfill(112)
return list(map(int,h))
def calc_kG(msg,r,s,Q):
e = hash(msg)
w = inverse_mod(s, order)
u1 = e*w % order
u2 = r*w % order
return u1*G + u2*Q
p = 4451685225093714772084598273548427
order = 4451685225093714776491891542548933
a = 4451685225093714772084598273548424
b = 2061118396808653202902996166388514
gx = 188281465057972534892223778713752
gy = 3419875491033170827167861896082688
curve = EllipticCurve(GF(p), [a,b])
G = curve((gx, gy))
sc = remote("signsystem.crewctf-2022.crewc.tf", 1337)
# sc = remote("localhost", 13337)
msg = 0x114514
e = hash(msg)
r,s = sign(msg)
X = curve.lift_x(GF(p)(r))
w = inverse_mod(s, order)
u1 = e*w % order
u2 = r*w % order
Q = inverse_mod(u2, order) * ( X - u1 * G)
msg = 0x364364
r,s = sign(msg)
kG = calc_kG(msg,r,s,Q)
if r != int(kG.xy()[0]):
Q = inverse_mod(u2, order) * ( -X - u1 * G)
m_list = []
h_and_kgs = []
for i in range(112):
print(i)
msg = randint(1, order)
r,s = sign(msg)
kG = calc_kG(msg, r, s, Q)
assert r == int(kG.xy()[0])
m_list.append(to_line(msg))
h_and_kgs.append((hash(msg), (r,s), kG))
mat = matrix(GF(order), m_list)
mat_inv = mat^-1
msg = randint(1, order)
r,s = sign(msg)
target_line = to_line(msg)
v = vector(target_line, GF(order))
x = v * mat_inv
m_list = []
v_list = []
for i in range(112):
l = [0 for _ in range(113)]
l[i] = h_and_kgs[i][1][1]
l[-1] = (-h_and_kgs[i][1][0]) % order
v_list.append(h_and_kgs[i][0])
m_list.append(l)
l = [0 for _ in range(113)]
for i in range(112):
l[i] = x[i] * s % order
l[-1] = (-r) % order
m_list.append(l)
v_list.append(hash(msg))
mat = matrix(GF(order), m_list)
v = vector(v_list, GF(order))
x = (mat^-1) * v
d = int(x[-1])
print(x[-1])
choice(3)
sc.recvuntil(b"target: ")
target = int(sc.recvline(), 16)
k = randint(1, order)
R = k * G
r = R.xy()[0]
r = int(r)
s = inverse_mod(k, order) * (hash(target) + r * d) % order
s = int(s)
sc.recvuntil(b"sig[0](hex): ")
sc.sendline(hex(r)[2:].encode())
sc.recvuntil(b"sig[1](hex): ")
sc.sendline(hex(s)[2:].encode())
sc.interactive()
Flag
crew{w3_533_7h3_p0w3r_0f_l1n34r_4l63br4}
Thanks for reading! Read other posts?