智能合约安全问题一直是区块链技术体系中探讨得比较多的话题之一。无论是以以太坊 EVM 虚拟机为代表的智能合约体系,还是以 EOS WASM 虚拟机为代表的智能合约体系,都或多或少地暴露过不同类型的智能合约漏洞。这些漏洞不仅使得项目方和用户损失惨重,而且也让用户对区块链的安全性产生了质疑。
——题记
01 引言
智能合约安全漏洞本身极少是由于区块链底层虚拟机引起的问题,其中大多数属于智能合约开发者本身编写的代码问题。
例如:当使用 Solidity 语言开发以太坊智能合约时,部分开发者对编写合约的语言并不是十分了解,因此增加了编写智能合约漏洞的风险。当然,也有少部分智能合约安全性漏洞和智能合约平台本身的一些特性相关。
另外,由于区块链技术中天然具有数据难以被篡改等特性,使得智能合约安全漏洞无法像传统应用那样通过程序升级或数据回滚等方式轻松解决。因此,智能合约开发者对区块链平台的智能合约体系的全面了解显得尤为重要。
本体智能合约目前使用 NeoVM 虚拟机,开发者可以使用他们所熟悉的语言,例如 C# 和 Python 等去编写智能合约,而无需再去学习一种新的语言,这极大地降低了智能合约开发的入门门槛。
但是,在大量的实践过程中,我们也了解并收集了一些本体智能合约开发过程中可能存在的安全问题。如果能有效避免这些安全隐患,项目方和用户就能够减少大量损失。
02 跨合约调用攻击
本期,我们讲解在本体上开发智能合约时可能遇到的一种漏洞攻击,即跨合约调用攻击。
当开发者在编写智能合约时,可能需要随机数,一般情况下可以使用 Ontology Oracle 来获取外部可信随机源的数据。在简化情况下,有的开发者通过取当前区块的 hash 来作为随机数。我们主要针对在编写智能合约时使用当前区块 hash 作为随机数的场景,分析该种方式下智能合约开发可能遇到的安全性问题和相关解决方案。
2.1 存在漏洞的合约
下面是一个使用当前区块 hash 作为随机数的例子代码,我们称为应用合约一:
代码语言:javascript复制from boa.interop.System.ExecutionEngine import GetCallingScriptHash, GetEntryScriptHash
from boa.interop.System.Runtime import Notify
from boa.interop.Ontology.Runtime import GetRandomHash
def Main(opration, args):
if opration == "cannotAvoidContractCallAttack":
guessNumber = args[0]
return cannotAvoidContractCallAttack(guessNumber)
return False
def cannotAvoidContractCallAttack(guessNumber):
randomNumber = getRandomNumber()
if guessNumber == randomNumber:
Notify(["You have won the big prize!"])
return True
def getRandomNumber():
randomHash = GetRandomHash()
randomNumber = abs(randomHash) % 100000000
return randomNumber
可以看到,该合约使用 getRandomNumber() 方法获取到了当前区块 hash 作为随机数源,并做了一些简单的处理,用户猜测的数值如果和合约生成的数值相等,用户可以获得一定的奖励。
2.2 漏洞攻击
初看起来上述应用合约好像是没有什么问题,因为当前区块的 hash 无法被出块节点以外的用户预测。但是,针对以上合约,攻击者却可以编写另一本智能合约来调用并攻击它。 下面的攻击合约二展示了如何攻击上述应用合约一:
代码语言:javascript复制from boa.interop.System.App import RegisterAppCall
from boa.interop.Ontology.Runtime import GetRandomHash
ContractToBeAttacked = RegisterAppCall('被攻击合约hash', 'operation', 'args')
def Main(opration, args):
if opration == "attack":
methodToBeAttack = args[0]
return attack(methodToBeAttack)
return False
def attack(methodToBeAttack):
randomNumber = getRandomNumber()
return ContractToBeAttacked(methodToBeAttack, [randomNumber])
def getRandomNumber():
randomHash = GetRandomHash()
randomNumber = abs(randomHash) % 100000000
return randomNumber
可以看到,攻击者编写了另外一本智能合约来调用应用合约,攻击者合约使用和应用合约相同的随机数生成算法,攻击者合约的执行和应用合约的执行在同一个区块中。
因此,攻击者合约可以知道当前应用合约生成的随机数,这样攻击者可以通过产生和当前应用合约一样的随机数而一直赢得游戏。
03 跨合约调用攻击的防范
那么,针对这种情况,智能合约开发者如何才能防范此种类型的攻击呢?
可以看到,只要阻止应用合约一被攻击合约二调用,就可以防范跨合约调用攻击。本体智能合约开发者可以使用 GetCallingScriptHash() 和 GetEntryScriptHash() 这两个方法来解决这个问题。
下面是在应用合约一基础上经过修改的应用合约三,该合约可以抵抗上述攻击。
代码语言:javascript复制from boa.interop.System.ExecutionEngine import GetCallingScriptHash, GetEntryScriptHash
from boa.interop.System.Runtime import Notify
from boa.interop.Ontology.Runtime import GetRandomHash
def Main(opration, args):
if opration == "avoidContractCallAttack":
guessNumber = args[0]
return avoidContractCallAttack(guessNumber)
return False
def avoidContractCallAttack(guessNumber):
randomNumber = getRandomNumber()
callerHash = GetCallingScriptHash()
entryHash = GetEntryScriptHash()
Notify(["randomNumber:", randomNumber, "guessNumber:", guessNumber])
if callerHash != entryHash:
Notify(["You are not allowed to invoke this method through contract!"])
return False
else:
Notify(["You can implement what you need to do here!"])
if guessNumber == randomNumber:
Notify(["You have won the big prize!"])
return True
def getRandomNumber():
randomHash = GetRandomHash()
randomNumber = abs(randomHash) % 100000000
return randomNumber
GetCallingScriptHash() 可以得到调用当前智能合约的合约 hash,而 GetEntryScriptHash() 可以得到调用合约入口的合约 hash。当调用当前合约的合约 hash 和调用入口的合约 hash 不是同一个 hash 时,我们可以判定当前智能合约的调用者为另外的智能合约,而不是用户使用钱包地址在调用当前这本合约。通过此种方式,我们就能很好地防范智能合约被跨合约调用攻击。
04 后记
本期列举了本体智能合约开发者在进行智能合约开发时可能遇到的一种攻击类型,这种类型的智能合约攻击比较隐晦。如果开发者对智能合约本身不是特别的了解,可能很难预知并防范这种漏洞攻击。在之后的技术视点中,我们将会展示更多的漏洞攻击类型,帮助本体智能合约开发者更好地避免合约开发中可能存在的漏洞。
同时,本体智能合约开发者可以使用本体智能合约集成开发环境 SmartX 中深度集成的高度自动化智能合约形式化验证平台 VaaS-ONT 来“一键式”精确定位到有风险的代码位置,迅速找出原因,有效验证智能合约或区块链应用的常规安全漏洞、安全属性和功能正确性,从而显著提高安全等级。