# 以太坊智能合约逆向分析与实战：（3）[实战篇] 访问私有动态数据类型

By [Hackit](https://paragraph.com/@hackbot) · 2022-07-24

---

通过[之前的学习](https://mirror.xyz/hackbot.eth/LvAQD7OjarsLrcRxGPiz-4MtXP0WGwFpwx51LIXSIx4)，我们了解到在 EVM 中，数据的存储是以”大端“ (bigendian) 的方式存储在”存储槽“ (slot)中的，变量的低位存储在 slot 的低地址中，每个 slot 的长度不超过 32 字节。关于[全局变量的存储方式](https://learnblockchain.cn/article/3510) ，一般来说，**静态类型** 在合约部署时已经按顺序存到了slot中，并从 slot 0 开始连续排列，按照类型的大小，有的变量单独占据一个slot，有些是好几个变量共用一个slot。然而，像映射、动态数组这些 **动态类型** 因为所需存储空间无法预计，因此并不是整个的放在某个slot 中，而是随用随存。以映射（MAP）为例，该类型首先会按以上的规则占个slot的位置，再通过一定的计算得到存放value的真实地址。只讲原理有些枯燥，我们举例说明吧！

本次我们以一个猜数字的游戏为例，为了方便演示，我对游戏合约做了一些简化。游戏很简单：合约提供一个数字，用户提交一个值，系统会提示用户的值是大于或者小于原定数字，直到猜中为止。这个数字存放在一个 privite 映射之中（如下图 **item\_2** 变量），通过区块浏览器是无法查询到这个数字的。但毕竟这是区块链，所有的数据和代码都在链上，”看不到“并不等于”不存在“，我们仍旧可以通过一些方法来获取这个值。

以下是示例合约代码以及对各种类型存储情况的分析：

![图1](https://storage.googleapis.com/papyrus_images/3cbe36e10cc5aaec8a73c37310c95e35c88be677eea010dde07174c8ebd2d5f3.png)

图1

![图2](https://storage.googleapis.com/papyrus_images/3e2cd0f2632c9edd675af44c0c8eb8339f85ce333ef6383ed93d733c66028224.png)

图2

由代码可知，游戏中我们需要猜测 映射 **item\_2**\[ count\_A \] 对应的 value。假设合约所有者以某种方式秘密设置了 **item\_2**\[ count\_A \] 的值（为简便起见，我直接在构造函数中设置了），我们可以跳过猜想，直接获取这个值吗？当然可以！

我们知道，**静态类型**的存储槽都是按顺序固定存放的，可以通过直接读取 slot \[n\] 的值来获取它们的值。比如 图1 中的 privite **count\_B** , 虽然是私有变量，在区块浏览器中无法查阅到，但其对应的值就在 slot 2 中存放，可以通过编程的方式轻松读取出来。但动态类型却不是这样，如果我们直接读取 **item\_2** 所占据的 slot 10 ，结果就会让你失望了。那么，映射类型的数据存储是怎样的呢？我们继续向下看。

合约中各种类型变量的存储和排序情况，可以用 [**Remix**](http://remix.ethereum.org/) 的 debug 功能很直观地看出来：

![图3](https://storage.googleapis.com/papyrus_images/18f87bb89ae82051cb56c249274616ee14549ea6a5af5efbdae0b98b4a97e7f6.png)

图3

可以看到，EVM 的存储方式有点像映射，以 key → value 的形式对应着 slot → 数据 。红色和蓝色方框中的 key 从 0 到 4 依次排开，正如 slot 0 ~ slot 4 分别存储着相应内容。

但下面的黄色方框是怎么回事呢？它的 slot 编号（key : 0x2cb73cd019c70b24b7128c3a8fa046c2e524595f0f21ef557221be7ab820bc99 ）为什么这么长？而且它存储的数据是 0x3039 , 正是 12345 的十六进制表示。很可能就是我们要寻找的值。

那这一串数字是怎么来的呢 ？带着这个疑惑，我们来使用**编译器 solc** 看看它的 ”汇编代码“（opcodes）：

在终端输入指令 `./solc -o asmOutputFolder --bin --asm --optimize ./contracts/slotHack.sol` 从 **OutputFolder** 目录中找到 **slotHack.evm** :

可以看到，在 汇编代码中，**set\_item\_1** 处也出现了类似的情况：

![图4](https://storage.googleapis.com/papyrus_images/cc0c277201c517e59a3336b9ecf6c6f525290fba95a2e0116349b9eaadcde718.png)

图4

看来这就是映射类型的存储方式了。通过[之前的学习](https://mirror.xyz/hackbot.eth/LvAQD7OjarsLrcRxGPiz-4MtXP0WGwFpwx51LIXSIx4)，我们可以得知这一长串数字是这样的得来的：`n = keecak256(h(k)+p)`_【对于值类型，h(k) 通过填充 0 的方式，将 k 填充为 32 字节; 对于字节或者字符串类型，h(k) 直接计算 k 的 keccak256 哈希。】_

在本例的 **set\_item\_1** \[ 0xC0FFEE \] 中，这个数值是这样计算出来的：`keccak256(bytes32(0xC0FFEE) + bytes32(9)))` 其中 **0xC0FFEE** 是该映射的 key, 而 **9** 是 **set\_item\_1** 所对应的 slot。

_【注：我们之所以能看到编译器可以提前计算_ **_set\_item\_1_** _的 key 的地址，是因为相关的值是常量。如果key使用的是变量（如_ **_set\_item\_2_** _），那么哈希就必须要在汇编代码中完成。】_

——也就是说，只要我们能够确定映射类型在合约部署时所占的 **slot** 以及 value对应的 **key**，就能计算并得到 **value** 的真实存放地址, 无论它是 privite 还是 public ，统统能读取出来。原理既然清楚了，那就开始代码实现吧：

**第一步：计算 slot**

**set\_item\_2** \[ 0x2560A0256 \] , key 为 **0x2560A0256** ，item\_2 的 slot 位置为 **10**

![图5](https://storage.googleapis.com/papyrus_images/ee8de1e5a3ae2ab66289f1e18c0513eeff382439c568c671034e7e89e6d0ce68.png)

图5

得到value存储的 slot 为 `0x2cb73cd019c70b24b7128c3a8fa046c2e524595f0f21ef557221be7ab820bc99`

**第二步：读取slot**

![图6](https://storage.googleapis.com/papyrus_images/0904caa3c1f1a45c94f81336fdeb90d492c7296749ce257d5a52bd9b9365268f.png)

图6

如上图，已经读取到了 **item\_2** \[ 0x2560A0256 \] 的值 0x3039 , 换算成十进制就是 **12345**

大功告成！

\==================================================

**相关代码：**

[

GitHub - 0xNezha/slotHack: 计算并读取evm存储槽中映射类型的value
-------------------------------------------------

计算并读取evm存储槽中映射类型的value. Contribute to 0xNezha/slotHack development by creating an account on GitHub.

https://github.com

![](https://storage.googleapis.com/papyrus_images/5e5845649f497fbb108b722f1337479778a0e03f0c60785a282b0e62d658fa92.png)

](https://github.com/0xNezha/slotHack)

**测试网合约：**

[https://rinkeby.etherscan.io/address/0xc81f73EdcA69ac1663d5b2b2E3CBa520d62e5425](https://rinkeby.etherscan.io/address/0xc81f73EdcA69ac1663d5b2b2E3CBa520d62e5425)

**Twitter：**

---

*Originally published on [Hackit](https://paragraph.com/@hackbot/3)*
