以太坊智能合约逆向分析与实战:(6)访问动态数据类型 2

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

图1
图1

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

图2
图2

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

keccak256(bytes32(1))

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

图3
图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 的一道题目,作为本次学习的课后习题:

图4
图4

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

在研究动态数组之前,我们先铺垫一下:Owner 是谁呢,怎么查询到它(变量的存放位置在哪里)?

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

图5
图5

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

图6
图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
图7

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

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

图8
图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
图9

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

关于作者:

Subscribe to Hackit
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.