Uninitialized Storage Pointer
原理
未初始化的存储指针是指在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);
}
}