作者:Hcamael@知道創宇404區塊鏈安全研究團隊
背景
最近學習了下以太坊的智能合約,而且也看到挺多廠家pr智能合約相關的漏洞,其中《ERC20智能合約整數溢出系列漏洞披露》文章中披露了6個CVE編號的漏洞,而這些漏洞都屬于整型溢出漏洞范疇,其中5個漏洞均需要合約Owner才能觸發利用。本文正是針對這些漏洞從合約代碼及觸發邏輯上做了詳細分析,并提出了一些關于owner相關漏洞的思考。
漏洞分析
1. CVE-2018-11809
該漏洞被稱為“超額購幣”,相關合約(EthLendToken)源碼: https://etherscan.io/address/0x80fB784B7eD66730e8b1DBd9820aFD29931aab03#code
在合約代碼中,buyTokensPresale和buyTokensICO兩個函數都是存在整型上溢出的情況:
function buyTokensPresale() public payable onlyInState(State.PresaleRunning)
{
// min - 1 ETH
require(msg.value >= (1 ether / 1 wei));
uint newTokens = msg.value * PRESALE_PRICE;
require(presaleSoldTokens + newTokens <= PRESALE_TOKEN_SUPPLY_LIMIT);
balances[msg.sender] += newTokens;
supply+= newTokens;
presaleSoldTokens+= newTokens;
totalSoldTokens+= newTokens;
LogBuy(msg.sender, newTokens);
}
function buyTokensICO() public payable onlyInState(State.ICORunning)
{
// min - 0.01 ETH
require(msg.value >= ((1 ether / 1 wei) / 100));
uint newTokens = msg.value * getPrice();
require(totalSoldTokens + newTokens <= TOTAL_SOLD_TOKEN_SUPPLY_LIMIT);
balances[msg.sender] += newTokens;
supply+= newTokens;
icoSoldTokens+= newTokens;
totalSoldTokens+= newTokens;
LogBuy(msg.sender, newTokens);
}
溢出點:
require(presaleSoldTokens + newTokens <= PRESALE_TOKEN_SUPPLY_LIMIT);
require(totalSoldTokens + newTokens <= TOTAL_SOLD_TOKEN_SUPPLY_LIMIT);
拿buyTokensPresale函數舉例,在理論上presaleSoldTokens + newTokens存在整型上溢出漏洞,會導致繞過require判斷,造成超額購幣。
接下來,我們再仔細分析一下,如果造成整型上溢出,先來看看presaleSoldTokens變量的最大值
uint public presaleSoldTokens = 0;
require(presaleSoldTokens + newTokens <= PRESALE_TOKEN_SUPPLY_LIMIT);
presaleSoldTokens+= newTokens;
該合約代碼中,presaleSoldTokens變量相關的代碼只有這三行,因為存在著require判斷,所以不論presaleSoldTokens + newTokens是否溢出,presaleSoldTokens <= PRESALE_TOKEN_SUPPLY_LIMIT恒成立,因為有著斷言代碼:
assert(PRESALE_TOKEN_SUPPLY_LIMIT==60000000 * (1 ether / 1 wei));
所以,presaleSoldTokens <= 60000000 * (1 ether / 1 wei),其中1 ether / 1 wei = 1000000000000000000,所以max(presaleSoldTokens) == 6*(10^25)
再來看看變量newTokens,該變量的值取決于用戶輸出,是用戶可控變量,相關代碼如下:
uint newTokens = msg.value * PRESALE_PRICE;
uint public constant PRESALE_PRICE = 30000;
如果我們向buyTokensPresale函數轉賬1 ether, newTokens的值為1000000000000000000*30000=3*(10^22)
下面來計算一下,需要向該函數轉賬多少以太幣,才能造成溢出
在以太坊智能合約中,uint默認代表的是uint256,取值范圍是0~2^256-1,所以,需要newTokens的值大于(2^256-1)-presaleSoldTokens。
最后計算出,我們需要向buyTokensPresale函數轉賬:
>>> (2**256-1)-(6*(10**25))/(3*(10**22))
115792089237316195423570985008687907853269984665640564039457584007913129637935L
才可以造成整型上溢出,超額購幣,整個以太坊公鏈,發展至今,以太幣總余額有達到這個數嗎?
雖然理論上該合約的確存在漏洞,但是實際卻無法利用該漏洞
2. CVE-2018-11810
該類漏洞被稱為:“超額定向分配”
相關事例( LGO )源碼:https://etherscan.io/address/0x123ab195dd38b1b40510d467a6a359b201af056f#code
根據該漏洞的描述:
管理員繞過合約中規定的單地址發幣上限,給指定地址分配超額的token
跟上一個漏洞相比,因為該漏洞存在于onlyOwner的函數中,只能Owner(管理員)才能調用該漏洞,所以我認為該類漏洞可以算做是“后門“類漏洞。
所以該類漏洞的利用有兩個思路:
- Owner留下來的“后門”,供自己使用,專門用來坑合約的其他使用者(所謂的”蜜罐合約“,就是這種情況)
- 該合約有其他漏洞,能讓自己成為Owener,或者可以說,結合提權漏洞進行利用
首先,我們先假設自己就是Owner,來研究該漏洞的利用流程,以下是存在漏洞的函數:
function allocate(address _address, uint256 _amount, uint8 _type) public onlyOwner returns (bool success) {
// one allocations by address
require(allocations[_address] == 0);
if (_type == 0) { // advisor
// check allocated amount
require(advisorsAllocatedAmount + _amount <= ADVISORS_AMOUNT);
// increase allocated amount
advisorsAllocatedAmount += _amount;
// mark address as advisor
advisors[_address] = true;
} else if (_type == 1) { // founder
// check allocated amount
require(foundersAllocatedAmount + _amount <= FOUNDERS_AMOUNT);
// increase allocated amount
foundersAllocatedAmount += _amount;
// mark address as founder
founders[_address] = true;
} else {
// check allocated amount
require(holdersAllocatedAmount + _amount <= HOLDERS_AMOUNT + RESERVE_AMOUNT);
// increase allocated amount
holdersAllocatedAmount += _amount;
}
// set allocation
allocations[_address] = _amount;
initialAllocations[_address] = _amount;
// increase balance
balances[_address] += _amount;
// update variables for bonus distribution
for (uint8 i = 0; i < 4; i++) {
// increase unspent amount
unspentAmounts[BONUS_DATES[i]] += _amount;
// initialize bonus eligibility
eligibleForBonus[BONUS_DATES[i]][_address] = true;
bonusNotDistributed[BONUS_DATES[i]][_address] = true;
}
// add to initial holders list
initialHolders.push(_address);
Allocate(_address, _amount);
return true;
}
該合約相當于一個代幣分配的協議,Owner可以隨意給人分配代幣,但是不能超過如下的限制:
代幣的總額: uint256 constant INITIAL_AMOUNT = 100 * onePercent;
給顧問5%: uint256 constant ADVISORS_AMOUNT = 5 * onePercent;
創始人要15%: uint256 constant FOUNDERS_AMOUNT = 15 * onePercent;
銷售出了60%: uint256 constant HOLDERS_AMOUNT = 60 * onePercent;
保留了20%: uint256 constant RESERVE_AMOUNT = 20 * onePercent;
對應到下面三個判斷:
require(advisorsAllocatedAmount + _amount <= ADVISORS_AMOUNT);
require(foundersAllocatedAmount + _amount <= FOUNDERS_AMOUNT);
require(holdersAllocatedAmount + _amount <= HOLDERS_AMOUNT + RESERVE_AMOUNT);
跟上一個CVE一樣,該漏洞本質上也是整型上溢出,但是上一個漏洞,用戶可控的變量來至于向合約轉賬的以太幣的數值,所以在實際情況中,基本不可能利用。但是在該漏洞中,用戶可控的變量_amount,是由用戶任意輸入,使得該漏洞得以實現
下面,利用漏洞給顧問分配超過5%的代幣:
- 給顧問A分配
2*onePercent數量的代幣:allocte("0xbd08e0cddec097db7901ea819a3d1fd9de8951a2", 362830104000000, 0)

