一种基于代码插桩的以太坊智能合约重入漏洞检测方法与流程

文档序号:17762373发布日期:2019-05-24 21:48阅读:466来源:国知局
一种基于代码插桩的以太坊智能合约重入漏洞检测方法与流程

本发明涉及到一种以太坊智能合约重入漏洞的检测方法,该方法基于静态代码插桩,属于信息技术领域。



背景技术:

近年来,随着比特币系统的提出和平稳运行,作为比特币底层技术的区块链技术也受到人们的关注,并且越来越多的基于区块链的产品争相出现,其中以太坊的出现是区块链发展的又一里程碑事件,以太坊相对于之前出现的区块链系统最为本质的区别在于以太坊支持智能合约的运行。以太坊中的智能合约,就是运行于区块链上的一段代码。

区块链由于其特殊的数据结构、共识算法以及使用到的密码学算法的所具有的性质,使得区块链这种特殊的分布式“账本”具备了数据不可篡改、数据可溯源、去中心化等特征。而以太坊作为搭建于区块链技术之上的区块链平台,同样具有区块链的所有特征。智能合约作为运行于区块链上的一段代码,在智能合约的编写过程中,往往存在着各式各样的漏洞。而智能合约存在的这些漏洞,伴随着区块链上数据的不可篡改性、系统的去中心化特性,存在着一经发布后便无法修复、无法更新等缺点,往往带来重大的经济损失。

智能合约的重入漏洞指的是具有如下特征的漏洞:以太坊智能合约能够调用和利用其他外部合约的代码,合约通常也处理以太币,因此智能合约可以将以太币发送到各种外部用户地址。调用外部合约或将以太币发送到地址的操作要求合约提交外部调用。这些外部调用可以被攻击者所劫持,从而迫使合约执行更多的代码(即通过合约自带的fallback回退函数),包括回调原合约本身。所以,合约代码在执行过程中将可以“重新进入”该合约,该过程比较类似于传统编程语言的间接递归函数调用。

因为以太坊是新生事物,智能合约更是新上加新,使得现有的重入漏洞的检测无论是从检测方法还是检测工具上都较为匮乏。在智能合约的编写中,重入漏洞的检测往往依靠于程序员的编写时的自觉和代码审查。在如今已经出现的智能合约漏洞检测工具中,oyente通过分析区块链上的字节码,并根据一套属性对字节码进行检查,以此来确定合约中存在的漏洞;reguard通过将solidity语言编写的智能合约转换为c++语言,再通过模糊引擎生成大量交易的方式覆盖智能合约的状态空间,以此来检测智能合约是否会到达可重入的状态。但是现有方法效率较低,分析数个合约往往需要数十分钟的时间,重入漏洞分析的准确性也并不令人满意。

随着以太坊的蓬勃发展,越来越多的智能合约被发布到区块链上,检测效率更好、检测准确性更高的以太坊智能合约重入漏洞检测方法的需求变得愈发明显。



技术实现要素:

发明目的:考虑到具有重入漏洞的智能合约往往具有以下两个特征:1、使用特定转账函数(如“地址.call.value(转账金额)”函数)进行以太币转账。2、转账操作先于“代币扣减”操作执行。而这两种特征能够通过获取程序执行内部数据信息的方式检测出来。本发明提供了一种基于代码插桩的以太坊智能合约重入漏洞检测方法,对可能引入重入漏洞的特定转账函数的检测以及相应的代币扣减、转账两操作之间的顺序的确定,通过在合适位置插入探针代码的方式,生成相应的测试合约,再将测试合约部署于私有链上,调用测试合约内插入的测试函数,通过观察测试函数的运行情况的方式,来判断被测合约中是否存在着重入漏洞。该方法具有生成测试合约效率快、重入漏洞识别率高等特点。

技术方案:本发明所述的一种基于代码插桩的以太坊智能合约重入漏洞检测方法,包括如下步骤:

(1)判断被测合约内是否存在通过继承相互关联的合约,若存在,则导入继承相关的代码;否则进入下一步;

(2)判断被测合约内有无函数使用指定转账函数方式进行以太币转账,若有,则进入下一步;否则报告被测合约无可重入漏洞,检测结束;

(3)收集所有函数体内调用了指定转账函数的函数名,记为直接调用函数;并基于直接调用函数进行递归查找,获得所有在执行过程中可能执行到指定转账函数语句的函数名,记为间接调用函数;

