EVM存储机制及安全性
群里还有一些关于solidity学习的心得分享
EVM存储结构
EVM 存储数据分为两类:
-
存储在 code 和 storage 里的数据是 non-volatile (不容易丢失的)
-
存储在 stack,args,memory 里数据是volatile(容易丢失的)
我们主要来了解一下关于Storage
中数据的存储机制。
storage
# 插槽式数组存储
----------------------------------
| 0 | # slot 0
----------------------------------
| 1 | # slot 1
----------------------------------
| 2 | # slot 2
----------------------------------
| ... | # ...
----------------------------------
| ... | # 每个插槽 32 字节
----------------------------------
| ... | # ...
----------------------------------
| 2^256-1 | # slot 2^256-1
----------------------------------
存储概述
变量类型
Solidity的数据变量类型分为两类
-
值类型-value type
-
引用类型-reference type
简单分析
1.对于大小在 32 字节以内的变量(常量),以其定义的顺序作为它的索引值来存储。即第一个变量的索引为 key(0),第二个变量的索引为 key(1)...
2.对于连续较小的值,可能被优化存储在同一个位置,比如:合约中前四个状态变量都是 uint64 类型的,则四个状态变量的值会被打包成一个 32 字节的值存储在 0 位置。这也被称为优化存储原则。
举个例子,先看下面的合约。
pragma solidity ^0.4.0;
contract C {
address a; // 0
uint8 b; // 0
uint256 c; // 1
bytes24 d; // 2
}
其布局如下
-----------------------------------------------------
| unused (11) | b (1) | a (20) | <- slot 0
-----------------------------------------------------
| c (32) | <- slot 1
-----------------------------------------------------
| unused (8) | d (24) | <- slot 2
-----------------------------------------------------
首先,a为地址类型,在solidity中,地址类型占160bit,即20个字节,此时插槽0
中还剩12字节。而b为unint8类型,为1字节,根据优化存储原则,此时b也储存在插槽0
中,此时slot0
中还剩11字节的空间未使用。
c为uint256,256bit为32字节,在slot0
中无法存储,但刚好能将slot1
存储满。
d为24比特数据,存储在slot2
中,此时slot2
中还剩8字节的空间未使用。
这就是简单的数据存储机制了。
练习
web3.eth.getStorageAt(address, position [, defaultBlock] [, callback])
使用getStorageAt
可以读取指定slot下的内容。
练习1-
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) public {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
定义为私有变量只能组织其他合约访问,但是无法阻止公开访问。
-----------------------------------------------------
| unused (31) | locked(1) | <- slot 0
-----------------------------------------------------
| password (32) | <- slot 1
-----------------------------------------------------
password的存储位置为slot1
尽管它是私有变量,但我们仍然可以通过web3.eth.getStorageAt(contract.address, 1)
来获取password
的值。
调用unlock
方法实现对合约的解锁。
提交实例,本关卡成功通过。
练习2-
变量的定义如下。
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(now);
bytes32[3] private data;
我们需要读取data[2]
中的数据。
data为字节数组,我们只需要读取数组中最后一组的数据值,按照之前所讲的分布原则,我们有槽存储分布如下:
-----------------------------------------------------
| unused (31) | locked(1) | <- slot 0
-----------------------------------------------------
| ID(32) | <- slot 1
-----------------------------------------------------
| unused (28) | awkwardness(2) | denomination (1) | flattening(1) | <- slot 2
-----------------------------------------------------
| data[0](32) | <- slot 3
-----------------------------------------------------
| data[1](32) | <- slot 4
-----------------------------------------------------
| data[2](32) | <- slot 5
-----------------------------------------------------
所以,data[2]
存储在slot 5里。
调用await web3.eth.getStorageAt(contract.address,5)
此时bytes16与bytes32之间存在转换。要注意,以太坊有两种存储方式,大端(strings & bytes,从左开始)及小端(其他类型,从大开始)。因此,从32到16转换时,需要砍掉右边的16个字节。
即'0x55a8396c56d3d9a30d6f4f5fcb51c4f9754917dce02788ddfd70769483c89716'.slice(0,34)
解锁合约:contract.unlock('0x55a8396c56d3d9a30d6f4f5fcb51c4f9')
。
此时合约已经完成解锁。
提交实例,本关卡成功!
安全问题
前面已经讲到EVM的存储结构及存储机制,现在我们再来探讨其安全问题。
未初始化变量
漏洞原理:
在官方手册中提到结构体,数组和映射的局部变量默认是放在 storage 中的,而 solidity 语言中函数中设置的局部变量的默认类型取决于它们本身的类型。
因此如果在函数内部设置以上 storage 类型变量却没有进行初始化,他们就相当于存储指针指向合约中的其他变量,当我们对其进行改变时改变的就是其指向的变量。
漏洞合约,目的修改 owner 为自己地址:
pragma solidity ^0.4.0;
contract testContract{
bool public unlocked = false;
address public owner = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c;
struct Person {
bytes32 name;
address mappedAddress;
}
function test(bytes32 _name , address _mappedAddress) public{
Person person;
person.name = _name;
person.mappedAddress = _mappedAddress;
require(unlocked);
}
}
具体操作:
调用test函数分别传入向_name 传入:0x0000000000000000000000000000000000000000000000000000000000000001(真值)
_mappedAddress 传入:0xd7471C7eaF78Dcb1bF19A33e470A039Dd72639dd(个人地址)
此时就_mappedAddress 传入的参数即指向了owner,即可修改owner为我们自己的地址。
总结
区块链上没有秘密。