S06. 签名重放
WTF Solidity 合约安全: S06. 签名重放
我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity 合约安全”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。
所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity
这一讲,我们将介绍智能合约的签名重放(Signature Replay)攻击和预防方法,它曾间接导致了著名做市商 Wintermute 被盗2000万枚 $OP。
签名重放
上学的时候,老师经常会让家长签字,有时候家长很忙,我就会很“贴心”照着以前的签字抄一遍。某种意义上来说,这就是签名重放。
在区块链中,数字签名可以用于识别数据签名者和验证数据完整性。发送交易时,用户使用私钥签名交易,使得其他人可以验证交易是由相应账户发出的。智能合约也能利用 ECDSA
算法验证用户将在链下创建的签名,然后执行铸造或转账等逻辑。更多关于数字签名的介绍请见WTF Solidity第37讲:数字签名。
数字签名一般有两种常见的重放攻击:
- 普通重放:将本该使用一次的签名多次使用。NBA官方发布的《The Association》系列 NFT 因为这类攻击被免费铸造了上万枚。
- 跨链重放:将本该在一条链上使用的签名,在另一条链上重复使用。做市商 Wintermute 因为跨链重放攻击被盗2000万枚 $OP。
漏洞合约例子
下面的SigReplay
合约是一个ERC20
代币合约,它的铸造函数有签名重放漏洞。它使用链下签名让白名单地址 to
铸造相应数量 amount
的代币。合约中保存了 signer
地址,来验证签名是否有效。
1 | // SPDX-License-Identifier: MIT |
注意 铸造函数 badMint()
没有对 signature
查重,导致同样的签名可以多次使用,无限铸造代币。
1 | function badMint(address to, uint amount, bytes memory signature) public { |
Remix
复现
1. 部署 SigReplay
合约,签名者地址 signer
被初始化为部署钱包地址。
2. 利用getMessageHash
函数获取消息。
3. 点击 Remix
部署面板的签名按钮,使用私钥给消息签名。
4. 反复调用 badMint
进行签名重放攻击,铸造大量代币。
预防办法
签名重放攻击主要有两种预防办法:
-
将使用过的签名记录下来,比如记录下已经铸造代币的地址
mintedAddress
,防止签名反复使用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24mapping(address => bool) public mintedAddress; // 记录已经mint的地址
function goodMint(address to, uint amount, bytes memory signature) public {
bytes32 _msgHash = toEthSignedMessageHash(getMessageHash(to, amount));
require(verify(_msgHash, signature), "Invalid Signer!");
// 检查该地址是否mint过
require(!mintedAddress[to], "Already minted");
// 记录mint过的地址
mintedAddress[to] = true;
_mint(to, amount);
}
```solidity
2. 将 `nonce` (数值随每次交易递增)和 `chainid` (链ID)包含在签名消息中,这样可以防止普通重放和跨链重放攻击:
```solidity
uint nonce;
function nonceMint(address to, uint amount, bytes memory signature) public {
bytes32 _msgHash = toEthSignedMessageHash(keccak256(abi.encodePacked(to, amount, nonce, block.chainid)));
require(verify(_msgHash, signature), "Invalid Signer!");
_mint(to, amount);
nonce++;
} -
对于由用户输入
signature
的场景,需要检验signature
的长度,确保其长度为65bytes
,否则也会产生签名重放问题。1
2
3
4function mint(address to, uint amount, bytes memory signature) public {
require(signature.length == 65, "Invalid signature length");
...
}
总结
这一讲,我们介绍了智能合约中的签名重放漏洞,并介绍了三个预防方法:
-
将使用过的签名记录下来,防止二次使用。
-
将
nonce
和chainid
包含到签名消息中。 -
对于由用户输入
signature
的场景,需要检验signature
的长度,确保其长度为65bytes
,否则也会产生签名重放问题。