區(qū)塊鏈安全—經(jīng)典溢出漏洞cve分析

一卜壕、前言

在上一篇文章中辩块,我們?cè)敿?xì)地講述了solidity中的整數(shù)溢出漏洞最岗。而在本文中帕胆,我們將重點(diǎn)放在真實(shí)事件中,從0開始針對(duì)某些真實(shí)環(huán)境中的CVE進(jìn)行復(fù)現(xiàn)操作般渡。一步一步帶領(lǐng)大家去了解合約內(nèi)部的秘密懒豹。

本文中涉及的漏洞內(nèi)容均為整數(shù)溢出的CVE芙盘,我們會(huì)對(duì)源代碼進(jìn)行詳細(xì)的分析,并在分析漏洞的過程中講述合約的搭建內(nèi)容脸秽。

二儒老、cve漏洞介紹

這次分析漏洞CVE-2018-11811。由于上次的文章我們?cè)敿?xì)的講述了?整數(shù)溢出漏洞的原理以及一些合約實(shí)例记餐。所以這次我們趁熱打鐵驮樊,針對(duì)真實(shí)?生產(chǎn)環(huán)境中的cve漏洞進(jìn)行分析、復(fù)現(xiàn)片酝。

這個(gè)漏洞是于今年6月12日由清華-360企業(yè)安全聯(lián)合研究中心的張超教授團(tuán)隊(duì)披露出來的囚衔,安比(SECBIT)實(shí)驗(yàn)室針對(duì)這些漏洞,對(duì)以太坊上已部署的23357個(gè)合約進(jìn)行了分析檢測(cè)雕沿,發(fā)現(xiàn)共有866個(gè)合約存在相同問題练湿。而今天我們講述的這個(gè)漏洞共影響了288個(gè)智能合約。

合約源碼如下:https://etherscan.io/address/0x0b76544f6c413a555f309bf76260d1e02377c02a

在詳細(xì)講述合約漏洞前审轮,我們將合約的具體內(nèi)容交代清楚鞠鲜。

這個(gè)合約發(fā)行了一套自己的token,并且可以用以太幣對(duì)這些token進(jìn)行交換断国。即包括:用戶可以使用以太幣兌換token贤姆,也可以出售token去獲得以太幣。而具體實(shí)現(xiàn)的函數(shù)如下:

這是owner對(duì)購買與賣出價(jià)格進(jìn)行設(shè)定的函數(shù):

function setPrices(uint256 newSellPrice, uint256 newBuyPrice) onlyOwner {
  sellPrice = newSellPrice;
  buyPrice = newBuyPrice;
}

下面是用戶對(duì)token進(jìn)行購買的函數(shù):

function buy() payable {
  uint amount = msg.value / buyPrice;               // calculates the amount
  _transfer(this, msg.sender, amount);              // makes the transfers
}

這是用戶出售token去換取以太幣的函數(shù):

function sell(uint256 amount) {
  require(this.balance >= amount * sellPrice);      // checks if the contract has enough ether to buy
  _transfer(msg.sender, this, amount);              // makes the transfers
  msg.sender.transfer(amount * sellPrice);          // sends ether to the seller. It's important to do this last to avoid recursion attacks
}

而問題就出在上面的三個(gè)函數(shù)中稳衬。最終達(dá)成的效果就是我平臺(tái)的搭建者可以令用戶用高價(jià)購買token卻在賣出的時(shí)候以很便宜的價(jià)格賣出霞捡。從而達(dá)成對(duì)平臺(tái)使用者的欺詐行為。

三薄疚、cve詳細(xì)分析

1 合約詳解

這個(gè)合約是多繼承于其他合約而部署的碧信。所以在分析的時(shí)候我們要有針對(duì)性。

首先街夭,我們需要看主要的合約內(nèi)容:

