# 以太坊智能合约逆向分析与实战：（6）访问动态数据类型 2

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

---

在之前的文章：[以太坊智能合约逆向分析与实战：（3）\[实战篇\] 访问私有动态数据类型](https://mirror.xyz/hackbot.eth/oq6e37ApuACqsKGYUcnrTbz26k8P6TsGb-AHBq6_piY) 中，我们以破解链上某“猜数字”游戏为例，讲解了**映射**这种动态数据类型在 EVM 中的存储与访问，这次我们把目标对准另一种动态数据类型： **动态数组** ，看看 EVM 是如何实现动态数组的存取。首先我们先看一个简单的合约：

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

图1

通过[之前的学习](https://mirror.xyz/hackbot.eth/oq6e37ApuACqsKGYUcnrTbz26k8P6TsGb-AHBq6_piY)，你一定能够轻松地判断出 slot 0 里面的值（没错就是 0x666666 ），但是对于动态数组，它的长度是随时变化的，其内容并不会像这些值类型一样固定在某个 slot 里。那么它是如何存储的呢？让我们先调用 record（）函数 ,给动态数组压入一些数值（0xff0000, 0xff0001, 0xff0002，0xff0003）, 此时的动态数组长度应该是4，我们通过其 length() 函数也可获知。然后打开调试器看一下：

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

图2

与之前讲的[映射](https://mirror.xyz/hackbot.eth/oq6e37ApuACqsKGYUcnrTbz26k8P6TsGb-AHBq6_piY)相比，我们可以发现**动态数组**的存储方式和映射有一些相似之处，却也有所不同。在动态数组中，进行变量声明的位置（slot2）存放着数组的长度，而数组内各元素的值，是按照 slot n、slot n+1、slot n+2 … 的顺序进行排列的。声明的位置我们找到了，但第一个元素所在的 slot n 该怎么找呢？答案是

`keccak256(bytes32(1))`

其中 bytes32(1)是指该动态数组声明时所占据的 slot 。我们可以计算一下：

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

图3

看，得到的结果与调试器中 codex\[0\] 对应的 key 相同。具体到本例， codex\[0\] = slot ( keccak256(bytes32(1)) + 0 )

codex\[1\] = slot ( keccak256(bytes32(1)) + 1 )

codex\[2\] = slot ( keccak256(bytes32(1)) + 2 )

……

然后我们通过读取对应的 slot ，就能够获取到动态数组的元素了。

* * *

本次篇幅较短，趁着还有时间，我们寻找一个被破解的对象：[ethernaut 的一道题目](https://ethernaut.openzeppelin.com/level/0xda5b3Fb76C78b6EdEE6BE8F11a1c31EcfB02b272)，作为本次学习的课后习题：

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

图4

从上图可以看到，这是一个很短的合约，合约里 import 了一个 Ownable.sol 文件，这个文件会让部署的合约里面有一个 Owner 变量（可以参考 OpenZeppelin 的 [Ownable.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol) ）。然而这个合约的 Owner 并不是我们的地址，而且合约也没有提供更改 Owner 的接口（即使引用的文件里实现了 transferOwnership（)，我们也因为不是 Owner 而无法更改）。简单来说：我们的任务，就是要通过对这个合约的 **动态数组** 进行一系列神奇操作，从而获取 Owner 权限。

在研究动态数组之前，我们先铺垫一下：Owner 是谁呢，怎么查询到它（变量的存放位置在哪里）？

答：该Solidity 文件中并没有直接出现 Owner 变量，而是以 import 的形式引入的。一般来讲，它的变量存储 slot 是这样排列的：

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

图5

也就是说，合约先继承谁，就把谁的变量放在靠前的 slot 里面，等所有合约继承完毕，再存放本合约的变量。所以在本例中， 其 slot 0 中很可能存放的就是 Owner 的地址。我们拿来前一节用过的脚本来读取一下：

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

图6

看，果然如此。此处注意：由于 EVM 优化的缘故，address 变量 **owner** 与 bool 变量 **contact** 被打包放进了同一个 slot ，所以我们在一个 slot 里得到了两个变量的值：

`owner = 0x3c34a342b2af5e885fcaa3800db5b205fefa3ffb`

`contact = false`

然而，我们如何修改它呢？这就需要我们上面讲解的关于**动态数组**的知识了。

首先我们看图4的合约：变量 **owner**, **contact** 存放在 slot 0, 动态数组 **codex\[\]** 占用了slot 1 。通过阅读合约中的函数定义可以发现，我们无法修改 owner，但是却可以修改 codex\[\] 。

那么我们可以通过修改 codex\[\] 来实现修改owner 吗？ 对于这个合约来说是可以的，因为它的代码存在漏洞——在 pragma solidity ^0.5.0 环境下，我们可以直接控制 codex\[\] 的长度（ codex.length-- ），也就是说可以自由读写 slot 1 的数值，而高版本的 solc 就补上了这个漏洞。

通过本篇开头的讲解我们知道，在修改动态数组内某个元素的值的时候，例如 codex\[9\] = 0x12345678，实质上是将 0x12345678 写入到 存储槽 slot **(keccak256(bytes32(1)**) + n) 里面，而 keccak256(**bytes32(1)**) 能够被计算出来，等于是向 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 + 9 这个地址内写入0x12345678，用汇编表示就是

`0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cff`

`0x12345678`

`sstore`

那么，如果把第一行改为 owner 所在的存储槽地址(slot 0)，第二行改为 我们自己的账户地址，不就可以修改合约 owner 为我们自己的地址了吗？

上面说过，codex\[n\] 的值，就是 第 ( **keccak256(bytes32(1)) + n** )个 slot 的值。如果我们通过构造一个 n ,让 keccak256(**bytes32(1)**) + n = 0 ,就可以实现我们的目标了。但 keccak256(**bytes32(1)**) 本身是大于0的，如何让他加上 n 之后 “归零”呢？答案是——**溢出**。slot共有 2^256 个，按照 0 ~ 2^256-1 的顺序排列。我们需要让 keccak256(**bytes32(1)**) + n = 2^256 ，从而出现数据**上溢**，即可实现越过数组下标限制来修改存储槽。

但这就意味着我们要输入一个很大的数组下标，这就需要有一个很大的数组。那我们如何构造出这么大的数组呢？依靠codex.push() 去填充显然是极其不划算的，答案仍是——**溢出**。在**slot 1** 里面存放着 codex\[\] 的初始大小：0 , 我们通过操作 retract() 函数，让 0 减去 1 从而实现数据**下溢**,达到构造超大数组的目标。

让我们开始操作吧。先调用 make\_contact(), 使 contact = true。看看 **slot 0** 的值：

![图7](https://storage.googleapis.com/papyrus_images/fa589312a81cca6db8de3057a845d473cb7fba27f5d3f6e7e969a0eee43c868e.png)

图7

可以看到，slot 0 存储的是 bool + address 变量，即 true + OwnerAddress .我们的目标是修改 OwnerAddress 为我们自己的地址。

再调用 retract() , 让codex的长度减1 。看看 **slot 1** 的值：

![图8](https://storage.googleapis.com/papyrus_images/ad14f3af20ae0aeb8505da7a31250f699f4738d2940eabc6a697f3da218e8c59.png)

图8

哦嚯，我们得到了一个“超级大”的动态数组，足足有 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 这么长！意味着我们可以在codex\[n\]里面存取任意位置的数据了。记得我们的目标吗？是要让keccak256(**bytes32(1)**) + n = 2^256 ，也就是说 n = 2^256 - keccak256(**bytes32(1)**)。我们计算出 n =

0x10000000000000000000000000000000000000000000000000000000000000000 - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 =

0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a

换算成十进制就是

35707666377435648211887908874984608119992236509074197713628505308453184860938

也就是说，我们修改 codex\*\*\[**35707666377435648211887908874984608119992236509074197713628505308453184860938**\]\*\* 的值为 0x000000000000000000000001AAA…..AAA 就可以了(AAA…..AAA 是我们自己的地址)。我们调用函数 revise（uint，bytes32），参数如下

参数1：35707666377435648211887908874984608119992236509074197713628505308453184860938

参数2：0x000000000000000000000001123456…..

然后再看一下存储槽，第一个 1 是 bool 变量 **true**，而后面的 1234567890…… 说明owner已经成功修改为我们的地址了（0x1234567890…….）：

![图9](https://storage.googleapis.com/papyrus_images/d4d73fb113e3c72669e29b06f90dc1beb6f6cb13bd9ebe635f6aeda65fbc4f43.png)

图9

那么，本期节目就到这里啦。

关于作者：

---

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