Article Summary
GPT 4

原理

未初始化的存储指针是指在EVM中未进行初始化的storage变量,这个变量会指向其他变量的区域,从而改变其他变量的值。

例子

pragma solidity ^0.4.24;

contract example1{
    uint public a;
    address public b;

    struct Wallet{
        uint value;
        address addr;
    }

    function setValue(uint _a,address _b) public {
        a = _a;
        b = _b;
    }

    function attack(uint _value, address _addr) public {
        Wallet wallet;
        wallet.value = _value;
        wallet.addr = _addr;
    }
}

在setValue设置(1 ,0x10aA1C20aD710B823f8c1508cfC12D5d1199117E)

这时发现a,b分别为 1 ,0x10aA1C20aD710B823f8c1508cfC12D5d1199117E

但是当 attack设置(3 ,0xa3b0D4BBF17F38e00F68Ce73f81D122FB1374ff6)

a和b也就变为了 3 ,0xa3b0D4BBF17F38e00F68Ce73f81D122FB1374ff6

这是因为在函数内部申明一个变量,通常默认是局部变量。但是Solidity的处理有些问题,在此处反直觉地默认让引用类型(Reference Type)变量,所以存储位置为storage。并且对于未初始化的storage 指针(类似传统语言中的空指针),Solidity 默认其指向 storage 的起始地址,即指向合约开头定义的状态变量。

也就是说,a指向slot0,b指向slot1.同时, wallet.value指向slot0,wallet.addr指向slot1。

同理,数组也有同样的问题。

pragma solidity ^0.4.0;

contract C {
    uint public someVariable;
    uint[] data;

    function f() public {
        uint[] x;
        x.push(2);
        data = x;
    }
}

x与someVariable的指向相同。

修复

实际上,这个问题只存在于solidity0.5.0之前的版本,编译器版本为0.4.26的话,报的还只是一个warning,不影响deploy;在下一个版本,0.5.0里面就变成了报error:

对于结构体,需要使用mapping对结构体进行初始化,并使用storage进行拷贝。

在 Solidity 中,一个未初始化的结构体并不会占用存储插槽。只有在对结构体进行实例化后,它才会占用实际的存储空间。

pragma solidity ^0.4.24;

contract example1{
    uint public a;
    address public b;

    struct Wallet{
        uint value;
        address addr;
    }
    
    mapping (uint=>Wallet)wallets;

    function setValue(uint _a,address _b) public {
        a = _a;
        b = _b;
    }

    function attack(uint _id,uint _value, address _addr) public {
        Wallet storage wallet=wallets[_id];
        wallet.value = _value;
        wallet.addr = _addr;
    }
}

数组的修复,则是在生命变量的时候对其初始化操作:

pragma solidity ^0.4.0;

contract C {
    uint public someVariable;
    uint[] data;

    function f() public {
        uint[] x=data;
        x.push(2);
        data = x;
    }

2019 BalsnCTF Bank

pragma solidity ^0.4.24;

contract Bank {
    event SendEther(address addr);
    event SendFlag(address addr);

    address public owner;
    uint randomNumber = 0;

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

    struct SafeBox {
        bool done;
        function(uint, bytes12) internal callback;
        bytes12 hash;
        uint value;
    }
    SafeBox[] safeboxes;

    struct FailedAttempt {
        uint idx;
        uint time;
        bytes12 triedPass;
        address origin;
    }
    mapping(address => FailedAttempt[]) failedLogs;

    modifier onlyPass(uint idx, bytes12 pass) {
        if (bytes12(sha3(pass)) != safeboxes[idx].hash) {
            FailedAttempt info;
            info.idx = idx;
            info.time = now;
            info.triedPass = pass;
            info.origin = tx.origin;
            failedLogs[msg.sender].push(info);
        }
        else {
            _;
        }
    }

    function deposit(bytes12 hash) payable public returns(uint) {
        SafeBox box;
        box.done = false;
        box.hash = hash;
        box.value = msg.value;
        if (msg.sender == owner) {
            box.callback = sendFlag;
        }
        else {
            require(msg.value >= 1 ether);
            box.value -= 0.01 ether;
            box.callback = sendEther;
        }
        safeboxes.push(box);
        return safeboxes.length-1;
    }

    function withdraw(uint idx, bytes12 pass) public payable {
        SafeBox box = safeboxes[idx];
        require(!box.done);
        box.callback(idx, pass);
        box.done = true;
    }

    function sendEther(uint idx, bytes12 pass) internal onlyPass(idx, pass) {
        msg.sender.transfer(safeboxes[idx].value);
        emit SendEther(msg.sender);
    }

    function sendFlag(uint idx, bytes12 pass) internal onlyPass(idx, pass) {
        require(msg.value >= 100000000 ether);
        emit SendFlag(msg.sender);
        selfdestruct(owner);
    }

}