(4)在直接调用函数的函数声明/定义后的第一条语句的位置处插入a类探针代码,在直接调用函数中的指定转账函数语句的前一句的位置处插入b类探针代码,在在直接调用函数中的指定转账函数语句的后一句的位置处插入c类探针代码;在调用链首的间接调用函数的函数声明/定义后的第一条语句的位置处插入a类探针代码,在被测合约的第一条语句位置插入d类探针代码;其中a类探针代码用于获取转帐流程开始前账户代币数量,b类探针代码用于获取调用转账前账户代币数量以及用于判定被测合约内是否存在重入漏洞,c类探针代码用于重置变量值;d类探针代码用于声明变量;探针代码中的账本名和地址名分别根据合约内声明的用于发挥账本作用的映射型变量的变量名及接收指定转账函数的地址名设定;

(5)向被测合约内插入模拟重入攻击的测试函数,所述测试函数执行语句包括:接收外部的以太币转账并且在账本中增加相应记录;调用直接调用函数和间接调用函数中的所有外部可见的函数作为重入攻击靶函数;

(6)生成被测合约的部署文件,编译、部署被测合约于私有链上;

(7)运行被测合约中的模拟重入攻击的测试函数,插入的探针代码获取到程序运行时的数据流信息,通过分析数据流信息,给出被测合约是否具有重入漏洞的检测结果。

在优选的实施方案中,所述步骤(1)中判断被测合约内是否存在通过继承相互关联的合约的方法是:按行读取被测合约,若任一行中出现合约声明的标志性字符“contract”或者是库声明的标志性字符“library”或者是接口声明的标志性字符“interface”中的一种,并且该行内同时包含了标明继承关系的关键字,则判定合约内存在继承情况。

在优选的实施方案中,所述步骤(1)中对合约的继承处理方法包括:

收集合约文件中所有的合约/库/接口的声明/定义语句,从中剥离出合约/库/接口名并保存,同时识别语句中是否包含标明继承关系的关键字,若存在,则将该合约/库/接口所直接继承的合约/库/接口名从语句中分离并保存;

根据用户指定的合约名,首先在合约中找到指定合约的所有直接继承的父合约/父接口/父库的声明/定义语句,再根据直接父合约/父库/父接口的声明/定义语句查找所有的指定合约的间接继承父合约/父库/父接口名,如此递归查找,直到指定合约的所有直接/间接继承的父合约/父库/父接口都被找到;

将指定合约的所有直接/间接继承的父合约/父库/父接口代码都拷贝至指定合约中,拷贝代码的插入位置为指定合约的定义之后第一行语句,原来代码顺次后移;然后,消除指定合约声明/定义语句中表明继承关系的语句段。

在优选的实施方案中,所述步骤(2)之前还包括滤除被测合约中的注释;

在优选的实施方案中,所述步骤(3)中获取指定转账函数的间接调用函数的方法包括:

(3.1)获取所有的直接函数名,将获取的直接函数名集合称为集合a;

(3.2)将集合a保存于全局变量中,该全局变量用于存储所有的直接/间接调用函数名;

(3.3)建立新集合b,初始化为空;

(3.4)检索被测合约中的每一行,若该行中调用了集合a中包含的函数,则将包含该调用语句的函数名添加进集合b中;

(3.5)如果集合b为空,则结束检索;若集合b不为空,则重置集合a内容为集合b,转步骤(3.2)。

在优选的实施方案中,所述步骤(4)中,a类探针代码在不覆盖前序有效数据的情况下,获取以太币转移的原子操作发生前以太币接收账户的代币数量aexe;b类探针代码获取以太币转移实际动作发生前以太币接收账户的代币数量bexe以及判断aexe、bexe大小关系;c类探针代码重置aexe以及bexe的值,避免干扰下一个被检测函数的检测。

具体地,a类探针代码语句为:if(bexe==0){bexe=账本名[接收以太币转账的地址];}

b类探针代码语句为:aexe=账本名[接收以太币转账的地址];

require(aexe<bexe);

c类探针代码语句为:aexe=0;bexe=0;

其中,账本名和接收以太币转账的地址分别从合约内声明的用于发挥账本作用的映射型变量及指定转账函数语句中获取。

在优选的实施方案中,所述步骤(7)中判断被测合约是否具有重入漏洞的方法是,若调用测函数出错,出错原因为不符合测试合约中b类探针代码的判断语句的要求而被终止执行,则认定被测试合约中存在有重入漏洞;若测试函数正确执行,则可认定被测试合约中不存在重入漏洞。

