之前的 文章 里有提到利用弱 k 推算 ECDSA 算法的私钥的理论基础,本文记录一下自己的实战过程。
思路
满足以下三种场景之一就可以推算出相关地址的私钥
- A 使用同一个 k 签了两笔交易
- A 和 B 都使用同一个 k1 签了一笔交易,然后又都使用了同一个 k2 签了另一笔交易
- 在已知 B 私钥的情况下,A 和 B 都使用了同一个 k 签了一笔交易
关于 k
ECDSA 签名的本质其实就是两个随机数(私钥)对一段文本(准确来讲,是文本的 hash 值)的数学运算(详情参见 旧文 ),这两个随机数一个就是用户手上的私钥,另一个就是 k,知道任意一个,就能推算出另一个。
签名分为 r 和 s 两个字段,其中 r 就是 k 的公钥(k 点乘 G)的 x 坐标 (公钥本质是一个点),也就是说只要发现两笔不同的交易的签名结果里的 r 部分相同,那就可以断定这两笔交易用了同一个 k 签名
以 BTC 为例,每笔交易的签名都在每个 input 的 ScriptSig 字段里(非隔离见证交易),我们可以收集全网所有的签名,找出 r 相同的交易,然后根据上面提到的场景 1 和 2 计算出私钥,接着再用已知的私钥继续迭代计算出满足场景 3 的私钥。
场景 1 和 2 的私钥推导代码在前面的文章里已经贴出来了,场景 3 也很简单(先用 B 的私钥算出 k ,再用 k 算出 A)
实操
理论没问题,接着就是实操,其实所有用到 ECDSA 算法的都存在这个问题,所以 BTC、LTC、ETH 等都可以,但是考虑到海量的数据,还是先暂时只撸 BTC 上的交易。
配合 AI 写代码很快,只花了一天基本逻辑就写完了,后续就是遇到奇葩的交易再反复调试,增加代码健壮性。下面是一些总结和有趣的发现:
- 运行了 3 周左右,才扫到 68w 的高度,当前最新区块高度 88w 多,区块数据总共只有 752G,但我的数据库已经超过 1T 了,因为加了不少索引(前几天发现有个字段忘记加索引,然后花了 5 个小时才加成功…数据膨胀了近 300 G)
- 共用过同一个 k 的地址有 3549 个,其中私钥已被成功破解的有 1267 个,可惜的是,目前为止被破解的私钥地址上面都没有钱,好多都是很早就 被别人撸过了 的
- 总的来讲,python-bitcoinlib 比 bitcoinlib 好用,但还是有不少交易两个都解析不了,需要自己手动写解析逻辑
- 有一些重复的 r 成周期性表现,猜测是在云服务器上使用钱包,然后服务器因为某些原因被还原/或者新建机器使用了旧的快照,导致随机算法来了轮二周目
- 更多的重复 r 是钱包软件 bug 导致的,blockchain.info 就爆过这个问题
- 从私钥恢复出的地址可能会遇到和链上显示的地址不匹配的情况,一般都是因为:
- 一般情况下 SIGHASH_SINGLE 的签名方式输入的索引应该和输出的索引一致,但是为了兼容,当遇到不一致的情况节点也会通过验证,但此时这个 SIGHASH_SINGLE 其实并没有做到应有的保护,其签名 hash 是一个 固定的 1,理论上这个可以发起重放攻击,不过我看他地址上也没啥钱就懒得折腾了
后续
巧合的是,在我撸 btc 上的签名的时候,慢雾刚好发了一篇关于弱 k 的 文章,这是一个利用前端签名库的 bug,诱导用户对一个精心构造好的内容进行签名,导致使用了相同的 k ,进而导致私钥被计算出来的攻击
这里多提一句,ECDSA 每次对同一个内容的签名结果都是不同的,这些签名都可以被验证成功,因为每次签名 k 都会重新随机选取。
但使用 metamask 对同一个内容签名的时候,出来的结果始终是一样的,这是因为它用了确定性随机数 k,也就是 RFC6979 ,它大致逻辑是把签名内容和私钥一起做一次 hash,把这个 hash 当作 k 来使用,这样就能在不损害安全性的前提下改善用户体验,这是一个很有意思的 trick,是不是很精妙?撸完 btc ,又花半天时间把 ltc 的代码也写完了,一来看看 ltc 上的 r 重复情况;二来还可以检测一下跨链的重复 r,因为有的人会用 hd 钱包,一旦 hd 钱包的签名组件有 bug ,就会导致跨链 r 重复,这些数据同样可以用来计算私钥。目前扫到 250w 块左右(最新 280w),只发现了 8 条跨链重复的 r
其实 hd 钱包正确的使用姿势很复杂,比如,用完一个地址就要丢弃掉,后续不能再使用(隐私保护),扫余额的时候要扫未来 20 个地址的余额等等,详细的约束可以去看下 BIP44。但其实真正实现的时候,并没有严格按照协议来做