欢迎来到Web3.0的世界:Solidity智能合约安全漏洞分析

智能合约概述

智能合约是运行在区块链网络中的一段程序,经由多方机构自动执行预先设定的逻辑,程序执行后,网络上的最终状态将不可改变。智能合约本质上是传统合约的数字版本,由去中心化的计算机网络执行,而不是由政府或银行等中央集权机构执行。智能合约程序可以用Solidity或Vyper等编程语言实现,并存储在区块链上,在公链网络上,任何人都可以访问和执行部署好的智能合约。

智能合约拥有防篡改、透明和自动化等特征,这使其非常适合于金融交易,供应链管理等应用场景,其次,在商业保险,游戏,环保等领域都有所应用。现如今,区块链被视作为一种潜在的革命性技术,可以改变许多行业的协议制定和执行方式。

安全问题分析解决

智能合约既然是一段程序代码,同样会存在着缺陷或者错误导致出现致命的安全漏洞,在执行过程中,存在诸多的风险,并不能保证其完全安全。事实上,大多数的智能合约都和金融资产有所关联,其对应的智能合约漏洞的利用,意味着用户资产的损失,比如代币失窃,执行未经授权的交易,甚至是拖垮整个区块链网络。在这篇文章中,我们将谈论最常见的智能合约安全问题,以及处理这些问题的方法。

不安全的算术运算(Insecure Arithmetic)

这是一类非常经典的漏洞,主要来源于未经检查的算术运算。在Solidity 0.8.x以前,当一个整数变量达到其范围的下限或上限时,它将自动变为一个较低或较高的数字。

