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

news2025/4/24 17:36:18

智能合约概述

智能合约是运行在区块链网络中的一段程序,经由多方机构自动执行预先设定的逻辑,程序执行后,网络上的最终状态将不可改变。智能合约本质上是传统合约的数字版本,由去中心化的计算机网络执行,而不是由政府或银行等中央集权机构执行。智能合约程序可以用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;
    }
}

总结

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

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

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

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

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

相关文章

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

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

【开源】基于Vue+SpringBoot的就医保险管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 科室档案模块2.2 医生档案模块2.3 预约挂号模块2.4 我的挂号模块 三、系统展示四、核心代码4.1 用户查询全部医生4.2 新增医生4.3 查询科室4.4 新增号源4.5 预约号源 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVue…

【Vue2+3入门到实战】(16)VUEVue路由的重定向、404、编程式导航、path路径跳转传参 详细代码示例

目录 一、Vue路由-重定向1.问题2.解决方案3.语法4.代码演示 二、Vue路由-4041.作用2.位置3.语法4.代码示例 三、Vue路由-模式设置1.问题2.语法 四、编程式导航-两种路由跳转方式1.问题2.方案3.语法4.path路径跳转语法5.代码演示 path跳转方式6.name命名路由跳转7.代码演示通过n…

工程(十七)——自己数据集跑R2live

博主创建了一个科研互助群Q&#xff1a;772356582&#xff0c;欢迎大家加入讨论。 r2live是比较早的算法&#xff0c;编译过程有很多问题&#xff0c;通过以下两个博客可以解决 编译R2LIVE问题&解决方法-CSDN博客 r2live process has died 问题解决了_required process …

优化算法3D可视化

编程实现优化算法&#xff0c;并3D可视化 1. 函数3D可视化 分别画出 和 的3D图 import numpy as np from matplotlib import pyplot as plt import torch# 画出x**2 class Op(object):def __init__(self):passdef __call__(self, inputs):return self.forward(inputs)def for…

【软考】系统集成项目管理工程师【总】

引言 本来整理这篇文章的目的是方便自己23年考试用的 效果不错 目标完成。 接下来的目标是把这篇文章 做成参加该软考 小伙伴的唯一参考资料&#xff08;有它就够了&#xff09;来持续更新。。。 这篇文章我将当作一个长周期&#xff08;以年为单位&#xff09;项目运维起来&am…

【Web】vulhub-httpd apache解析漏洞复现(1)

目录 ①CVE-2017-15715 ②apache_parsing_vulnerability ①CVE-2017-15715 贴出源码&#xff1a; <?php if(isset($_FILES[file])) {$name basename($_POST[name]);$ext pathinfo($name,PATHINFO_EXTENSION);if(in_array($ext, [php, php3, php4, php5, phtml, pht]))…

linux常见基础指令

入门常见基础指令 ls、stat、 pwd 、cd、tree、 whoami、 touch、 mkdir、 rm 、 man、 cp、mv、cat、tac、echo、>、 >>、 < 、more、 less、 head、 tail、date、 cal、 find、 which、alias、whereis、grep、zip与unzip、 tar、bc、uname、xargs... 热键Tab、…

时序分解 | Matlab实现贝叶斯变化点检测与时间序列分解

时序分解 | Matlab实现贝叶斯变化点检测与时间序列分解 目录 时序分解 | Matlab实现贝叶斯变化点检测与时间序列分解效果一览基本介绍程序设计参考资料 效果一览 基本介绍 Matlab实现贝叶斯变化点检测与时间序列分解 1.Matlab实现贝叶斯变化点检测与时间序列分解&#xff0c;完…

STM32——通用计时器

通用计时器框图 1.时钟源 1&#xff09;内部时钟(CK_INT) 2&#xff09;外部时钟模式 1&#xff1a;外部输入引脚(TIx)&#xff0c;x1&#xff0c;2&#xff08;即只能来自于通道 1 或者通道 2&#xff09; 3&#xff09;外部时钟模式 2&#xff1a;外部触发输入(ETR) 4&#…

