智能合约中常见漏洞和攻击方式简介
什么是智能合约?
以太坊有两种常见的账户类型:外部拥有账户(EOA)和智能合约账户(SCA)。
EOA 与我们常用的电子金融账户非常相似,用于存储资金并与应用程序交互。例如,用户通过 PayPal 存入法定货币,并与各种网站、商店和应用程序进行支付交互。DeFi 矿工通常将加密货币存储在他们的 EOA 中,与 DeFi dApp 交互,并将资金存入 dApp 以获取利润。然而,EOA 具有电子金融账户所不具备的一个特点:用户必须通过拥有私钥来验证对 EOA 的控制权——不是你的私钥,就不是你的币。
SCA 也是一种账户类型,本质上与一段可执行的字节码(也称为智能合约)相关联。智能合约描述了各种业务逻辑,并作为 dApp 的后端。然而,尽管与传统的图灵完备开发语言相比有更多限制,准图灵完备的智能合约仍然容易受到多种攻击,给区块链行业造成了无数打击。
常见的智能合约攻击
1.重入攻击
最常见和臭名昭著的攻击是重入攻击,这种攻击导致了以太坊分叉,进而产生了以太坊经典。2016年,黑客对 DAO 合约执行了重入攻击,窃取了当时价值超过1.5亿美元的3,600,000 ETH。这次攻击发生在以太坊的早期阶段,对生态系统造成了毁灭性打击,摧毁了投资者的信心,最终导致了分叉。
具体逻辑
这里有一个例子可以帮助你更好地理解重入攻击的原理。B 银行之前借了一些钱给 A 银行。有一天,B 银行发起一笔转账给 A 银行,要求将所有钱转回 B 银行。正常的路径如下:
步骤 1:B 银行请求提取资金
步骤 2:A 银行将资金转给 B 银行
步骤 3:A 银行确认成功转账给 B 银行
步骤4:银行A更新银行B的账户余额。
然而,如果银行B在步骤2之后创建一个漏洞,并在步骤3没有确认的情况下继续从银行A请求所有资金,那么银行B在银行A的账户余额将保持不变。这种递归调用将耗尽银行A的所有资产。
:quality(80)/2024-06-12/00BA7889B1C774695FD35EC286921E80.jpg)
相关智能合约
银行A的合约包括两个函数:
- deposit():存款函数,用于向银行A存入资金并更新用户余额;
- withdraw():提款函数,允许用户从银行A提取所有资金。
:quality(80)/2024-06-12/663337160F9075D91709173CC894966C.png)
- 银行B的攻击合约主要涉及一个触发receive()回调函数的循环,该函数反过来调用银行合约的withdraw()函数,通过1次存款、1次提款和receive()回调函数调用的序列来耗尽银行A的资产,最后更新B在A中的余额。它包括两个函数:receive():在接收ETH时触发的回调函数,递归调用银行合约的withdraw()函数进行提款。
- attack():首先调用银行合约的deposit()函数刷新余额,然后调用withdraw()函数发起第一次提款,并触发receive()回调函数递归调用withdraw()以耗尽银行合约的资产。
:quality(80)/2024-06-12/BAE440DF529E4A08896A3FD4C2EB3E5A.png)
解决方案
实施重入锁
重入锁是一种用于防止重入的修饰符,确保在一次调用完成执行之前不能再次被调用。例如,由于Bank B的攻击需要多次调用Bank合约的withdraw()函数,在实施重入锁后,这种攻击将会失败。
:quality(80)/2024-06-12/F5A9D61404DE2A7CFAC0557C35A3B2A0.png)
如何使用
:quality(80)/2024-06-12/B44E9137F7F01168E686D9531A62893A.png)
2. tx.origin的误用
tx.origin 在智能合约中的主要功能是获取发起交易的原始账户。在此,我们将讨论智能合约中的两个常见变量:msg.sender和tx.origin。msg.sender获取直接调用智能合约的账户,而在区块链世界中,由于不同智能合约的嵌套和相互调用(如 DeFi Lego),需要使用tx.origin来获取发起交易的原始账户。当 dApp 开发者在代码中仅验证tx.origin的安全性,而忽略了攻击者部署中间合约绕过tx.origin发起攻击的安全验证时,就会产生漏洞。
具体逻辑
这里有一个例子,可以帮助你深入了解常见的攻击场景。Bill 有一个智能钱包,用于验证 Bill 是否是转账的发起者。有一次,Bill 在一个钓鱼网站上铸造了一个 NFT。这使得该网站获得了 Bill 的身份,并使用他的身份从他的智能钱包发起转账,导致资产损失。在正常情况下,用户不太可能落入这个陷阱,但在使用钱包与 dApp 交互时,他们经常忘记检查交互提示。例如,如果两者都涉及 Mint() 函数,粗心的用户可能很容易落入钓鱼陷阱。钓鱼网站内的业务逻辑充满陷阱,因此在常规交互过程中检查交互提示是否有错误很重要。
智能钱包合约
智能钱包合约包含一个功能:
- transfer():一个提款功能,只能由钱包所有者(在这种情况下是 Bill)发起。
:quality(80)/2024-06-12/DB747D56125A3A4E4CB3E44DA4752417.png)
钓鱼攻击合约
在钓鱼攻击合约中,Mint()函数诱导用户将资金转移到黑客的地址。它包含一个函数:
- Mint():一旦被调用,钓鱼函数会在内部执行Wallet合约的transfer()函数。由于原始发起者是用户(在本例中是Bill)本人,因此验证require(tx.origin == owner, "Not owner")不会成为问题。然而,转账的目标地址已被篡改为黑客的地址,导致资金被盗。
:quality(80)/2024-06-12/5AD33F696CBAC114CB1842880AF58F22.png)
解决方案
1.使用msg.sender而不是tx.origin
无论涉及多少合约调用(合约A → 合约B →…→ 目标合约),只验证msg.sender,即直接调用者,以避免恶意中间合约造成的攻击。
:quality(80)/2024-06-12/C39571E9DE1754D93825BFB70801D20C.png)
2. 验证tx.origin == msg.sender
这种方法可以阻止恶意合约,但开发者需要考虑自身业务实际情况,因为它实际上隔离了所有其他外部合约调用。
:quality(80)/2024-06-12/A0FE949EEF1699DF1C11234BF9D5456B.png)
3. 随机数生成器(RNG)攻击
这要追溯到2018年和2019年左右的博彩或投注去中心化应用(dApp)趋势。通常,开发者会在智能合约中使用某些种子来生成随机数,以在抽奖过程中选择获胜者。常见的种子包括block.number、block.timestamp、blockhash和keccak256。然而,矿工可以完全控制这些种子,因此在某些情况下,恶意矿工可能会操纵这些变量以获取利益。
常见的骰子合约
骰子合约包含一个函数:
- Bet():一个投注函数,用户输入一个投注数字并支付一定数量的ETH。使用多个种子生成一个随机数,如果投注数字与随机数匹配,用户就赢得整个奖池。
:quality(80)/2024-06-12/CF4DBBA2FA1C86F7F107BA74EA02A692.png)
矿工的攻击合约
只要矿工预先计算出获胜的随机数并在同一区块中执行,就可以获胜。这包括一个函数:
- attack():一个投注攻击函数,矿工预先计算出获胜的随机数。由于它在同一区块中执行,blockhash(block.number - 1)和block.timestamp在同一区块中是相同的。然后矿工调用骰子合约的Bet()函数来完成攻击。
:quality(80)/2024-06-12/8A69F0E663032EA6848A1D856541880B.png)
解决方案
使用预言机项目提供的链下随机数
通过Chainlink等预言机项目提供的服务,链上随机数被注入到链上合约中,以确保随机性和安全性。然而,预言机项目也存在中心化风险,因此需要更加成熟的预言机服务。
4. 重放攻击
重放攻击涉及使用先前使用过的签名重新发起交易以窃取资金。近年来最著名的重放攻击之一是市场做市商Wintermute在Optimism上遭受的2000万$OP代币被盗事件,这是一次跨链重放攻击。由于Wintermute的多重签名钱包账户仅临时部署在以太坊主网上,黑客利用Wintermute在以太坊上部署多重签名地址的交易签名,在Optimism链上重新执行相同的交易,从而获得了Optimism上多重签名钱包账户的控制权。多重签名钱包账户本质上是一个智能合约账户,这也展示了SCA和EOA之间的显著差异。对于EOA,普通用户只需一个私钥就可以控制以太坊和所有EVM兼容链上的所有地址(地址字符串完全相同),而SCA在部署后仅在一条链上有效。
具体逻辑
这里,我们提供一个典型重放攻击(同链重放攻击)的例子。比尔有一个智能钱包,每次交易执行前都需要他输入电子签名。现在,黑客露西已经窃取了比尔的电子签名,她可以发起无限次交易来榨干比尔的智能钱包。
示例
一个存在漏洞的合约包含三个函数:
- checkSig():ECDSA验证函数,确保验证结果为最初设置的签名者。
- getMsgHash():生成哈希的函数,将to和amount组合形成哈希。
- transfer():转账函数,允许用户从流动性池中提取资金。由于缺乏对签名的限制,同一签名可以被重复使用,使黑客能够持续窃取资金。
:quality(80)/2024-06-12/CAE52DE4E3B750CE9F99FA9A503FF0FC.png)
解决方案
在签名组合中包含nonce以防止重放攻击。参数原理如下:
- nonce:它描述了区块链网络中EOA交易次数的变量。它具有顺序性和唯一性。每增加一笔交易,nonce值就会增加1。区块链网络将检查交易的nonce是否与账户当前的nonce一致。因此,如果黑客使用已使用过的签名,由于签名组合中的nonce值小于EOA当前的nonce值,攻击将会失败。
:quality(80)/2024-06-12/546F169E51761020D9CAE39776A22424.png)
5. 拒绝服务(DoS)攻击
拒绝服务(DoS)攻击在传统Web2世界中并不新鲜。它指的是对服务器的任何干扰,例如发送大量垃圾或破坏性信息,阻碍或完全破坏可用性。同样,智能合约也受到此类攻击的困扰,其本质目的是使智能合约无法正常运行。
具体逻辑
让我们看一个例子。项目方A正在进行协议代币的公开发售,所有用户都可以向流动性池(智能合约)贡献资金以先到先得的方式购买配额,多余的资金将返还给参与者。黑客Alice利用攻击合约参与公开发售。一旦流动性池试图将资金返还给Alice的攻击合约,就会触发DoS攻击,导致返还操作永远无法实现。因此,大量资金被锁定在智能合约中。
公募合约实例
示例
公募合约包含两个功能:
- deposit():存款功能,记录存款人的地址和贡献的金额。
- refund():退款功能,项目团队通过该功能向投资者返还资金。
:quality(80)/2024-06-12/04757B0F8779FB0CE77147D719A58350.png)
DoS 攻击合约
DoS 攻击合约包含一个功能:
- attack():虽然是攻击功能,但它本身并没有任何问题。主要问题在于内置在 Hacker 合约中的 receive() 支付回调函数,该函数包含异常判断。任何外部合约向 Hacker 合约转账都会通过 revert() 触发异常,从而阻止操作完成。
:quality(80)/2024-06-12/95E6D076B9A174AAE43B1EA6F4CCBD2E.png)
解决方案
1.避免在调用外部合约时关键功能被卡住
从上述 PublicSale 合约的 refund() 函数中移除 require(success, "Refund Fail!");,确保即使单个地址退款失败,退款操作也能继续进行。
2. 解耦
在上述 PublicSale 合约的 refund() 函数中,允许用户自行申请退款,而不是分发退款,从而最大限度地减少与外部合约的不必要交互。
6. permit 攻击
在许可攻击中,账户A预先向指定方提供签名,然后账户B获得签名后可以执行授权的代币转移,从而窃取一定数量的代币。在这里,我们主要讨论智能合约中两种常见的代币授权功能:approve()和permit()。
在常见的ERC20合约中,账户A可以调用approve()来为账户B授权一定数量的代币,使后者能够从前者那里转移这些代币。此外,EIP-2612在ERC20合约中引入了permit(),而Uniswap于2022年11月发布了一个新的代币授权标准Permit2。
具体逻辑
这里举个例子。有一天,比尔正在浏览一个区块链新闻网站,突然出现了一个Metamask签名弹窗。由于许多区块链网站或应用程序使用签名来验证用户登录,比尔没有多想就直接完成了签名。五分钟后,他的Metamask资产被清空。比尔随后在区块链浏览器中发现,一个未知地址发起了一个permit()交易,紧接着是一个transferFrom()交易,清空了他的钱包。
示例
这两个函数如下:
- approve():标准授权功能,账户A授权一定数量的资金给账户B。
- permit():签名授权功能,账户B提交并完成签名验证以获得账户A授权的金额。参数包括授权的所有者、被授权的花费者、授权金额、签名截止日期,以及所有者的签名数据v、r和s。
:quality(80)/2024-06-12/AA5ADC472E3C1598E95C5E120D2D6961.png)
:quality(80)/2024-06-12/E3724BA4E50B55EAFB83EA6E5BD3784F.png)
解决方案
1. 关注链上交互中的每一个签名
尽管一些钱包采取措施解码和显示approve()授权签名信息,但它们几乎不对permit()签名钓鱼提供任何警告,这增加了攻击风险。因此,强烈建议严格检查每个未知签名,以确保它是否针对permit()函数。
2. 将日常交互的钱包与存储资产的钱包分开
这对加密货币用户来说极为重要,尤其是对空投猎人而言,因为他们每天与无数的去中心化应用程序或网站进行交互,容易陷入陷阱。在用于日常交互的钱包中只存储少量资金可以将损失控制在可管理的范围内。
7. 蜜罐攻击
在区块链行业,蜜罐攻击指的是项目方部署的一种恶意代币合约。该合约只授予项目方出售的权限,而普通用户只能买入而无法卖出,从而遭受损失。
具体逻辑
这里有一个例子。在Telegram的一则公告中,项目A通知用户代币已在主网上部署并可以交易。由于代币只能买入而不能卖出,价格起初持续上涨,担心错过机会的用户不断买入。一段时间后,当用户发现无法卖出时,项目方抓住机会抛售代币,导致价格暴跌。
示例
核心功能:
- _beforeTokenTransfer():一个在代币转移过程中调用的内部函数,只有在所有者调用时才能成功;来自其他账户的调用将失败。
:quality(80)/2024-06-12/173238C63F95AECEDB818ECEC82A16CF.png)
解决方案
使用安全扫描工具
a. 用于以太坊代币的Token Sniffer
b. 用于其他链上代币的Ave Check
c. 具有内置检测工具的市场网站,如Dextools
避免交易得分较低的代币。
8. 抢先交易攻击
抢先交易最初出现在传统金融市场,信息不对称使金融中介机构能够根据特定行业信息采取迅速行动获利。在区块链行业,抢先交易主要源于链上抢先交易,即通过操纵矿工优先将自己的交易打包上链以获取利润。
在区块链领域,矿工可以通过操纵他们打包进区块的交易来获利,例如排除某些交易和重新排序交易。这种利润可以用矿工可提取价值(MEV)来衡量。在用户的交易被添加到以太坊主网之前,大多数交易都会聚集在内存池中。矿工在这个内存池中搜索gas价格较高的交易,并优先打包它们以最大化收益。通常,gas价格较高的交易更容易被矿工打包。同时,一些MEV机器人也会在内存池中搜索具有盈利能力的交易。
具体逻辑
以下是一个例子。比尔发现了一个价格波动较大的新热门代币。为了确保在Uniswap上成功进行代币交易,比尔设置了一个异常宽松的滑点范围。不幸的是,爱丽丝的MEV机器人在内存池中检测到这笔交易,迅速提高gas费用,在比尔之前发起买入交易,并在同一区块内在比尔的交易之后插入一笔卖出交易。在区块确认后,这导致比尔遭受重大滑点损失,而爱丽丝则从低买高卖的套利操作中获利。
示例
该函数如下所示:
- solve():一个猜谜函数,任何人都可以提交答案,如果提交的答案与目标答案匹配,提交者可以获得10个以太币。
:quality(80)/2024-06-12/A38769A9E35B9AE8FD287D2474A2A7AD.png)
- 过程:Bill找到了正确答案。
- Alice监控内存池,等待有人提交正确答案。
- Bill调用solve()提交答案,并将gas价格设置为100 Gwei。
- Alice看到Bill发送的交易并发现了答案。她设置了比Bill更高的gas价格200 Gwei并调用solve()。
- Alice的交易在Bill之前被矿工打包。
- Alice赢得了10个以太币的奖励。
解决方案
三个主要函数如下所示:
- commitSolution():提交结果的函数,将用户提交的答案solutionHash、提交时间commitTime和状态revealed放入Commit结构中。
- getMySolution():获取结果的函数,允许用户查看他们提交的答案和相关信息,包括用户提交的答案solutionHash、提交时间commitTime和状态revealed。
- revealSolution():猜谜领奖的函数,允许用户在提供答案和他们设置的密码后领取奖励。
:quality(80)/2024-06-12/CE3A062F1FD436DDA8B4D97AEAC4454F.png)
:quality(80)/2024-06-12/6CF29DE911C98547EF0A5A88DB33A765.png)
过程:
- Bill找到正确答案。
- Bill调用commitSolution()提交正确答案。
- 在下一个区块中,Bill调用revealSolution(),提供答案和他设置的密码以领取奖励。
在commitSolution()中,Bill提交一个加密字符串,只有他自己知道提交的明文数据。在这一步骤中,还记录了提交区块时间commitTime。接下来,在revealSolution()中,会检查区块时间以防止同一区块内的抢先交易。由于调用revealSolution()需要提交明文答案,这一步骤旨在防止他人绕过commitSolution()直接调用revealSolution()。验证成功后,如果答案被检查正确,奖励将会发放。
结论
智能合约在区块链技术中扮演着至关重要的角色,并提供了诸多优势。首先,它们实现了去中心化的自动执行,确保了交易的安全性和可靠性,无需第三方参与。其次,智能合约减少了中间步骤和成本,提高了交易效率。
尽管有如此多的优点,智能合约仍面临着可能给用户造成经济损失的攻击风险。因此,对于链上用户来说,一些习惯是至关重要的。首先,用户应该始终谨慎选择用于交互的去中心化应用程序(dApps),并仔细审查合约代码和相关规则。此外,他们应该定期更新并使用安全的钱包和合约交互工具,以降低黑客攻击的风险。再者,建议将资金存储在多个地址中,以最大限度地减少合约攻击可能造成的潜在损失。
对于行业参与者而言,确保智能合约的安全性和稳定性同样重要。首要任务应该是加强对智能合约的审计,识别并纠正潜在的漏洞和安全风险。其次,行业参与者应该及时了解与合约攻击相关的最新区块链发展,并采取相应的安全措施。最后但同样重要的是,他们还应该加强用户教育和安全意识,特别是在正确使用智能合约方面。
总之,通过用户和行业参与者的共同努力,智能合约带来的安全风险可以得到显著降低。用户应始终谨慎选择合约并保护个人资产,而行业参与者则应加强合约审计、紧跟技术进步,并提高用户教育和安全意识。我们携手共进,推动智能合约的安全可靠发展。
参考文献:
Solidity示例:https://solidity-by-example.org/
慢雾安全的区块链知识:https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU4ODQ3NTM2OA==&action=getalbum&album_id=1378673890158936067&scene=173&from_msgid=2247498135&from_itemidx=1&count=3&nolastread=1#wechat_redirect
Chainlink - DeFi安全最佳实践十大要点:https://blog.chain.link/defi-security-best-practices/#post-title
WTF - Solidity 104 合约安全:https://www.wtf.academy/solidity-104/
DeFi智能合约漏洞4大类38种场景:https://www.weiyangx.com/381670.html
OpenZeppelin:https://github.com/OpenZeppelin/