有益效果:与现有技术相比,本发明方法先对被测合约代码进行代码分析、然后进行代码插桩、最后通过插入的测试函数触发重入攻击靶函数的方式来判断被测试合约中是否存在着重入漏洞,具有检测准确性高、检测所需时间短等优点。

附图说明

图1为本发明实施例的总体方法流程图;

图2为本发明实施例中合约继承处理流程图。

图3为本发明实施例中转账间接调用函数获取流程图;

图4为本发明实施例中转账直接调用函数插桩位置示意图;

图5为本发明实施例中转账间接调用函数插桩位置示意图。

具体实施方式

下面结合附图和具体实施例,进一步阐明本发明,应理解这些实施例仅用于说明本发明而不用于限制本发明的范围,在阅读了本发明之后,本领域技术人员对本发明的各种等价形式的修改均落于本申请所附权利要求所限定的范围。

如图1所示,本发明实施例公开的一种基于代码插桩的以太坊智能合约重入漏洞检测方法,主要包括代码预处理、收集直接调用函数和间接调用函数、确定探针插入位置、插入探针代码、插入模拟重入攻击的测试函数、编译部署及运行测试函数判定是否存在重入漏洞等几个步骤。为便于详细介绍本实施例的详细步骤,本实施例对给定的被测合约文件做如下约定:(1)、该文件使用solidity编程语言编写,是语法正确的、可以执行的智能合约。(2)、该文件中代码不与注释混写于一行内。(3)、该文件中合约声明/定义、函数声明/定义必须书写于一行内。(4)、合约中用于发挥“账本”作用的mapping(address=>uin256)型变量必须是合约内第一个声明的该类型变量。(5)、合约文件中可以包含多个合约,允许输入文件内存在合约继承的情况。(6)、合约/库/接口定义的合约体/库体/接口体的最后一个’}’单独处于一行。(7)、父合约与子合约中无冲突定义的代码。

本实施例的重入漏洞检测方法具体步骤如下:

步骤1:对输入的合约文件进行代码分析,用以确定该合约内是否存在合约的继承关系。若存在,则对合约的继承进行处理,然后转至下一步;若不存在,则直接转至下一步。如图2所示,该步骤具体包括:

步骤11:接收用户输入的被测合约文件名。

步骤12:读入用户指定的文件,按行读取。若任一行中同时出现合约声明的标志性字符“contract”或者是库声明的标志性字符“library”或者是接口声明的标志性字符“interface”中的其中一种,且该行内同时包含了“is”关键字(is为solidity中标明继承关系的关键字),则判定合约内存在继承情况,转入步骤13。若合约文件内任一行都不符合以上条件,则认定合约文件内不存在继承情况,则将用户输入的文件名返回给主程序,转入步骤108。

步骤13:根据读入的合约内容,收集所有的合约/库/接口的声明/定义语句,从该语句中剥离出合约/库/接口名,将这些合约/库/接口名保存。同时,根据合约/库/声明的声明/定义语句,识别语句中是否包含“is”关键字符,若存在,则将该合约/库/接口所直接继承的父合约/父库/父接口名从语句中分离,保存。

步骤14:列出合约文件中所有声明的合约/库/接口名,要求用户指定本次检测的合约名。

步骤15:根据用户指定的合约名,首先在合约中找到指定合约的所有直接父合约/父库/父接口的声明/定义语句,再根据直接父合约/父库/父接口的声明/定义语句查找所有的指定合约的间接继承父合约/父库/父接口名,如此递归查找,直到指定合约的所有直接/间接继承的父合约/父库/父接口都被找到。

