本文深入探讨了智能合约安全审计中的自动化工具,包括其功能、使用方法以及在实际合约中应用的经验。通过分析工具的有效性和面临的挑战,文章指出审计中高层逻辑错误通常是严重问题,同时介绍了代码效率和Gas费用在智能合约中的重要性。通过一系列的分析,目标是帮助开发人员快速识别和修复常见漏洞,从而提升合约的安全性。
随着我们的 MixBytes 团队执行智能合约安全审计,自动化工具的使用变得非常重要。它们是否是识别可能缺陷的最有效手段?我们应该如何使用它们?它们的功能是什么?在这个领域工作的具体细节是什么?
这些问题和相关问题是本文的主要关注点。我将描述我们在使用最有趣的工具处理真实合同时的尝试,并分享一些如何使用这种引人入胜的软件的技巧。起初,我想把所有东西放在一篇文章中,但随着时间的推移数据量的增长,我决定制作一系列文章,每篇介绍一个自动分析器。你可以在这里找到我将提到的工具列表:https://consensys.github.io/smart-contract-best-practices/security_tools/#static-and-dynamic-analysis。然而,如果我发现其他值得关注的工具,我会将它们添加到列表中,进行测试并为你们解释。
我必须承认,审计任务变得非常有趣,因为开发者到目前为止对算法的经济方面和内部优化并没有给予太多关注。智能合约审计揭示了一些在寻找错误时需要考虑的特殊攻击向量。此外,出现了相当多的自动测试工具:静态分析器、字节码分析器、模糊测试工具和其他良好的软件。
本文的目的是促进合约代码安全,并允许开发人员快速消除那些常常令人烦恼的愚蠢错误。协议可能看起来相当可靠并解决严重问题,但如果你在测试中遗漏了一个小错误,则可能会严重影响整个项目。因此,至少让我们学会如何使用能够帮助我们轻松避开众所周知问题的工具。
提前说一下,逻辑层面的高漏洞是审计中最常见的关键错误来源,而典型的漏洞(如访问权限、整数溢出、重入等等)则较少见,或者其影响没有破损逻辑那么大。关于高层合约逻辑、生命周期、操作方面和任务合规性的复杂且彻底的审计只能由经验丰富的开发人员执行,他们能够对合约进行常见模式及不常见漏洞和逻辑错误的测试。
自动分析器对于检测常见错误、警告、典型漏洞、小错误和代码风格建议很有用。有时,它们会指出严重错误,通常在处理标准任务方面比人类表现得更好——这就是我们将要检查的内容。
智能合约代码审计是一个棘手的领域。尽管规模小,但每个以太坊智能合约都代表了一个复杂的程序,能够生成复杂的分叉、循环、决策树等。即使是一个自动化处理某些交易的基本智能合约,也需要在每一步都考虑所有潜在的分叉。从这个角度看,区块链开发是一个低级任务,消耗了大量的努力和资源;这与 C/C++ 和汇编语言中系统软件和固件的开发有很多共同之处。因此,在面试开发者时,我们欢迎那些熟悉低级算法、网络栈、高负载服务、曾经处理过低级优化和代码审计的人员。
从开发者的角度看,Solidity 也相当具体,尽管几乎任何程序员都可以轻松跟随它,而第一步似乎非常简单。Solidity 代码相当容易阅读;对于任何知道 C/C++ 语法和面向对象编程基础(例如,JavaScript)的开发人员来说都很熟悉。
代码效率对于智能合约的存活至关重要,这就是为什么区块链开发者积极使用低级开发技巧,并利用资源有效使用和节省内存:Merkle 树、布隆过滤器、“懒惰”加载资源、循环展开、垃圾回收等等。
智能合约字节码的大小受限于常量上限的Gas(gas)。现在,以太坊区块链最多可以存储约 10Kb 的数据——使用空间不多。有关Gas价格和智能合约部署成本的文章在这里:https://hackernoon.com/costs-of-a-real-world-ethereum-contract-2033511b3214。在几十个短函数的操作下,智能合约的代码无法承受聚合方法、附加结构(如索引)或复杂逻辑。因此,开发者创建单独的库,设置智能合约系统,向部署过程添加新步骤。将大量代码放入区块链的唯一方法是将其拆分为具有各自专用存储的单独库类。这些类又可以方便地放置在单独的文件中。因此,得益于整洁的原始结构,合约代码非常好阅读。几乎没有其他办法可以制作一个可用的合约系统。来看看 openzeppelin-solidity 中 ERC721 代币合约的优秀示例:https://github.com/OpenZeppelin/openzeppelin-solidity/tree/master/contracts/token/ERC721。
Gas为合约代码执行带来了额外的逻辑层,并确实要求审计。此外,相同的代码序列可能具有不同的Gas价格。EVM 操作码表可以帮助理解Gas限制:https://github.com/trailofbits/evm-opcodes。
为了说明为什么Gas价格评估如此耗时,我们建议考虑以下伪代码片段:
// 函数在区块链上记录事件代码
function fixSomeAccountAction(uint _actionId) public onlyValidator {
// …
events[msg.sender].push(_actionId);
}// 用户调用函数以汇总奖励
// 并为每种类型的操作支付
function receivePaymentForSavedActions() {
// …
for (uint256 i = 0; i < events[msg.sender].length; i++) {
// 从数组中获取 actionId
uint actionId = events[msg.sender][i];
// 计算操作奖励
uint payment = getPriceByEventId(actionId);
if (payment > 0) {
paymentAccumulators[msg.sender] += payment;
}
emit LogEventPaymentForAction(msg.sender, actionId, payment);
// …
// 从数组中删除 "events[msg.sender][i]"
}
}
问题在于,合约循环执行了 events[msg.sender].length 次。每次迭代都意味着在区块链上进行写操作(transfer() 和 emit()):transfer 存储新的余额和地址,emit — 保存日志事件。如果数组很小,循环工作了几十次,一切都没有问题。然而,一个较大的 events[msg.sender] 数组需要大量的迭代,Gas价格将达到硬编码限制的约 8m。然后,事务失败,无法将其通过,因为合约不允许缩减 events[msg.sender] 数组。如果在计算单个值的同时,循环还意味着在区块链上进行记录(例如,支付费用、佣金等),可用的迭代次数也是严格有限的。例如:记录一个新的 256 位值要消耗 20K 的 8m Gas限制。简单地说,你只能存储或更新几百个带有一些元数据的 256 位地址。
此外,一个重要的事实是,更新已存在的数据仅需 5k,因此,举例来说,将代币“转移”到一个已存储代币的账户就要便宜四倍(5k vs. 20k 的每次写入Gas)。
毫不奇怪,Gas问题与合约安全性密切相关:对于资金所有者而言,资金在合约中永远被卡住与被盗没有什么区别。考虑到 ADD 命令的成本为 3 gas,而 SSTORE 命令(保存到存储)的成本为 20,000 gas,存储显然是区块链中最昂贵的资源,合约代码优化任务与 C 和 ASM 中处理有限存储大小的嵌入式系统的低级开发任务有很多共同之处。
从智能合约审计员的角度看,区块链技术对安全方面是有利的。智能合约代码的确定性特性确保了调试以及错误和漏洞的再现。技术上,任何合约函数调用都可以在任何平台和任何时间完全再现,这反过来又允许简单测试及其支持,以及可靠无争议的事件调查。我们知道谁调用了函数,它的参数,处理它的代码,以及结果。算法是纯确定性的,即可以在任何地方再现,甚至在 JS 语言的网页上。在以太坊中,你可以轻松用 JavaScript 编写任何测试用例,包含模糊参数,然后在 Node.js 上运行。
听起来不错,是吧?但是,我们仍然应该记住,最常见的关键错误涉及合约逻辑,而确定性是一个非常好的但正交的特性。
在这篇文章中,我选择了一个为 Smartz 平台制作的旧试用合约,用于预定住宿:https://github.com/smartzplatform/constructor-eth-booking。该合约允许创建对象(公寓或酒店房间)的记录,设置预订价格和日期。然后,如果收到付款,合约注册预订的事实,并在客人入住并确认到达之前保留资金。此时,房间的拥有者会收到付款。实际上,该合约是一种状态机,其状态和转换可以在 Booking.sol 中查看。我们写的相当快,没时间进行彻底的测试。其编译器版本过时,但逻辑或多或少是值得的。让我们看看分析器将如何处理它,看看它们是否会找到任何错误——如果没有,我们再添加一些错误。
各种分析器在 Solidity 编译器的使用模式上各不相同。一个工具使用 Docker 镜像,另一个工具处理已编译的字节码,审计员必须处理许多不同的合约集和编译器版本。因此,需要能够轻松更改主机系统、Docker 镜像或 truffle 环境中的 solc 版本。以下是一些不太正规的技巧:
1. 在 Truffle 中
毫无困难。从 Truffle 5.0.0 开始,你可以在 truffle.js 中直接指示编译器版本,如此处所示:https://github.com/smartzplatform/constructor-eth-booking/commit/62b0628b60de53e9267426ee92dae423878bd852。
然后,Truffle 将下载所需的编译器并启动它。我非常感谢 Truffle 团队——Solidity 是一门年轻语言,重大变化相当频繁。与开发者不同,审计员无法迁移到更新版本——这会导致新的错误并掩盖以前的错误。
当分析器作为 Dockerfile 分发时,可以在构建 docker 镜像时进行替换。需要在 Dockerfile 中添加一行,以便从导入的镜像获取所需的 solc 版本,并简单替换 /usr/bin/solc:
COPY --from=ethereum/solc:0.4.19 /usr/bin/solc /usr/bin
最肮脏的技巧。当没有其他选择时,可以偷偷地使用脚本替换 /usr/bin/solc 二进制文件,该脚本直接在 Docker 镜像中运行所需版本的 solc(别忘了备份原始文件!):
##!/bin/bash
## 运行给定版本的 Solidity 编译器,传递所有参数
## 你可以运行 "SOLC_DOCKER_VERSION=0.4.20 solc - version"
SOLC_DOCKER_VERSION="${SOLC_DOCKER_VERSION:-0.4.24}"
docker run \
-- entrypoint "" \
-- tmpfs /tmp \
-v $(pwd):/project \
-v $(pwd)/node_modules:/project/node_modules \
-w /project \
ethereum/solc:$SOLC_DOCKER_VERSION \
/usr/bin/solc \
"$@"
该脚本下载并缓存具有所需 solc 版本的 Docker 镜像。然后它导航到活动目录并用接收到的参数启动 /usr/bin/solc。这不是一种公平的方式,但它允许在主机系统中轻松伪造编译器及其版本,而无需进行重大更改。
现在,我们要处理源代码。从理论上讲,自动工具(特别是用于静态源分析的工具)应该构建合约,导入所有依赖项,将其全部合并并进行分析。然而,正如我之前提到的,版本之间的变化可能是巨大的。我将一次又一次面临需要向 Docker 添加新目录并配置内部路径以使其正确加载所需导入的需求。有些分析器能够处理它,但有些则不能。对于能够处理单个文件的分析器,避免创建额外目录的全面方法是将所有数据汇集到单个文件中,然后进行分析。
为此可以使用 truffle-flattener:https://github.com/nomiclabs/truffle-flattener。
这是一个标准的易于使用的 npm 模块:
truffle-flattener contracts/Booking.sol > contracts/flattened.sol
如果需要一个自定义的扁平化代码,可以编写自己的扁平化工具。例如,有时我们使用基于 Python 的版本:https://github.com/mixbytes/solidity-flattener。
让我们坚持使用老样本 https://github.com/smartzplatform/constructor-eth-booking,继续进行分析。该合约使用旧的编译器版本“0.4.20”。我故意选择了一个过时的合约来处理编译器问题。更糟糕的是,一个自动分析器(例如,处理字节码的那个)可能依赖于这个 solc 版本,这些版本差异可能会对结果产生严重影响,甚至导致全面失败。因此,即使你始终更新到最新版本,仍然有可能陷入设计用于旧编译器版本的分析器。
首先,让我们从 github 上拉取项目并尝试编译它:
git clone https://github.com/smartzplatform/constructor-eth-booking.git
cd constructor-eth-booking
npm install
truffle compile
当然,你会在这里遇到编译器问题。自动化分析器也遇到这些问题,因此,尽量获取与 0.4.20 兼容的编译器并构建项目。我所做的只是将所需的编译器版本指定在 truffle.js 中,太好了,成功了!
也启动
truffle-flattener contracts/Booking.sol > contracts/flattened.sol
正如我在“扁平化代码”部分提到的,我们将分析 contracts/flattened.sol。
现在我们有了 flattened.sol 文件和任意版本的 solc,可以开始分析。我将省略运行 truffle 和测试的问题,关于这个主题有大量文档,你可以自己解决。毫无疑问,必须运行测试并成功通过。此外,审计员经常需要添加他自己的测试来检查潜在漏洞的逻辑,例如,在数组边界分析合约功能,测试所有变量,甚至那些严格用于数据存储的变量等等。建议有很多;此外,这是我们公司提供的主要服务,因此逻辑审计纯粹是一个人类任务。
我们将检查最有趣的分析器,以了解它们如何处理我们故意添加“虚假”漏洞的合约。下一篇文章将讨论 Slither,而我们的总体计划如下:
第 1 部分。介绍。编译、扁平化、Solidity 版本(本文)
第 2 部分。 Slither
第 3 部分。 Mythril
我列出这样的分析器清单是因为审计员必须能够东拼西凑不同的方法,进行不同类型的分析——静态和动态。我们的任务是学习每种分析类型应该使用的基本工具。在详细研究期间,我可能会考虑添加新的审查候选人或更改文章的顺序,因此请保持关注。要进入下一部分,请 点击这里。
如果你对使用自动化智能合约安全工具为你的项目有具体问题,随时通过我们的 网站 或通过 Telegram 联系我们。
- 原文链接: medium.com/mixbytes/a-pr...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!