使用Crytic进行漏洞挖掘:智能合约安全检测实战

本文详细介绍Trail of Bits开发的Crytic工具在多个知名项目中发现的9个智能合约安全漏洞,包括未检查返回值导致的免费铸币、重入攻击风险等关键问题,并给出具体修复建议。Crytic通过90多个检测器实现自动化安全审计。

使用Crytic进行漏洞挖掘

Crytic是我们开发的GitHub应用程序,用于发现智能合约缺陷:它无需人工干预即可检测安全问题,在您工作时提供持续保障,并在部署前保护代码库。

Crytic能发现许多其他工具无法检测的漏洞,包括一些尚未广为人知的问题。目前Crytic拥有90多个检测器,我们持续添加新检查并改进现有功能。它会在每次提交时运行这些漏洞和优化检测器,并评估您自己添加的自定义安全属性!

今天,我们将分享Crytic在主要项目中发现的12个问题(包括一些高危漏洞),这些问题均由Crytic独立发现。

已发现的问题清单

  • 未使用的返回值可能允许免费铸币
  • registerVerificationKey总是返回空字节
  • MerkleTree.treeHeight和MerkleTree.zero可声明为常量
  • 修饰符可能返回默认值
  • 危险的严格相等可能使合约陷入陷阱
  • ABI encodePacked碰撞
  • 缺失继承容易导致错误
  • Msg.value在fundWithAward中被重复使用
  • 重入攻击可能导致代币盗窃
  • [待确认]
  • [待确认]
  • [待确认]

Ernst & Young Nightfall

Crytic在EY的Nightfall项目中发现三个漏洞,包括一个允许任何人免费铸币的关键漏洞。

问题1:未使用的返回值可能允许免费铸币

描述
FTokenShield.mint未检查transferFrom的返回值。如果FTokenShield使用的代币在错误转账时不回滚而仅返回false(例如BAT),任何人都可以免费创建承诺——攻击者可以免费铸币。

FTokenShield.burn中存在类似但影响较小的问题:此处未检查fToken.transfer。

建议
检查transferFrom和transfer的返回值。

[Crytic检测报告]

问题2:registerVerificationKey总是返回空字节

描述
FTokenShield.registerVerificationKey和NFTokenShield.registerVerificationKey返回空的bytes32。不清楚正确的返回值应该是什么,这可能给第三方和合约带来意外行为。

建议
考虑返回一个值,或从签名中移除返回值。

[Crytic检测报告]

问题3:MerkleTree.treeHeight和MerkleTree.zero可声明为常量

描述
在MerkleTree.sol中,treeHeight和zero可声明为常量以允许编译器优化代码。

建议
永不更改的状态变量应声明为常量以节省gas费。

[Crytic检测报告]

DeFiStrategies

Crytic在DeFiStrategies中发现一个异常问题:修饰符中缺少占位符执行导致调用函数返回默认值。此外,Crytic还发现与balanceOf调用返回值的严格相等相关的问题。

问题4:修饰符可能返回默认值

描述
SuperSaverZap.onlyInEmergency()和SuperSaverZap.stopInEmergency()修饰符在无效访问时不会回滚。如果修饰符既不执行也不回滚,函数执行将返回默认值,这可能误导调用者。

建议
在两个修饰符中用require替换if()条件。

[Crytic检测报告]

问题5:危险的严格相等可能使合约陷入陷阱

描述
ERC20toUniPoolZapV1_General.addLiquidity对_UniSwapExchangeContractAddress余额使用严格相等比较。此行为可能允许攻击者通过向_UniSwapExchangeContractAddress发送代币来陷阱合约。

建议
在比较中将==改为<=。

[Crytic检测报告]

DOSNetwork

Crytic在DOSNetwork中发现另一个不太为人知的问题:如果abi.encodePacked使用多个动态参数调用,可能在不同参数下返回相同值。

问题6:ABI encodePacked碰撞

描述
DOSProxy使用encodePacked Solidity函数处理两个连续字符串(dataSource和selector):eth-contracts/contracts/DOSProxy.sol。

此模式易受碰撞攻击,两个不同dataSource和selector的调用可能产生相同queryId(详见Solidity文档)。

建议
不要在encodePacked中使用多个动态类型,考虑先使用keccak256对dataSource和selector进行哈希处理。

[Crytic检测报告]

ethereum-oasis/Baseline

Crytic在Baseline协议中发现一个架构问题:实现接口的合约未从接口继承。

问题7:缺失继承容易导致错误

描述
Shield是ERC1820ImplementerInterface的实现,但未从该接口继承。此行为容易出错,可能阻止实现或接口正确更新。

建议
使Shield继承ERC1820ImplementerInterface。

[Crytic检测报告]

EthKids

Crytic在EthKids中发现另一个异常问题:this.balance包含当前交易金额(msg.value),可能导致数值计算错误。

问题8:Msg.value在fundWithAward中被重复使用

描述
fundWithAward中使用this.balance未考虑已由msg.value添加的金额,导致价格计算错误。

fundWithAward通过使用msg.value调用calculateReward来计算代币数量:

1
2
function fundWithAward(address payable _donor) public payable onlyWhitelisted {
   uint256 _tokenAmount = calculateReward(msg.value, _donor);

calculateReward调用calculatePurchaseReturn,其中_reserveBalance为this.balance,_depositAmount为msg.value:

1
return bondingCurveFormula.calculatePurchaseReturn(_tokenSupply, _tokenBalance, address(this).balance, _ethAmount);

在calculatePurchaseReturn中,baseN通过将_depositAmount(msg.value)加到_reserveAmount(this.balance)计算:

1
uint256 baseN = _depositAmount.add(_reserveBalance);

msg.value已包含在this.balance中。例如,如果交易前this.balance为10 ether,msg.value为1 eth,交易期间this.balance将为11 ether。因此,msg.value被重复使用,calculatePurchaseReturn计算错误。

建议
修改价格计算,使_reserveBalance不包含交易中发送的金额。

[Crytic检测报告]

HQ20

最后,Crytic在HQ20中发现一个知名问题:重入攻击。此重入攻击有趣之处在于,当合约与具有回调功能的代币(如ERC777)使用时会发生。这与最近的uniswap和lendf.me黑客攻击类似。

问题9:重入攻击可能导致代币盗窃

描述
Classifieds在外部代币上调用transferFrom时未遵循检查-效果-交互模式。如果目标代币具有回调机制(例如ERC777代币),攻击者可利用此重入漏洞。

存在重入问题的方法:

  • Classifieds.cancelTrade(uint256)
  • Classifieds.executeTrade(uint256)

建议
遵循检查-效果-交互模式。

[Crytic检测报告]

立即开始使用Crytic!

Crytic可以保护您的代码库免受关键缺陷影响,并帮助您设计更安全的代码。何乐而不为?

立即注册Crytic。有问题?加入我们的Slack频道(#crytic)或在Twitter上关注@CryticCi。

如果您喜欢这篇文章,请分享: Twitter | LinkedIn | GitHub | Mastodon | Hacker News

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计