EVM存储机制及安全性

欢迎加入我们的区块链社区

群里还有一些关于solidity学习的心得分享

还有一些免费小工具分享

 

 

EVM存储机制及安全性

EVM存储结构

EVM 存储数据分为两类:

  • 存储在 code 和 storage 里的数据是 non-volatile (不容易丢失的)

  • 存储在 stack,args,memory 里数据是volatile(容易丢失的)

我们主要来了解一下关于Storage中数据的存储机制。

storage

Storage 是一个可以读写修改的持久存储的空间,也是每个合约持久化存储数据的地方。Storage 是一个巨大的 map,一共 2^256 个插槽 (slot),每个插糟有 32byte,合约中的“状态变量”会根据其具体类型分别保存到这些插槽中。

# 插槽式数组存储
----------------------------------
|               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-Vault

// 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-Privacy

变量的定义如下。

  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为我们自己的地址。

总结

区块链上没有秘密

EVM 的存储器是和智能合约语言进行交互的,当其中一些规则发生冲突很可能就被别有用心的人用来作恶,所以规范的使用智能合约语言是避开漏洞的必要条件。

posted @ 2022-08-14 10:20  小小小怪将军  阅读(314)  评论(0编辑  收藏  举报