2000 万枚 OP 被盗细节分析

背景

地址列表

地址 说明
0x8bcfe4f1 攻击者 EOA 地址
0x60b28637 攻击者 EOA 地址
0xe7145dd6 攻击者的合约地址地址,可以循环调用 gosis factory 的 createProxy 接口,也有代理转账接口(用于转移 100 万 op 的)
0x76e2cfc1 gnosis factory 合约,用于生成多签合约的
0x4f3a120E 受害地址,被攻击者抢先部署了自己的恶意合约
0x1aa7451d gnosis 合约的部署地址之一
0x34cfac64 本次攻击中没有登场
0x34f5c67d 本次攻击中没有登场

攻击时间线

  1. 2022-06-01 2:46:22 0x8bcfe4f1 从 tornado 拿到 0.1 个 eth 用于部署 0xe7145dd6 攻击合约。转账 tx 部署 tx
  2. 2022-06-04 12:04:062022-06-04 12:09:31 攻击者用连续三笔交易从 tornado 转了累计大概 11 个 eth 的启动资金到 0x60b28637 tx
  3. 2022-06-04 16:11:46 0x8bcfe4f1 从 0x60b28637 拿到 0.5 个 eth 的启动资金,tx
  4. 2022-06-05 3:50:17 0x60b28637 给 0x1aa7451d 转了 0.1 eth 用于部署 0x34cfac64 合约
  5. 2022-06-05 3:50:48 0x1aa7451d 创建了 0x34cfac64 合约 tx
  6. 2022-06-05 3:53:48 0x60b28637 给 0x1aa7451d 又转了 0.1 eth
  7. 2022-06-05 3:54:04 攻击者使用 0x1aa7451d 误调用一个 EOA 地址 0x34f5c67d 的 setImplementation(string,address) 接口,试图设置 logic 合约为 0x34cfac64,但是失败了(这肯定的)。 tx
  8. 2022-06-05 3:54:19 攻击者用 0x1aa7451d 创建了 gnosis factory : 0x76e2cfc1
  9. 2022-06-05 03:54:372022-06-05 03:56:13 攻击开始,最终攻击成功的 nonce 为 8884 攻击成功的 tx
  10. 2022-06-05 03:58:32 0x8bcfe4f1 调用了 0x4f3a120 合约里的函数在这笔 交易 给 0x60b28637 转了 100 万 op
  11. 2022-06-05 4:00:502022-06-05 4:02:53 分两笔通过去中心化交易聚合器将 100 万 op 换成 eth,最终累计转走大概 730 个 eth

攻击细节

攻击成功的 nonce

我用 python 写了个计算攻击成功的 nonce 值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from web3 import Web3
import rlp
from rlp.sedes import (
binary,
big_endian_int,
)

class GenContractAddr(rlp.Serializable):
fields = [('address', binary), ('nonce', big_endian_int)]

def get_nonce():
target = "4f3a120e72c76c22ae802d129f599bfdbc31cb81"
contract = "76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b"
for i in range(10000):
nonce = i
addr = GenContractAddr(bytes.fromhex(contract), nonce)
tx_addr = Web3.keccak(rlp.encode(addr))
if tx_addr.hex()[26:] == target:
print("found nonce: ", nonce)
break

get_nonce()
输出的结果就是 8884(我人肉数了下链上的攻击次数,数字完全吻合),这数字攻击者早就(像我上面的代码一样)链下计算好了,链上那么多交易仅仅是为了将 nonce 刷到 8884,所以没有什么运气成分,全都是精心计算好了的。

转账 100 万 OP

攻击者在这笔 交易 将钱转走,(尽管这个操作不关键了,但是)我们可以尝试分析一下如何转出的:

  1. 将交易的 data 的前 4 个字节:0xad8d5f48 贴到这个 网址 撞一下,看看函数签名长啥样,确实有结果:exec(address,bytes,uint256)
  2. 然后解出所有参数(可以在 这里 解):
    • 第一个参数:0x420000000000…000000000042 (OP 合约地址)
    • 第二个参数:0xa9059cbb00000…cecceda1000000 (将会被代理执行的 calldata)
    • 第三个参数:0 (我也猜不出来干嘛用的)
  3. 继续用上面的办法解第二个参数,发现头 4 个字节代表的函数签名是:transfer(address,uint256)
  4. 接着继续拆解参数:
    • 第一个参数: 0x60B28637879B5…FFbf7dbA5107 (收款地址)
    • 第二个参数: 1000000000000000000000000 (正是那 100 万 OP)

小插曲

攻击者的第 4~7 步非常有意思,整个攻击过程中好像没有必要这么大费周章(可以用另外的操作来刷新 nonce 到需要的数值)。之前我一直以为这是一笔误操作,那是因为 0x34f5c67d 在 eth 上是有合约的,但是在 op 上还没部署合约呢,以为攻击者这里是搞错链环境了。

