一、前言
在前一篇的蜜罐合約中悠反,我們介紹并測(cè)試了部分由于繼承等問(wèn)題而搭建的蜜罐合約嗤军。蜜罐合約顧名思義注盈,就是利用了受害者的投機(jī)想法,從而另普通用戶自行進(jìn)行轉(zhuǎn)賬的行為叙赚。在我們文章中演示的相關(guān)合約對(duì)owner友好老客,即普通用戶很難從合約中獲得利益,所以讀者如果看到類(lèi)似的合約請(qǐng)不要輕易的使用以太幣進(jìn)行嘗試震叮。
而本文中胧砰,我們?cè)诿酃藓霞s之上分析由Solidity的結(jié)構(gòu)體產(chǎn)生的漏洞,而此漏洞危害性極大苇瓣,倘若合約開(kāi)發(fā)不到位會(huì)導(dǎo)致owner的篡改尉间,即普通用戶的提權(quán)操作。
二击罪、由合約漏洞而導(dǎo)致的蜜罐
1 蜜罐合約介紹
pragma solidity ^0.4.19;
/*
* This is a distributed lottery that chooses random addresses as lucky addresses. If these
* participate, they get the jackpot: 1.9 times the price of their bet.
* Of course one address can only win once. The owner regularly reseeds the secret
* seed of the contract (based on which the lucky addresses are chosen), so if you did not win,
* just wait for a reseed and try again!
*
* Jackpot chance: 50%
* Ticket price: Anything larger than (or equal to) 0.1 ETH
* Jackpot size: 1.9 times the ticket price
*
* HOW TO PARTICIPATE: Just send any amount greater than (or equal to) 0.1 ETH to the contract's address
* Keep in mind that your address can only win once
*
* If the contract doesn't have enough ETH to pay the jackpot, it sends the whole balance.
*
* Example: For each address, a random number is generated, either 0 or 1. This number is then compared
* with the LuckyNumber - a constant 1. If they are equal, the contract will instantly send you the jackpot:
* your bet multiplied by 1.9 (House edge of 0.1)
*/
contract OpenAddressLottery{
struct SeedComponents{
uint component1;
uint component2;
uint component3;
uint component4;
}
address owner; //address of the owner
uint private secretSeed; //seed used to calculate number of an address
uint private lastReseed; //last reseed - used to automatically reseed the contract every 1000 blocks
uint LuckyNumber = 1; //if the number of an address equals 1, it wins
mapping (address => bool) winner; //keeping track of addresses that have already won
function OpenAddressLottery() {
owner = msg.sender;
reseed(SeedComponents((uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp)); //generate a quality random seed
}
function participate() payable {
if(msg.value<0.1 ether)
return; //verify ticket price
// make sure he hasn't won already
require(winner[msg.sender] == false);
if(luckyNumberOfAddress(msg.sender) == LuckyNumber){ //check if it equals 1
winner[msg.sender] = true; // every address can only win once
uint win=(msg.value/10)*19; //win = 1.9 times the ticket price
if(win>this.balance) //if the balance isnt sufficient...
win=this.balance; //...send everything we've got
msg.sender.transfer(win);
}
if(block.number-lastReseed>1000) //reseed if needed
reseed(SeedComponents((uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp)); //generate a quality random seed
}
function luckyNumberOfAddress(address addr) constant returns(uint n){
// calculate the number of current address - 50% chance
n = uint(keccak256(uint(addr), secretSeed)[0]) % 2; //mod 2 returns either 0 or 1
}
function reseed(SeedComponents components) internal {
secretSeed = uint256(keccak256(
components.component1,
components.component2,
components.component3,
components.component4
)); //hash the incoming parameters and use the hash to (re)initialize the seed
lastReseed = block.number;
}
function kill() {
require(msg.sender==owner);
selfdestruct(msg.sender);
}
function forceReseed() { //reseed initiated by the owner - for testing purposes
require(msg.sender==owner);
SeedComponents s;
s.component1 = uint(msg.sender);
s.component2 = uint256(block.blockhash(block.number - 1));
s.component3 = block.difficulty*(uint)(block.coinbase);
s.component4 = tx.gasprice * 7;
reseed(s); //reseed
}
function () payable { //if someone sends money without any function call, just assume he wanted to participate
if(msg.value>=0.1 ether && msg.sender!=owner) //owner can't participate, he can only fund the jackpot
participate();
}
}
下面我們簡(jiǎn)單的分析一下這個(gè)類(lèi)彩票合約哲嘲。
為何稱(chēng)這個(gè)合約為蜜罐合約么?我們根據(jù)合約內(nèi)容可以知道外邓,合約在起始時(shí)賦值LuckyNumber
為1撤蚊,而在參與函數(shù)中根據(jù)參與者的地址生成隨機(jī)數(shù)0 or 1
古掏,之后如果為1损话,那么就返還value * 1.9的賭金〔弁伲看似0.5的高概率丧枪,但是合約利用了一種以太坊的bug,從而導(dǎo)致用戶永遠(yuǎn)不可能取到錢(qián)庞萍。下面請(qǐng)看我們的分析拧烦。
首先,合約定義了一個(gè)結(jié)構(gòu)體钝计。(我認(rèn)為本來(lái)不需要結(jié)構(gòu)體這樣的類(lèi)型來(lái)進(jìn)行隨機(jī)數(shù)的生成恋博,所以我覺(jué)得這里的結(jié)構(gòu)體是為了觸發(fā)合約的漏洞)
struct SeedComponents{
uint component1;
uint component2;
uint component3;
uint component4;
}
之后定義了五個(gè)變量齐佳,分別代表合約的owner、隨機(jī)數(shù)種子债沮、上一次的記錄值炼吴、幸運(yùn)數(shù)、競(jìng)猜獲勝者集合
疫衩。
address owner; //address of the owner
uint private secretSeed; //seed used to calculate number of an address
uint private lastReseed; //last reseed - used to automatically reseed the contract every 1000 blocks
uint LuckyNumber = 1; //if the number of an address equals 1, it wins
mapping (address => bool) winner; //keeping track of addresses that have already won
而下一個(gè)部分是構(gòu)造函數(shù)硅蹦。
function OpenAddressLottery() {
owner = msg.sender;
reseed(SeedComponents((uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp)); //generate a quality random seed
}
構(gòu)造函數(shù)將owner
賦初值為合約創(chuàng)建者,之后調(diào)用reseed
函數(shù)闷煤。而我們下面就看一看這個(gè)函數(shù)的作用童芹。
function reseed(SeedComponents components) internal {
secretSeed = uint256(keccak256(
components.component1,
components.component2,
components.component3,
components.component4
)); //hash the incoming parameters and use the hash to (re)initialize the seed
lastReseed = block.number;
}
在這個(gè)函數(shù)中,我們會(huì)傳入components
結(jié)構(gòu)體鲤拿,并使用keccak256 ()哈希函數(shù)
更新secretSeed
的值假褪,并初始化lastReseed
。
也就是說(shuō)皆愉,我們?cè)跇?gòu)造函數(shù)中調(diào)用此函數(shù)來(lái)更新secretSeed
的值嗜价。
之后,我們來(lái)看participate()
幕庐,此函數(shù)是用戶調(diào)用參與接口久锥,用于競(jìng)猜的環(huán)節(jié)。
function participate() payable {
if(msg.value<0.1 ether)
return; //verify ticket price
// make sure he hasn't won already
require(winner[msg.sender] == false);
if(luckyNumberOfAddress(msg.sender) == LuckyNumber){ //check if it equals 1
winner[msg.sender] = true; // every address can only win once
uint win=(msg.value/10)*19; //win = 1.9 times the ticket price
if(win>this.balance) //if the balance isnt sufficient...
win=this.balance; //...send everything we've got
msg.sender.transfer(win);
}
if(block.number-lastReseed>1000) //reseed if needed
reseed(SeedComponents((uint)(block.coinbase), block.difficulty, block.gaslimit, block.timestamp)); //generate a quality random seed
}
在函數(shù)中异剥,我們看到用戶必須傳入value >= 0.1 eth
瑟由,并且用戶還未贏得過(guò)獎(jiǎng)勵(lì)。之后合約會(huì)將LuckyNumber
與luckyNumberOfAddress(msg.sender)
進(jìn)行比較冤寿。倘若兩者的值相等歹苦,那么記錄下該用戶的中獎(jiǎng)記錄并進(jìn)行【value * 1.9】的轉(zhuǎn)賬獎(jiǎng)勵(lì)(余額不足的將所有余額轉(zhuǎn)入)。
而我們?cè)诳?code>luckyNumberOfAddress函數(shù)督怜。
function luckyNumberOfAddress(address addr) constant returns(uint n){
// calculate the number of current address - 50% chance
n = uint(keccak256(uint(addr), secretSeed)[0]) % 2; //mod 2 returns either 0 or 1
}
傳入一個(gè)地址殴瘦,之后根據(jù)傳入的地址產(chǎn)生隨機(jī)數(shù),并%2号杠,得到1或0 蚪腋。
然后是一個(gè)測(cè)試函數(shù)forceReseed
。
function forceReseed() { //reseed initiated by the owner - for testing purposes
require(msg.sender==owner);
SeedComponents s;
s.component1 = uint(msg.sender);
s.component2 = uint256(block.blockhash(block.number - 1));
s.component3 = block.difficulty*(uint)(block.coinbase);
s.component4 = tx.gasprice * 7;
reseed(s); //reseed
}
合約創(chuàng)建者在這個(gè)函數(shù)后面添加了注釋//reseed initiated by the owner - for testing purposes
姨蟋。表達(dá)用于測(cè)試的目的屉凯。
然而問(wèn)題就是出在這個(gè)地方。
整體來(lái)看眼溶,這個(gè)合約并沒(méi)有什么問(wèn)題悠砚。gamble的過(guò)程也十分清晰。
然而我們進(jìn)行一個(gè)合約測(cè)試堂飞。
2 攻擊手段分析
我們先看一個(gè)測(cè)試合約:
pragma solidity ^0.4.24;
contract test
{
address public addr = 0xa;
uint public b = 555;
uint256 public c = 666;
bytes public d = "abcd";
struct Seed{
uint256 component1;
uint256 component2;
uint256 component3;
uint256 component4;
}
function change() public{
Seed s;
s.component1 = 1;
s.component2 = 2;
s.component3 = 3;
s.component4 = 4;
}
}
在這個(gè)合約中灌旧,我們?cè)O(shè)置了4個(gè)變量绑咱,而這四個(gè)變量均有初始值。之后我們又設(shè)置了結(jié)構(gòu)體Seed
枢泰。在這個(gè)結(jié)構(gòu)體中擁有四個(gè)變量羡玛,而我們?cè)?code>test()函數(shù)中初始化結(jié)構(gòu)體并賦初值,之后我們看看效果宗苍。
部署合約:
查看變量?jī)?nèi)容:
之后我們調(diào)用change
函數(shù)稼稿。并查看,發(fā)現(xiàn)我們的變量被修改了讳窟,而修改的內(nèi)容就是結(jié)構(gòu)體中的內(nèi)容让歼。
這就是我們的漏洞所在。
我們的合約中并沒(méi)有修改變量的值丽啡,但是由于solidity機(jī)制的問(wèn)題而導(dǎo)致了變量修改問(wèn)題谋右。
而這個(gè)漏洞對(duì)我們上述介紹的蜜罐合約有什么影響呢?我們進(jìn)行一下測(cè)試补箍。
為了方便我們查看測(cè)試效果改执,我們?yōu)?code>LuckyNumber添加查看函數(shù)。
倘若此時(shí)owner
不進(jìn)行任何操作坑雅,任憑用戶進(jìn)行下一步的賭博辈挂,那么用戶還是有很大的概率獲得獎(jiǎng)勵(lì)的。例如:(為了方便演示裹粤,我在函數(shù)中添加了event事件)
emit back(msg.sender,win,true);
终蒂。
此時(shí)我們能夠看到,LuckyNumber
是初始值1 遥诉。
之后拇泣,我們更換用戶進(jìn)行參與。我們投入1 eth進(jìn)行競(jìng)猜矮锈。
第一次:
沒(méi)有獲得獎(jiǎng)勵(lì)霉翔,所以1 eth賠進(jìn)去了。
繼續(xù)更換用戶參與:
直到最后一個(gè)用戶:
我們得到了獎(jiǎng)勵(lì)金1900000000000000000 wei
苞笨,所以競(jìng)猜成功债朵。
而我們大致能夠發(fā)現(xiàn),其實(shí)我們是擁有很大的概率獲得獎(jiǎng)勵(lì)的猫缭。這個(gè)合約真的就是拼概率的傳統(tǒng)賭博合約嗎葱弟?然而事實(shí)并非如此壹店。
根據(jù)我們前文所測(cè)試的漏洞猜丹,這個(gè)合約中同樣存在惡意篡改的行為。我們發(fā)現(xiàn)了合約中其實(shí)存在著結(jié)構(gòu)體
硅卢。
而這個(gè)結(jié)構(gòu)體在合約中存在修改函數(shù):
所以射窒,如果owner
調(diào)用了此函數(shù)藏杖,那么會(huì)不會(huì)發(fā)起漏洞從而將競(jìng)猜值惡意修改呢?
我們更換地址為owner脉顿,并且調(diào)用此函數(shù)蝌麸。
我們驚奇的發(fā)現(xiàn),果然此時(shí)的競(jìng)猜值從1變成了7 艾疟。
而我們合約中的判斷條件是luckyNumberOfAddress(msg.sender) == LuckyNumber
来吩。而我們函數(shù)中luckyNumberOfAddress(msg.sender)
只能是0或者1兩種可能。這里的LuckNumber是7蔽莱,也就是說(shuō)無(wú)論我們?nèi)绾胃?jìng)猜弟疆,永遠(yuǎn)都不會(huì)成功。
三盗冷、賭博怠苔?莊家永遠(yuǎn)更勝一籌
在看完上述的高級(jí)蜜罐后,我們來(lái)看一下常規(guī)的蜜罐合約仪糖。
pragma solidity ^0.4.19;
// CryptoRoulette
//
// Guess the number secretly stored in the blockchain and win the whole contract balance!
// A new number is randomly chosen after each try.
//
// To play, call the play() method with the guessed number (1-20). Bet price: 0.1 ether
contract CryptoRoulette {
uint256 private secretNumber;
uint256 public lastPlayed;
uint256 public betPrice = 0.1 ether;
address public ownerAddr;
struct Game {
address player;
uint256 number;
}
Game[] public gamesPlayed;
function CryptoRoulette() public {
ownerAddr = msg.sender;
shuffle();
}
function shuffle() internal {
// randomly set secretNumber with a value between 1 and 20
secretNumber = uint8(sha3(now, block.blockhash(block.number-1))) % 20 + 1;
}
function play(uint256 number) payable public {
require(msg.value >= betPrice && number <= 10);
Game game;
game.player = msg.sender;
game.number = number;
gamesPlayed.push(game);
if (number == secretNumber) {
// win!
msg.sender.transfer(this.balance);
}
shuffle();
lastPlayed = now;
}
function kill() public {
if (msg.sender == ownerAddr && now > lastPlayed + 1 days) {
suicide(msg.sender);
}
}
function() public payable { }
}
為什么說(shuō)蜜罐的owner更勝一籌呢柑司?我們?cè)陂喿x了合約的所有函數(shù)內(nèi)容后就知道,在合約中的shuffle()
函數(shù)%20锅劝,也就意味著它最后的范圍是0~19
攒驰,而用戶能夠傳入的數(shù)是多少呢?在play()
函數(shù)中故爵,用戶需要傳入一個(gè)number
讼育,而其規(guī)定值<=10。
其概率值相對(duì)來(lái)說(shuō)還是極低的稠集。并且在一天之后奶段,倘若用戶還未猜對(duì)那么owner便可以調(diào)用kill()
函數(shù)進(jìn)行自殺操作。將余額轉(zhuǎn)入到自己的賬戶中剥纷。
四痹籍、參考鏈接
本稿為原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)標(biāo)明出處晦鞋。謝謝蹲缠。