欢迎来到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.hqwc.cn/news/310482.html

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

相关文章

啊哈c语言——4.6循环嵌套来了(练习)

1&#xff0e;请尝试用while循环打印下面的图形。 还未解出&#xff1a;有会的大神可以提点一下 &#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff0…

【Linux】chage命令使用

chage命令 chage用来更改linux用户密码到期信息&#xff0c;包括密码修改间隔最短、最长日期、密码失效时间等。 语法 chage [参数] 用户名 chage命令 -Linux手册页 选项及作用 执行令 &#xff1a; chage --help 执行命令结果 参数 -d, --lastday 最近日期 …

go的json数据类型处理

json对象转slice package mainimport ("encoding/json""fmt""github.com/gogf/gf/container/garray" )func main() {// JSON 字符串jsonStr : ["apple", "banana", "orange"]//方法一&#xff1a;// 解析 JSON 字…

操作系统:linux(在虚拟机上详细步骤安装)Centos

文章目录 前言&#xff1a;一、如何在自己的电脑上安装centos?二、在虚拟机上安装centos2.1安装步骤&#xff1a; 前言&#xff1a; 操作系统有&#xff1a;windows server 不开源的收费的、linux 开源的免费的&#xff0c;精简安装&#xff08;没有UI)。国产的操作系统有&am…

#前后端分离# 头条发布系统

头条业务简介 用户功能 注册功能登录功能jwt实现 新闻 新闻的分页浏览通过标题关键字搜索新闻查看新闻详情新闻的修改和删除 预览界面 开源上线 https://gitcode.net/NVG_Haru/NodeJS_5161447 数据库设计 数据库脚本 CREATE DATABASE sm_db;USE sm_db;SET NAMES utf8mb4…

Spark的错误处理与调试技巧

Apache Spark是一个强大的分布式计算框架&#xff0c;用于处理大规模数据。在开发和运行Spark应用程序时&#xff0c;经常会遇到各种错误和问题。了解Spark的错误处理与调试技巧是解决这些问题的关键。本文将深入探讨Spark中常见的错误类型、调试工具和技巧&#xff0c;并提供丰…

软件测试/测试开发丨Python 内置库 正则表达式re

什么是正则表达式 正则表达式就是记录文本规则的代码可以查找操作符合某些复杂规则的字符串 使用场景 处理字符串处理日志 在 python 中使用正则表达式 把正则表达式作为模式字符串正则表达式可以使用原生字符串来表示原生字符串需要在字符串前方加上 rstring # 匹配字符…

[C#]C# winform实现imagecaption图像生成描述图文描述生成

介绍&#xff1a; 地址&#xff1a;C#https://github.com/ruotianluo/ImageCaptioning.pytorch 效果&#xff1a; 测试环境&#xff1a; vs2019 onnxruntime1.16.3 opencvsharp4.8 代码&#xff1a; using System; using System.Collections.Generic; using System.Comp…

网络故障排查和流量分析利器-Tcpdump命令

Tcpdump是一个在Unix/Linux系统上广泛使用的命令行网络抓包工具。它能够捕获经过网络接口的数据包&#xff0c;并将其以可读的格式输出到终端或文件中。Tcpdump是一个强大的命令行工具&#xff0c;能够捕获和分析网络数据包&#xff0c;为网络管理员和安全专业人员提供了深入了…

Linux权限的基本理解

一:&#x1f6a9;Linux中的用户 1.1&#x1f966;用户的分类 &#x1f31f;在Linux中用户可以被分为两种用户: 超级用户(root):可以在Linux系统中做各种事情而不被约束普通用户:只能做有限的事情被权限约束 在实际操作时超级用户的命令提示符为#,普通用户的命令提示符为$,可…

基于JWT的用户token验证

1. 基于session的用户验证 2. 基于token的用户身份验证 3. jwt jwt代码实现方式 1. 导包 <dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.18.2</version> </dependency> 2. 在登录…

androidStudio 没有新建flutter工程的入口?

装了flutter dart 插件 执行了 flutter doctor 也执行了 flutter doctor --android-license 最后重启了 androidStudio 还是没发现在哪新建flutter项目工程 原来 plugins 下的 Android APK Support没有勾选