-
給顧問B分配一個巨大數量的代幣,導致溢出:
allocte("0x63ac545c991243fa18aec41d4f6f598e555015dc", 115792089237316195423570985008687907853269984665640564039457583645083025639937, 0) -
查看顧問B的代幣數:
balanceOf("0x63ac545c991243fa18aec41d4f6f598e555015dc") => 115792089237316195423570985008687907853269984665640564039457583645083025639937

經過后續的審計,發現該合約代碼中的own變量只能由Owner修改,所以該漏洞只能被Owner利用
3. CVE-2018-11809
該漏洞被稱為:”超額鑄幣“,但實際和之前的漏洞沒啥區別
含有該漏洞的合約Playkey (PKT)源碼:https://etherscan.io/address/0x2604fa406be957e542beb89e6754fcde6815e83f#code
存在漏洞的函數:
function mint(address _holder, uint256 _value) external icoOnly {
require(_holder != address(0));
require(_value != 0);
require(totalSupply + _value <= tokenLimit);
balances[_holder] += _value;
totalSupply += _value;
Transfer(0x0, _holder, _value);
}
比上一個漏洞的代碼還更簡單,只有ico(相當于之前的owner)能執行該函數,閱讀全篇代碼,ico是在合約部署的時候由創建人設置的,后續無法更改,所以該漏洞只能被ico(owner)利用
該合約本身的意圖是,ico能隨意給人分配代幣,但是發行代幣的總額度不能超過tokenLimit,但是通過整型上溢出漏洞,能讓ico發行無限個代幣,利用流程如下:
- 部署合約,設置ico為自己賬戶地址,設置發行代幣的上限為100000:
PTK("0x8a0b358029b81a52487acfc776fecca3ce2fbf4b", 100000)

