# WTF Solidity 合约安全 S06. 签名重放 **Published by:** [0xAA](https://paragraph.com/@wtfacademy/) **Published on:** 2022-11-02 **URL:** https://paragraph.com/@wtfacademy/wtf-solidity-s06 ## Content 我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:@0xAA_Science|@WTFAcademy_ 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲,我们将介绍智能合约的签名重放(Signature Replay)攻击和预防方法,它曾间接导致了著名做市商 Wintermute 被盗2000万枚 $OP。签名重放​上学的时候,老师经常会让家长签字,有时候家长很忙,我就会很“贴心”照着以前的签字抄一遍。某种意义上来说,这就是签名重放。 在区块链中,数字签名可以用于识别数据签名者和验证数据完整性。发送交易时,用户使用私钥签名交易,使得其他人可以验证交易是由相应账户发出的。智能合约也能利用 ECDSA 算法验证用户将在链下创建的签名,然后执行铸造或转账等逻辑。更多关于数字签名的介绍请见WTF Solidity第37讲:数字签名。 数字签名一般有两种常见的重放攻击:普通重放:将本该使用一次的签名多次使用。NBA官方发布的《The Association》系列 NFT 因为这类攻击被免费铸造了上万枚。跨链重放:将本该在一条链上使用的签名,在另一条链上重复使用。做市商 Wintermute 因为跨链重放攻击被盗2000万枚 $OP。漏洞合约例子​下面的SigReplay合约是一个ERC20代币合约,它的铸造函数有签名重放漏洞。它使用链下签名让白名单地址 to 铸造相应数量 amount 的代币。合约中保存了 signer 地址,来验证签名是否有效。// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; // 权限管理错误例子 contract SigReplay is ERC20 { address public signer; // 构造函数:初始化代币名称和代号 constructor() ERC20("SigReplay", "Replay") { signer = msg.sender; } /** * 有签名重访漏洞的铸造函数 * to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 * amount: 1000 * 签名: 0x5a4f1ad4d8bd6b5582e658087633230d9810a0b7b8afa791e3f94cc38947f6cb1069519caf5bba7b975df29cbfdb4ada355027589a989435bf88e825841452f61b */ function badMint(address to, uint amount, bytes memory signature) public { bytes32 _msgHash = toEthSignedMessageHash(getMessageHash(to, amount)); require(verify(_msgHash, signature), "Invalid Signer!"); _mint(to, amount); } /** * 将to地址(address类型)和amount(uint256类型)拼成消息msgHash * to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 * amount: 1000 * 对应的消息msgHash: 0xb4a4ba10fbd6886a312ec31c54137f5714ddc0e93274da8746a36d2fa96768be */ function getMessageHash(address to, uint256 amount) public pure returns(bytes32){ return keccak256(abi.encodePacked(to, amount)); } /** * @dev 获得以太坊签名消息 * `hash`:消息哈希 * 遵从以太坊签名标准:https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] * 以及`EIP191`:https://eips.ethereum.org/EIPS/eip-191` * 添加"\x19Ethereum Signed Message:\n32"字段,防止签名的是可执行交易。 */ function toEthSignedMessageHash(bytes32 hash) public pure returns (bytes32) { // 32 is the length in bytes of hash, // enforced by the type signature above return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); } // ECDSA验证 function verify(bytes32 _msgHash, bytes memory _signature) public view returns (bool){ return ECDSA.recover(_msgHash, _signature) == signer; } 注意 铸造函数 badMint() 没有对 signature 查重,导致同样的签名可以多次使用,无限铸造代币。 function badMint(address to, uint amount, bytes memory signature) public { bytes32 _msgHash = toEthSignedMessageHash(keccak256(abi.encodePacked(to, amount))); require(verify(_msgHash, signature), "Invalid Signer!"); _mint(to, amount); } Remix 复现​1. 部署 SigReplay 合约,签名者地址 signer 被初始化为部署钱包地址。2. 利用getMessageHash函数获取消息。3. 点击 Remix 部署面板的签名按钮,使用私钥给消息签名。4. 反复调用 badMint 进行签名重放攻击,铸造大量代币。预防办法​签名重放攻击主要有两种预防办法:将使用过的签名记录下来,比如记录下已经铸造代币的地址 mintedAddress,防止签名反复使用:mapping(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 将 nonce (数值随每次交易递增)和 chainid (链ID)包含在签名消息中,这样可以防止普通重放和跨链重放攻击: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++; } 总结​这一讲,我们介绍了智能合约中的签名重放漏洞,并介绍了两个预防方法:将使用过的签名记录下来,防止二次使用。将 nonce 和 chainid 包含到签名消息中。 ## Publication Information - [0xAA](https://paragraph.com/@wtfacademy/): Publication homepage - [All Posts](https://paragraph.com/@wtfacademy/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@wtfacademy): Subscribe to updates - [Twitter](https://twitter.com/0xAA_Science): Follow on Twitter