关于以太坊智能合约安全的笔记

date
Mar 23, 2022
slug
evm-smart-contract-security
status
Published
tags
Blockchain
summary
Ethereum Book 关于合约安全的一些笔记,总结了常见地一些合约安全隐患。
type
Post
  • Reentrancy 二次进入。
    • 问题:想象整个 EVM 系统作为一个单线程的全球计算机,而“二次进入”的本质就是递归调用,使得 EVM 状态变更与预想的一次调用不一致。
    • 解决:确保状态变更在 ether 发送出之前发生;使用内置的 transfer 函数发送 ether(因为 transfer 默认只有 2300 gas,不足以发起递归调用);使用互斥锁。
  • 数值溢出
    • 问题:EVM 在数值溢出从0开始重新计算
    • 解决:使用 SafeMath
  • “意料之外的 Ether”
    • 问题:Ether 不只可以通过 payable function 发送,所以通过累加 payable function 计算出的 balance 与实际的 balance 可能不一致。两种特例情况:
      • 合约销毁时可以指定将 ether 发送到某个地址,如果这个地址是合约,这次发送并不会触发合约调用。因为合约地址是通过 deployer 的地址和 transaction nounce 计算出来的,而这两个值都是可以预先知道的,所以可以在该合约部署前往这个“未来的合约地址”发送以太而不触发任何调用。
    • 解决:尽量避免合约逻辑之间依赖 this.balance 的值。
  • Delegate Call
    • 问题:DelegateCall 在执行时会复制上下文(有点像 JS 当中的 apply(this)?),被调用方的状态很有可能会被调用方覆盖。
    • 解决:使用 library 关键字构建库函数,库函数应该是“无状态的(stateless)”。
  • 函数可见性
    • 问题:默认情况下,Solidity 合约的成员函数是 public 的,意味着任何人都可以调用这个函数。
    • 解决:显式指定所有成员函数的可见性。
  • 虚假的墒值
    • 问题:试图在 EVM 系统上实现随机数是不可能的。不能认为未来区块信息(当前or之前区块信息当然更不行)是无序不可预测的,因为实际上这些值是可以被控制的。
    • 解决:使用区块链系统外部生成随机数,如 commit-reveal 技术、RandDAO 或者其他预言机(Oracle)系统。
  • 引用外部合约
    • 问题:使用合约地址引用外部合约时,如果地址不是 public 的(不可审查的),那么很可能这是一个恶意的合约,因为 Solidity 并没有 instanceof 这一类的检查。
    • 解决:当调用外部合约时,可以直接 new 一个 instance 出来。或者,保证引用的外部合约是可审查的(public)的。
  • 短参数攻击
    • 问题:参数在被发送给智能合约之前会根据 ABI 规范经过一次编码,当编码后的参数字节数长度小于预期长度时,EVM 会自动在后面补上若干个零,导致实际的调用参数和预期的调用参数不一致。尽管合约代码仍然会执行(如果参数仍然有效的话),但是结果和预期的结果很可能会不一样。
    • 解决:外部应用程序在调用合约时,应当始终验证用户输入是否是有效的。
  • 不检查返回值
    • 问题:call / send 方法会返回一个布尔值表示调用是否成功(而不是 throw)。如果不验证返回值,会导致程序异常不会正确的处理,甚至导致合约进入 “dead state”。
    • 解决:尽可能使用 transfer 而不是 send 语法。始终检查 call / send 等方法的返回值。使用 withdrawal 模式,隔离合约内部逻辑和 send ether 能力,这样最差的情况也就是用户调用失败,而不会导致合约内部状态错误。
  • 竞态/超前执行
    • 问题:出于性能考虑,因为区块链系统(包括以太坊)不会为每一个链上交易(transaction)生成一个区块,而是会不停地从交易池中取出一批交易进行打包,在被打包之前这些交易都没有被记录在链上,此外这些活动还是可以被任何人(包括矿工和攻击者)读到的。攻击者可以利用这一点,构造一个与交易池中某个交易相同的交易出来,并且提高 gasPrice 使其优先被处理。需要注意的是,矿工(区块打包者)也有可能是攻击者。
    • 解决:
      • 设定 gasPrice 上限以阻止普通攻击者通过提高 gasPrice 的方法使其伪造的交易被优先处理。采用 commit-reveal 技术,先发送一个隐藏了信息的 transaction,然后再发送一个能解开之前隐藏信息的 transaction。
  • 拒绝服务攻击(DoS)
    • 问题:可能导致 DoS 的方式有很多。主要有:
      • 利用合约漏洞写入大量数据导致合约无法正常执行(突破区块 gasLimit)。发生单点故障。例如合约设置了某个地址为 Owner,而该地址的密钥永久丢失了。依赖外部调用来转移状态。当外部调用永远无法执行成功时,状态将无法正常转移
    • 解决:
      • 使用 withdrawal pattern,避免循环遍历可以被外部修改的数据结构。避免引入单点故障,例如使用 MultiSig or Time Lock等。尽量避免依赖外部调用来转移状态,如果需要的话,应该考虑外部调用失败的情况。
  • 操控区块时间戳
    • 问题:矿工可以小幅度地随意调整区块时间戳,区块时间戳只能保证其是单调递增的,不能保证其是准确的。
    • 解决:不应使用区块时间戳来生成随机数,因为它不是可靠的墒源。某些情况下,可以使用区块高度结合当前区块速度模拟时间戳,区块高度是往往是更可靠的。
  • 错位的构造函数
    • 问题:Solidity 0.4.22 之前的版本中,构造函数为与合约名同名的成员函数。所以如果只改了合约名而忘记改构造函数名的话,这个构造函数就退化成了一个普通的成员函数。
    • 解决:使用 Solidity 0.4.22 之后的版本提供的 constructor 关键字。
  • 未初始化的 Storage 变量指针
    • 问题:与 EVM 处理 storage 变量的方式有关,未初始化的 storage 变量可能包含其他 storage 变量的值。
    • 解决:编译器会对未初始化的 storage 变量给出报警,应当关注这类报警。同时,处理复杂类型时,最好显式标记出变量类型:memory or storage
  • 浮点数相关
    • 问题:因为 Solidity 目前不提供浮点类型数据,使用整数类型就是运算时可能出现精度丢失。
    • 解决:进行数学运算时保持谨慎,尤其是除法运算。另外,在合约内部使用 uint256 进行数学运算可以尽可能的避免精度丢失。
  • 使用 tx.origin 进行验证
    • 问题:tx.origin 保存的永远是调用栈发起者的地址,使用 tx.origin 验证身份可能为钓鱼攻击留下开口。
    • 解决:使用 msg.sender 而不是 tx.origin 做身份验证。
    •  

© Hopsken 2021 - 2022