今天,我们继续介绍文献中提到的以太坊合约的安全性弱点与案例。
数据保密
Solidity 智能合约中的变量分 public 和 private 两种。Private 变量表示这个数值不能被其他合约直接读取。
但是,将一个变量标记为 private 不意味着里面的信息就是保密的。因为以太坊是公开的,每次合约函数执行的字节码,参数都是公开的,任何人都可以推断出每次函数执行后是否修改了变量,修改后的数值是多少。Private 只是能够保证其他合约执行的时候,无法读取其中的变量罢了。
然而,一些本应该隐藏玩家数据的游戏,却错误地使用了这一点。以下是一个示例:
两个玩家在玩一个赌博游戏,每个玩家选一个正整数,两个正整数加起来的奇偶性,决定了获胜者。
存储玩家选择的变量是 players, 这个变量具有 private 属性。玩家通过调用智能合约函数选数的时候,决策并不是保密的。
为了实现保密性,更好的方式是使用一个称为“委托”的密码学原语。当玩家需要秘密地做出一个决定时,可以将 sha3(决策内容,随机数) 得到的哈希值存到合约里。当玩家需要公开自己选择的时候,将 决策内容 与 随机数 公开,并由智能合约验证哈希值与先前存储的是否一致。可以实现类似于“先在纸上秘密写下选择,到后面一个环节再亮出来”的功能。
随机数生成
EVM 的字节码执行是确定性的。因此,如果智能合约想生成一个随机数,就需要通过一个随机数种子生成一个伪随机数。而随机数种子的选取方式,对生成随机数的公平性有很大的影响。
一个常用的做法是,使用一个给定时间或给定高度区块的哈希值或时间戳。这件事情在给定区块被确认以后,在每个矿工看来都是一样的。
看起来这是一个公平的做法,因为没有人能预测未来的区块。但是,一个恶意的矿工可能尝试操纵自己生成的区块来影响随机数的生成与合约的执行结果。一个分析表示,一个控制少数算力的矿工,只需要投入 50 个比特币就可以显著地改变输出结果的概率分布。
另一个方式是使用“延时委托协议”。在这个协议中,每个参与者选择一个秘密的随机数,并将哈希值广播给其他人。在晚些时候,所有参与者公布它们选取的秘密随机数,或者丢失押金。最终随机数由所有公布的秘密随机数和一个公开的算法生成。攻击者可以在“其他所有人都已经公布了随机数,自己还没有公布”时,预先计算最终生成的随机数,如果生成的结果它不满意,就通过拒绝公布自己选择的随机数方式,来使这个结果无效。当然,攻击者要损失一些押金。所以,押金的设定要足够高,高于随机数生成中改变结果可能带来的收益。
不可预测状态
一个合约的状态包括合约变量和合约余额。一般情况下,当用户通过一笔交易调用合约函数的时候,从交易广播到交易被加入区块之间,可能有其他的交易改变了合约的状态。也就是说,当用户发起一笔交易时,并不能确定这笔交易被执行时,合约的状态是什么。
一个基于 library 和不可预测状态的攻击
下面我们来看一个例子,以下的 Solidity 代码定义了一个名为 Set 的 library。
下面是一个名为 SetProvider 的合约,提供了一个 Set library 的地址,合约拥有者可以修改这个地址,任何人/合约可以获取这个地址。
假设 Bob 合约是一个使用 SetProvider 的诚实用户,他的代码如下:
Bob 记录了一个 SetProvider 合约的地址,在 getSetVersion() 中,使用这个地址获取了一个 Set library 的版本号。
现在,假设 SetProvider 合约的控制者是个坏人。他制造了一个恶意的 Set library, 希望偷得一些钱存到他自己的钱包地址 0x42 中。
如果合约 SetProvider 中 setLibAddr 的地址被修改为 MaliciousSet 的地址, Bob 合约中调用 getSetVersion() 函数时,会调用 MaliciousSet 的 version() 函数,而不是 Set 的。因为 Bob 合约中将 Set 声明为一个 library, 所以对 version() 的调用采用的是 delegatecall 模式。 delegatecall 模式意味着转账操作 attackerAddr.send(this.balance); 是从 Bob 合约中转账出来。 Bob 合约中的钱将被偷走。
上述例子说明了,对 library 函数的调用使用的是较为危险的 delegatecall 模式。因此合约编写者在使用其他合约地址作为 library 时,一定要保证通过 library 加载进来的代码是自己可控的。比如,手动指定一个已经在区块链上不可修改的 library 的地址。而不是在本例中依靠于一个不可靠的 SetProvider 合约来获取 library 的地址。
另外,“不可预测状态”问题也加剧了这件事情。即使在调用 Bob 合约的 getSetVersion() 函数时, SetProvider 指向的是诚实的 Set library. 攻击者也可以在这一交易还没有被加入区块的时候,通过发起一笔交易费数额较大的交易,抢在 getSetVersion() 被加入区块之前,将 SetProvider 的指向修改为 MaliciousSet。
Conflux 是致力于打造下一代高性能的 DAPP 公链平台
欢迎关注我们的微信公众号:Conflux中文社区(Conflux-Chain)
添加微信群管理员 Confluxgroup 回复“加群”加入 Conflux官方交流群