漏洞原理
关于签名重放,从字面意思不难理解,就是相同的签名被重复使用,攻击者利用已经被使用过的签名获利。
漏洞原因
有以下几种情况可能造成签名重放:
1.合约未追踪已被使用的消息签名,导致消息被重用。
2.签名被其他合约使用。
3.签名可能被跨链使用。
4.由于签名的延展性,由一个使用过的签名可以推出另一个有效签名。
漏洞演示与解决
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;
contract example{
mapping (address => uint256) public balanceOf;
constructor(){
balanceOf[msg.sender] = 1000;
}
//0xd957e414d8e68839bcb16e9fafe1806d8461060c8c1ff01faff1f93ddc7c660a39d34342848067a3c94974e8d5523c28cb2d8c3a9706008808a4b5d120b541421c
function transfer (address from, address to ,uint256 amount,bytes memory signature) public returns(bool){
bytes32 msghash = getMsgHash(from,to,amount);
require(verify(from, msghash, signature),"InVaild Signer!");
balanceOf[from] -= amount;
balanceOf[to] += amount;
return true;
}
function getMsgHash(address from,address to,uint256 amount) public pure returns(bytes32 hash){
hash = keccak256(abi.encodePacked(from,to, amount));
}
function verify(address owner,bytes32 msghash,bytes memory signature) public pure returns(bool success){
if (signature.length == 65) {
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
address signer = ecrecover(msghash, v, r, s);
if (signer == owner) {
success = true;
}
}
}
}
此合约提供一个函数来实现转账,需要from地址向to地址提供链下签名,这样to地址就可以调用transfer函数来转账。
现在合约的部署者address1(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4)向address2(0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2)提供了这样一段签名信息,授权address2转移自己100代币
from:0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
to:0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
amount:100
signature:0xd957e414d8e68839bcb16e9fafe1806d8461060c8c1ff01faff1f93ddc7c660a39d34342848067a3c94974e8d5523c28cb2d8c3a9706008808a4b5d120b541421c
我们尝试一下,可以发现addres2可以通过调用transfer函数获得100代币。
但是由于未做任何限制,address2和链上任意其他地址可以重复使用这段签名,直到耗尽address1的余额。
那么如何防范这个问题呢?
可以在消息中增添一个nonce属性,然后追踪已经使用的消息,签名延展性也可以通过加入nonce解决。
+ mapping (bytes32 => bool) public usedHash;
+ function transfer (address from, address to ,uint256 amount,uint256 nonce,bytes memory signature) public returns(bool){
+ bytes32 msghash = getMsgHash(from,to,amount,nonce);
+ require(!usedHash[msghash],"Used Hash!");
require(verify(from, msghash, signature),"InVaild Signer!");
+ usedHash[msghash] = true;
balanceOf[from] -= amount;
balanceOf[to] += amount;
return true;
}
+ function getMsgHash(address from,address to,uint256 amount,uint256 nonce) public pure returns(bytes32 hash){
+ hash = keccak256(abi.encodePacked(from, to, amount, nonce));
}
这样在address1提供的相关签名就只能被使用一次
例:
from:0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
to:0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
amount:100
nonce:1
signature:0x32c07b0ddca1d561bec1fc351c9db1c1f82ece099e9ea40db668915c829a604b147fbcde21b77de31838d7f0c41ff8bf06703808d06caee54d4829d043f673f81c
下一个问题,如果存在其他合约和此合约有相同的消息格式,例如,另外一个合约2的消息打包也为`hash = keccak256(abi.encodePacked(from, to, amount, nonce));`并且恰好address1在合约2也有足够的代币余额,那么address2就可以在合约2重复使用这个签名,跨链重放也是此原理。可以通过部署两个相同的合约,签名在每个合约都可以使用来简单验证。
如何解决?
在打包消息时添加address(this)字段,此外,还可以加上chainId
function getMsgHash(address from,address to,uint256 amount,uint256 nonce) public view returns(bytes32 hash){
hash = keccak256(abi.encodePacked(block.chainid ,from, to, amount, nonce,address(this)));
}
总结
签名重放漏洞是利用已使用过的签名重复执行操作的安全漏洞。解决方法包括添加唯一nonce、使用链标识和合约地址等措施,以防止签名被重复使用和跨链攻击。
作者:张凯
编辑:舒婷
原文始发于微信公众号(ChainSecLabs):签名重放