从编译器的基本概念讲起,介绍了 Solidity 编译器漏洞,并分析了其在实际以太坊开发环境中可能导致的安全风险。
撰文:Cobo 安全团队
原文:《Cobo安全团队:一文讲透 Solidity 编译器漏洞》
编译器漏洞
编译器[1]是现代计算机系统的基本组件之一。编译器本身也是一种计算机程序,他的功能是将人类易于理解和编写的高级程序语言源代码转化成计算机底层 CPU 或字节码虚拟机可以执行的指令代码。
大多数开发者和安全人员通常会比较关注程序应用代码的安全,但可能会忽略编译器自身的安全。实际上编译器也是计算机程序,因此也会存在安全漏洞,而编译器产生的安全漏洞,在特定场景下也可以带来严重的安全风险。比如浏览器在编译并解析执行 Javascript 前端代码的过程中,就可能由于Javascript 解析引擎的漏洞[2],导致用户在访问恶意页面时被攻击者利用漏洞实现远程代码执行,最终完成对受害者浏览器甚至操作系统的控制。笔者在从事区块链安全研究之前,在传统安全研究工作中就曾发现多个 Google Chrome、Microsoft Edge 浏览器的 Javascript 脚本引擎的高危漏洞。而笔者曾经参与的另一项研究[3]也表明,Clang C++ 编译器的 bug 也可能导致远程代码执行这类严重后果。
Solidity 编译器也不例外,根据 Solidity 开发团队的安全预警[4],在多个不同版本的 Solidity 编译器中都存在安全漏洞。
Solidity 编译器漏洞
Solidity 编译器的作用是将开发人员编写的智能合约代码转化成以太坊虚拟机(EVM)指令代码,这些 EVM 指令代码通过交易打包被上传到以太坊上,最终通过 EVM 进行解析执行。
这里需要将 Solidity 编译器漏洞与 EVM 自身的漏洞进行区分。EVM 的漏洞是指虚拟机在执行指令时产生的安全漏洞。由于攻击者可以上传任意代码到以太坊上,这些代码最终将运行在每个以太坊 P2P 客户端程序中,如果 EVM 存在安全漏洞,那么将影响整个以太坊网络,造成整个网络的拒绝服务(DoS)甚至导致整个链完全被攻击者接管。不过由于 EVM 本身设计比较简单,且核心代码不会频繁更新,因此产生上述问题的概率相对较低。
Solidity 编译器漏洞是指编译器将 Solidity 转化成 EVM 代码时存在漏洞。与浏览器这种会运行在用户客户端计算机编译运行 Javascript 的场景不同,Solidity 编译过程只运行在智能合约开发者的计算机上,并不运行在以太坊上。因此Solidity 编译器漏洞不会影响以太坊网络本身。
一种攻击场景是,攻击者通过社会工程学等手段诱导 Solidity 开发者下载编译攻击者恶意构造的 Solidity 代码,然后利用 Solidity 编译器漏洞实现代码执行完成对受害者计算机的控制。这种攻击的目标只针对智能合约开发者,不会影响普通以太坊用户,因此本文将不会深入讨论。
Solidity 编译器漏洞的另一种危害在于,可以导致其 Solidity 源码生成的 EVM 代码与智能合约开发者的预期存在不一致的情况。由于以太坊上的智能合约通常与用户的加密货币资产有关,因此编译器导致智能合约产生的任何 bug 都可能导致用户资产受损,从而产生严重后果。
开发者和合约审计人员可能会重点关注合约代码逻辑实现问题,以及重入、整数溢出等 Solidity 层面的安全问题。而对于 Solidity 编译器的漏洞,仅通过对合约源码逻辑的审计,是很难发现的。需要结合特定编译器版本与特定的代码模式共同分析,才能确定智能合约是否受编译器漏洞的影响。
Solidity 编译器漏洞示例
下面将以几个真实的 Solidity 编译器漏洞为例,展示 Solidity 编译器漏洞的具体形式、成因及危害。
SOL-2016-9 HighOrderByteCleanStorage[5]
该漏洞存在于上古时期的 Solidity 编译器版本中(>=0.1.6 <0.4.4)。
考虑如下代码
其 storage 变量 b 没有经过任何修改,因此run()函数应该返回默认值 0。但实际在漏洞版本编译器生成的代码run()中将返回 1。
在不了解该编译器漏洞的情况,普通开发者很难通过简单的 code review 发现上述代码中存在的 bug。上述代码只是一个简单示例,因此不会造成特别严重的危害。但如果上述 b 变量被用于一些如权限验证、资产记账等用途时,这种与预期的不一致将可能导致十分严重的后果。
那么为什么会产生上述奇怪的现象呢?原因在于 EVM 使用栈式虚拟机,栈中每个元素均为 32 字节大小(即 uint256 变量大小)。另一方面底层存储 storage 的每个 slot 也为 32 字节大小。而 Solidity 语言层面支持 uint32 等各类低于 32 字节的数据类型,编译器在处理这种类型的变量时,需要对其高位进行适当的清除操作(clean up)以保证数据的正确性。上述情况中,在加法产生整数溢出时,编译器没有正确地对结果高位进行 clean up,导致溢出后高位的 1 bit 被写入 storage 中,最终覆盖 a 变量后面的 b 变量,使 b 变量的值被修改成了 1。
SOL-2022-4 InlineAssemblyMemorySideEffects[6]
考虑如下代码:
该漏洞存在>=0.8.13 <0.8.15版本的编译器中。Solidity 编译器在将 Solidity 语言转化成 EVM 代码的过程中,并不只是简单的进行翻译。还会进行深入的控制流与数据分析,实现各种编译优化流程,以缩减生成代码的体积,优化执行过程中的 gas 消耗。这类优化操作在各种高级语言的编译器中都十分常见,但由于这类优化要考虑的情况十分复杂,也十分容易出现 bug 或安全漏洞。
上述代码的漏洞就源于这类优化操作。考虑这样一种情况,如果某个函数中存在修改内存 0 偏移处数据的代码,但后续没有任何地方使用到该数据,那么实际可以将修改内存 0 的代码直接移除掉,从而节约 gas,并且不影响后续的程序逻辑。
这种优化策略本身并没有任何问题,但在具体的 Solidity 编译器代码实现中,此类优化只应用于单一的assembly block中。对上述 PoC 代码中的情况,对内存 0 的写入和访问存在于两个不同的assembly block中,而编译器却只对单独的assembly block 进行了分析优化,由于第一个assembly block中在写入内存 0 后没有任何读取操作,因此判定该写入指令是冗余的,会将该指令进行移除,从而产生 bug。在漏洞版本中f()函数将返回值 0,而实际上上述代码应该返回正确的值是0x42。
SOL-2022-6 AbiReencodingHeadOverflowWithStaticArrayCleanup[7]
考虑如下代码:
该漏洞影响>= 0.5.8 < 0.8.16版本的编译器。正常情况下,上述代码返回 a 变量应为"aaaa"。但在漏洞版本中会返回空字符串""。
该漏洞的成因是 Solidity 对 calldata 类型的数组进行abi.encode操作时,错误的对某些数据进行了 clean up,导致修改了相邻的其他数据,造成了编码解码后的数据存在不一致。
值得注意的是,Solidity 在进行external call和emit event时,会隐式地对参数进行abi.encode,因此上述漏洞代码出现的概率会比直观感觉上更大。
值得一提的是,本漏洞被改编成了在国内知名安全竞赛比赛 0ctf 2022 中一道区块链题目,题目中展示了真实开发场景下编译器漏洞对智能合约的影响。前 Cobo 实习生同学 s3cunda 编写了相应的题目解析文章[8],对题目感兴趣的读者可以参考。
安全建议
Cobo 区块链安全团队经过对 Solidity 编译器漏洞威胁模型的分析以及历史漏洞的梳理,对开发者和安全人员提出以下建议。
对开发者:
- 使用较新版本的 Solidity 编译器。尽管新版本也可能引入新的安全问题,但已知的安全问题通常较旧版本要少。
- 完善单元测试用例。大部分编译器层面的 bug 会导致代码执行结果与预期不一致。这类问题很难通过 code review 发现,但这类问题很容易在测试阶段暴露出来。因此通过提高代码覆盖率,可以最大程度地避免此类问题。
- 尽量避免使用内联汇编、针对多维数组和复杂结构体的 abi 编解码等复杂操作,没有明确需求时避免追求炫技而盲目使用语言新特性和实验性功能。根据 Cobo 安全团队对 Solidity 历史漏洞的梳理,大部分漏洞与内联汇编、abi 编码器等操作有关。编译器在处理复杂的语言特性时确实更容易出现 bug。另一方面开发者在使用新特性时也容易出现使用上的误区,导致安全问题。
对安全人员:
- 在对 Solidity 代码进行安全审计时,不要忽略 Solidity 编译器可能引入的安全风险。在 Smart Contract Weakness Classification(SWC) 中对应的检查项为SWC-102: Outdated Compiler Version[9]
- 在内部 SDL 开发流程中,敦促开发团队升级 Solidity 编译器版本,并可以考虑 CI/CD 流程中引入针对编译器版本的自动检查。
- 但对编译器漏洞无需过度恐慌,大部分编译器漏洞只在特定的代码模式下触发,并非使用有漏洞版本的编译器编译的合约就一定存在安全风险,实际的安全影响需要根据项目情况具体评估。
一些实用资源:
- Solidity Team 定期发布的 Security Alerts posts https://blog.soliditylang.org/category/security-alerts/
- Solidity 官方 repo 定期更新的 bug list https://github.com/ethereum/solidity/blob/develop/docs/bugs.json
- 各版本编译器 bug 列表 https://github.com/ethereum/solidity/blob/develop/docs/bugs_by_version.json 。据此可在 CI/CD 过程中引入自动进行编译器版本的检查,提示当前版本中存在的安全漏洞。
- Etherscan 上Contract -> Code页面右上角的三角形感叹号标志可提示当前版本编译器所存在的安全漏洞。
小结
本文从编译器的基本概念讲起,介绍了 Solidity 编译器漏洞,并分析了其在实际以太坊开发环境中可能导致的安全风险,最终对开发者和安全人员提供了若干实际的安全建议。
参考资料
[1]编译器: https://en.wikipedia.org/wiki/Compiler
[2]Javascript 解析引擎的漏洞: https://bugs.chromium.org/p/v8/issues/list
[3]Clang 编译器漏洞研究: https://i.blackhat.com/eu-20/Wednesday/eu-20-Wu-Finding-Bugs-Compiler-Knows-But-Does-Not-Tell-You-Dissecting-Undefined-Behavior-Optimizations-In-LLVM.pdf
[4]Solidity 开发团队的安全预警: https://blog.soliditylang.org/category/security-alerts/
[5]SOL-2016-9 HighOrderByteCleanStorage: https://blog.soliditylang.org/2016/11/01/security-alert-solidity-variables-can-overwritten-storage/
[6]SOL-2022-4 InlineAssemblyMemorySideEffects: https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug/
[7]SOL-2022-6 AbiReencodingHeadOverflowWithStaticArrayCleanup: https://blog.soliditylang.org/2022/08/08/calldata-tuple-reencoding-head-overflow-bug/
[8]题目解析文章: https://s3cunda.github.io/2022/09/19/0ctf-2022-NFT-Market.html
[9]SWC-102: Outdated Compiler Version: https://swcregistry.io/docs/SWC-102