智能合约审计需要的思维模型

本文作者分享了智能合约安全审计的经验模型,强调了资源收集、智能合约概览、手动代码审查、功能测试、自动化审查、报告编写和修复代码审查等关键步骤。文章旨在帮助审计人员系统性地进行安全审计,并识别潜在的安全漏洞。

请持续关注关于区块链桥中常见漏洞、我的审计思维模型以及更多内容的资源 ✨🔒

介绍:

本文档不是漏洞的检查清单,而是一个用于处理智能合约安全审计的结构化思维模型。 随着时间的推移,我发现坚持这个框架会产生一致的结果,无论是在识别细微的错误,还是在培养对代码库健壮性的更高程度的信心方面。

当以有意识的深度应用时,它是最有效的。

随着我改进我的工作流程并整合新的想法,这个思维模型将继续发展。

收集资源:

  1. 范围。
  2. commit 哈希值。
  3. 文档和规范链接。
  4. 不变量列表,如果已经存在。

智能合约更高层面的概述:

  1. 文档和白皮书演练。
  2. 如果有任何自定义/花哨的公式实现,从开发者那里获取(简化的)数学公式。 这将帮助你在审计中继续前进,而无需在关键时刻等待太久。
  3. 非常大概地了解项目是什么以及它背后的想法。 例如,链抽象、质押应用程序等。开发者项目演练很有帮助。
  4. 检查智能合约是如何工作的,项目和智能合约的结构是什么。 它们是如何集成到其他合约中的。 基于此创建一个图表。
  5. 创建一个项目图 -
    1. 随着你获得更多理解,可以随着时间的推移进行修改。
    2. 该图可以根据时间推移变得更加花哨和详细。 我通常使用 Excalidraw 和 Lucidchart。
    3. 示例:具有一些跨链功能的质押项目:

手动审查:

  1. 了解单元测试的流程:

测试用例很重要。 它可以让你了解设置是如何进行的,用户需要处于什么状态才能开始使用系统的入口点。 此外,它还可以让你有时间测试更多的攻击性场景(在单元测试验证之后),而不是将大部分时间用于测试一般场景。

  1. 当涉及到数学公式或复杂的互连调用时,调试变量的值有助于查看状态如何变化,例如,在特定的内部/外部调用之前,变量 x/totalStakedEth 的值是多少。 或者在调用之后它变成了什么。
  2. 调试可以通过多种方式完成。 取决于你使用的框架。 一些例子是。
    1. Foundry - 原生调试器、合约中的控制台日志、Tenderly 调试器、simbolik ( Foundry 欺骗码支持即将推出)。
    2. Hardhat - 合约中的控制台日志、像 hardhat-tracer 这样的调试器(已过时的示例文章在这里)、Tenderly 调试器(示例文章在这里)。
  3. 选择依赖性最小的合约逐一审查:

这可以通过查看图表和当前对智能合约功能的理解来完成。 例如,在这种情况下,Token 合约的依赖性最小(事实上,该合约中的功能正在其他合约中使用,因此其他智能合约依赖于它)。 它用蓝色虚线高亮显示。

  1. 检查清单审查:
    1. 已知的智能合约漏洞。
    2. 智能合约弱点分类(SWC)
    3. 在需要时维护每个函数的不变量列表,有时会很耗时。 但可用于针对函数进行检查(以后可用于其他测试/模糊测试/F.V)。
    4. 在手动和固定审查中,全面地以攻击性的方式分析代码。
    5. 访问所有路径:

从安全角度来看,访问智能合约中的所有路径至关重要。 例如,在下面的 withdraw() 示例中,每个路径都有其自身的潜在安全影响和需要验证的行为。 有时,这些意想不到的路径会将我们引向潜在的错误。

自动化的测试方法,比如模糊测试和形式化验证在这里会很有用。

正如在下面能看到的,一切都归结于每个路径。 即使是三元表达式显然也是路径。

// 它可能缺乏一般含义,但旨在演示具有多个执行路径的代码。
function withdraw(uint256 amount, bool emergency) public {
        require(amount > 0, "Withdraw amount must be greater than 0");
        uint256 balance = balances[msg.sender];
        bool success;

        if (emergency) {
            success = (address(this).balance >= amount) ? payable(msg.sender).send(amount) : false;
        } else {
            if (balance >= amount) {
                balances[msg.sender] -= amount;
                totalBalance -= amount;
                success = payable(msg.sender).send(amount);
            } else {
                success = false;
            }
        }

        if (!success) {
            // 调用其他合约来处理失败的提款
        }
        emit Withdrawn(msg.sender, amount, success);
}

下面的第二个例子展示了每个值都是动态的

因为每个值(_amountminRequired)都可以高于和低于,结果可能会有所不同。 如果该结果用于一些操作,例如减少 burnFuelAndReduce() 中的 drivingScore

例如,取决于金额是否相同(与加注Gas时一样)或增加或减少,fuelReduction 的值将发生变化,并且在从 drivingScore[msg.sender] 中减去它时,需要相应地处理,这也会创建不同的路径。

如果你能看到,那就是真正的混乱!

function fillFuelAndCalculate(uint256 _amount) public {
        require(_amount > minRequired, "ERR");
        fuelTank[msg.sender] += _amount;
        drivingScore[msg.sender] += (_amount * multiplier) / minRequired;
}