contract INTToken is owned, token {
  uint256 public sellPrice;
  uint256 public buyPrice;

  mapping (address => bool) public frozenAccount;

  /* This generates a public event on the blockchain that will notify clients */
  event FrozenFunds(address target, bool frozen);

  /* Initializes contract with initial supply tokens to the creator of the contract */
  function INTToken(
      uint256 initialSupply,
      string tokenName,
      uint8 decimalUnits,
      string tokenSymbol
  ) token (initialSupply, tokenName, decimalUnits, tokenSymbol) {}

  /* Internal transfer, only can be called by this contract */
  function _transfer(address _from, address _to, uint _value) internal {
      require (_to != 0x0);                               // Prevent transfer to 0x0 address. Use burn() instead
      require (balanceOf[_from] > _value);                // Check if the sender has enough
      require (balanceOf[_to] + _value > balanceOf[_to]); // Check for overflows
      require(!frozenAccount[_from]);                     // Check if sender is frozen
      require(!frozenAccount[_to]);                       // Check if recipient is frozen
      balanceOf[_from] -= _value;                         // Subtract from the sender
      balanceOf[_to] += _value;                           // Add the same to the recipient
      Transfer(_from, _to, _value);
  }

  /// @notice Create `mintedAmount` tokens and send it to `target`
  /// @param target Address to receive the tokens
  /// @param mintedAmount the amount of tokens it will receive
  function mintToken(address target, uint256 mintedAmount) onlyOwner {
      balanceOf[target] += mintedAmount;
      totalSupply += mintedAmount;
      Transfer(0, this, mintedAmount);
      Transfer(this, target, mintedAmount);
  }

  /// @notice `freeze? Prevent | Allow` `target` from sending & receiving tokens
  /// @param target Address to be frozen
  /// @param freeze either to freeze it or not
  function freezeAccount(address target, bool freeze) onlyOwner {
      frozenAccount[target] = freeze;
      FrozenFunds(target, freeze);
  }

  /// @notice Allow users to buy tokens for `newBuyPrice` eth and sell tokens for `newSellPrice` eth
  /// @param newSellPrice Price the users can sell to the contract
  /// @param newBuyPrice Price users can buy from the contract
  function setPrices(uint256 newSellPrice, uint256 newBuyPrice) onlyOwner {
      sellPrice = newSellPrice;
      buyPrice = newBuyPrice;
  }

  /// @notice Buy tokens from contract by sending ether
  function buy() payable {
      uint amount = msg.value / buyPrice;               // calculates the amount
      _transfer(this, msg.sender, amount);              // makes the transfers
  }

  /// @notice Sell `amount` tokens to contract
  /// @param amount amount of tokens to be sold
  function sell(uint256 amount) {
      require(this.balance >= amount * sellPrice);      // checks if the contract has enough ether to buy
      _transfer(msg.sender, this, amount);              // makes the transfers
      msg.sender.transfer(amount * sellPrice);          // sends ether to the seller. It's important to do this last to avoid recursion attacks
  }
}

這是最終的合約砰碴。這也是安全隱患發(fā)生的地方。

下面我們?cè)敿?xì)的分析一下這個(gè)合約的功能板丽。

首先呈枉,這個(gè)合約繼承了owned, token合約。而我們向上看埃碱,owned合約為:

contract owned { 
  address public owner;
  function owned() {
      owner = msg.sender;
  }
  //構(gòu)造函數(shù)猖辫,初始化合約owner

  modifier onlyOwner {
      require(msg.sender == owner);
      _;
  }

  function transferOwnership(address newOwner) onlyOwner {
      owner = newOwner;
  }

}

這個(gè)合約比較簡(jiǎn)單,構(gòu)造函數(shù)將合約的owner變量初始化砚殿,并且定義了限定函數(shù)--onlyOwner啃憎。這個(gè)函數(shù)限定了調(diào)用者必須是合約擁有者。之后為了后面開發(fā)方便似炎,合約又定義了transferOwnership辛萍,用這個(gè)函數(shù)來改變現(xiàn)在的合約owner悯姊。

這也為我們的開發(fā)提醒,因?yàn)閟olidity合約部署之后是無法改變的贩毕,所以我們要盡可能的為后來的修改做考慮挠轴,增加一些必要的函數(shù)。

下面我們看第二個(gè)父合約:token耳幢。

我們的主合約在啟動(dòng)時(shí)調(diào)用構(gòu)造函數(shù):

 function INTToken(
      uint256 initialSupply,
      string tokenName,
      uint8 decimalUnits,
      string tokenSymbol
  ) token (initialSupply, tokenName, decimalUnits, tokenSymbol) {}

而構(gòu)造函數(shù)只有簡(jiǎn)單的傳參操作,所以很明顯它是調(diào)用了父合約token的構(gòu)造函數(shù):

