每日一学-day004

  • 0xKk
  • 更新于 2024-04-09 23:29
  • 阅读 813

每日一学

最近学习了合约中整型的溢出攻击和签名重放攻击

整型溢出攻击(Integer Overflow)

原理:
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,这段代码无论如何都可以通过,所以会被攻击

签名重放攻击(Signature Replay)

原理:
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都不相同,即使交易的内容相同,但是交易时的条件也不相同,从而避免签名重放。
点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
0xKk
0xKk
0xb9c0...f744
知识就是力量-- 知识提高认知,认知可以变现,现金就是力量!