智能合约 空指针引用(Null Pointer Dereference)

260 阅读3分钟

攻击解释

在合约中未正确处理变量为空的情况,导致对空指针进行操作而导致合约异常终止。例如,在合约中使用了未初始化或未赋值的变量进行操作

漏洞代码

pragma solidity ^0.8.0;

contract NullPointerDereference {
    mapping(address => uint256) private balances;

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

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        payable(msg.sender).transfer(amount);
    }

    function getBalance(address user) public view returns (uint256) {
        return balances[user];
    }
}

合约解释:

在这个合约中,存在一个潜在的空指针解引用漏洞。在 withdraw 函数中,合约会从发送方地址转移指定数量的以太币。

然而,在调用 transfer 函数之前,没有对余额进行有效性检查。如果发送方的地址没有在 balances 映射中具有非零余额,将会导致空指针解引用。这可能导致合约在执行转账操作时引发异常,终止合约的执行。

攻击者可以利用这个漏洞,向合约发送一笔以太币,并使用一个没有存款的地址调用 withdraw 函数。这将导致空指针解引用,使合约无法正常执行转账操作,可能导致合约的异常终止。

为了修复空指针解引用漏洞,合约应该在执行转账操作之前,先进行有效性检查以确保发送方的地址在 balances 映射中具有足够的余额。只有在余额足够的情况下,才执行转账操作。这样可以避免空指针解引用和合约异常终止的风险。

攻击方法

pragma solidity ^0.8.0;

contract AttackContract {
    address public vulnerableContract;

    constructor(address _vulnerableContract) {
        vulnerableContract = _vulnerableContract;
    }

    function performAttack() public payable {
        // 尝试调用漏洞合约的未初始化函数
        (bool success, ) = vulnerableContract.call(
            abi.encodeWithSignature("nonExistentFunction()")
        );
        require(success, "Attack failed");
    }
}

合约解释:

由于 Solidity 的设计和 EVM 的限制,直接在合约中攻击空指针解引用漏洞是非常困难的。因为 Solidity 在访问映射(balances)时会自动初始化为零值,而不会引发空指针异常。

在实际场景中,空指针解引用攻击通常涉及与其他智能合约的交互,特别是调用低级别的函数接口(例如 calldelegatecallcallcode)。

在这个攻击合约中,攻击者创建了一个名为 AttackContract 的合约,构造函数接收一个参数 _vulnerableContract,用于指定目标合约 NullPointerDereference 的地址。

攻击者可以通过调用 performAttack 函数来尝试执行攻击。在该函数中,攻击者调用目标合约的一个不存在的函数(nonExistentFunction),试图触发空指针解引用漏洞。然而,由于 Solidity 的特性,这个调用不会导致空指针异常,而是返回一个布尔值表示调用的成功与否。

需要注意的是,攻击的成功与否取决于目标合约是否存在真正的空指针解引用漏洞。在示例合约中,目标合约已经修复了漏洞,因此无法通过直接调用未初始化的函数来触发异常。

修复方法

contract FixedNullPointerDereference {
    mapping(address => uint256) private balances;

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

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        payable(msg.sender).transfer(amount);
    }

    function getBalance(address user) public view returns (uint256) {
        return balances[user];
    }
}

合约解析

修复后的合约 FixedNullPointerDereference 中,在执行转账操作之前添加了有效性检查,以确保发送方的地址在 balances 映射中具有足够的余额。只有在余额足够的情况下,才执行转账操作。

通过这种修复方式,合约能够正确处理转账操作,并避免了空指针解引用漏洞。在合约中进行有效性检查可以确保只有合法的发送方可以执行转账,并且不会触发空指针异常。这样可以增强合约的安全性和可靠性。