function token(
      uint256 initialSupply,
      string tokenName,
      uint8 decimalUnits,
      string tokenSymbol
      ) //傳入?yún)?shù)

  {
      balanceOf[msg.sender] = initialSupply;              // Give the creator all initial tokens 初始化用戶金額
      totalSupply = initialSupply;                        // Update total supply 更新總金額
      name = tokenName;                                   // Set the name for display purposes  
      symbol = tokenSymbol;                               // Set the symbol for display purposes 顯示小數(shù)的符號(hào)
      decimals = decimalUnits;                            // Amount of decimals for display purposes 更新小數(shù)的值
  }

其中傳入了四個(gè)參數(shù)欧啤,分別代表莊家初始化總金額睛藻、token的名字、小數(shù)部分的具體值邢隧、token的標(biāo)志類型

之后是主合約的_transfer函數(shù)店印,這個(gè)函數(shù)從新定義了其父合約的同名函數(shù)。在父合約中倒慧,其函數(shù)定義為:

  function _transfer(address _from, address _to, uint _value) internal {
      require (_to != 0x0);                               // Prevent transfer to 0x0 address. Use burn() instead
      require (balanceOf[_from] > _value);                // Check if the sender has enough
      require (balanceOf[_to] + _value > balanceOf[_to]); // Check for overflows  檢查溢出
      balanceOf[_from] -= _value;                         // Subtract from the sender 
      balanceOf[_to] += _value;                            // Add the same to the recipient
      Transfer(_from, _to, _value);
  }

傳入轉(zhuǎn)賬方已經(jīng)接收方的地址以及轉(zhuǎn)賬的值按摘。之后調(diào)用了一些判斷,包括:“接收方地址不為0纫谅、轉(zhuǎn)賬方的余額>轉(zhuǎn)賬金額炫贤;增加了防止溢出的判斷”。

而子合約中付秕,在上述基礎(chǔ)上又增加了判斷是否轉(zhuǎn)賬方已經(jīng)接受方賬戶被凍結(jié)的函數(shù)兰珍。

  function _transfer(address _from, address _to, uint _value) internal {
      require (_to != 0x0);                               // Prevent transfer to 0x0 address. Use burn() instead
      require (balanceOf[_from] > _value);                // Check if the sender has enough
      require (balanceOf[_to] + _value > balanceOf[_to]); // Check for overflows
      require(!frozenAccount[_from]);                     // Check if sender is frozen
      require(!frozenAccount[_to]);                       // Check if recipient is frozen
      balanceOf[_from] -= _value;                         // Subtract from the sender
      balanceOf[_to] += _value;                           // Add the same to the recipient
      Transfer(_from, _to, _value);
  }

之后,合約擁有者可以調(diào)用下面的函數(shù)來增加某個(gè)賬戶代幣的數(shù)量询吴。PS:這里我覺得是為了合約的健壯性考慮掠河。合約的owner需要有足夠的能力去增刪改查所有用戶的token數(shù)量。

之后增加金額后會(huì)將總金額-totalSupply進(jìn)行更新猛计。

/// @notice Create `mintedAmount` tokens and send it to `target`
  /// @param target Address to receive the tokens
  /// @param mintedAmount the amount of tokens it will receive
  function mintToken(address target, uint256 mintedAmount) onlyOwner {
      balanceOf[target] += mintedAmount;
      totalSupply += mintedAmount;
      Transfer(0, this, mintedAmount);
      Transfer(this, target, mintedAmount);
  }

除了上述增加token外唠摹,合約還定義了burn函數(shù)。

  /// @notice Remove `_value` tokens from the system irreversibly
  /// @param _value the amount of money to burn
  function burn(uint256 _value) returns (bool success) {
      require (balanceOf[msg.sender] > _value);            // Check if the sender has enough
      balanceOf[msg.sender] -= _value;                      // Subtract from the sender
      totalSupply -= _value;                                // Updates totalSupply
      Burn(msg.sender, _value);
      return true;
  }

這個(gè)函數(shù)的含義我還沒有搞懂奉瘤,但是從函數(shù)內(nèi)容來看勾拉,它是用于銷毀賬戶賬面的部分金額。(就是說賬戶里有10塊錢盗温,我可以調(diào)用此函數(shù)來永久銷毀2塊錢)望艺。