- 給賬戶A分配一定額度的代幣:
mint("0xbd08e0cddec097db7901ea819a3d1fd9de8951a2", 50000)

- 利用整型上溢出給賬戶B分配大量的代幣:
mint("0x63ac545c991243fa18aec41d4f6f598e555015dc", 115792089237316195423570985008687907853269984665640564039457584007913129589938) - 查看賬戶B的余額:
balanceOf("0x63ac545c991243fa18aec41d4f6f598e555015dc") => 115792089237316195423570985008687907853269984665640564039457584007913129589938

4. CVE-2018-11812
該漏洞被稱為:“隨意鑄幣”
相關漏洞合約 Polymath (POLY)源碼:https://etherscan.io/address/0x9992ec3cf6a55b00978cddf2b27bc6882d88d1ec#code
具有漏洞的函數:
function mintToken(address target, uint256 mintedAmount) onlyOwner {
balanceOf[target] += mintedAmount;
Transfer(0, owner, mintedAmount);
Transfer(owner, target, mintedAmount);
}
這個漏洞很簡單,也很好理解,Owner可以隨意增加任意賬戶的代幣余額,可以想象成,銀行不僅能隨心所欲的印鈔票,還能隨心所以的扣你的錢
因為Owner是在合約部署的時候被設置成合約部署者的賬戶地址,之后也只有Owner能修改Own賬戶地址,所以該漏洞只能被Owner利用
這個我覺得與其說是漏洞,不如說是Owner留下的“后門”
5. CVE-2018-11687
該漏洞被稱為:“下溢增持”
相關漏洞合約Bitcoin Red (BTCR)源碼:https://etherscan.io/address/0x6aac8cb9861e42bf8259f5abdc6ae3ae89909e11#code
相關的漏洞函數:
function distributeBTR(address[] addresses) onlyOwner {
for (uint i = 0; i < addresses.length; i++) {
balances[owner] -= 2000 * 10**8;
balances[addresses[i]] += 2000 * 10**8;
Transfer(owner, addresses[i], 2000 * 10**8);
}
}
該合約限制了發行代幣的上限: uint256 _totalSupply = 21000000 * 10**8;
并且在合約部署的時候把能發行的合約都分配給了Owner: balances[owner] = 21000000 * 10**8;
然后Owner可以把自己賬戶的代幣,任意分配給其他賬戶,分配的代碼就是上面的函數,給別人分配一定額度的代幣時,自己減去相應額度的代幣,保證該合約總代幣數不變
但是因為沒有判斷Owner的賬戶是否有足夠的余額,所以導致了減法的整型下溢出,同樣也存在整型上溢出,但是因為uint256的上限是2^256-1,但是利用過于繁瑣,需要運行非常多次的balances[addresses[i]] += 2000 * 10**8;
而減法的利用就很簡單了,或者我們可以根本不考慮這個減法,Owner可以給任意賬戶分配2000 * 10**8倍數的代幣,該漏洞的功能和上一個漏洞的基本一致,可以任意發行代幣或者減少其他賬戶的代幣數
因為Owner是在合約部署的時候被設置為部署合約人的賬戶地址,后續沒有修改own的功能,所以該漏洞也只有Owner可以利用
6. CVE-2018-11811
該漏洞被稱為:“高賣低收”
相關漏洞合約 Internet Node Token (INT)源碼:https://etherscan.io/address/0x0b76544f6c413a555f309bf76260d1e02377c02a
在該CVE的描述中,存在漏洞的函數是:
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
}
并且描述的漏洞原理是:
sellPrice被修改為精心構造的大數后,可導致amount * sellPrice的結果大于整數變量(uint256)最大值,發生整數溢出,從而變為一個極小值甚至歸零`
相關函數如下:
function buy() payable {
uint amount = msg.value / buyPrice; // calculates the amount
_transfer(this, msg.sender, amount); // makes the transfers
}
function setPrices(uint256 newSellPrice, uint256 newBuyPrice) onlyOwner {
sellPrice = newSellPrice;
buyPrice = newBuyPrice;
}
該漏洞的利用流程如下:
- 管理員設置
buyPrice = 1 ether,sellPrice = 2^255 - 用戶A買了兩個以太幣價格的代幣: buy({value:toWei(2)})
- 用戶A賣掉兩個代幣: send(2)
- 用戶A將會收到
2*sellPrice = 2^256價格的Wei - 但是因為
transfer的參數是uint256, 所以發生了溢出,用戶A實際得到0Wei
表面上看這個漏洞還是有危害的,但是我們仔細想想,這個漏洞其實是比較多余的,我們可以使用更簡單的步驟達到相同的目的:
- 管理員設置
buyPrice = 1 ether,sellPrice = 0 - 用戶A買了兩個以太幣價格的代幣: buy({value:toWei(2)})
- 用戶A賣掉兩個代幣: send(2)
- 用戶A將會收到
2*sellPrice = 0價格的Wei
我認為該合約最大的問題在于Owner可以隨意設置代幣的買入和賣出價格。
順帶提一下這個問題也是前面peckshield公布的“tradeTrap”漏洞(https://peckshield.com/2018/06/11/tradeTrap/)提到的“Security Issue 2: Manipulatable Prices and Unfair Arbitrage” 是同一個問題。
總結
經過上面的分析,在這6個CVE中,雖然都是整型溢出,但第一個CVE屬于理論存在,但實際不可實現的整型上溢出漏洞,剩下5個CVE都屬于對管理者有利,會損害用戶利用的漏洞,或者可以稱為“后門”,也正是這個原因也導致了一些關于需要Owner觸發漏洞意義討論
如果我們把智能合約類比為傳統合同,智能合約代碼就是傳統合同的內容,但是和傳統的合同相比,智能合約擁有三個利益團體,一個是編寫合約代碼的人(智能合約中的Owner,或者我們可以稱為甲方),使用該合約的其他人(我們可以稱為乙方),跟該智能合約無關的其他人(比如利用合約漏洞獲利的黑客)。從這個角度來看Owner條件下觸發的漏洞在理論上是可以損害到乙方的利益,如對于存在“惡意”的owner或者黑客配合其他漏洞獲取到owner權限的場景上來說,還是有一定意義的。
另外從整個上市交易流程來看,我們還需要關注到“交易所”這個環節,交易所的風控體系在某種程度上可以限制這種“惡意”的owner或黑客利用。
由此可見合約審計對于“甲方”、“乙方”、交易所都有重要的意義。
知道創宇智能合約安全審計:http://www.scanv.com/lca/index.html
參考鏈接
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/627/
暫無評論