步骤16:将指定合约的所有直接/间接继承的父合约/父库/父接口代码都拷贝至指定合约中,拷贝代码的插入位置为指定合约的定义之后第一行语句,原来代码顺次后移。然后,消除指定合约声明/定义语句中表明继承关系的语句段,即“is”字符至合约体开始字符“{”中间的所有字符,包含“is”字符,但不删除“{”字符。

步骤17:将得到的新的合约代码输出为智能合约文件,输出文件名为指定合约名+“_inherit.sol”。将输出的新文件名返回给主程序,转入步骤2。

步骤2:对处理后的被测合约进行代码分析,滤除单行注释、多行注释、文档注释等对于合约行为无作用的语句,在滤除注释过程中,将代码语句保存入c++语言中的std::vector<std::string>结构中,并将该结构返回给步骤3。该步骤具体包括:

步骤21:根据步骤1返回给主程序的文件名,打开并按行读取文件。

步骤22:若某行为空行、单行注释、多行注释中的任意一行、文档注释,则丢弃该行,继续读入下一行;若该行为代码,则将其保存于c++中的std::vector<std::string>结构中。

步骤23:读取过滤整个文件,然后将存储代码过滤结果的std::vector<std::string>结构返回给主程序,转入步骤3。

步骤3:对滤除注释后的被测合约源代码进行代码分析,确定被测合约内有无函数调用了指定转账函数(以太坊提供的转账函数为“地址.call.value(转账金额)”函数)。若有,则进入步骤4;若无,则报告“被测合约无可重入漏洞”,检测结束。本步骤中,按行检查前一步骤中返回std::vector<std::string>结构,若某一行中存在“.call.value(”语句段(因被测合约语法正确,可以认为包含该语句段的语句调用的是以太坊提供的转账函数“地址.call.value(转账金额)”),则转入步骤4;若返回的std::vector<std::string>结构中任意一行都不含“.call.value(”语句段,则意味着被测试合约中无重入漏洞,检测结束,输出“被测合约中无重入漏洞”的检测结果。

步骤4:对被测合约进行代码分析,收集所有调用了“地址.call.value(转账金额)”的函数名。称该部分直接调用了“地址.call.value(转账金额)”函数的函数为直接调用函数。该步骤具体包括:

步骤41:根据包含“.call.value(”语句段的语句位置,向前搜索距离该语句最近的函数声明/定义语句(因用于检测的合约文件语法正确,故可认为距离该语句最近的、位置在该语句之前声明/定义的函数为包含该语句的函数)。

步骤42:从该函数声明/定义语句中,分离出函数名,将该函数名保存。

步骤43:搜索整个保存有合约代码的std::vector<std::string>结构,将所有在函数体内语句包含着“.call.value(”语句段的函数名保存,称此部分函数为直接调用函数。转入步骤5。

步骤5:对被测合约进行代码分析,以步骤4获得的直接调用函数名为种子,递归寻找,最终获得所有在执行过程中可能执行到“地址.call.value(转账金额)”语句的函数名。称此部分函数为间接调用函数。如图3所示,该步骤具体包括:

步骤51:获取所有的直接函数名,将该步骤获取的直接函数名集合称为集合a。

步骤52:将集合a保存于全局变量chain中,该变量用于存储所有的直接/间接调用函数名。

步骤53:建立新集合b,初始化为空。

步骤54:检索被测合约中的每一行,若该行中调用了集合a中包含的函数,则将包含该调用语句的函数名添加进集合b中。

步骤55:如果集合b为空,则结束检索;若集合b不为空,则重置集合a内容为集合b,转步骤52。

上述步骤使用伪代码表示如下:

步骤6:对被测合约进行代码分析,获得需要插入探针代码位置和合约内声明的、用于作为“账本”数据结构的mapping(address=>uint256)型变量的变量名以及接收“地址.call.value(转账金额)”转账的地址名,以账本变量名和地址名来构建要插入合约的探针代码语句。

本步骤中,根据函数调用关系及“地址.call.value(转账金额)”语句的语句位置,确定探针代码的插入位置,探针代码的插入位置依据直接调用和间接调用函数的区别按如下方法确定插入位置:

对于直接调用函数,按如图4所示方法确定插桩位置,图4中,假设函数a为直接调用函数,函数中不影响代码插入位置的代码都表示为“/*函数业务逻辑*/”。

对于间接调用函数,按照函数的调用关系,在函数调用链的末端,必然是直接调用函数,则按照如图5所示方法确定探针代码的插入位置,图5中,假设此处的针对的间接调用函数的函数名为b,b中调用了另一个间接调用函数c,c中调用了直接调用函数d。其余与插桩位置确定无关的业务逻辑代码此处皆忽略。

总之,a类探针代码插入位置为调用链首函数的函数声明/定义后的第一条语句的位置,函数原来的语句皆顺次后移。而b类探针代码插入到调用链尾函数中可能导致重入攻击的“地址.call.value(转账金额)”语句的前一句的位置。c类探针代码插入到调用链尾函数中可能导致重入攻击的“地址.call.value(转账金额)”语句的后一句的位置。其中,直接调用函数既是调用链首函数,也是调用链尾函数。

根据直接调用函数或者间接调用函数的调用链尾部的直接调用函数中所使用“地址.call.value(转账金额)”语句中“地址”部分(也就是此条转帐语句中以太币的接收者)地址的不同,以及被测合约中“账本”结构的mapping(address=>uint256)型的变量名,构造a类、b类和c类探针代码。其构造规则如下:

对于a类探针代码,构造目的为:在不覆盖前序有效数据的情况下,获取本次以太币转移的原子操作发生前、以太币接收账户的代币数量aexe;其组成结构可为:

if(bexe==0){bexe=账本名[接收以太币转账的地址];}

对于b类探针代码,构造目的为:获取本次以太币转移实际动作发生前(既向外部/合约地址使用调用方式发送以太币前)、以太币接收账户的代币数量bexe以及判断aexe、bexe大小关系;其语句内容可为:

aexe=账本名[接收以太币转账的地址];

require(aexe<bexe);

对于c类探针代码,其构造目的为:重置aexe以及bexe的值,避免干扰下一个被检测函数的检测,其语句内容可为:

aexe=0;

bexe=0;

构造完准备插入被测合约的探针代码以及确定好探针代码的插入位置后,转入步骤7。

步骤7:将探针代码插入到合适位置。具体包括:

步骤71:将步骤6中构造好的探针代码,按调用链尾函数中“地址.call.value(转账金额)”语句中接收以太币转账地址的不同,分别将其插入到合适的位置。

步骤72:在整个被测合约的合约体中的第一条语句的位置,插入d类探针代码,用于声明变量,插入语句分别为:uint256aexe=0;uint256bexe=0;。其余代码顺次后移,插入完成后转入步骤8。

步骤8:向被测合约内插入模拟重入攻击的测试函数,在测试函数内完成向被测合约发送以太币以及调用所有外部可见的重入攻击靶函数的行为。具体包括:

步骤81:在直接调用函数和间接调用函数中,选取外部可见性为public或者是external的函数,这些函数称为重入攻击的目标靶函数。

步骤82:要求用户对于每一个被选取的目标靶函数给定合适的参数。若被选取的函数无参数,则跳过该函数;若所有被选取的函数都已给定参数或所有被选取的函数都无参数,则转入步骤83;若选取的函数使用到用户自定义的结构,那么要求用户手动编辑生成的测试合约,给定合适的参数值。给定合适参数的意义在于可以正确的驱动函数,用户给定所有靶函数的目标参数后,转入步骤83。

步骤83:构造要插入的用于模拟重入攻击的测试函数的内容。函数声明为“functiondeposit_test()publicpayable”,函数内第一条语句为“账本名[msg.sender]+=msg.value;”,本例中测试函数默认命名为deposit_test,该函数可接收外部转账,且函数的第一条语句为向本合约注资并且修改注资账户在本合约内的代币数量,其后语句皆为重入攻击目标靶函数的调用语句,此时所有函数调用所需要的参数都已经被用户给定为合适的值。

步骤84:寻找被测合约中最后一个“}”的位置,将deposit_test函数插入到该“}”之前一行,该“}”所处位置顺次后移。

步骤85:将完成代码插桩的被测合约输出为测试合约文件,输出文件名为“被测试合约名_test.sol”。

步骤9:生成测试合约的部署文件。然后将测试合约以及测试合约的部署文件,拷贝到truffle框架项目内的合适位置,编译、部署被测合约于私有链上。具体步骤包括:

步骤91:获取被测合约的合约名。

步骤92:构造用于部署该合约的部署文件内容的字符串。

步骤93:将部署文件的内容输出到以“n_deploy_被测试合约名test.js”为文件名的部署文件。

步骤10:运行被测合约中的测试函数,使得插入的探针代码获取到程序运行时的数据流信息,通过分析数据流信息,给出被测合约是否具有重入漏洞的检测结果。该步骤具体包括:

步骤101:将生成的测试文件拷贝到创建好的truffle项目中,拷贝位置为项目中的contracts文件夹下。

步骤102:将生成的部署文件拷贝到创建好的、与之前测试合约拷贝目标相同的truffle项目中的migrations文件夹下。

步骤103:根据migrations文件夹内容的不同,用户需要手动重命名部署文件名,将文件名中的“n”修改为合适数值,使得部署文件名符合truffle框架的部署要求。

步骤104:编译、部署测试合约于私有链上。

步骤105:调用测试合约中的测试函数(默认函数名为deposit_test),需要给定合适的msg.sender和msg.value,msg.value要求大于在deposit_test中所有被调用靶函数所转账的以太币数额。

步骤106:若调用deposit_test函数出错,出错原因为“revert”(即因为不符合测试合约中某条“require”语句的要求而被终止执行),则可认定被测试合约中存在有重入漏洞;若deposit_test函数正确执行,则可认定被测试合约中不存在重入漏洞。

当前第1页1 2 
网友询问留言 已有0条留言
  • 还没有人留言评论。精彩留言会获得点赞!
1