為了彰顯owner的絕對(duì)權(quán)力,我們合約中能夠查看到凍結(jié)賬戶的函數(shù)肌访。

/// @notice `freeze? Prevent | Allow` `target` from sending & receiving tokens
  /// @param target Address to be frozen
  /// @param freeze either to freeze it or not
  function freezeAccount(address target, bool freeze) onlyOwner {
      frozenAccount[target] = freeze;
      FrozenFunds(target, freeze);
  }

合約的owner可以調(diào)用這個(gè)函數(shù)來凍結(jié)或者解凍賬戶找默,被凍結(jié)了的賬戶是無法進(jìn)行轉(zhuǎn)賬操作的。

2 合約盲點(diǎn)

在我細(xì)致的分析合約過程中吼驶,我發(fā)現(xiàn)了合約中不易理解的一個(gè)點(diǎn):
mapping (address => mapping (address => uint256)) public allowance;惩激。

合約定義了一個(gè)mapping變量店煞,而這個(gè)變量存儲(chǔ)的也是賬戶賬面的金額數(shù)量。雖然這次漏洞中與這個(gè)變量沒有太大的關(guān)系风钻,但是為了給讀者提供一個(gè)可參考的例子顷蟀,我還是在這里對(duì)這個(gè)變量的作用進(jìn)行分析。

簡(jiǎn)單來說骡技,我認(rèn)為這個(gè)變量就類似于我們生活中的工資的概念鸣个。而這個(gè)工資是使用的公司內(nèi)部自己印刷的錢財(cái),可以在公司內(nèi)部進(jìn)行交易轉(zhuǎn)賬等等布朦。

首先我們可以看賦權(quán)函數(shù):

  function approve(address _spender, uint256 _value)
      returns (bool success) {
      allowance[msg.sender][_spender] = _value;
      return true;
  }

這個(gè)函數(shù)使調(diào)用方給予_spender一個(gè)代替轉(zhuǎn)賬的權(quán)利囤萤。即 B向C轉(zhuǎn)賬,但是由于A給予B某些權(quán)利是趴,那么我B就可以使用A給的額度來變相的向C轉(zhuǎn)賬涛舍。于是可以調(diào)用下面的轉(zhuǎn)賬函數(shù):

function transferFrom(address _from, address _to, uint256 _value) returns (bool success) {
      require (_value < allowance[_from][msg.sender]);     // Check allowance
      allowance[_from][msg.sender] -= _value;  
      _transfer(_from, _to, _value);
      return true;
  }

所以C確實(shí)收到了錢,而B使用AB花費(fèi)了的某些約定來進(jìn)行支付唆途。

除此之外富雅,合約同樣定義了銷毀函數(shù)burnFrom。用于銷毀AB直接的某些約定肛搬。

  function burnFrom(address _from, uint256 _value) returns (bool success) {
      require(balanceOf[_from] >= _value);                // Check if the targeted balance is enough
      require(_value <= allowance[_from][msg.sender]);    // Check allowance没佑??温赔?图筹??让腹?远剩??骇窍?瓜晤??腹纳?痢掠??嘲恍?足画??佃牛?淹辞??俘侠?象缀?蔬将??央星?霞怀??莉给?毙石?
      balanceOf[_from] -= _value;                         // Subtract from the targeted balance
      allowance[_from][msg.sender] -= _value;             // Subtract from the sender's allowance
      totalSupply -= _value;                              // Update totalSupply
      Burn(_from, _value);
      return true;
  }

3 安全漏洞點(diǎn)

闡述完了合約中的一些細(xì)節(jié)部分,下面我們來進(jìn)一步對(duì)合約中存在的安全漏洞進(jìn)行分析颓遏。

用戶在合約中可以使用以太幣來對(duì)合約中自定義的token進(jìn)行兌換徐矩。

 /// @notice Buy tokens from contract by sending ether
  function buy() payable {
      uint amount = msg.value / buyPrice;               // calculates the amount
      _transfer(this, msg.sender, amount);              // makes the transfers
  }

用戶可以傳入value來購買數(shù)量為msg.value / buyPrice的token。而這個(gè)具體的單價(jià)是由合約擁有者調(diào)用函數(shù)得到的州泊。

function setPrices(uint256 newSellPrice, uint256 newBuyPrice) onlyOwner {
      sellPrice = newSellPrice;
      buyPrice = newBuyPrice;
  }

