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

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

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

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

图1
图1
图2
图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
图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
图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 位置为 10

图5
图5

得到value存储的 slot 为 0x2cb73cd019c70b24b7128c3a8fa046c2e524595f0f21ef557221be7ab820bc99

第二步:读取slot

图6
图6

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

大功告成!

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

相关代码:

测试网合约:

Twitter:

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.