引子:横看成岭侧成峰,远近高低各不同。不识庐山真面目,只缘身在此山中。
—— 《题西林壁》苏轼
上回说到 :
底层函数调用险象环生,
外部功能慎用防患未然,
真假难辨黑客诡计多端,
完善规则杜绝内忧外患。
没看过的请戳:合约安全漏洞连载之四
本回咱们聊聊:
地址恢复功亏一篑,
身份判断轻虑浅谋。
逻辑补全自圆其说,
原理辨析帷幄运筹。
智能合约作为以太坊上各种加密数字货币的基础,承载着巨大的经济利益。放眼以太坊的繁忙景象,日交易频率在50-75万次之间,而五月和二月更有两日交易次数高达100万次。合约的调用在整个以太坊生态系统犹如战场上穿梭的子弹,不计其数。可见其地位举足轻重,功能五花八门,但万变不离其宗的是,每个合约都有一个共性,会继承一个地址对象,也就是说合约的基础是地址。回顾前四期漏洞分析,我们了解攻击者使出了浑身解数从合约的各个部分试图不劳而获,而从合约的地址漏洞入手,釜底抽薪自然也会成为攻击手段之一。
下面我们就来聊聊,与地址有关的tx.origin变量和ecrecover()函数相关漏洞。
一言以蔽之
Solidity作为以太坊的官方语言,将地址定为20个字节,160位。所有合约都有一个地址对象,也可以对其他地址的代码进行调用。地址类型的成员有我们上一期讲到的call()和delegatecall(),这两个函数相关的漏洞请见右方链接:偷天换日合约易主,地址变脸移花接木——底层函数误用漏洞 | 漏洞分析连载之四 。不同的合约地址代表了不同的合约,也代表了他们所拥有的权限。所以合约地址就相当于合约的“身份证”。
本期相关两个名词专业的解释是, tx.origin是Solidity的一个全局变量,它遍历整个调用栈并返回最初发送调用(或事务)的帐户的地址。ecrecover()是内嵌的函数,可以用来恢复签名公钥,传值正确的情况下,可以利用这个函数来验证地址。
通俗来说,tx.origin是整个交易过程最初的那个合约拥有者。当这个合约A直接调用合约B的时候,拥有者也是中间人没错,都是合约A。
但是当中间多出了一个合约C之后,合约A调用合约C,合约C调用合约B,这时中间人是C,但是tx.origin是合约A。
利用tx.origin的漏洞可以比喻为:
小明未满十八岁,但对手中的游戏爱不释手,但是游戏方为了限制未成年人的游戏时间,只有身份证号验证已满十八周岁的玩家才能无限制畅玩。于是小明使用长辈的身份证号进行防沉迷验证。这样,本来是游戏方对小明身份的验证,变成了对其长辈身份的验证,验证结果予以通过,给予小明他不应有的权限和利益。
至于ecrecover函数的问题,上面说到传值正确的情况下,使用是没有问题的,但普遍存在的问题是,传值如果异常,没有进行相应的判定和排除。
我们来具体分析案例进行代码层面的剖析。
深度分析
1. tx.origin使用错误
漏洞分析
tx.origin是Solidity的一个全局变量,它遍历整个调用栈并返回最初发送调用(或事务)的帐户的地址。在智能合约中使用此变量进行身份验证会使合约容易受到类似网络钓鱼的攻击。有关进一步阅读,请参阅StackExchangeQuestion, PeterVenesses博客和Solidity-tx.origin攻击。
对如下案例合约进行分析:
该合约有三个函数:constructor构造函数,指定合约owner;fallback函数,通过添加payable关键字以便接收用户转账;withdrawAll函数,对tx.origin进行判断,如果tx.origin是owner,则将合约地址所拥有的ether发送到_recipient中。
现在,一个攻击者创建了下列合约:
攻击者诱使原合约(Phishable.sol)的owner发送ether到攻击合约(POC.sol)地址,然后调用攻击合约的fallback函数,执行attack()函数,此时phOwner == msg.sender,将会调用原合约的withdrawAll()函数,程序执行进入原合约,此时msg.sender是攻击合约的地址,tx.origin是最初发起交易的地址,即原合约的owner,require(tx.origin == owner);条件满足,_recipient.transfer(this.balance);可以执行,即将原合约地址里的ether转给攻击者。
漏洞修复
tx.origin不应该用于智能合约的授权。这并不是说永远不应该使用tx.origin变量。它在智能合约中确实有一些合法的用例。例如,如果想要拒绝外部合约调用当前合约,他们可以通过require(tx.origin == msg.sender)实现。这可以防止使用中间合约来调用当前合约[2]。
参考链接:https://blog.sigmaprime.io/solidity-security.html#tx-origin
2.ecrecover 未作0地址判断
漏洞分析
keccak256() 和 ecrecover()都是内嵌的函数,keccak256() 可以用于计算公钥的签名,ecrecover()可以用来恢复签名公钥。传值正确的情况下,可以利用这两个函数来验证地址。
我们来看一个案例合约:
这个合约看似正常,但思索再三,如果ecrecover传入错误参数(例如_v = 29,),函数返回0地址。如果合约函数传入的校验地址也为零地址,那么将通过断言,导致合约逻辑错误。
函数transferProxy中,如果传入的参数_from为0,那么ecrecover函数因为输入参数错误而返回0值之后,if判断将通过,从而导致合约漏洞[2]。
函数decode()传入经过签名后的数据,用于验证返回地址是否是之前用于签名的私钥对应的公钥地址[3]。以太坊提供了web3.eth.sign方法来对数据生成数字签名。上面的签名数据可以通过下面的js代码获得:
js代码运行结果如下:
漏洞修复
对0x0地址做过滤,例如:
参考资料
transferProxy-keccak256
approveProxy-keccak256
高枕无忧居安思危
细心的朋友会发现,我们这一期并没有相应的事件回顾,那是因为早在2016年6月25日就有用户在github上向Solidity官方反应存在tx.origin的概念混淆,并建议移除tx.origin这个变量。
事关地址权限验证,而且theDAO事件风波未平(发生于6月17日),大量的讨论者参与了进来。究竟应不应该停止使用这个变量呢?我们在漏洞修复的环节中已经提到,如果想要拒绝外部合约调用当前合约,可以通过require(tx.origin ==msg.sender)实现。这可以防止使用中间合约来调用当前合约,这可以防止外部函数调用时产生的风险,我们在上一期已经讨论过相关的漏洞,详情请见右方链接:偷天换日合约易主,地址变脸移花接木——底层函数误用漏洞 | 漏洞分析连载之四 。
于是,有用户新提出了一个使用tx.origin产生警告的建议。
官方警惕性非常高,及时处理了这个问题,于是这个漏洞暂时还没有被利用于恶性攻击的历史事件。
那是不是可以说,这个漏洞并不重要呢?
目前合约的发展趋势正在发生变化,重心渐渐向以太坊游戏倾斜。以太坊游戏以娱乐和休闲为外衣,对数字货币的交易进行了整合和包装。但是游戏智能合约的漏洞依然层出不穷,不但有新型的非合约漏洞,而且还存在已经报道出的一些漏洞。更有甚者,故意暴露出看似明显的合约或机制缺陷,引诱投机倒把的玩家或者黑客,但暗中附加套路,玩起了“螳螂捕蝉黄雀在后”。这种新型的诈骗合约我们称为“蜜罐”。一些已知的缺陷通过黑客的修饰摇身一变,成为新型的骗局合约,例如近期有相关安全公司报道的名为QUESTION (中文含义:问题)的游戏合约,利用的是etherscan.io的缺陷,明修栈道,暗度陈仓,卷走玩家的钱财。
面币思过
无论是地址验证的欺骗,还是蜜罐合约的引诱,不怀好意的人总是瞄准了深陷币圈投资者的口袋。成都链安团队提醒玩家和投资者们提高安全意识,以及对智能合约的了解程度,切不可盲目跟风。我们从这个早期漏洞可以受到的启发是,要辨析一些容易混淆的原理之后,才能认清某些不易察觉的陷阱和漏洞,提高警惕,理智投资,稳健操作,会当凌绝顶,一览众山小。
DappBrowser,是一款安全、易用、优质的Dapp导航产品,志在为用户提供深度优化的Dapp交易和管理体验。目前已经支持云斗龙等优质的Dapp,可以直接在手机端运行,不需要下载或在PC端安装插件。新版本增加交易所搬砖神器,合约安全检测功能 。下载链接
也可以扫码下载:
本文引用:
[1] Solidity Security https://blog.sigmaprime.io/solidity-security.html#tx-origin
[2] ERC20 Token 合约安全风险问题汇总transferProxy-keccak256
[3] ERC20 Token 合约安全风险问题汇总approveProxy-keccak256