撰文:AustinZhang,JonLi,AsymmetriesTechnologies
智能合约的安全性问题一直是业界的一个重点话题,由于程序员的某些疏忽造成了思维和逻辑上的漏洞,从而导致黑客有了可乘之机。我们搜集了目前在DeFi领域已经发生了安全事故的智能合约,并根据我们编写的示例代码来实证分析其中的原因,希望能给到同事和同行们一些启示。
重入攻击
主要攻击方式之一:合约调用恶意外部合约结束之前,恶意外部合约函数反向调用原合约函数利用相关漏洞。
示例代码
案例1:2021年12月22日UniswapV3流动性管理协议Visor被盗120ETH
事故原因:deposit函数没有防重入锁也没有验证from地址是否是合法的Visor合约地址。攻击者传入攻击合约地址,重复调用deposit函数绕过取款金额检查多次取款。
案例2:2021年6月5日BurgerSwap被盗700万美金
事故原因:类似Uniswap的原创dex,分为Platform和Pool两个合约。Platform类似Uniswap的Router,Pair类似Uniswap的Pool,开发者错误的将K值校验放在Platform计算,攻击者在Platform中进行重入攻击,多次以旧的K值换取代币,造成流动性提供者损失。
解决方案:调用外部合约前确保所有中间状态变量已更新并使用再入锁。
未检查函数返回值
调用外部合约函数时,有些函数调用失败不会抛出错误回滚交易而是返回false,如果忘记检查函数返回值会导致误以为调用成功。
示例代码
案例:2021年4月4日ForceDao到被攻击损失183ETH
中币(ZB)第二十一期投票上币将于4月20日14:00正式开启:据官方公告,第二十一期投票上币将于2021年4月20日14:00 - 2021年4月22日14:00开启为期2天的社区投票上币,参与本期投票上币的候选项目分别为BED(Bit Ecological)、HEDG(HedgeTrade)、MLK(MiL.k)。并沿用前期规则,中币用户使用ZB平台积分进行投票。在投票期间获得超过200万ZB票数的项目可获得上币资格,本期投票上币没有设置项目代币购买环节,投票也没有相关糖果赠送,满足条件的项目按照投票达标的先后顺序进行上币对接,请广大投资者仔细考虑之后再进行投票。投票正式开启后,可登录中币官网进入投票页面。[2021/4/20 20:39:15]
事故原因:Force代币的transferFrom余额不足时返回false而不是直接回滚交易,合约中未做判断导致转账失败时也被认为成功,可以换取到对应代币。
解决方案:使用call函数调用外部合约时必须检查调用是否成功。注:call调用外部合约未匹配到函数时,会调用外部合约fallback或者receive函数,如果外部合约有定义receive函数且call函数未携带calldata则会调用外部合约receive函数,其他情况调用fallback函数。
未正确设置函数可见性
Solidity中函数默认为public,可以被外部调用,一旦未将关键函数设置为Private,就会导致安全风险。
示例代码
案例1:2022年1月22日DexCrosswise被攻击损失80万美金
事故原因:Crosswise虽然实现了权限验证函数onlyOwner,但忘记设置setTrustedForwarder为private,导致被攻击者利用,将自己设置为池子的Owner将代币全部转走。
EOS、BSV市值跌出前二十:据coingecko行情显示,EOS暂报4.37美元,24小时下跌12%,市值跌出前二十,目前排名21。BSV暂报197.43美元,24小时下跌18.3%,市值跌出前二十,目前排名23。[2021/2/23 17:42:26]
案例2:2020年6月18日跨链桥BancorNetwork被攻击损失14万美金
事故原因:合约用于转账的函数默认为public,攻击者可以直接调用转走合约中的代币。
解决方案:提款函数事关合约资产的转移,需谨慎设置权限控制,确保初始化函数只能运行一次。
未验证Map中Key不存在的情况
Solidity中的Mapping在获取对应Key的Value时,如果Key不存在,会返回对应类型的默认值,而不是报错。例如Mapping(int→int),如果对应int的Key不存在,会返回默认值0。
示例代码
案例:2021年7月11日跨链桥ChainSwap被攻击损失400万美金
事故原因:ChainSwap依赖其网络中的validator进行转账。为了限制validator一次转走超过其质押的代币,设置了配额。结果合约中存在漏洞可以绕过配额限制,当地址变量signatory不存在时,authQuotes和lasttimeUpdateQuoteOf会返回0,导致配额计算错误返回预期外的大量配额。
解决方案:使用map时必须检查key是否存在。
在状态变更前进行转账
转账时有可能被重入,利用未变更的状态进行攻击。
案例:2021年8月17日XSURGE被攻击损失500万美金
事故原因:在转账后才修改totalSupply,转账时被重入另外一个未加重入锁的函数损失500万美金。
LBank 将于12月24日启动第二十八期 LBK周四GO专场售卖:据悉,LBank将于2020年12月24日16:00启动第二十八期LBK周四GO八折GRT专场售卖。届时,将有50000USDT等值GRT在“登月计划”专区中8折出售。本次认购采用加权均分的模式,认购币种为USDT,用户通过KYC实名认证,并持有相应的LBK即可参与。
The Graph是一个用于去中心化应用的索引协议,该协议允许开发者高效地访问区块链数据。开发人员可以通过构建子图来定义如何用一种可验证的方式来获取,索引和提供区块链数据。
据了解,“LBank周四GO”是LBank为回馈用户开启的主流币打折售卖系列活动。活动会于每周四下午16:00进行,精选优质币种进行8折售卖。更多详情可登录LBank官网查看。[2020/12/21 15:56:44]
解决方案:使用了再入锁也要在所有状态变更之后在转账。
初始化函数未做调用和权限限制
很多合约需要初始化子合约,例如Uniswap需要通过Factory合约初始化Pool合约,这时候如果忘记对子合约的初始化函数做权限和重复初始化限制,可能被攻击者进行恶意初始化。
案例:2021年8月11日PunkProtocol被攻击损失400万美金
事故原因:池子的initialize函数未做权限和重复调用限制,攻击者调用该函数将自己设置为Forge管理员权限,并调用withdrawToForge将池子所有资金都发送到攻击者地址。
解决方案:初始化函数必须设置成只能初始化一次。
未正确检查对应合约函数实现
通常智能合约被调用的函数不存在时会报错,但如果合约实现了fallback函数,则会自动调用fallback函数。有时fallback函数并不会报错,导致调用方误以为调用成功。
经济学家弗里德曼近二十年前已预言会出现匿名电子货币:据推特用户发布的相关视频,美国著名经济学家弗里德曼(已故)在1999年接受采访时表示,将来在互联网中会出现一种可靠地E-Cash(电子货币),人们用它来传输资金,比如将资金从A传到B,此时A不知道B,B也不知道A。这种货币也有其坏处,不法分子以及其它从事非法活动的人更容易从以他们的活动。[2018/6/4]
案例:2022年1月18日跨链桥Multichain被攻击损失450ETH
事故原因:通常ERC20的合约会实现permit函数,用于签名检查与授权操作。但WETH、PERI、OMT、WBNB、MATIC、AVAX六种代币的合约没有实现permit却实现了fallback,Multichain在检查这些代币的权限时误以为用户已经授权转账给攻击者,导致代币被盗。
解决方案:不同代币的实现方式不同,引入新代币之前应仔细检查其具体实现。
未正确处理带转账费的代币
有些代币在转账时会销毁一部分转账费用,导致实际收到的代币余额偏少,如果开发者没考虑到这一点,以转账值计算,会导致出现偏差。
案例:2021年8月19日Pinecone被盗20万美金
事故原因:Pinecone使用其代币PCT作为资金池的质押代币,PCT转账会有手续费的损耗。合约并没有考虑相关损耗导致用户份额和质押的PCT总额出现偏差,被攻击者利用领取多余的奖励。
解决方案:谨记不是所有的代币转账费都为nativetoken。
签名验证漏洞
签名被重复使用,或者利用椭圆曲线签名算法的对称性,根据已有签名构造合法签名。
案例:2021年7月12日AnySwap被盗800万美金
全球市值前二十加密货币 BCN独涨:据coinmarketcap平台数据,截至目前,市值排名第19的BCN(Bytecoin)全球均价为0.042元,涨幅5.43%。与此同时,全球市值前二十的其他加密货币均有不同程度下跌。BCN是第一个基于cryptonote技术,致力于匿名反机池的超前的一种货币,于2012年发布。[2018/5/31]
事故原因:对交易签名除了私钥外需要一个随机数R,但是Anyswap部署新合约失误,导致在BSC上的V3路由器MPC帐户下有两个交易具有相同的R值签名,攻击者反推到这个MPC账户的私钥转走了被盗资金。
解决方案:使用EIP-712标准验证签名,参考OpenZeppelin的实现:https://docs.openzeppelin.com/contracts/3.x/api/drafts。
未考虑合约余额可能产生的变化
矿工挖出块时或者智能合约调用selfdestruct函数销毁自己时可以向任意地址强行打币改变其原生代币的余额。当使用余额函数返回值作为判断条件时,余额有可能被强行改变导致风险,极端情况下甚至导致合约拒绝服务。
示例代码
即使捐赠合约不能接受代币转账,合约余额也可能在部署后被改变,严格检查已空投总量与合约余额之和等于总供应量可能导致捐赠合约拒绝服务。
解决方案:在合约中避免对合约余额做严格相等的检查。
使用delegatecall调用外部合约
delegatecall可以将对应合约的函数代码内嵌到当前上下文中执行,就像调用内置函数一般。如果不小心调用了恶意合约极易导致攻击。
示例代码
当攻击者调用forward函数并传入Attack合约地址以及函数setOwner()作为参数时,Proxy合约owner将被修改为攻击者地址。
解决方案:不推荐使用delegatecall调用外部合约。
授权tx.origin
tx.origin是交易的发起者地址,合约如果使用tx.origin做权限检查,当合约的授权用户与恶意合约交互时,恶意合约调用合约即可通过合约权限检查。
示例代码
当MyWallet合约owner使用transferTo函数向Attack合约转账时,Attack合约会重入MyWallet合约,并调用transferTo函数,此时tx.origin仍然为MyWalletowner,require条件满足,MyWallet余额将被全部转移至Attack合约。
解决方案:不使用tx.origin做权限检查。
交易排序竞争
全节点运行者可以在交易被确认之前获取交易信息,进而根据获取的交易信息,构造高手续费交易,让矿工优先打包自己的交易以执行对自己有利的策略。例如,谜语合约奖励最快找出谜底的用户,恶意用户可以在获悉诚实用户提交的谜底后,构造高手续费交易优先诚实用户提交谜底,从而获取奖励;又如当用户更新授权额度时,被授权用户可以在更新授权额度交易被确认之前转移旧的授权额度,如此,被授权人实际获得的授权额度为两次授权额度之和。
解决方案:针对谜语合约,获得谜底的用户先提交「随机数+自身地址+谜底」的哈希值,谜语合约存储该哈希值后,用户再提交随机信息与答案,合约检查哈希值匹配后再发放奖励;更新授权额度时先置零授权额度。
使用block.timestamp或者block.number作为合约时间参考
block.timestamp与block.number都不能获得精确都时间,用作智能合约的时间参考会引入潜在的风险。
解决方案:使用oracle获取时间信息。
Denial-of-Service(DoS)拒绝服务
调用外部合约可能永久失败导致本合约不能接受新的指令,例如当合约主动对另外一个合约转账,而被转账合约没有接受转账的函数时,转账失败,此时合约可能进入拒绝服务状态。
示例代码
当合约向其中一个账号转账失败会导致所有转账全部失败。
解决方案:合约调用外部合约时可能出现的失败,合约需包含处理调用失败情况的代码,防止合约进入拒绝服务状态。
使用链属性作为随机源
链属性如block.timestamp,blockhash,bock.difficulty以及其他属性可被矿工操控,存在风险。
解决方案:考虑使用RANDAO,oracle或比特币区块hash作为随机源。
继承顺序错误
多个被继承合约都定义了同一个函数时,继承合约调用该函数的优先级由继承顺序决定,错误的继承顺序将导致函数调用错误。
解决方案:继承顺序说明请参考官方实例:https://solidity-by-example.org/inheritance/。
Gas不足攻击
多签情况下或者需要其他人帮自己代付Gas时,用户准备好签名交易并交给代执行人,代执行人再将用户交易提交给执行合约,代执行人可以提前审查用户代交易,恶意的代执行人或当交易内容不利于代执行人时,可以通过限制Gas的供给,使交易的执行失败,从而阻止交易的执行。
示例代码
当Relayer调用者通过限制Gas使用导致某个交易失败,那么失败的交易将永远不能再被提交。
解决方案:选择信任的代执行人,或者在执行合约中检查代理人提供的Gas费是否足够。
函数类型变量跳转
solidity支持函数类型变量,当函数类型变量使用汇编指令赋值时,函数类型变量有可能被指向恶意构造当函数。
解决方案:如无必要,尽量避免在智能合约中使用汇编指令。
GasLimit服务拒绝攻击
区块设置有Gas使用上限,如果合约当执行超过了区块Gas使用上限,则合约永远不能被执行成功。
示例代码
当操作的循环次数过大时,执行合约所需Gas将超过区块上限,导致合约执行失败。
解决方案:在智能合约中谨慎操作大数组,或循环。
abi.encodePacked()哈希碰撞
abi.encodePacked()采用非填充序列化,当序列化参数包含多个变长数组时,攻击者可以在保持所有元素顺序不变的前提下,改变两个变长数组的元素,如此序列化的结果相同。
示例代码
通过构造addUser的输入,攻击者可以将regularUsers的成员加入admins成员,但是构造的输入和原输入的签名相同。
解决方案:使用定长数组,或者不让调用者传入abi.encodePacked()的参数,或者使用abi.encode()。
transfer()和send()函数Gas不足
transfer()和send()函数使用2300gas以防止重入攻击,公链升级后可能导致gas不足。
解决方案:推荐使用call()函数,但需做好重入攻击防护。
链上未加密隐私数据
链上数据完全透明,合约的private关键字不能阻止合约的隐私数据泄漏。
示例代码
虽然players为private,但攻击者仍然可以通过解析链上数据读取players。
解决方案:隐私数据需要加密放在链上。
以上是我们分析和总结的二十三种安全事故类型汇总,希望能够给到您些许参考和启示。
郑重声明: 本文版权归原作者所有, 转载文章仅为传播更多信息之目的, 如作者信息标记有误, 请第一时间联系我们修改或删除, 多谢。