本文揭示了在Solidity编译器版本0.8.3及以下中存在的内存隔离漏洞,该漏洞影响了ABI反序列化过程,可能导致恶意字节缓冲区的攻击。作者详细解释了ABI规范、序列化和编码格式,以及引入的具体漏洞和影响,并指出该漏洞已在0.8.4版本中修复。
Certora 的研发副总裁 John Toman 发现了 Solidity 编译器版本 0.8.3 及以下中的一个未知的代码生成漏洞。该漏洞允许恶意构造的字节缓冲区破坏 ABI 反序列化例程,访问意外的内存和/或引入非确定性。
在 EVM 中,数据通过无结构的字节缓冲区在合约之间交换。为了支持结构化类型,例如结构体和数组,以太坊社区开发了 ABI 规范,该规范定义了一种序列化格式,用于将结构化类型编码为字节缓冲区。接收方可以反序列化缓冲区,以重构原始的类型数据。至关重要的是,序列化缓冲区并不包含编码在缓冲区中的数据类型的任何信息。相反,接收方必须事先知道缓冲区中预期编码的数据类型。如果缓冲区不符合预期的编码类型,反序列化应当回滚。
ABI 规范区分了动态类型和静态类型。动态类型是任何(传递性)包含动态大小数组的类型;静态类型则包含所有其他类型。动态类型和静态类型的编码方式不同。静态类型的值直接写入缓冲区的当前位置。
相比之下,动态类型通过在缓冲区的当前位置写一个偏移量来编码。动态类型的实际数据可以在缓冲区的后面找到,如偏移量所示。该偏移量不是编码缓冲区中的绝对索引。相反,它是相对于“父”对象编码的开始位置。
例如,考虑从缓冲区索引 i
开始编码下面定义的结构体 s
类型。字段 a
直接写入索引 i
。字段 b
通过将偏移量 o
编码到下一个索引(i + 32
)来进行编码。偏移量 o
是相对于 i
的,因为 i
是父类型 s
编码的起始索引。数组 b
的实际数据写入字节缓冲区索引 i + o
。见下图。
类型为 s
的结构体的序列化
Solidity 编译器不信任序列化缓冲区的正确性。在每一步,反序列化过程应检查编码是否良好,如果发现不良情况则回滚。例如,如果编码的 uint 长度数组大于正在解码的缓冲区中剩余的字节数,反序列化过程将回滚。
这些检查同样适用于动态类型生成的偏移量:如果偏移量导致超出范围的字节缓冲区索引,则反序列化应当回滚。这个检查是使用指针算术和比较操作实现的。在反序列化过程的开始,代码计算编码缓冲区的“结束指针”,该指针是缓冲区最后一个字节的下一个位置。然后,为了检查动态类型偏移量是否有效,代码首先检查该偏移量是否小于 2**64(以防止溢出)。然后,代码计算指向由偏移量指定位置的指针,并检查该指针是否小于结束指针。如果不符合,代码将回滚。最后,代码计算编码数据的预期大小,并检查缓冲区中是否还有足够的字节以使其成立。同样地,如果预期大小超出了缓冲区的结束,代码将回滚。
这里有两个相关的代码生成漏洞。首先,在动态类型内部省略了偏移量小于 2**64
的检查。例如,在编码类型 uint[][]
时,内层 uint[]
的偏移量检查被省略。从理论上讲,恶意构造的缓冲区可以包含旨在导致指针算术溢出的大的偏移量,这将允许“解码”任意内存区域。
第二个错误是预期大小检查执行不正确。当解码动态类型的静态大小数组时,反序列化代码没有检查静态大小数组的正确大小,而只验证缓冲区中至少剩余 32 字节。恶意构造的缓冲区可能导致反序列化代码读取内存中超出缓冲区末尾的字节。
相关的一个漏洞出现在解码动态类型数组时:解码例程只检查缓冲区中的字节是否足以容纳编码数组的长度字段。恶意缓冲区可以导致解码器错误地认为超出缓冲区末尾的任意字节是偏移量。这两个漏洞因上述第一个错误而加剧,因为对这些虚假偏移量的溢出检查被省略。
Solidity 编译器版本 0.8.3 及更早版本的漏洞复现可以在这个链接获取:https://github.com/Certora/MemoryIsolationPOC。
该漏洞影响任何在 EVM 内存中解码序列化数据的代码。特别是,它可以破坏依赖于演员进行独立计算的代码:恶意演员可以构造格式不正确的缓冲区,欺骗反序列化例程以共享存储在交易内存中其他地方的结果。
Solidity 团队已承认此漏洞,并在 Solidity 版本 0.8.4 中修复了它。有关此漏洞的更多细节,请参阅 Solidity 团队的 博客文章。
- 原文链接: medium.com/certora/memor...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!