setImplementation 的原因:

将这笔操作的 交易 里的 data 的前 4 字节在 https://sig.eth.samczsun.com/ 查一下,就能知道是 setImplementation(string,address) 接口,再分析后面的参数不难得出,攻击者在试图设置 0x34f5c67d 的后端实现合约为 0x34cfac64

后来经过朋友圈大佬们的提醒,对照着主网 0x1aa7451d 的操作记录,发现攻击者基本是在复制 0x1aa7451d 在主网的操作流水,前几笔操作的 tx hash 确实一样,我不太明白这是如何做到的,理论上跨链重放攻击很早就避免了的。

谁是攻击者

gnosis 部署的各种合约在各链的地址都一样的原因在前面的背景资料里都有解释,我这里就不再详细说明了。

经过上面的分析,再加上朋友们的提醒,应该有下面三种可能:

  1. 攻击者重放攻击(虽然我还不清楚如何做到的),模拟了 0x1aa7451d 在主网的操作,强行创建了 gnosis factory 合约,为攻击搭建了平台
  2. gnosis 的 0x1aa7451d 持有者自己有问题或者 0x1aa7451d 的私钥泄漏
  3. 0x1aa7451d 的持有者利用重放漏洞贼喊捉贼,自导自演

第一种可能性会更高点吧

UPDATE 经过朋友的指点,大概明白了如何跨链重放的可能性是怎么来的了。尽管 EIP155 是 2016 年就有了的,0x1aa7451d 本人在主网部署的那笔 交易 是在 2019 年的,但是,EIP155 并不会拒绝继续使用旧的签名。所以用旧的签名方法依旧可以正常上链,而 0x1aa7451d 好巧不巧当时用的签名方式就是会被重放攻击的旧的签名……

人肉查看一笔交易的 v 值,可以在 etherscan.io 的交易详情页面点击右上角的三个点,选择 Get Raw Tx Hex,然后取倒数第 133 和 134 个字符。当然也可以将 raw data 粘贴到 这里 直接解析出内容来。

不过当下的 op 网络还没有开放 Get Raw Tx Hex 这个功能,顺手写个代码来取 v 值

1
2
3
4
5
6
7
8
from web3 import Web3
import web3
def get_sig(tx_hash):
w3 = Web3(web3.HTTPProvider('https://mainnet.optimism.io/'))
tx = w3.eth.getTransaction(tx_hash)
print(tx.v)

get_sig("0x4828032153bae611838012dbe6b35d310c8cebac72504448a3fbe8149623ea3f")
目前来说,当前 ETH 主网链上会存在以下三种 v 值:

  • 27/28 不遵循 EIP155 协议的比较老的 交易,可以被重放
  • 37/38 遵循 EIP155 协议的 交易,因为 v 值包含了 chainid 信息,所以无法被重放
  • 0/1 遵循 EIP1559 协议的 交易,因为 chainid 作为新字段被单独提出,并参与签名过程了,所以也无法被重放

关于 v 值的演变历史可以参见站点的另一篇文章( eth 签名机制 ) 里有提到

不过目前还有一些疑点:

  1. 我 google 了一下从 op 官方打钱给 wintermute 开始到攻击者开始准备攻击的第一步这段时间内(从 5.24 到 6.1)相关的资讯,发现相关项目方(op 官方和 wintermute)并没有大张旗鼓地做关于这笔 2000 万 op 资金流向的 PR,攻击者是如何知道这个转账的信息的呢?是专门部署了监控脚本监听 op 官方账户 的资金出入记录么?
  2. wintermute 为什么要提供 0x4f3a120E 这个奇怪的地址?有人说是因为 wintermute 用了老版本的 gnosis client 端导致的,但我很好奇什么样的老逻辑会生成一个离当前 nonce 还有 8k 多距离的一个多签合约地址?所以我觉得这个可能性不大,结合这个地址在 ETH 主网确实已经属于 wintermute 管理,所以更大的可能性是他们以为这个地址在其他 evm 链同样属于他们管理,却忽略了这是一个合约地址,犯了个低级错误。

综上,个人猜测这次攻击的结果可能是:

  1. wintermute 操作失误 且 外面有一个嗅觉灵敏、一直伺机而动的黑客
  2. wintermute 操作失误 且 0x1aa7451d 的持有人有问题(要么他跟黑客有关联,要么他的私钥泄漏了)
  3. wintermute 故意这么做的,通过这种方式监守自盗,将责任推给一个根本不存在的黑客

无论什么结果,wintermute 这锅是甩不掉了,同时 gnosis 的做法其实也不是很严谨,大家都挺业余的。