solidity 特性导致的漏洞

目录

1、默认可见性

2、浮点数精度缺失

3、错误的构造函数

4、自毁函数

5、未初始化指针-状态变量覆盖


1、默认可见性

Solidity 的函数和状态变量有四种可见性:external、public、internal、private。函数可见性默认为 public,状态变量可见性默认为 internal。

可见范围:private < internal < external < public

  • private:只有当前合约可见
  • internal:外部合约不可见,只有当前合约内部和子类合约可见
  • external:只能被外部合约或者外部调用者可见
  • public:公共函数和状态变量对所有智能合约可见

solidity 0.4 版本,函数不设置访问修饰符编译不会报错,函数默认的可见性是 public,如果一下敏感函数没有设置访问修饰符,就可能发生越权函数调用

漏洞场景:

敏感函数忘记设置访问修饰符

漏洞代码示例:

pragma solidity ^0.4.5;

contract HashForEther {
    function withdrawWinnings() {
        // Winner if the last 8 hex characters of the address are 0. 
        require(uint32(msg.sender) == 0);
        sendWinnings();
     }

     function sendWinnings() {
         msg.sender.transfer(this.balance);
     }
}

sendWinnings 函数忘记设置函数访问修饰符了,而默认可见性是 public,于是导致任意地址都可以调用改函数而获得转账。

2、浮点数精度缺失

浮点型,定长浮点型——Solidity目前暂时不支持浮点型,也不完全支持定长浮点型。

fixed/ufixed 表示有符号和无符号的定长浮点数,浮点型可以用来声明变量,但不可以用来赋值。

除法运算:除法运算的结果会四舍五入,如果出现小数,小数点后的部分都会被舍弃,只取整数部分

pragma solidity ^0.4.0;

contract C {
    uint constant public weiPerEth = 1e18;
    uint public token1;
    uint256 public token2;
    uint256  public token3;

    function testC(uint n1, uint n2) external  {
        token1 = 200 wei / weiPerEth;
        token2 = 80 / 10;
        token3 = n1 /n2;
    }
}

token1 由于除法运算出现了小数(0.0...02),取整数部分,变成了0。当 n1 小于 n2 时,如 8/10 ,token3 也将取 0.8 的整数部分,变成 0。

执行结果:

漏洞场景:

转账发送以太时,以太数量由除法运算结果所得,运算数字可控,可能导致结果精度丢失,最终导致以太丢失

漏洞示例:

pragma solidity ^0.4.23;

contract FunWithNumbers{
    uint constant public tokensPerEth = 10;
    uint constant public weiPerEth = 1e18;
    mapping(address => uint) public balances;

    function buyTokens() public payable {
        uint tokens = msg.value/weiPerEth * tokensPerEth; // 第一处浮点和精确度问题
        balances[msg.sender] += tokens;
    }

    function sellTokens(uint tokens) public {
        require(balances[msg.sender] >= tokens);
        uint eth = tokens/tokensPerEth;                ?// 第二处浮点和精确度问题
        balances[msg.sender] -= tokens;
        msg.sender.transfer(eth * weiPerEth);
    }
}

3、错误的构造函数

在 Solidity 0.4.22 版本之前,在Solidity中的0.4.22版本之前,所有的合约名和构造函数同名。编写合约时,如果构造函数名和合约名不相同,合约会添加一个默认的构造函数,自己设置的构造函数就会被当作普通函数,导致自己原本的合约设置未按照预期执行,从而造成安全漏洞。

漏洞场景:

  1. Solidity 0.4.22 前,构造函数与合约名相同,但是大小写不一样
  2. 构造函数错误地声明为了 public 或者 external

示例 1:

pragma solidity ^0.4.20;

contract OwnerWallet {

    address public owner;

    function ownerWallet(address _owner) public {
        owner = _owner;
    }

    function () payable {}

    function withdraw() public {
        require(msg.sender == owner);
        msg.sender.transfer(this.balance);
    }
}

示例 2:采用更安全的 constructor 

pragma solidity ^0.4.0;

contract C {
    address owner;
    constructor() public {
        owner = msg.sender;
    }
}

4、自毁函数

Solidity 智能合约中存在一个 selfdestruct() 自毁函数,该函数可以对创建的合约进行自毁,并且可强制将合约里的 Ether 转到自毁函数定义的地址中。

contract DeleteContract {


    constructor() payable {}

    receive() external payable {}

    function deleteContract() external {
        // 调用selfdestruct销毁合约,并把剩余的ETH转给msg.sender
        selfdestruct(payable(msg.sender));
    }

}

漏洞场景:

合约限制账户转入的以太数量,而攻击者可以使用自毁函数强制转入任意数量,并且使用 this.balance 作为敏感操作的判断条件

漏洞示例:

contract EtherGame {

    uint public payoutMileStone1 = 3 ether;
    uint public mileStone1Reward = 2 ether;
    uint public payoutMileStone2 = 5 ether;
    uint public mileStone2Reward = 3 ether; 
    uint public finalMileStone = 10 ether; 
    uint public finalReward = 5 ether; 

    mapping(address => uint) redeemableEther;
    function play() public payable {
        require(msg.value == 0.5 ether); 
        uint currentBalance = this.balance + msg.value;
        require(currentBalance <= finalMileStone);
        if (currentBalance == payoutMileStone1) {
            redeemableEther[msg.sender] += mileStone1Reward;
        }
        else if (currentBalance == payoutMileStone2) {
            redeemableEther[msg.sender] += mileStone2Reward;
        }
        else if (currentBalance == finalMileStone ) {
            redeemableEther[msg.sender] += finalReward;
        }
        return;
    }

    function claimReward() public {
        require(this.balance == finalMileStone);
        require(redeemableEther[msg.sender] > 0); 
        redeemableEther[msg.sender] = 0;
        msg.sender.transfer(redeemableEther[msg.sender]);
    }
}

攻击者角度分析

由于合约设置参与者每次提交的 Ether 数为 0.5,提交多次后就会达到10 Ether,因此攻击者就可以创建带有 selfdestruct() 函数的合约,通过 selfdestruct() 函数强制给它提供 0.1 Ether,当强制转入的 0.1 Ether 进入条件判断,最终的计算数值将永远不会成为整数,this.balance==finalMileStone 判断将永远不会成立,导致参与者永远不会获得奖励。所有参与者的 Ether 就会永远锁在 EtherGame 合约中。

5、未初始化指针-状态变量覆盖

合约中状态变量存储在 storage 中,会按声明顺序存入卡槽 slot

contract A{
    address owner;
    B addrB;
}

Solidity 对于复杂的数据类型,在函数中作为局部变量时,会默认存储在 storage 中。当声明的复杂数据类型局部变量未初始化时,它会默认成为指向 storage 的指针,就会指向 slot 0,这时如果声明了状态变量,那么第一个状态变量将会被覆盖。

pragma solidity ^0.4.0;

contract CC { 

    string public _name1;
    string public _name2;

    struct NameRecord {  
        string name1;   
        string name2; 
    }

    function CC() {
        _name1 = "makabaka";
        _name2 = "nigubigu";
    } 

    function register(string n1, string n2) public { 
        // 设置新的NameRecord ,未初始化
        NameRecord newRecord; 
        newRecord.name1 = n1; 
        newRecord.name2 = n2;  
    } 
    
}

register 函数中的结构体类型局部变量 newRecord,由于未初始化,默认会指向 slot 0,于是 newRecord.name1 会覆盖状态变量 _name1,newRecord.name2 会覆盖状态变量 _name2

猜你喜欢

转载自blog.csdn.net/SHELLCODE_8BIT/article/details/134998492