欢迎来到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);
    // 使用除法换算出来的值要等于传入的_value
    require(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位全是0
        require(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位全是0
        require(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");
		// 将地址对应的修改为0
        balances[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 将是 0
        require(_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");
        // 将地址对应的修改为0
        balances[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 {
        // 中奖号码设置为一个固定的数字6
        secretNumber = 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;
    }
}

总结

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

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android Studio配置国内镜像源和HTTP代理/解决:Android Studio下载gradle速度慢的问题

&#xff08;方案一&#xff09;Android Studio配置国内镜像源和HTTP代理 一、配置国内镜像源/依赖库 1.1 打开项目的setting.gradle.kts文件 配置进去 pluginManagement {repositories {maven { urluri ("https://www.jitpack.io")}maven { urluri ("https:…

Qt 5.9.4 转 Qt 6.6.1 遇到的问题总结(一)

最近公司对大家的开发的硬件环境进行了升级&#xff0c;电脑主机的配置、显示器&#xff08;两台大屏显示器&#xff09;变得的逼格高多了。既然电脑上的开发环境都需要重装&#xff0c;就打算把开发环境也升级到最新版本&#xff0c;要用就用最新版本。下面对升级后的开发环境…

每日一题——LeetCode976

方法一 个人方法 找规律&#xff1a; 要求要围成三角形且周长最大&#xff0c;那么三条边自然是越大且越接近越好。那么我们就从最大的三条边开始看能不能围成三角形。如果不能组成三角形&#xff0c;则丢弃最长的那条&#xff0c;再取剩余边里最长的那条再看能不能组成三角形…

汽车制造厂批量使用成华制造弹簧平衡器

数年来&#xff0c;成华制造都在不断的向各行各界输出着自己的起重设备&#xff0c;与众多企业达成合作&#xff0c;不断供应优质产品。近些年&#xff0c;成华制造以其卓越的产品质量和高效的生产能力&#xff0c;成功实现了弹簧平衡器的大规模批量供应&#xff0c;为重庆数家…

R统计学1 - 基础操作入门问题1-20

R统计学入门基础问题 1. 如何生成100个高斯&#xff08;正态&#xff09;分布随机数 x <- rnorm(100, mean 5, sd 0.1) x # [1] 4.893534 5.046611 5.081097 4.979164 5.181700 5.038192 5.135376 5.173346 4.968877 4.986146 # [11] 4.946258 5.198199 5.055531 4.9430…

Git:远程仓库的使用

查看当前的远程库 要查看当前配置有哪些远程仓库&#xff0c;可以用git remote 命令&#xff0c;它会列出每个远程库的简短名字。在克隆完某个项目后&#xff0c;至少可以看到一个名为origin 的远程库&#xff0c;Git 默认使用这个名字来标识你所克隆的原始仓库&#xff1a; 也…

RHCE9学习指南 第13章 硬盘管理

新的硬盘首先需要对硬盘进行分区和格式化&#xff0c;首先了解一下硬盘的结构&#xff0c;如图13-1所示。 图13-1 磁盘上的磁道和扇区 硬盘的磁盘上有一个个的圈&#xff0c;每两个圈组成一个磁道。从中间往外发射线&#xff0c;把每个磁道分成一个个的扇区&#xff0c;每个扇…

QT上位机开发(第一个应用)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 不管是软件&#xff0c;还是硬件&#xff0c;如果我们能够顺利启动第一个应用&#xff0c;点亮第一个电路的话&#xff0c;这对我们的信心来说会有…

【MySQL】主从异步复制配置

您好&#xff0c;我是码农飞哥&#xff08;wei158556&#xff09;&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f4aa;&#x1f3fb; 1. Python基础专栏&#xff0c;基础知识一网打尽&#xff0c;9.9元买不了吃亏&#xff0c;买不了上当。 Python从入门到精…

Spring Boot快速搭建一个简易商城项目【完成登录功能且优化】

完成登录且优化&#xff1a; 未优化做简单的判断&#xff1a; 全部异常抓捕 优化&#xff1a;返回的是json的格式 BusinessException&#xff1a;所有的错误放到这个容器中&#xff0c;全局异常从这个类中调用 BusinessException&#xff1a; package com.lya.lyaspshop.exce…

unity学习笔记----游戏练习03

一、修复植物种植的问题 1.当手上存在植物时&#xff0c;再次点击卡片上的植物就会在手上添加新的植物&#xff0c;需要修改成只有手上没有植物时才能再次获取到植物。需要修改AddPlant方法。 public bool AddPlant(PlantType plantType) { //防止手上出现多个植…

《深入理解C++11:C++11新特性解析与应用》笔记五

第五章 提高类型安全 5.1 强类型枚举 5.1.1 枚举&#xff1a;分门别类与数值的名字 具名枚举类型一般声明类似&#xff1a;enum Gender { Male, Female }。 匿名枚举类型可以使用三种方式实现&#xff1a; 第一种方式时宏&#xff0c;比如 #define Male 0 #define Femal…

安全生产知识竞赛活动方案

为进一步普及安全生产法律法规知识&#xff0c;增强安全意识&#xff0c;提高安全技能&#xff0c;经研究&#xff0c;决定举办以“加强安全法治、保障安全生产”为主题的新修订《安全生产法》知识竞赛活动&#xff0c;现将有关事项通知如下&#xff1a; 一、活动时间&#xf…

机器学习(二) -- 数据预处理(3)

系列文章目录 机器学习&#xff08;一&#xff09; -- 概述 机器学习&#xff08;二&#xff09; -- 数据预处理&#xff08;1-3&#xff09; 未完待续…… 目录 前言 tips&#xff1a;这里只是总结&#xff0c;不是教程哈。本章开始会用到numpy&#xff0c;pandas以及matpl…

【Kubernetes】什么是 kubectl ?

什么是 kubectl &#xff1f; 1.什么是 kubectl &#xff1f;2.Kubernetes 内部结构3.Kubernetes API 的作用 1.什么是 kubectl &#xff1f; 在学习如何更有效地使用 kubectl 之前&#xff0c;您应该对它是什么以及它如何工作有一个基本的了解。从用户的角度来看&#xff0c;…

助力打造智慧数字课堂,基于YOLOv8全系列【n/s/m/l/x】不同参数量级模型开发构建教学课堂场景下学生课堂行为检测识别分析系统

近年来&#xff0c;随着行为检测技术的发展&#xff0c;分析学生在课堂视频中的行为&#xff0c;以获取他们的课堂状态和学习表现信息已经成为可能。这项技术对学校的教师、管理人员、学生和家长都非常重要。使用深度学习方法自动检测学生的课堂行为是分析学生课堂表现和提高教…

计算机视觉技术-目标检测数据集

目标检测领域没有像MNIST和Fashion-MNIST那样的小数据集。 为了快速测试目标检测模型&#xff0c;我们收集并标记了一个小型数据集。 首先&#xff0c;我们拍摄了一组香蕉的照片&#xff0c;并生成了1000张不同角度和大小的香蕉图像。 然后&#xff0c;我们在一些背景图片的随机…

分享好用稳定快递查询api接口(对接简单)

提供实时查询和自动识别单号信息。稳定高效&#xff0c;调用简单方便&#xff0c;性价比高&#xff0c;一条链接即可对接成功。 使用数据平台该API接口需要先注册后申请此API接口。申请成功后赠送免费次数&#xff0c;可直接在线请求接口数据。 接口地址&#xff1a;https://…

ArkUI中自定义组件的生命周期

文章概叙 本文主要是介绍下在作为page以及component的时候的生命周期&#xff0c;以及调用API等应该在哪个生命周期使用。 书接上回 之前的博客已经结束了对底部栏的操作&#xff0c;现在开始需要关注到具体内容的对接了。 而开发的第一步&#xff0c;我们对页面的生命周期…

你了解螺杆螺纹吗?

螺杆的螺纹部分是其核心部分之一&#xff0c;主要作用是传递旋转运动和力矩&#xff0c;丝杆的螺纹形状和参数对其性能和使用寿命有着重要影响&#xff1b;常用的螺杆螺纹可以分为&#xff1a;三角牙螺纹、梯形牙螺纹、矩形牙螺纹、锯齿牙螺纹、滚珠螺纹。 1、三角牙&#xff1…