每日一学
最近学习了合约中整型的溢出攻击和签名重放攻击
原理:
Solidity中uint8,取值范围为0-255,一个数字num = 10,将num = num - 20,那么num = 245,同样上溢也如此。
contract A{
uint8 public totalSupply;
mapping(address => uint8) public balanceOf;
constructor(uint8 _totalSupply){
balanceOf[msg.sender] = totalSupply = _totalSupply;
}
function transfer(address to,uint8 amount)external returns(bool){
unchecked{
require(balanceOf[msg.sender] - amount >= 0,"have no enougth token");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
}
return true;
}
}
注:使用unchecked{},可以使内部的代码会避开整型溢出攻击,否则在好像0.8.0后就会报错。关键代码,balanceOf[msg.sender] - amount >= 0,这段代码无论如何都可以通过,所以会被攻击
原理:
1,一笔交易验证过程中,用户对一个msg(假设只包括 to_ 和amount_两个)的hash签名为signature,然后用户发送hash,signature,signer(公钥)去验证。
2,验证方通过hash和signature来恢复出用户的公钥来与signer对比,相同则说明用户是私钥的拥有者。
3,验证通过。然后验证方让用户去mint,第一次用户mint了100个token,接着用户再去mint,由于msg的hash也没有变,所以签名也不会变,验证也会通过,用户又mint了。
正常的是为我们希望用户mint一次后就不可以mint了,第二次的signature应该跟第一次的signature不相同。造成函数的bug,使得签名可以重放
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
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("Signature","SR"){
signer = msg.sender;
}
/*
一笔交易的签名,可以重复用多次,我们希望的是一个签名用一次
*/
// function badMint(address to_, uint256 amount_, bytes memory signature_)external{
// //获得交易信息
// bytes32 hash = getMsgHash(to_,amount_);
// //获得交易以太坊签名
// bytes32 ethHash = getEthMsgHash(hash);
// //验证公钥
// require(vertify(ethHash, signature_),"have no right");
// //mint
// _mint(to_,amount_);
// }
uint public nonce = 0;
uint256 public blockChainID = 57210;
function goodMint(address to_,uint256 amount_,bytes memory signature_)public {
//获得交易信息
bytes32 hash = getMsgHash(to_, amount_, nonce, blockChainID);
//获得交易的以太坊签名
bytes32 ethHash = getEthMsgHash(hash);
//验证公钥
require(vertify(ethHash, signature_));
//mint
_mint(to_, amount_);
nonce++;
}
//5b地址 1000 hash:0xb4a4ba10fbd6886a312ec31c54137f5714ddc0e93274da8746a36d2fa96768be
//signature:0x5a4f1ad4d8bd6b5582e658087633230d9810a0b7b8afa791e3f94cc38947f6cb1069519caf5bba7b975df29cbfdb4ada355027589a989435bf88e825841452f61b
function getMsgHash(address to_,uint256 amount_,uint256 nonce_,uint256 blockChainID_)public pure returns(bytes32){
return keccak256(abi.encodePacked(to_,amount_,nonce_,blockChainID_));
}
function getEthMsgHash(bytes32 hash_)public pure returns(bytes32){
string memory string_= "\x19Ethereum Signed Message:\n32";
return keccak256(abi.encodePacked(string_,hash_));
}
function vertify(bytes32 hash_,bytes memory signature_)public view returns(bool){
return ECDSA.recover(hash_,signature_) == signer;
}
}
注:
这里面的badmint(),发送的消息只需要to_,amount_,如果只是这两个参数进行签名的话,无论调用mint多少次,这里面的ethHash都会跟signature恢复出公钥。相比与goodmint()里面,每调用一次nonce就会改变,让第二次的ethHash跟第一次不相同,虽然signature跟第一次相同,到那时它们两个已经不能生出我们的公钥了。
nonce可以防止单个合约的签名重放,chainID可以防止不同链之间跨链签名重放攻击。至于使用create2在同一个地址生成合约后在自我销毁的合约,nonce和chainID已经不能阻挡了,这里我思考的是加入一个时间戳或者区块号(由于我还没太熟悉create2,不知道区块号可不可以改变)或其他一些线性的唯一的信息一起hash,是每一笔交易的signature都不相同,即使交易的内容相同,但是交易时的条件也不相同,从而避免签名重放。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!