前言
Move语言是一种可编译运行在实现了MoveVM的区块链环境中的智能合约语言。其诞生之初,考虑到了诸多区块链和智能合约的安全性问题,并参考了一些RUST语言的安全设计。作为新一代以安全为主要特点的智能合约语言,其安全性究竟如何?是否在语言层面或相关机制方面可以避开常见于EVM、WASM等合约虚拟机的安全威胁?其本身是否存在特有的安全性问题?
NumenCyberLabs在对基于MoveVM实现智能合约的两条公链——APTOS和SUI的研究过程中,发现了一些虚拟机层面较为底层的漏洞(https://mp.weixin.qq.com/s/IY8nj73J1oyOJ-oomDcOPg和https://mp.weixin.qq.com/s/gNGtQVVPTE2tYlshRfDhmg)并得到了官方的确认和修复。
而本篇文章将从语言特性、运行机制和验证工具三个层面尝试解答有关Move语言的安全性问题。
1、Move语言的安全特性
编写正确的代码是困难的,即使经过多次测试,我们也无法保证自己编写的代码不出漏洞。在与不受信任的代码交互时,编写能够保持关键安全属性的代码则更加困难。有许多技术可以强制执行一些运行时的安全性:如沙箱、进程隔离、对象锁等编程模式;或者,可以在编译时指定静态的安全性,如强制静态类型或断言检查。
有时,我们也可以借助语义分析与静态分析工具,以确保其代码和安全规则相一致,即确保即使在代码链接到不受信任的代码并与之交互时,在一些可证明的逻辑规约上也会保持不变。
看上去这些都是很好的方案,能够避免运行时的开销,并且提前发现安全问题。然而不幸的是,编程语言能够通过使用这些方法获得安全性极其有限,我们将其归因于两个原因。首先,他们通常具有无法使用静态分析工具的语言的特性,例如动态分派、共享可变性和反射等非线性逻辑,违反了安全不变量规则,从而给黑客们提供了广泛的攻击面。所以,大多数编程语言不能轻易地使用与安全相关的静态工具或表达性强的规约化语言进行扩展。尽管这两种扩展都是至关重要的,可被预先定义的。
与许多现有的编程语言不同,Move语言被设计为既支持编写与不受信任的代码安全交互的程序,又支持静态验证。Move语言具备这样的安全特性,是因为舍弃了所有的基于灵活性考虑的非线性逻辑,不支持动态分派,也不支持递归的外部调用,而是使用了泛型、全局存储、资源等概念来实现一些具有替代性的编程模式。例如,Move省略的动态调度和递归调用特性,这些特性在其他智能合约语言中导致了代价高昂的重入漏洞。
Sui:MovEx违反锁定协议,基金会已终止与MovEx团队的合作关系:金色财经报道,Sui在社交媒体上发文称,加密代币是任何区块链经济的基石,对网络的安全、运行和发展至关重要。在一个新的区块链诞生之初,最初的一部分本地代币会被投入流通,用于转移、抵押和持有。然后按照预设的时间表释放额外的代币。可在普通市场上使用的代币被视为流通供应。
在6月27日的一系列推文之后,Sui基金会发现,MovEx在DeepBook的工作中收到了受合同锁定的 SUI代币作为报酬,但MovEx却违反合同锁定,向钱包发起了交易。MovEx没有将这些交易通知Sui基金会,Sui基金会也没有以任何方式同意这些交易。在Sui基金会的要求下,MovEx将代币退还给了主要接收地址,然后将代币转给了合格的托管商,以执行合同锁定限制。
由于MovEx的违约行为,Sui基金会已经终止了与MovEx团队的合作关系。未来将不会向MovEx分发任何一批SUI代币,MovEx也不会继续作为DeepBook智能合约的主要贡献者之一。目前,所有其他受合约锁定的代币都存放在同意执行此类锁定条款的合格托管账户中。[2023/7/27 16:02:16]
为了更加了解Move语言的特性,我们看以下的示例程序:
Move中一个币资产的实现
a)模块:每个Move模块由一系列结构类型和过程定义组成。模块可以导入类型定义(例如,在第2行use0x1::signer)并调用在其他模块中声明的过程。模块的完全限定名以存储模块代码的16字节帐户地址开始(在这里,我们编写一个帐户地址,如0x1,作为16字节十六进制地址的简写,用前导0填充)。帐户地址充当一个命名空间,用于区分具有相同名称的模块;例如,0x1::TestCoin和0x2::TestCoin是不同的模块,具有自己的类型和过程。
b)结构体:模块中定义了两个结构体Coin和Info。一个Coin代表分配给模块用户的代币,而Info记录了该代币总共存在多少。由声明上的haskey语法可知,这两个结构体都被定义为了资源类型,表示这两个结构体都可以存储在持久全局键/值存储中。
MoveFlow将接入多重签名钱包解决方案提供商MSafe的Dapp生态:5月4日消息,加密资产流支付协议MoveFlow与多重签名钱包解决方案提供商MSafe达成合作,MoveFlow流支付产品将接入MSafe的Dapp生态,为使用MSafe多签钱包的客户提供支付解决方案。
同时,MSafe的用户可以在多签的环境下使用MoveFlow流支付产品,合作将增加双方产品应用和链上资金流动性。据了解,MoveFlow 曾入围 Zonff Partners 旗下加速器 Aceleap。[2023/5/4 14:42:13]
c)过程:代码定义了一个初始化、一个安全过程和一个不安全过程。创建任何Coin之前必须调用initialize过程,它将单例Info值的total_supply初始化为零。在这里,signer是一个特殊的类型,它表示一个由Move之外的逻辑验证的用户。断言签名者的地址等于ADMIN确保这个过程只能由指定的管理员帐户调用。过程mint允许管理员创建所需数量的新代币(第25行);这是在硬币总数更新之后(第23行)。像初始化一样,这个过程有访问控制,以确保它只能由管理员帐户调用(第20和21行)。value_mut过程接受一个对Coin的可变引用作为输入,并返回一个指向Coin值字段的可变引用。
可以看到,合约结构上和其他的智能合约语言没有太大差别,但我们需要对资源类型以及全局存储(PersistentGlobalStore)的概念进行更详细的解释,这是Move语言中对存储和资源安全的关键机制。
全局存储允许Move程序存储持久数据(例如,Coin余额),这些数据只能由拥有它的模块以编程方式读/写,但也存储在公共账本中,可以由在其他模块中运行代码的用户查看。
全局存储中的每个键都由完全限定的类型名称(例如,0x1::TestCoin::Coin)和存储该类型值的帐户地址(帐户地址存储模块代码和结构数据)组成。虽然全局存储在所有模块之间共享,但每个模块都对其声明的key具有独占的读/写访问权。也就是说声明了资源类型(比如Coin)的模块可以:
?????通过move_to<Coin>指令将一个值发布到全局存储(例如,第14行);
?????通过move_from<Coin>指令从全局存储中删除一个值;
?????通过borrow_global_mut<Coin>指令获取全局存储中一个值的引用(例如,第22行)。
电影订阅服务MoviePass完成种子轮融资,Animoca Brands领投:金色财经报道,电影订阅服务MoviePass完成种子轮融资,Animoca Brands领投,Claritas Capital、Emerald Plus、Gaingels、Harlem Capital、PKO VC和Sandhill Angels等参投。作为交易的一部分,Animoca Brands的执行主席兼联合创始人Yat Siu将加入MoviePass的董事会。
MoviePass联合创始人Stacy Spikes于2021年底回购了该公司,他表示计划在2023年底或2024年进行另一轮融资。
MoviePass表示,它计划利用新资金加速其电影院订阅服务的测试版重新启动,并制定和实施公司的Web3战略,其中包括虚拟现实影院体验和使用技术来推动影院流量。Spikes表示,Animoca Brands认为电影客户与元宇宙和区块链的粉丝之间存在重叠。[2023/1/13 11:09:01]
由于模块“拥有”由其通过key控制的全局存储条目,它可以对该存储强制约束。例如,确保只有ADMIN帐户地址可以持有类型为0x1::TestCoin::Info的结构体。它只通过定义一个过程(initialize)来实现这一点,该过程在Info类型上使用move_to,并强制执行在ADMIN地址上调用move_to的前置条件(第13行)。这些约束不同于静态不变量,因为它们需要运行时检查。在这种情况下,由于参数account是在运行时提供的,程序员不能静态地强制它始终是ADMIN的,因此需要在第13行进行不变量检查。
以下就是两个保证该模块代码安全的两个静态检查特性机制:不变量规约和字节码验证器
a)不变量检查:模块的第10行,表示静态检查的不变量——系统中所有Coin对象的值字段之和必须等于存储在ADMIN地址的Info对象的total_value字段。所谓不变量,是形式化验证中的一个术语,表示了一种状态的守恒性,也可以称为不变式或不变性。我们希望守恒属性适用于模块的所有可能的客户端(包括恶意的客户端):任何违反都会破坏Coin的完整性。因此,不变式不只是影响单个对象,而是影响它们的集合(即所有的Coin对象)。该处其实就是我们下一节要介绍的moveprover中可用于形式化检查的specificationlanguage。
b)字节码验证器:安全类型和线性化是字节码验证器的主要验证范围:如本例,尽管其他模块不能访问由0x1::TestCoin::Coin控制的全局存储单元,但它们可以在自己的过程和结构声明中使用这种类型。例如,另一个模块可以公开一个支付过程,该过程接受0x1::TestCoin::Coin作为输入。
WeMade战略投 Move-to-Earn项目SNKRZ:8月4日消息,韩国网络游戏公司WeMade宣布对Move-to-Earn项目SNKRZ进行战略投资,投资细节未披露。据悉,SNKRZ建立在Klaytn上,允许用户购买运动鞋NFT并通过步行、跑步、骑自行车等方式获得代币奖励。[2022/8/4 12:01:33]
乍一看,允许Coin等敏感值流出创建它们的模块可能看起来很危险——恶意客户端模块会创建假冒硬币,人为增加其拥有的硬币的价值,或复制/销毁现有硬币。幸运的是,Move有一个字节码验证器(一个在字节码级别强制执行的类型系统),它允许模块所有者防止这些不希望出现的结果。只有声明了结构类型Coin的模块才可以:
?????创建一个Coin类型的值(例如,第25行);
?????“解包”一个Coin类型的值到它的组件字段中(在本例中为value);
?????通过rust风格的可变或不可变borrow(例如&mutCoin)获得对Coin字段的引用。
这允许模块作者在模块中声明的结构的创建值和字段值。验证器还强制结构默认为线性。以确保线性地防止在声明结构的模块之外复制和销毁(例如,通过覆盖存储结构的变量或允许它超出作用域)。同时,验证器还会对一些类型的常见内存问题进行强制检查。
检测过程主要有三类:
1)结构体合法检查:保证字节码的完整性,检测错误非法的引用,重复的资源实体以及非法的类型签名等
2)过程逻辑的语义检测:包括参数类型错误,循环索引,空索引以及重复定义变量等。
3)链接时错误,非法调用内部过程,或者链接一个声明和定义不匹配的流程。
验证器会首先创建一个CFG,因为没有非线性逻辑,这个控制流图可以清晰的描述程序块间的调用关系,而无需考虑递归深度的问题。
随后,验证器会检测栈里面被调用者的访问范围,保证合约的被调用者不能访问到调用者的栈空间。例如一个过程被执行的时候,调用者首先在CallStackFrame里面初始化局部变量,然后将局部变量放入到栈里面,假设当前栈的高度是n,那么有效的bytecode必须满足不变量:当调用过程结束的时候,栈的高度依然还是n。验证器主要是通过分析每个指令块的指令对栈的可能影响,保证不操作高度高于n的栈空间。这里有一个例外就是,一个以return结尾的指令块,他退出的时候高度必须是n+m,其中m是过程返回值的个数。
行情 | Gate.io Startup首发区项目MovieBloc今日跌超20%:Gate.io Startup首发区项目MovieBloc(MBL)持续下跌,现跌至0.0010美元,较开盘价(0.0012美元)已下跌23%,较今日最高价格(0.002114美元)下跌超过50%。跌幅较大,请做好风险控制。[2019/5/6]
同时为了检查类型,每一个Value栈都维护了一个对应的Type栈,执行的时候Type栈也跟这指令执行进行pop和push。
接下来是资源检查和引用检查。资源主要检查资源的不可双花、不可销毁、必有归属等约束。而引用检查结合了动态和静态分析。静态分析利用类似rust类型系统的borrowchecking机制,保证:1.所有引用必须指向一个已经被分配的存储上,防止空指针;2.所有的引用都有安全的读写权限。
而borrow_global调用的时候会动态的对全局变量的引用进行了计数,解释器会对每个发布了的资源进行判断,如果被borrow或者move了,再次引用就会报错。
最后是链接检查,需要对链接的对象和声明是否匹配,过程的访问控制等做再次的检查。
可见,通过不变量检查和字节码验证器两种机制,双重保障了代码在编译时的安全性。接下来我们通过分析move的运行机制,来看看MoveVM如何保证运行时的安全性。
2、Move的运行机制
首先,Move程序运行在虚拟机中,并且在运行时不能访问系统内存。这使得Move可以在不信任的环境中安全地运行,并且不会被破坏或滥用。
其次,Move程序是在堆栈上执行的,形式上,之前提到的全局存储被分为两部分:内存和全局变量。内存是一个一阶存储,因此它的单元不能用来存储指向内存单元的指针。全局变量被用来存储指向内存单元的指针,但是它们的索引方式与内存不同。为了访问全局变量,代码提供了一个地址和一个绑定到该地址的类型。这种划分简化了操作,使得move语言在语义上更容易形式化。
而Move的字节码指令在一个栈式的解释器进行执行,栈式虚拟机的好处是易于实现和控制,对硬件环境的要求较少,非常适合区块链场景。同时相对寄存器式的解释器,栈式解释器在不同的变量之间进行copy和move更容易控制和检测。
在Move语言中,任何被定义为资源的值都只能被破坏性地移动(使以前保存该值的存储位置无效),但只有某些值(例如,整数)可以被复制。
Move程序运行在堆栈上的时候,其状态为?C,M,G,S?的四元组,由:调用栈,内存,全局变量和操作数组成。堆栈还维护一个函数表(模块本身)来解析包含函数体的指令。
调用栈包含了一个过程执行的所有上下文信息以及指令编号。当在一个过程中执行Call指令调用其他的过程的时候,会创建一个新的调用栈对象,然后将对应的调用参数存储到内存和全局变量上面,最后解释器开始以此执行新的合约的指令。执行过程遇到分支指令的时候,会在本过程内部发生一个静态跳转,所谓静态跳转实际上是指跳转的offset是事先已经确定好的,不会像evm一样动态跳转。这也就是之前提到的不支持动态分派的特性。也就是说模块内的过程依赖是无环的,加上模块本身的没有动态指派,这样就加强了执行期间的函数调用的不可变性:一个procedure在执行过程的callframe必然是相邻的,从而避免了重入的可能性。最后调用return结束调用,同时返回值放在栈顶。
通过研究MoveVM的代码,我们可以清晰的看到,MoveVM将数据的存储和调用堆栈的存储是分开的,这点是和EVM最大的不同的地方,例如,在EVM中,要实现一个ERC20的Token,需要在一个合约中写好逻辑和对每个用户的状态进行记录,而在MoveVM中,用户状态是独立存储的,程序调用必须符合权限和关于资源的强制性规则,虽然牺牲了一定的灵活性,但是在安全性和执行效率上获得了很大的提升。
3、MoveProver
最后,我们来了解下Move提供的能够进行辅助自动化审计的工具Moveprover。
MoveProver是一种基于推理的形式化验证工具。它使用形式化语言来描述程序的行为,并使用推理算法来验证程序是否符合预期,可以帮助开发人员确保智能合约的正确性,从而减少交易风险。简单来说,形式化验证就是用数学方法去证明我们的系统是无Bug的。
目前主流的自动软件验证算法是基于可满足性模理论求解器的。虽然这名字看起来有点唬人,但其实SMTsolver就是一个公式求解器。上层的软件验证算法将其验证目标拆分为一些公式,交由SMTsolver求解,再根据求出的结果进一步分析,最终报告验证目标成立,或者是发现了一个反例。
一种基本的验证算法是演绎验证,也有不少其他的验证算法,比如有界模型检测、k归纳法、谓词抽象和路径抽象等
MoveProver就是使用了演绎验证算法来验证程序是否符合预期。这意味着,MoveProver可以根据已知的信息推断出程序的行为,并确保其与预期的行为相匹配。这有助于确保程序的正确性,并减少了人工手动测试的工作量。
MoveProver的总体架构如下图所示:
首先,MoveProver接收一个Move源文件作为输入,该文件中需要设置程序输入规范。之后MoveParser会在源码中把规范提取出来。Move编译器将源文件编译为字节码,和规范系统(specification)共同转化为验证者对象模型(ProverObjectModel)。
这个模型会被翻译成一种名为Boogie的中间语言。这段Boogie代码会被传入Boogie验证系统,该系统对输入进行“验证条件生成”(verificationconditiongeneration)。该验证条件会被传入一个名为Z3的求解器中,这是微软研发的一种可满足性理论求解器。
VC被传进Z3程序后,该验证器会检查SMT公式是否是不可满足的。如果是这样,这意味着规范是成立的。否则,将生成一个满足条件的模型,并将其转换回Boogie格式,以便发布诊断报告。然后将诊断报告还原为与标准编译器错误类似的源码级错误。
为了描述规范系统,move使用MoveSpecificationLanguage,其是Move语言的子集,它引入了对静态描述有关程序正确性的行为的支持,而不影响生产。同时可以独立为专门的规约化文件,从而让业务代码和形式化验证代码分开。
网上已经有很多其他的关于MoveSpecificationLanguage的教程,感兴趣的可以自行学习,建议合约程序员们多加了解,以提高自己代码的安全性。同时,由于MoveSpecificationLanguage可以不侵入代码的单独编写,对于安全性要求更高的项目来说,可以将该规约代码交由安全经验更加丰富的第三方安全公司来撰写,在审计代码的同时,可以给出更严格的形式化验证报告。
总的来说,MoveProver是一种非常有用的工具,可以帮助开发人员确保智能合约的正确性。它使用形式化语言来描述程序的行为,并使用推理算法来验证程序是否符合预期。这有助于减少交易风险,并使开发人员能够更自信地将智能合约部署到生产环境中。
4、总结
Move语言的设计在安全性的考虑上非常出色,在语言特性、虚拟机执行和安全工具层面,都给出了非常全面的考虑。语言特性上牺牲了部分灵活性,强制类型检查和线性逻辑,方便在编译检查、形式化验证时更加的自动化和具备安全可验证性,而MoveVM在设计上将状态与逻辑分开,更加贴合区块链上资产安全管理的需求。
综上,在语言层面,常见于EVM的重入、溢出、Call/DelegateCall注入等漏洞可以有效避免,但是鉴权、代码逻辑、大整数结构溢出等问题并不能依赖语言层面的特性去避免,而MoveProver并不能在整体的大意疏忽时产生作用。
虽然Move语言在安全层面已经为程序员考虑了很多,但没有完全安全的语言,也没有完全安全的程序。我们仍然建议,Move智能合约的开发者使用第三方安全公司审计服务,并且将specification部分代码的编写和验证交由第三方安全公司来完成。
5、Numen现已支持Move合约审计及项目安全评估服务
NumenCyberLabs可以为Move语言的开发者提供以下服务:
a、合约审计,包括溢出、随机数、DDos、鉴权、业务逻辑、价格操纵、套利限制、代码规范、市场公平性等审计项
b、形式化验证,基于moveprover提供SpecificationLanguage编写及形式化验证服务。
c、安全测试,使用Fuzzer和模拟交易脚本的安全性测试服务。
d、合约监控,对合约相关的transaction进行意图分析及安全响应。
欢迎Move生态的开发者联系我们进行交流和咨询,邮箱contact@numencyber.com。更多信息,欢迎关注我们微信公众号@NumenCyberLabs,或通过下方链接关注,阅读。
Move语言安全性解析-智能合约语言的gamechanger
郑重声明: 本文版权归原作者所有, 转载文章仅为传播更多信息之目的, 如作者信息标记有误, 请第一时间联系我们修改或删除, 多谢。