【面试】 Maven 的八大核心概念

Maven 的八大核心概念 在这里&#xff0c;举出这个标题&#xff0c;自然大家知道Maven是干啥的&#xff0c;就不过多进行赘述&#xff01;我们主要对于Maven的八大核心概念做一个解释补充&#xff0c;这也是我自己的一个学习历程&#xff0c;我们一起共勉&#xff01; 文章概述…

STM32实战之IAP代码升级

目录 1 IAP介绍 2 内存分区 3 整体设计流程图 4 Boot Loader的代码编写 5 APP1代码编写 6 APP2代码编写 stm32内部flash操作相关函数 1 IAP介绍 IAP&#xff08;In Application Programming&#xff09;即在应用编程&#xff0c; IAP 是用户自己的程序在运行过程中…

大创项目推荐 深度学习交通车辆流量分析 - 目标检测与跟踪 - python opencv

文章目录 0 前言1 课题背景2 实现效果3 DeepSORT车辆跟踪3.1 Deep SORT多目标跟踪算法3.2 算法流程 4 YOLOV5算法4.1 网络架构图4.2 输入端4.3 基准网络4.4 Neck网络4.5 Head输出层 5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; *…

HCIA-Datacom题库(自己整理分类的)——ARP协议【完】

一、单选 1.ARP 属于哪一层协议&#xff1f; 数据链路层 网络层 物理层 传输层 2.ARP请求是____发送的 点播 广播 组播 单播 关于ARP报文的说法错误的是? ARP请求报文是广播发送的 ARP报文不能被转发到其他广播域 ARP应答报文是单播方发送的 任何链路层协议都需…

day7--java高级编程:Junit单元测试框架、泛型,集合:集合数组互转,迭代器,增强for循环,集合工具类

补充&#xff1a;Junit单元测试框架 1. 简介 概述&#xff1a; JUnit是使用Java语言实现的单元测试框架&#xff0c;它是开源的&#xff0c;Java开发者都应当学习并使用JUnit编写单元测试。此外&#xff0c;几乎所有的IDE工具都集成了JUnit&#xff0c;这样我们就可以直接在…

STM32CubeMX教程13 ADC - 单通道转换

目录 1、准备材料 2、实验目标 3、ADC概述 4、实验流程 4.0、前提知识 4.1、CubeMX相关配置 4.1.1、时钟树配置 4.1.2、外设参数配置 4.1.3、外设中断配置 4.2、生成代码 4.2.1、外设初始化调用流程 4.2.2、外设中断调用流程 4.2.3、添加其他必要代码 5、常用函数…

2023年03月18日_微软office365 copilot相关介绍

文章目录 Copilot In WordCopilot In PowerpointCopilot In ExcelCopilot In OutlookCopilot In TeamsBusiness Chat1 - copilot in word2 - copilot in excel3 - copilot in powerpoint4 - copilot in outlook5 - copilot in teams6 - business chat word 1、起草草稿 2、自动…

【LeetCode每日一题】1185.一周中的第几天(模拟+调用库函数:三种方法)

2023-12-30 文章目录 一周中的第几天方法一&#xff1a;模拟思路步骤 方法二&#xff1a;调用库函数方法三&#xff1a;调用库函数 一周中的第几天 ​ 提示&#xff1a;给出的日期一定是在 1971 到 2100 年之间的有效日期。 方法一&#xff1a;模拟 思路 1.可以根据1970年的…

Java关键字(1)

Java中的关键字是指被编程语言保留用于特定用途的单词。这些关键字不能用作变量名或标识符。以下是Java中的一些关键字&#xff1a; public&#xff1a;表示公共的&#xff0c;可以被任何类访问。 private&#xff1a;表示私有的&#xff0c;只能被定义该关键字的类访问。 cl…

用CSS中的动画效果做一个转动的表

<!DOCTYPE html> <html lang"en"><head><meta charset"utf-8"><title></title><style>*{margin:0;padding:0;} /*制作表的样式*/.clock{width: 500px;height: 500px;margin:0 auto;margin-top:100px;border-rad…