了解solidity合约的一些安全(攻击方式)问题整理(避免踩坑)

先来复习一些基础知识:

在 Solidity 中,有三种主要的数据存储区域:memory、storage 和 stack

  1. memory:
    memory 用于临时存储数据,通常在函数调用期间使用。
    用于存储动态数组、字符串以及函数调用期间的局部变量。
    在函数调用结束时,memory 中的数据会被清空。
    操作 memory 的 gas 成本相对较高。
function concatenateStrings(string memory a, string memory b) public pure returns (string memory) {
    
    
    return string(abi.encodePacked(a, b));
}
  1. storage:
    storage 用于永久存储数据,数据会存储在区块链上。
    用于存储合约状态变量和持久化的数据。
    数据在函数调用结束后仍然存在,对区块链上的状态进行修改需要消耗 gas。
    storage 操作的 gas 成本较高。
contract StorageExample {
    
    
    uint public storedData; // 存储在 storage 中的状态变量

    function setStoredData(uint newData) public {
    
    
        storedData = newData;
    }

    // ...
}
  1. stack:
    stack 用于保存临时变量和计算的结果。
    只能存储固定大小的数据,通常用于存储基本数据类型。
    操作 stack 的 gas 成本相对较低。
function add(uint a, uint b) public pure returns (uint) {
    
    
    uint result = a + b; // result 存储在 stack 中
    return result;
}

在编写 Solidity 智能合约时,合理选择数据存储区域是至关重要的,以确保合约的正确性和高效性。
一般而言:
尽量在函数内使用 memory,特别是用于临时变量和动态数组。
使用 storage 存储合约的状态变量和持久化数据。
尽量避免过多的数据存储和频繁的读写操作,以降低 gas 成本。

solidity编码的时候一些不安全的部分

1、整数溢出和下溢:

// 不安全的整数运算
function unsafeAdd(uint256 a, uint256 b) public pure returns (uint256) {
    
    
    return a + b;
}

攻击方式: 如果 a 和 b 很大,相加可能导致整数溢出,结果将不是预期的值。攻击者可以构造恶意输入,使得整数溢出,导致结果变为较小的值,从而绕过一些检查。

2、重入攻击:

// 未防范重入攻击的代码
function unsafeWithdraw() public {
    
    
    // 取款逻辑
    msg.sender.transfer(balance);
    balance = 0; // 重置余额
}

攻击方式: 攻击者可以创建一个恶意合约,该合约在调用 unsafeWithdraw 时反复调用自身,导致合约重入并反复提取余额。

3、随机数安全性:

// 不安全的随机数生成
uint256 unsafeRandom = uint256(keccak256(abi.encodePacked(block.timestamp)));

攻击方式: 使用区块哈希和时间戳生成的随机数是可预测的,攻击者可以通过操纵区块哈希或等待特定的区块时间来预测随机数,从而影响合约的随机性。

4、权限管理不当:

// 不安全的权限管理
function unsafeTransfer(address to, uint256 value) public {
    
    
    require(msg.sender == owner, "Not the owner");
    to.transfer(value);
}

攻击方式: 如果攻击者获得了合约所有者的私钥或滥用了合约中的其他漏洞,他们可以绕过权限检查,调用 unsafeTransfer 函数,并将资金发送到受攻击者控制的地址。

5、不安全的外部合约调用:

// 不安全的外部合约调用
function unsafeTokenTransfer(address token, address to, uint256 value) public {
    
    
    IToken(token).transfer(to, value);
}

攻击方式: 如果外部合约 IToken 的 transfer 函数没有实现安全的资金转移检查,攻击者可以构造恶意合约或利用 approve 和 transferFrom 函数进行不当的转账。

在操作资产时候的安全问题

transfer 方法攻击:

// 被攻击的合约
contract VictimContract {
    
    
    function transferTo(address payable to, uint256 value) public {
    
    
        to.transfer(value);
    }
}

// 攻击合约
contract AttackContract {
    
    
    function attackTransfer(VictimContract victim, address payable to, uint256 value) public {
    
    
        // 攻击者通过多次调用 transferTo 耗尽合约余额
        for (uint i = 0; i < 10; i++) {
    
    
            victim.transferTo(to, value);
        }
    }
}

send 方法攻击:

// 被攻击的合约
contract VictimContract {
    
    
    function sendTo(address payable to, uint256 value) public {
    
    
        to.send(value);
    }
}

// 攻击合约
contract AttackContract {
    
    
    function attackSend(VictimContract victim, address payable to, uint256 value) public {
    
    
        // 攻击者通过多次调用 sendTo 耗尽合约余额
        for (uint i = 0; i < 10; i++) {
    
    
            victim.sendTo(to, value);
        }
    }
}

call 方法攻击:

// 被攻击的合约
contract VictimContract {
    
    
    function vulnerableFunction(address to, uint256 value, bytes memory data) public {
    
    
        to.call.value(value)(data);
    }
}

// 攻击合约
contract AttackContract {
    
    
    function attackCall(VictimContract victim, address to, uint256 value) public {
    
    
        // 攻击者构造恶意 data,使被攻击的合约执行不安全的操作
        bytes memory maliciousData = abi.encodeWithSignature("maliciousFunction()");
        victim.vulnerableFunction(to, value, maliciousData);
    }
}