之后,當(dāng)用戶有了足夠的token后漂洋,他可以用token換回自己的以太幣遥皂。即出售token。

function sell(uint256 amount) {
      require(this.balance >= amount * sellPrice);      // checks if the contract has enough ether to buy
      _transfer(msg.sender, this, amount);              // makes the transfers
      msg.sender.transfer(amount * sellPrice);          // sends ether to the seller. It's important to do this last to avoid recursion attacks
  }

而漏洞就在上面的函數(shù)中刽漂。因?yàn)橘I賣的單價(jià)均可以由owner進(jìn)行定義演训。倘若owner進(jìn)行作惡,將單價(jià)設(shè)置為特殊的值贝咙,那么便可以達(dá)到意想不到的效果样悟。例如:

管理員設(shè)置buyPrice = 1 ether, sellPrice = 2^255。即用戶可以花1以太幣購買一個(gè)token庭猩,然后可以以2^255的價(jià)格賣出窟她。看起來很劃算對(duì)嗎蔼水?但是這樣便產(chǎn)生了整數(shù)溢出震糖。

倘若我們賣出兩個(gè)token:amount * sellPrice = 2^256 。而我們這里為uint256類型趴腋,所以直接產(chǎn)生溢出吊说,使變量為0 。也就是說优炬,系統(tǒng)便可以不用花費(fèi)任何一分錢就可以收入2個(gè)token颁井,而用戶直接白白損失了2個(gè)以太幣。

除此之外蠢护,我們還可以簡(jiǎn)單地將賣出價(jià)格設(shè)置為0雅宾,這樣同樣使amount * sellPrice為0 。不過有可能無法使那些投機(jī)的用戶上鉤葵硕。

我們也可以對(duì)合約進(jìn)行簡(jiǎn)單的部署秀又。

image.png

之后傳入?yún)?shù):

image.png

成功后對(duì)買賣變量進(jìn)行設(shè)置:

之后便可以使用第二個(gè)測(cè)試賬戶進(jìn)行token的購買操作单寂。

四、參考資料

本稿為原創(chuàng)稿件吐辙,轉(zhuǎn)載請(qǐng)標(biāo)明出處宣决。謝謝。

首發(fā)于先知社區(qū)昏苏,https://xz.aliyun.com/t/3743
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末尊沸,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子贤惯,更是在濱河造成了極大的恐慌洼专,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,036評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件孵构,死亡現(xiàn)場(chǎng)離奇詭異屁商,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)颈墅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門蜡镶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人恤筛,你說我怎么就攤上這事官还。” “怎么了毒坛?”我有些...
    開封第一講書人閱讀 164,411評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵望伦,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我煎殷,道長(zhǎng)屯伞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,622評(píng)論 1 293
  • 正文 為了忘掉前任豪直,我火速辦了婚禮愕掏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘顶伞。我一直安慰自己饵撑,他們只是感情好冯遂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評(píng)論 6 392
  • 文/花漫 我一把揭開白布烫饼。 她就那樣靜靜地躺著,像睡著了一般查近。 火紅的嫁衣襯著肌膚如雪锨咙。 梳的紋絲不亂的頭發(fā)上语卤,一...
    開封第一講書人閱讀 51,521評(píng)論 1 304
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼粹舵。 笑死钮孵,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的眼滤。 我是一名探鬼主播巴席,決...
    沈念sama閱讀 40,288評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼诅需!你這毒婦竟也來了漾唉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,200評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤堰塌,失蹤者是張志新(化名)和其女友劉穎赵刑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體场刑,經(jīng)...
    沈念sama閱讀 45,644評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡般此,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了牵现。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铐懊。...
    茶點(diǎn)故事閱讀 39,953評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖施籍,靈堂內(nèi)的尸體忽然破棺而出居扒,到底是詐尸還是另有隱情概漱,我是刑警寧澤丑慎,帶...
    沈念sama閱讀 35,673評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站瓤摧,受9級(jí)特大地震影響竿裂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜照弥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評(píng)論 3 329
  • 文/蒙蒙 一腻异、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧这揣,春花似錦悔常、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至片迅,卻和暖如春残邀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工芥挣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留驱闷,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,119評(píng)論 3 370
  • 正文 我出身青樓空免,卻偏偏與公主長(zhǎng)得像空另,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子鼓蜒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容