非确定性 Solidity 交易 — Certora漏洞披露

文章详细介绍了在Solidity编译器中发现的一种代码生成漏洞,允许恶意存储字段欺骗Solidity,将不相关信息插入通过abi.encodePacked等方法生成的数组中。该漏洞已在Solidity 0.8.0版本中修复。

非确定性 Solidity 交易 — Certora 漏洞披露

Certora 开发团队的 John Toman 发现了 Solidity 编译器中的一个以前未曾知晓的代码生成漏洞。该漏洞允许恶意的字节/字符串存储字段欺骗 Solidity 将内存中的不相关信息包含到通过 abi.encodePacked 等方法生成的数组中。该漏洞已在 Solidity 版本 0.8.0 中修复。

背景

存储中的字节和字符串

正如我们在之前的文章中所描述的,字节、数组和字符串使用专门的表示法来保存存储数据。特别是,如果数组/字符串长度少于 31 个元素(字节),则 Solidity 会将数组的长度存储在与元素数据本身相同的存储槽中。最低有效字节存储数组的长度 乘以二(出于以下解释原因),其余的 31 个字节存储数组数据。

这种“打包”表示法与通常在存储中放置数组的方式形成对比:数组的长度存储在一个槽中,而数组元素则存储在从通过哈希函数计算得出的某个位置开始的连续槽中。请注意,这种表示法也用于长度大于 31 的字节数组,尽管稍有修改。Solidity 编译器不会直接存储字节数组的长度,而是将长度乘以二 加一 来存储。这个附加项确保代码可以区分使用的是哪种布局(“打包”还是“标准”):如果最低有效位未设置,则必须使用打包表示法。然而,如果最低有效位被设置,则数组数据位于存储中的其他地方。

理解 Bump 分配器

正如我们在关于内存损坏的文章中描述的那样,以太坊内存是一个单一的、连续的字节数组。Solidity 编译器通过使用一个“空闲指针”来赋予这段代码结构,该指针在合约的生命周期中单调递增。特别是,在分配新的内存对象时,编译器生成的代码会读取空闲指针的当前值,然后增加一个足够大以容纳分配对象的值,然后更新空闲指针。在递增空闲指针之前,空闲指针的旧值被用作指向专门为分配的内存对象保留的以太坊内存段的“指针”。换句话说,分配后,通过递增空闲指针保留的内存区域不得用于存储分配的对象以外的任何其他目的。

然而,Solidity 分配的内存区域 并不 一定是未使用的。Solidity 通常会将空闲指针之后的所有内存用作“临时”区域。这一临时区域用于构造合约间调用的输入、日志消息等。与 Java 等托管语言不同,“分配”的内存不一定为零,可能最初包含任意数据。然而,Solidity 编译器 通常 会生成代码以初始化新分配的数据段为默认值。

一个例外:abi 方法

如上所述,bump 分配器将预计算内存对象的大小,并将空闲指针递增该大小。然而,与(常规)数组或结构体不同,使用诸如 abi.encodePacked 之类的方法创建的数组的长度并未预计算。相反,该数组是在“原地”构造的。对于诸如 abi.encodePacked(包括 encodeWithSelectorencodeWithSignature 等)的调用,编译器生成了(大致上)以下伪代码:

var ptr = freePointer + 32; // 为长度字段保留 32 个字节
for(x in argumentsToAbiFunction) {
   copy(src=x, dst=ptr)
   ptr += sizeOf(x)
}
val lengthInBytes = (ptr - freePointer) - 32; // 不计算长度字段!
memory[freePointer] = lengthInBytes;
freePointer = ptr;

这里 copy 是一个伪操作,复制 xencodePacked 的一个参数)的表示到从 ptr 开始的内存区域。请注意,生成的数组的长度是通过简单地从 ptr 中减去当前的空闲指针值来计算的,该 ptr 正好指向最后一个字节的后面。长度计算完成后,空闲指针最终被更新。关键是,生成的数组的内存 并未 由 Solidity 初始化。Solidity 假定 copy 操作将覆盖之前作为临时内存使用的所有数据。

漏洞:通过 abi 从存储到内存

当 Solidity 判定传递给 abi 函数的一个参数是存储中的字节数组或字符串时,它会测试该字节数组存储槽的最低有效位。在 pragma experimental ABIEncoderV2 下,如果该位 设置(表示字节数组是打包的),则整个槽会被除以 2 然后与 0x7f (即 127)进行掩码操作。有效地,这个操作提取存储在最低有效字节槽中的长度,将长度上限设定为 127不是 31)。该存储槽的上 31 个字节随后会被复制到内存中的 ptr,最后,ptr 将按所生成的掩码值递增,即,打包字节数组的 sizeOf(x)(slot >> 1) & 0x7f

假设存储字段被恶意构造以使其最低有效字节包含大于 62 的值。在这种情况下,通过调用 abi 生成的数组将包括未初始化的内存:ptr 的值将被“过度递增”。例如,如果最低有效字节的值为 254,则 ptr 将递增 127 字节。然而,如上所述,只从存储中复制了 31 个字节的数据,剩下的 96 个字节未初始化,可能包含之前写入临时内存的任意值。

影响和解决方案

Solidity 编译器团队已确认该漏洞的有效性,并在 Solidity 版本 0.8.0 中修复了它。触发此漏洞需要一个合约恶意地设置自己的存储以包含编码不正确的最低有效字节。虽然我们不知道目前有任何合约利用此漏洞,但恶意合约可以通过委托调用将非确定性引入其自身的执行或受信任的库代码。请注意,恶意行为者可以首先破坏合约的存储,然后使用公认的升级技术将其“升级”为一个看似可信或良性的合约,从而仍然表现出这种行为。

  • 原文链接: medium.com/certora/non-d...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
uri_kirstein
uri_kirstein
江湖只有他的大名,没有他的介绍。