智能合约安全问题一直是区块链技术体系中探讨得比较多的话题之一。无论是以以太坊 EVM 虚拟机为代表的智能合约体系,还是以 EOS WASM 虚拟机为代表的智能合约体系,都或多或少地暴露过不同类型的智能合约漏洞。这些漏洞不仅使得项目方和用户损失惨重,而且也让用户对区块链的安全性产生了质疑。
在上面的三期中,我们介绍了在本体上开发智能合约时可能会遇到的三种漏洞攻击,即跨合约调用攻击、强制交易失败攻击与存储注入攻击,并提供了相应的解决方案。
为了更好地帮助合约开发者规避这些安全漏洞,我们将持续收集这些可能会产生安全威胁的合约案例。
同时,社区开发者如果在本体上进行智能合约开发时遇到其它的安全威胁,可以和我们一起探讨,一起打造更加安全的智能合约体系。
本期我们将介绍在智能合约开发中更常见的一种安全漏洞及其解决方案——边界攻击。
在介绍这种攻击之前,我们需要先注意这一点:本体智能合约支持正数与负数的数学运算。
最大值 MAX_ 是
0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff,即2^255。
最小值 MIN_ 为0x8000000000000000000000000000000000000000000000000000000000000002,即-(2^255-1)。
所以合约内对数值的操作要做好边界限制或处理,否则可能会产生一些意想不到的错误,导致合约函数包含漏洞,易被利用攻击。
边界攻击
开发者在开发智能合约时,需要将数字相关的数据作为参数调用合约。对于大多数的场景,开发者们可能只使用了正数,此时,在被调用函数内部,开发者需要注意对传入的参数进行边界的合规性检查,否则可能会被攻击。
以实现 OEP-4协议的合约为例,我们取协议中的 transfer() 方法为例。
def transfer(from_acct,to_acct,amount): """ Transfer amount of tokens from from_acct to to_acct :param from_acct: the account from which the amount of tokens will be transferred :param to_acct: the account to which the amount of tokens will be transferred :param amount: the amount of the tokens to be transferred :return: True means success, False or raising exception means failure. """ if len(to_acct) != 20 or len(from_acct) != 20: raise Exception("address length error") if CheckWitness(from_acct) == False: return False fromKey = concat(BALANCE_PREFIX,from_acct) fromBalance = Get(ctx,fromKey) if amount > fromBalance: return False if amount == fromBalance: Delete(ctx,fromKey) else: Put(ctx,fromKey,fromBalance - amount) toKey = concat(BALANCE_PREFIX,to_acct) toBalance = Get(ctx,toKey) Put(ctx,toKey,toBalance + amount) TransferEvent(from_acct, to_acct, amount) return True
在上面的 transfer() 方法中,我们实现了:
1. 对 from_acct 和 to_acct 进行合规性长度检查;
2. 对 from_acct 进行验签;
3. 对转账 amount 小于等于 from_acct 的余额进行检查;
4. 将 from_acct 的余额更新为 fromBalance (转出帐户的原始资产余额)- amount (被转出资产数量);
5. 将 to_acct 的余额更新为 toBalance (转入帐户的原始资产余额)+ amount (转入资产的数量)。
乍一看,上面的实现代码及其逻辑梳理没有问题。假设我们根据上面的代码布署一本 OEP-4的合约,且当前 from_acct 余额为0,to_acct 余额为0,可以发现,我们竟然可以成功调用:
transfer (from_acct, to_acct, -100)
然后我们可以调用 balanceOf() 方法对 from_acct 和 to_acct 的余额进行查询,返回值为64和9c,通过 SDK 对返回数据进行解析,即得到100和-100。
我们可以发现该 OEP-4合约中的总量并没有发生变化,但此时,from_acct 的确拥有了100个合法的 token。
03 边界攻击的防范
防止这类问题出现的方法是在被调用合约函数的内部对传入的参数(尤其是数字类型数据)进行边界的规范性检查。比如对于资产类型,我们应该严格禁止负数的传入。对于上述易被攻击的 transfer() 函数,我们可以这样实现。
def transfer(from_acct,to_acct,amount): """ Transfer amount of tokens from from_acct to to_acct :param from_acct: the account from which the amount of tokens will be transferred :param to_acct: the account to which the amount of tokens will be transferred :param amount: the amount of the tokens to be transferred, >= 0 :return: True means success, False or raising exception means failure. """ if len(to_acct) != 20 or len(from_acct) != 20: raise Exception("address length error") if CheckWitness(from_acct) == False or amount < 0: return False fromKey = concat(BALANCE_PREFIX,from_acct) fromBalance = Get(ctx,fromKey) if amount > fromBalance: return False if amount == fromBalance: Delete(ctx,fromKey) else: Put(ctx,fromKey,fromBalance - amount) toKey = concat(BALANCE_PREFIX,to_acct) toBalance = Get(ctx,toKey) Put(ctx,toKey,toBalance + amount) TransferEvent(from_acct, to_acct, amount) return True
在上面的 transfer() 方法中,我们增加了对 amount 的检查,即确保该值为非负数。如此一来,即避免了边界攻击。
对于其他的数字型数据,需要根据场景进行边界规范检查。
另外,需要格外注意的是:
1. 本体区块链合约体系不存在向上溢出或向下溢出问题。当合约内对数字型数据进行数学运算时,一旦向上超过MAX_ 或向下超过MIN_,合约(或虚拟机)会直接抛出错误;
2. 对于合约函数,无论传入的参数是什么类型,都进行有意义的规范性(如边界、长度等)检查总是一种好的习惯。
比如 account 是地址类型,确保其长度为20,即:
assert(len(account)==20)
比如 amount 数字类型,确保其范围合法;
比如 str 字符串类型,确保其长度合法。
总结
以上是我们讲解的第四种在本体上开发智能合约的漏洞,并附带了解决方案。至此,我们的智能合约安全与漏洞分析就告一段落,在之后的开发中如果我们发现另外的智能合约漏洞,我们会再以案例讲解的形式公布给大家,希望大家可以和我们共同打造本体上更加安全的智能合约。