一卜壕、前言
在上一篇文章中辩块,我們?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)單的部署秀又。
之后傳入?yún)?shù):
成功后對(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