function burnFuelAndReduce(uint256 _amount) public {
        fuelTank[msg.sender] -= _amount;
        uint256 fuelReduction = (_amount * multiplier) / minRequired;
        drivingScore[msg.sender] = drivingScore[msg.sender] > fuelReduction ? drivingScore[msg.sender] - fuelReduction : 0;
}
  1. 写下笔记、疑问和边缘情况。

我喜欢写下疑问、想法、可能的问题的原因之一是,当你开始浏览这些问题时,当你遇到新路径时,你可以看到更多的疑问,你脑海中可能出现的问题。 因此,有时它会变成一个循环,在其中你会在探索之前/当前的想法时不断思考新的想法。 这就是为什么我认为最好将这些东西记录下来,这样你才不会在混乱中忘记它们。

  1. 做笔记是为了理解或进一步检查。
  2. 疑问:研究它,向开发者提出适用的疑问。
  3. 边缘情况:用于以后的测试。 例如:[test] 在 unstake() 函数中可能存在重入。
  4. 在浏览每个代码块时,尝试打破业务逻辑。(再次记下需要测试的内容)
  5. 记下在代码修复后需要重新访问的内容。 例如:在审查中,你注意到,如果基于提出的疑问或基于建议的修复等添加了特定功能,则可能会出现问题。 记下它并在已修复代码审查中检查。
  6. 我用于记笔记的最小 markdown 文件:
## 项目名称
### 范围:
GH链接:
Commit 哈希值:

### 流程:
1. 手动审查
2. 功能测试:
    - 单元测试
    - 边缘情况测试
3. 自动化测试:
    - ...
4. ...

### 资源:
1. 规范文档:
2. 白皮书:
3. 任何其他文档和链接。

### 待办事项:
*需要做的事情/剩余事情。*
1. 关于审计的高层次事项
2. 开发者项目演练。
3. ...

### 常见/疑问/研究:
1. 重新学习正在使用的特定跨链服务/协议。

### ContractName.sol
*这包含关于 ContractName.sol 的疑问/要点/研究/潜在错误*
1. 确认的问题 1。
2. 确认的问题 2。
    ### 疑问/研究:
    1. 检查特定公式。
    2. 检查特定疑问。
    3. 检查 L453 上的减法可能是一个问题。
    * 测试用例:
        1. [test] 用户应该能够质押。
        2. [test] 多个用户应该能够质押。
        3. [test] ...

### ContractName2.sol
*这包含关于 ContractName2.sol 的疑问/要点/研究/潜在错误*
...

---
### 给开发者的问题:
1. 公式的解释。
2. 关于预期逻辑的问题。
3. ...
  1. 空闲时间是研究时间: 这更像是给自己的便条,如果你/我在项目截止日期之前有空闲时间,那么就利用它来进行研究、访问更多路径、创建图表、场景。

功能测试:

  1. 检查是否缺少应该被覆盖的测试用例。
  2. 从手动审查合约时所做的笔记中编写和测试边缘情况。

自动审查:

  1. 从像 SlitherWakeaderyn 等工具中分类静态分析警告。
  2. 有时来自这些工具的警告可以很好地注意到一些线索。
  3. 在像 Slither 这样的静态分析工具之上,还提供其他模块、打印机等。
  4. 向模糊测试/F.V团队提供书面不变量。
  5. 审计代理,有人吗?

报告编写阶段

  1. 与分配给项目的内部团队讨论疑问和发现的问题。
  2. 只要有可能,我就会为每个审计过的合约编写报告(至少是我到那时为止发现的问题)。 稍后,我们可以随着审计的进行,获得更多的理解和问题来修改报告)
  3. 与开发者讨论问题,以便给他们一个概述(当完成时,针对每个合约):目标是团队的技术开发成员应该能够获得问题,以便他们可以尝试修复它们。 一旦交付最终报告,这可以节省大型项目的时间。(如果可能)
  4. 编写完整的报告:
    1. 添加/协作所有问题。
    2. 格式化:缩进、语法、相同主题的代码段。
    3. 在与团队/公司合作时,遵循内部流程。

已修复代码审查

  1. 代码比较(有助于了解哪些内容已更新,以及是否存在超出预期的情况):
  2. 如果根据建议的修复程序预期之外地更新了某些内容,请概述更新后的代码。
  3. 浏览来自开发者的更新代码的测试。
  4. 如果需要,为更新后的代码编写新的测试。
  5. 浏览更新功能检查清单(取决于更新的内容)。
  6. 检查 手动审查 中需要重新访问的内容。
  7. 根据更新后的代码更新问题状态。
    1. 更改状态。
    2. 添加“已修复”commit 链接/哈希值。
    3. 添加来自审计团队的评论(当需要时)。
    4. 添加来自项目团队的评论(当需要时)。

在与团队合作时,单独处理每个合约会产生更好的结果。 它还可以帮助你从整个项目的角度进行思考。 这种方法可以是协作式的,在其中可以进行大量的头脑风暴,然后在最后分享发现。 你可以看到许多优秀的团队都在遵循这种结构。

有时审计可能是一个非常并行/实时的过程。 例如,你脑海中刚刚出现了一些东西,你很兴奋地去浏览它等等。

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

0 条评论

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