Hackit

Posted on Jul 24, 2022Read on Mirror.xyz

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

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

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

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

图1

图2

由代码可知,游戏中我们需要猜测 映射 item_2[ count_A ] 对应的 value。假设合约所有者以某种方式秘密设置了 item_2[ count_A ] 的值(为简便起见,我直接在构造函数中设置了),我们可以跳过猜想,直接获取这个值吗?当然可以!

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

合约中各种类型变量的存储和排序情况,可以用 Remix 的 debug 功能很直观地看出来:

图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

看来这就是映射类型的存储方式了。通过之前的学习,我们可以得知这一长串数字是这样的得来的: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, 而 9set_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 位置为 9

图5

得到value存储的 slot 为 0x2cb73cd019c70b24b7128c3a8fa046c2e524595f0f21ef557221be7ab820bc99

第二步:读取slot

图6

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

大功告成!

==================================================

相关代码:

https://github.com/0xNezha/slotHack

测试网合约:

https://rinkeby.etherscan.io/address/0xc81f73EdcA69ac1663d5b2b2E3CBa520d62e5425

Twitter:

https://twitter.com/0xNezha