漏洞描述
function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {uint cnt = _receivers.length;// 计算应付总金额uint256 amount = uint256(cnt) * _value;require(cnt > 0 && cnt <= 20);require(_value > 0 && balances[msg.sender] >= amount);balances[msg.sender] = balances[msg.sender].sub(amount);for (uint i = 0; i < cnt; i++) {balances[_receivers[i]] = balances[_receivers[i]].add(_value);Transfer(msg.sender, _receivers[i], _value);}return true;}

以上的智能合约函数实现了一个批量转账的功能,将合约账户上的资金分别转给多个地址(不超过20个)。主要漏洞在以下这行代码:

uint256 amount = uint256(cnt) * _value;

攻击者可以传入一个比较大的数值,使得计算出来的amount值很小,小于了自己账户里的可用余额,从而通过了可用余额的校验,最终得到了一大笔资金入账。

解决方案
  • 将Solidity编译器升级至0.8.0及其以上的版本,会自动检测数值溢出的异常;
  • 如果不方便升级Solidity编译器的话,可以考虑使用安全的三方库(比如Open Zeppelin),实现安全可信的算术运算;
  • 将以上的有漏洞的代码改为:
function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {uint cnt = _receivers.length;// 计算应付总金额uint256 amount = uint256(cnt) * _value;require(cnt > 0 && cnt <= 20);// 使用除法换算出来的值要等于传入的_valuerequire(amout / uint256(cnt) == _value)require(_value > 0 && balances[msg.sender] >= amount);balances[msg.sender] = balances[msg.sender].sub(amount);for (uint i = 0; i < cnt; i++) {balances[_receivers[i]] = balances[_receivers[i]].add(_value);Transfer(msg.sender, _receivers[i], _value);}return true;
}

越权攻击(Exceed Authority Access Attack)

通常有两种情况会导致越权攻击:

  • 不恰当的函数可见性设置。如果不显式指定函数可见性,那么默认为public,意味着允许未经授权的用户调用该函数;
  • 没有设置owner,某些关键性的函数不可被任意访问,而是应该指定特定的使用者。
漏洞描述

如以下代码所示,由于_sendWinnings函数没有设置可见性,默认是 public,攻击者可以通过调用此函数直接窃取资金。

contract HashForEther {function withdrawWinnings() {// 钱包地址十六进制的后8位全是0require(uint32(msg.sender) == 0);_sendWinnings();}function _sendWinnings() {	msg.sender.transfer(this.balance);}
}
解决方案
  1. _sendWinnings函数的可见性设置为private
  2. _sendWinnings函数限制调用者,通常是管理员或者合约部署者
contract HashForEther {address private _owner;constructor(address owner) {_owner = owner;}modifier ownerable() {require(_owner == msg.sender);_;}function withdrawWinnings() public {// 钱包地址十六进制的后8位全是0require(uint32(msg.sender) == 0);_sendWinnings();}function _sendWinnings() public ownerable {msg.sender.transfer(this.balance);}
}

重入攻击(Reentrancy attack)

重入攻击是存在以太坊上最常见的智能合约安全漏洞。在以太坊中,对其他智能合约函数的调用并非异步进行的,也就是意味着自身的智能合约继续执行之前,会等待外部方法的执行结束,这将非常有可能导致被调用的合约的中间状态被不合理的利用。

漏洞描述
pragma solidity 0.8.17;
contract EtherStore {// 存储链上地址与对应的可用余额mapping(address => uint) public balances;function deposit() public payable {// 消息调用者在该合约中的存款加上账户当余额balances[msg.sender] += msg.value;}function withdraw() public {uint bal = balances[msg.sender];// 判断是否有可用余额require(bal > 0);// 提取全部的金额(bool sent, ) = msg.sender.call{value: bal}("");require(sent, "Failed to send Ether");// 将地址对应的修改为0balances[msg.sender] = 0;}
}

以上是一个简单的存款/提款的智能合约,漏洞主要出现在以下的一行代码:

(bool sent, ) = msg.sender.call{value: bal}("");

能使得以上漏洞被成功利用,是具备了三个条件:

  • call函数的调用没有交易手续费(Gas)限制,默认会使用所有剩余的Gas,这是用于执行智能合约的以太坊虚拟机的特性;
  • msg.sender是来自另外一个恶意智能合约的地址,当收到交易转账后,会触发fallback函数;
  • 发起攻击的智能合约实现fallback函数,主要是再一次触发被攻击的智能合约的提款函数。
    重入攻击的递归黑洞

实际上,两个智能合约之间的调用已经进入了“递归黑洞”,攻击者只需要向被攻击的智能合约中存入少量的资金,通过不断调用提款函数,可以提取超额的回报。

解决方案
  • 使用send()或者transfer()函数,因为有Gas限制,最多消耗2300Gwei;

  • 慎用外部函数,检查每一个直接或者间接调用外部函数的地方,确保状态变更完成之后,再调用;

    function withdraw() external {uint bal = balances[msg.sender];require(bal > 0);// 先更新余额变化,再发送资金// 重入攻击的时候,balances[msg.sender]已经被更新为0了,不能通过上面的检查。balanceOf[msg.sender] = 0;(bool success, ) = msg.sender.call{value: bal}("");require(success, "Failed to send Ether");
    }
    
  • 为每一个账户地址增加重入标识,操作执行完成之前,不允许重复执行相同的逻辑。

    uint private _status; // 重入锁
    // 重入锁
    modifier nonReentrant() {// 在第一次调用 nonReentrant 时,_status 将是 0require(_status == 0, "ReentrancyGuard: reentrant call");// 在此之后对 nonReentrant 的任何调用都将失败_status = 1;_;// 调用结束,将 _status 恢复为0_status = 0;
    }
    // 只需要用nonReentrant重入锁修饰withdraw()函数,就可以预防重入攻击了。
    function withdraw() external nonReentrant {uint bal = balances[msg.sender];// 判断是否有可用余额require(bal > 0);// 提取全部的金额(bool sent, ) = msg.sender.call{value: bal}("");require(sent, "Failed to send Ether");// 将地址对应的修改为0balances[msg.sender] = 0;
    }
    

拒绝服务攻击(DoS Attack)

正常情况下,一个智能合约对外提供稳定的服务是基于一个大前提:在耗尽交易手续费(Gas)之前,智能合约程序可以正常执行结束。攻击者正是破坏了这一个大前提,使得智能合约不能正常提供服务。

漏洞描述

以下是一个拍卖的智能合约,主要的功能是价高者胜出,未中标的买家将会被立即退还竞拍保证金。

contract Auction {address currentLeader;uint highestBid;constructor () {currentLeader = msg.sender;highestBid = 1;}function bid() payable {require(msg.value > highestBid);(bool success, ) = currentLeader.call{value: highestBid}("");require(success, "Refund failed");currentLeader = msg.sender;highestBid = msg.value;}
}

将会产生漏洞的代码是:

(bool success, ) = currentLeader.call{value: highestBid}("");

攻击者可以制造一个恶意的智能合约,实现了fallback回调函数,在fallback函数内回滚交易。这个智能合约持续向拍卖合约发起攻击,一旦自己成为了最高价者,在试图退还竞拍保证金的时候,由于恶意智能合约的fallback函数,返回的success的值是false,导致退还失败,在这之后的赋值新的竞拍者的代码逻辑将永远不会执行到,其他竞拍者也就没有机会获得成功。

解决方案

解决以上漏洞,最主要是分开竞拍和退款两个操作。若竞拍失败,先记录退款地址,再单独提供退款的操作,由用户自行提取竞拍保证金。

contract Auction {address highestBidder;uint highestBid;mapping(address => uint) refunds;constructor () {currentLeader = msg.sender;highestBid = 1;}function bid() payable external {require(msg.value >= highestBid);if (highestBidder != address(0)) {// 记录要退款的金额refunds[highestBidder] += highestBid;}highestBidder = msg.sender;highestBid = msg.value;}// 单独提供退款操作function withdrawRefund() external {uint refund = refunds[msg.sender];refunds[msg.sender] = 0;(bool success, ) = msg.sender.call.value(refund)("");require(success);}
}

值得注意的是,这里不建议开启一个循环自动处理退款,有两个原因:

  1. 退款地址可能是一个恶意攻击的合约地址;
  2. 退款地址数量很大,Gas耗费巨大,不能保证全部的退款能到账。

蜜罐攻击(Honeypot Attack)

一些智能合约会故意暴露显而易见的“漏洞”,通常情况下,用户会发送资金,以期获得超额的回报,最终却被该智能合约“反咬一口”,不但没有获得预期的回报,反而损失了本金。

漏洞描述
contract CryptoRoulette {uint256 private secretNumber;uint256 public lastPlayed;uint256 public betPrice = 0.001 ether;address public ownerAddr;struct Game {address player;uint256 number;}Game[] public gamesPlayed;constructor() public {ownerAddr = msg.sender;shuffle();}function shuffle() internal {// 中奖号码设置为一个固定的数字6secretNumber = 6;}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) {// 如果传入的数字正好是中奖号码,则可以赢取奖金msg.sender.transfer(this.balance);}//shuffle();lastPlayed = now;}function kill() public {if (msg.sender == ownerAddr && now > lastPlayed + 6 hours) {suicide(msg.sender);}}function() public payable { }
}

如上述代码所示,很容易被注意到,初始化的中奖号码是6,但是实际调用play(6)之后,并不会如期赢取奖金。其原因,主要是Game变量未实例化,EVM的存储机制决定了secretNumber最终的值已不再是6了,而是智能合约的调用者的地址,所以参与者始终都不会得到奖金。
EVM Storage
如上图所示,EVM的存储结构是由 2^256 个插槽 (Slot)组成,每个插糟有 32byte,等同于256bit,正好是可以存放一个uint256类型的变量,合约中的状态变量会根据其具体类型分别顺序保存到这些插槽中。

play函数中,因为Game并没有初始化,对game.playergame.number的赋值,实际上是分别对Slot0Slot1进行了赋值,按照变量定义的顺序,其分别是secretNumberlastPlayed。如果用户传入的number是6的话,与实际的secretNumber的值是不相等的,非但不能获得奖金,而且还损失了本金。

解决方案

从用户视角来看,作为合约的调用方/使用者,需要甄别对方的智能合约的实现是否合理,除了使用未经实例化的局部变量,还有诸如Solidity版本过低,使用了未知的代理合约,引用了恶意的代码库等等。

除此之外,应该多关注业界发生的安全事件,及其相关的资讯文章,比如 mirror、DL News,也可以借助一些工具和平台,辅助交易,比如 BlockSec,Flashbots。

智能合约升级

智能合约与传统应用程序有一个不同的地方在于智能合约一经发布于区块链上就无法篡改,即使智能合约中有漏洞需要修复,或者需要对业务逻辑进行变更,它也不能在原有的合约上直接修改再重新发布,因此在设计之初就需要结合业务场景考虑合理的升级机制。

按照程序升级的通常意义来理解,升级后的程序首先是要满足用户的正常使用,用户的信息和资产没有丢失,其次是最好能做到兼容和适配以往的版本。

实现原理

如果要编写可升级的智能合约,通常的做法是使用代理模式来实现。用户请求的是代理合约(Proxy Contract),再通过代理合约进行委托调用实际的逻辑合约(Logic Contract)。因为是通过delegatecall函数调用逻辑合约,实际上是由代理合约来存储状态变量,即它是存储层。这就像你只是执行了逻辑合约的程序,并在代理合约所在的上下文中存储状态变量。代理合约通常有两种实现方式:透明代理,UUPS。这两种方式最核心的区别在于智能合约升级的逻辑在哪里实现,透明代理模式把升级的逻辑放在了代理合约里,而UUPS则放在了逻辑合约里。
智能合约升级实现原理

示例代码

在代理合约中,完成对实际的逻辑合约重定向的功能(setLogicAddress),以及通过委托调用,对主要函数的实现(setNumber)。

contract proxy {uint256 private number;address private logicAddress;address private owner;constructor(address _logicAddress) {logicAddress = _logicAddress;owner = msg.sender;}modifier ownerable() {require(owner == msg.sender);_;}function setLogicAddress(address _logicAddress) ownerable public {logicAddress = _logicAddress;}function setNumber(uint256 _number) public returns(bool) {(bool success,) = logicAddress.delegatecall(abi.encodeWithSignature("setNumber(uint256)", _number));return success;}
}

在第一个版本的逻辑合约中,我们实现的功能是对number进行+1操作,部署logic1合约,调用proxy合约中的setLogicAddress方法,传入logic1合约的地址。

contract logic1 {uint256 private number;function setNumber(_number) public {number = _number + 1;}function getNumber() public view returns(uint256) {return number;}
}

随后需要升级,改为对number进行×2操作,部署logic2合约,调用proxy合约中的setLogicAddress方法,传入logic2合约的地址,即可完成升级。

contract logic2 {uint256 private number;function setNumber(_number) public {number = _number * 2;}function getNumber() public view returns(uint256) {return number;}
}

总结

智能合约的开发技术相对较新,暂未形成工业级的标准规范,开发者缺乏明确的指导,不能保证所开发的代码的安全性。另外,既然都是由人创造的,就会受限于主观意识,一些人为因素也将会导致事故的发生。对于智能合约的安全验证,暂未出现正式的并且广泛使用的技术规范。

智能合约的安全性是区块链技术的一个重要方面,也正是其复杂之处。智能合约在带来诸多好处的同时,也容易受到各种潜在安全风险和漏洞的影响。在开发基于区块链的应用程序时,智能合约的安全性是一个值得考虑的重要因素,必须采取积极主动的方法来识别和减少漏洞,以确保合约及其所管理资产的完整性和安全性。

转载申明:未经作者本人同意,本篇文章不可转载或者作为文摘、资料刊登。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/586052.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MIT 6.S081---Lab util: Unix utilities

环境搭建 基本环境 选择的是Vmwareubuntu的配置&#xff0c;注意ubuntu的版本一定要是20.04&#xff0c;作者试过16版本&#xff0c;不行&#xff0c;建议直接安装20.04版&#xff0c;不然环境配置都浪费不少时间有点得不偿失。&#xff08;Vmware可以用Virtualbox代替&#…

【消息中间件】Rabbitmq消息可靠性、持久化机制、各种消费

原文作者&#xff1a;我辈李想 版权声明&#xff1a;文章原创&#xff0c;转载时请务必加上原文超链接、作者信息和本声明。 文章目录 前言一、常见用法1.消息可靠性2.持久化机制3.消息积压批量消费&#xff1a;增加 prefetch 的数量,提高单次连接的消息数并发消费&#xff1a;…

门诊病历系统教程,社区诊所电子处方系统软件操作教程

一、软件程序问答 门诊病历系统教程&#xff0c;社区诊所电子处方系统软件操作教程 1、电子处方软件在开处方时候&#xff0c;可以一键导入模板吗&#xff1f; 如下图&#xff0c;软件以 佳易王诊所电子处方软件V17.1为例说明 软件右侧点击 配方模板&#xff0c;只需输入症…

从零开始学Python系列课程第17课:容器型数据类型之列表(上)

前言 列表算是 Python 中比较常用的一种容器型数据类型&#xff0c;那么什么是列表&#xff0c;列表有什么样的作用致使它在 Python 中这么受欢迎呢&#xff1f;这便是接下来我们要一起讨论的问题。 在不久之前我们讲过变量&#xff0c;我们将数据使用变量保存&#xff0c;但是…

08.哲说建造者模式(Builder Pattern)

“The odds that we’re in ‘base reality’ is one in billions.” —— Elon Musk 这段话出自马斯克在2016年的一次演讲&#xff0c;“人类活在真实世界的几率&#xff0c;可能不到十亿分之一”。此言一出&#xff0c;可谓一石激起千层浪。有人嘲讽马斯克是“语不惊人死不休…

[2024] 十大免费电脑数据恢复软件——轻松恢复电脑上已删除文件

哈喽大家好&#xff01;你有没有需要适用于电脑的免费数据恢复软件呢&#xff1f;数据丢失可是个烦心事&#xff0c;无论是硬件故障还是软件损坏&#xff0c;甚至是意外删除、格式化或计算机病毒&#xff0c;都让人郁闷至极。当你遇到数据丢失的情况时&#xff0c;你一定希望能…

【Git】Git的基本操作

前言 Git是当前最主流的版本管理器&#xff0c;它可以控制电脑上的所有格式的文件。 它对于开发人员&#xff0c;可以管理项目中的源代码文档。&#xff08;可以记录不同提交的修改细节&#xff0c;并且任意跳转版本&#xff09; 本篇博客基于最近对Git的学习&#xff0c;简单介…

Python中的用户交互函数详解,提升用户体验!

更多Python学习内容&#xff1a;ipengtao.com 用户进行交互的Python应用程序&#xff0c;有许多常用的用户交互函数可以帮助创建更具吸引力和友好的用户界面。本文将介绍一些常用的Python用户交互函数&#xff0c;并提供详细的示例代码&#xff0c;以帮助大家更好地理解它们的用…

右键添加 idea 打开功能

1.开始运行regedit 2.找到: HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Directory\shell _3.开始设置 一、右键shell目录新建项Idea二、右键Idea新建command三、选择Idea 右侧空白出新建字符串 名字为Icon 值填入idea的运行程序地址 四、选择command 默认项填入idea的运行程序地址…

技术探秘:在RISC Zero中验证FHE——RISC Zero应用的DevOps(2)

1. 引言 前序博客&#xff1a; 技术探秘&#xff1a;在RISC Zero中验证FHE——由隐藏到证明&#xff1a;FHE验证的ZK路径&#xff08;1&#xff09; 技术探秘&#xff1a;在RISC Zero中验证FHE——由隐藏到证明&#xff1a;FHE验证的ZK路径&#xff08;1&#xff09; 中&…

【privateGPT】使用privateGPT训练您自己的LLM

了解如何在不向提供商公开您的私人数据的情况下训练您自己的语言模型 使用OpenAI的ChatGPT等公共人工智能服务的主要担忧之一是将您的私人数据暴露给提供商的风险。对于商业用途&#xff0c;这仍然是考虑采用人工智能技术的公司最大的担忧。 很多时候&#xff0c;你想创建自己…

【GOLANG】使用插件 Goanno 的方式来对方法、接口、结构体注释模板配置

直接 使用插件 Goanno 的方式来对方法、接口、结构体注释模板配置 1、简单安装 Goanno 插件 File->Settings->Plugins , 搜索 Goanno Normal Method 配置内容如下&#xff1a; // Title ${function_name} // Description ${todo} // Author mumu ${date} ${time} // Par…

技能分析:这就是人们写在简历上的内容

您希望您的技能部分听起来像其他人一样吗&#xff1f;另一方面&#xff0c;您是否想遗漏一项顶级技能&#xff0c;因为许多其他简历也列出了它&#xff1f;在脱颖而出和涵盖雇主寻求的所有技能之间找到平衡可能是一个挑战。 优秀的简历技能部分会考虑到每个雇主所寻求的特质。…

海云安亮相2023北京国际金融安全论坛,助力金融企业数字化转型降本增效

近日&#xff0c;2023北京国际金融安全论坛暨金融科技标准认证生态大会在北京金融安全产业园成功举办。深圳海云安网络安全技术有限公司&#xff08;以下简称“海云安”&#xff09;受邀参展亮相此次大会。海云安作为国内领先的金融科技服务商&#xff0c;展示了开发安全系列产…

Unity坦克大战开发全流程——开始场景——排行榜数据逻辑

开始场景——排行榜数据逻辑 排行榜单条数据 排行榜列表 然后在数据管理类中声明一个对应的字段 初始化数据 然后再在上一节课所编写的UpdatePanelInfo函数中处理数据更新的逻辑 时间换算算法 然后再在数据管理类中编写一个在排行榜中添加数据的方法以提供给外部 直到当前RankI…

【BERT】深入理解BERT模型1——模型整体架构介绍

前言 BERT出自论文&#xff1a;《BERT&#xff1a;Pre-training of Deep Bidirectional Transformers for Language Understanding》 2019年 近年来&#xff0c;在自然语言处理领域&#xff0c;BERT模型受到了极为广泛的关注&#xff0c;很多模型中都用到了BERT-base或者是BE…

搜索算法和推荐算法、广告算法的区别

广告和推荐算法的技术框架比较相似&#xff0c;在线计算时都分为召回&#xff08;candidates generating&#xff09;和排序&#xff08;candidates ranking&#xff09;两个阶段&#xff08;这似乎是计算资源有限条件下&#xff0c;所有检索问题的通用架构&#xff09;。 在某…

RabbitMQ详解

RabbitMQ 概念 RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。 AMQP &#xff1a;Advanced Message Queue&#xff0c;高级消息队列协议。它是应用层协议的一个开放标准&#xff0c;为面向消息的中间件设计&#xff0c;基于此协议的客户端与消息中间件可传递消息&a…

2023年全国网络安全行业职业技能大赛数据安全管理员操作技能赛题(样题)

2023年全国网络安全行业职业技能大赛数据安全管理员操作技能赛题(样题) 2023年全国网络安全行业职业技能大赛数据安全管理员操作技能赛题(样题) 第一部分&#xff1a;数据安全防护(30%) 第二部分&#xff1a;数据安全管理(30%) 第三部分&#xff1a;数据安全处置(40%) 项目介绍…

【JavaEE】多线程(7) -- 线程池的概念和简单实现

目录 1.线程池是什么 2.标准库中的线程池 2.1ThreadPoolExecutor 2.2构造方法参数介绍 2.3拒绝策略(面试易考) 2.4Executor的使用 3.实现线程池 1.线程池是什么 线程池是一种用来管理线程的机制&#xff0c;它可以有效地控制线程的创建、复用和销毁&#xff0c;从而提高程…