delegatecall 方法攻击:

// 被攻击的合约
contract VictimContract {
    
    
    function vulnerableDelegateCall(address to, bytes memory data) public {
    
    
        to.delegatecall(data);
    }
}

// 攻击合约
contract AttackContract {
    
    
    function attackDelegateCall(VictimContract victim, address to) public {
    
    
        // 攻击者构造恶意 data,使被攻击的合约执行不安全的操作
        bytes memory maliciousData = abi.encodeWithSignature("maliciousFunction()");
        victim.vulnerableDelegateCall(to, maliciousData);
    }
}

在这个攻击中,攻击者构造了一个恶意的 data,通过 to.delegatecall(data) 触发 to 合约的执行,可以执行不安全的操作,例如在攻击者控制的合约上下文中执行操作。

不安全的接收方法攻击:

// 被攻击的合约
contract VictimContract {
    
    
    function unsafeReceive() external payable {
    
    
        // 不安全的接收方法
        maliciousContract.withdraw();
    }
}

// 攻击合约
contract AttackContract {
    
    
    // 攻击者通过构造恶意合约触发不安全的接收函数
    function attackUnsafeReceive(VictimContract victim) external payable {
    
    
        // 调用受害者合约的不安全接收函数
        victim.unsafeReceive{
    
    value: msg.value}();
    }
}

在这个攻击中,攻击者通过构造一个具有恶意 withdraw 操作的合约(maliciousContract),并调用受害者的不安全接收函数,从而导致不安全的操作。

接下来说明一下to.call.value(value)(data)通过构造data进行攻击

ps:实际上,to.call.value(value)(data) 是在攻击合约中使用的代码,用于调用被攻击合约的函数。在被攻击合约中,并不需要显式写这一行代码,因为攻击者通过构造 data,在调用 to.call.value(value)(data) 时,会将 data 中包含的函数及其参数传递给被攻击合约。

// 被攻击的合约
// 被攻击的合约
contract VictimContract {
    
    
    uint256 public balance;

    function deposit() external payable {
    
    
        // 存款操作
        balance += msg.value;
    }

    function withdraw(uint256 amount) external {
    
    
        // 不安全的提现操作
        require(amount <= balance, "Insufficient balance");
        msg.sender.transfer(amount);
        balance -= amount;
    }
}

攻击合约和第三方合约

// 攻击合约
contract AttackContract {
    
    
    function attackFunction(address to, uint256 value) external {
    
    
        // 构造 data 触发提现操作
        bytes memory maliciousData = abi.encodeWithSignature("withdraw(uint256)", value);
        // 发起攻击
        to.call.value(value)(maliciousData);
    }
}

// 第三方合约
contract ThirdPartyContract {
    
    
    AttackContract public attackContract;

    constructor(AttackContract _attackContract) {
    
    
        attackContract = _attackContract;
    }

    function executeAttack() external payable {
    
    
        // 调用攻击合约进行攻击
        attackContract.attackFunction(address(this), msg.value);
    }
}

在上面例子中,被攻击合约 VictimContract 并没有主动调用 to.call.value(value)(data),这是攻击者(AttackContract)负责构造并调用的部分。被攻击合约只是执行提现操作,并且攻击的成功与否取决于攻击者的构造和调用。

如何避免上面的构造data进行攻击,

在上述的被攻击代码VictimContract 中,主要的问题是 withdraw 函数的设计,它直接使用 msg.sender.transfer(amount),并没有足够的权限检查。为了避免攻击可以按如下操作:

  • 1、使用权限控制: 通过使用合适的权限控制,确保只有授权的用户或合约能够执行提现操作。

  • 2、避免在提现函数中使用 transfer: 使用 transfer 时,如果目标地址是合约,并且合约代码超过2300gas(也就是说,目标合约在调用结束前执行的计算量超过了2300 gas),那么提现操作可能会失败。因此,建议在提现函数中使用send或者更安全的 address.call.value(amount)(“”)。

下面下修改后的:

// 被攻击的合约
contract VictimContract {
    
    
    address public owner;
    uint256 public balance;

    constructor() {
    
    
        owner = msg.sender;
    }

    function deposit() external payable {
    
    
        // 存款操作
        balance += msg.value;
    }

    function withdraw(uint256 amount) external {
    
    
        // 使用权限控制,只有合约的所有者能够提现
        require(msg.sender == owner, "Permission denied");

        // 使用更安全的 address.call.value(amount)(""),而非 transfer
        (bool success, ) = msg.sender.call.value(amount)("");
        require(success, "Withdrawal failed");

        balance -= amount;
    }
}

在这个修改后的版本中,添加了合约的所有者(owner)属性,并在构造函数中初始化。提现函数进行了权限检查,只有合约的所有者才能执行提现操作。同时,提现操作使用更安全的 address.call.value(amount)(“”),以避免在合约调用时可能的 gas 不足问题。

猜你喜欢

转载自blog.csdn.net/weixin_45047825/article/details/134469220