漏洞简介
在智能合约语言 Solidity当中,存在Storage(存储器)和Memory(内存)两个不同的概念,Storage变量是指永久存储在区块链中的变量,Memory变量是临时的,这些变量在外部调用结束后会被移除。
Solidity中struct和数组在局部变量中默认是存放在storage中的,因此可以利用未定义的存储指针的安全问题,下面的p会被当成一个指针,并默认指向slot[0]和slot[1],因此在进行p.name和p.mappedAddress赋值的时候,实际上会修改变量testA,testB的值:
数组也是类似的情况:
解决方案
结构体Unintialised Storage Pointers问题的正确的解决方法是将声明的struct进行赋值初始化,通过创建一个新的临时memory结构体,然后将它拷贝到storage中:
数组Unintialised Storage Pointers问题的正确解决方法是在声明局部变量x的时候,同时对x进行初始化操作:
Solidity编译器开发团队不出意外将在下一个版本(Solidity 0.4.25)中对存在Unintialised Storage Pointers问题的代码进行修复,否则将无法正常通过编译,开发人员需要关注 Solidity 0.4.25 版本的发布,并且使用Solidity 0.4.25编写代码。
案例分析
未初始化的结构体局部变量
pragma solidity ^0.4.22;
contract NameRegistrar {
bool public unlocked = false; // registrar locked, no name updates
struct NameRecord { // map hashes to addresses
bytes32 name;
address mappedAddress;
}
mapping(address => NameRecord) public registeredNameRecord; // records who registered names
mapping(bytes32 => address) public resolve; // resolves hashes to addresses
function register(bytes32 _name, address _mappedAddress) public {
// set up the new NameRecord
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;
resolve[_name] = _mappedAddress;
registeredNameRecord[msg.sender] = newRecord;
require(unlocked); // only allow registrations if contract is unlocked
}
}
第一次部署后检查合约的unluck状态:
当输入_name=\\”0x0000000000000000000000000000000000000000000000000000000000000001\\”(63个0),地址任意地址时,会覆盖unlocked的值,使其变为true
未初始化的数组局部变量
pragma solidity ^0.4.24;
pragma experimental ABIEncoderV2;
contract UnfixedArr {
bool public frozen = false;
function wrongArr(bytes[] elements) public {
bytes[1] storage arr;
arr[0] = elements[0];
}
}
当输入elements=[\\”0x0000000000000000000000000000000000000000000000000000000000000001\\”](63个0),会覆盖frozen的值,使其变为true
相关资料
以太坊中数据的存储
在以太坊中对于固定大小的已知变量,EVM会直接将它们按顺序从0开始存储在每个存储位里,因为EVM虚拟机是一个256位的机器,所以它的每个存储位也都是256bit,即32个字节比如下面这个简单的合约
pragma solidity ^0.4.23;
contract test{
uint256 public a=6;
uint256[2] public b;
bytes32 c=\\\"asdwq\\\";
address q;
function test(){
q=msg.sender;
b[0]=88;
b[1]=99;
}
}
我们来看看以太坊上如何存储这些变量的,这里我是在remix里通过debug页面来直接查看的,这样比较方便
可以看到存储位即key为0的位置上存储的就是a的值,然后下面1和2的存储位上就是b的值,3号位和4号位分别是c和q,应该还是比较清楚的,所以事实上你所有的数据差不多都是在块上可见的,对于这些存储位里的数据我们可以直接通过web3.eth.getStorageAt来读取。
当然EVM中对于固定长度的变量的存储也并不就是这么简单,因为在EVM中消耗gas最多的操作就是存储操作了,不论是永久的storage还是内存的memory,当然storage又要比内存要多很多,所有solidity里进行了一定的存储优化,简单来讲,即你定义的这个变量所占的空间小于32个字节时,它所占据的这个存储位的空间可以与它后面的变量共享,当然前提是这个变量塞的下去,因为在EVM里将数据写入一个新位置和写入一个已经分配出来的位置所需的gas是不一样的,对于这部分内容就不多说了,举个简单的例子。
pragma solidity ^0.4.23;
contract test{
uint16 public a=6;
uint16 public b=8;
bytes16 c=\\\"4648\\\";
}
这里我定义的就不是256bit大小的变量了,我们再来看看它们怎么占用存储位的
很有意思,上面的三个变量仅占用了一个存储位,它们分别占据着空间的不同位置,实现了存储的共享。
接下来我们再来简单谈谈动态数据的存储,这包含了动态的数组和映射。
映射相对来讲简单一些,还是来看一个简单的例子。
pragma solidity ^0.4.23;
contract test{
mapping(uint256 => uint256) z;
function test(){
z[233]=123;
}
}
其存储如下
这里其存储位置的计算规则就是
keccak256(bytes32(key) + bytes32(position))
此处key即为映射的key也就是233,而position也就是该变量本来的位置,这里它是定义的第一个变量,所以位置即为0,根据此式我们可以手动算出变量存储位置
然后我们来看看动态数组的存储,这种的情况相对比较多也更为复杂,我们还是来认识一些简单的情况,毕竟这也不是今天的重点,有兴趣的可以去深入了解。
还是先来看一个简单的例子
pragma solidity ^0.4.23;
contract test{
uint256[] public a;
function test(){
a.push(123);
a.push(456);
a.push(789);
}
}
其存储的分布如图
我们看到其占据了四个存储位,其中position为0的位置存放的是数组的长度,下面的三个位置存放的就是数组的值,而且我们不难发现其key是依次递增的,第一个位置的计算方式也很简单,就是keccak256(position)其中的position就是存放数组长度的位置,此处即0,验证如下
然后我们再看看增加了结构体以后的存储方式
pragma solidity ^0.4.11;
contract test {
mapping(uint256 => gg) gg1;
struct gg {
uint256 a;
uint256 b;
uint256 c;
}
function test(){
gg1[233].a=123;
gg1[233].b=456;
gg1[233].c=789;
}
}
其存储结果如下
跟上面的情况其实类似,首位是根据映射的计算规则得到,后面的两个存储位置在此基础上递增,应该算是比较简单了,复杂点的情况我感觉都可以拿来给ctf出题了。
一个有问题的合约
合约代码如下:
pragma solidity ^0.4.11;
contract test {
uint256 public a=0x123;
uint256 public b=0x456;
uint256 public c=0x789;
struct G {
uint256 a;
uint256 b;
uint256 c;
}
function testforfun(){
G g;
g.a=1;
g.b=2;
g.c=3;
}
}
在我们执行testforfun函数前a,b,c的值都是我们部署合约时的值
调用testforfun函数后便成功被函数里初始化的结构体覆盖:
感觉solidity也是比较奇葩了,同样作用的两段代码一段报错另一段却报warning,而且同样让人奇怪的就是在函数里初始化的结构体的默认的存储类型竟然就是storage,难道跟其它那些固定长度的变量一样默认使用memory存储不好么,对于这一点我们也可以直观地在汇编代码里看见。
调用testforfun时使用的存储代码为sstore,这就表示其中的结构体使用的是storage,而对应地使用memory关键字以后或者那些默认即为memory存储的变量使用的存储代码为mstore。
然后我便想看看其它类型的变量是否也会存在类似的情况,然后便发现了更有意思的数组类型,不管是固定长度的数组函数动态的数组,在函数内定义时没有加上memory关键字的话都会默认使用storage存储,去占领全局变量的空间。
例如一个简单的定长数组
pragma solidity ^0.4.11;
contract test {
uint256 public a=0x123;
uint256 public b=0x456;
uint256 public c=0x789;
function testforfun(){
uint256[3] z;
z[0]=1;
z[1]=2;
z[2]=3;
}
}
得到的结果如下
调用testforfun之后:
依然是很直接的覆盖,同样的,对于动态的数组情况也是类似
pragma solidity ^0.4.11;
contract test {
uint256[] public a;
function test(){
a.push(123);
a.push(456);
}
function testforfun(){
uint256[] z;
z[0]=1;
z[1]=2;
}
}
调用之后:
通过这一次的分析过程也让我对以太坊的数据存储有了更深的理解,可以看到它跟其它的编程语言还是有着很大的区别的,如果不了解EVM的运行机制,一个初来乍到的程序员很容易就会犯错,写出存在漏洞的代码,毕竟这很多也是其它编程语言里带过来的习惯,当然我感觉这次的这种漏洞的锅还得solidity来背,感觉明明这种情况应该是需要报错的,结果却只是弹了warning,这样不排除有些开发者可能完全不考虑gas的高消耗的情况下来滥用storage存储,而且我感觉solidity里对函数中初始化的默认存储形式也该改改。
原创文章,作者:七芒星实验室,如若转载,请注明出处:https://www.sudun.com/ask/34235.html