ERC20漏洞被这位大哥扒透了!满篇的代码废话少,程序员一定很喜欢

作者 | 于超

责编 | Eli

智能合约作为区块链的核心组成部分,到底有什么特别的运行规则,这些特别的运行规则会造成什么样特殊的的安全问题?你是否有想到过,即便像以太坊这样成熟的区块链系统也面临着致命的安全问题? 当前以太坊ERC20代币存在较为严重的几个安全问题——假充值漏洞、重入攻击以及函数访问绕过等。本文作者将从实际操作层面出发,从黑客与安全专家两个角度,深入挖掘ERC20面临的漏洞并给出务实的解决方案。 于超,北京知道创宇安服技术专家,高级渗透测试工程师。专注于互联网安全领域,擅长Web安全技术与渗透测试,曾特邀担任Kcon2017渗透测试高级培训讲师,获得过ISW2017内网渗透比赛冠军,拥有丰富的实战经验。曾为中国平安集团、拉勾网、陆金所、凡普金科、比特大陆等多家大型互联网集团企业提供渗透测试、应急响应、安全培训等多项服务。

于超(和他犀利的小表情)

大家下午好,我今天给大家演讲的题目叫做《安全工程师眼中的智能合约》,其实就是我眼中的智能合约。

我先自我介绍一下,我目前就职于知道创宇安服,主要是做智能测试方面的工作。

记得我第一次接触智能合约,是在去年年底。当时正在做一个关于区块链游戏的渗透,类似于DApp。我当时不知道DApp的概念,就把它当成一个正常的BS架构应用。

到今年上半年,随着一系列的智能合约,如SMT、BEC曝出严重安全问题,我才开始关注到智能合约上可能存在的安全风险和漏洞。

今天主要给大家介绍三个内容:

第一,智能合约是什么。这里“是什么”的意思,指在我的眼里它是什么,即我对智能合约的第一印象、第二印象等。

第二,智能合约有什么特别之处。这里特别之处,指的是它的运行规则,和它的特性。智能合约是代码,那么它的代码跟其他程序的代码有什么不一样的地方,这是我需要关注的。

第三,智能合约中可能出现的安全问题。我会用几个有代表性的,典型漏洞来举例。结合这些漏洞,我会谈谈自己的感悟和思考。

智能合约

智能合约,一个是智能,一个是合约。

首先,智能合约是一个程序,而且是运行在以太坊上的一个程序。可以这么说,以太坊对相当于一个操作系统,而智能合约就是运行在以太坊上OS的一个APP。

其次,它会提供接口供外部调用,即,它允许有外部调用的函数,public的函数。在这一点上,它类似CS架构,或者BS架构的服务端代码。

实际上,我认为智能合约在DApp实现中相当于一个客户端,一般是服务端与智能合约交互,或者也有从前端直接与智能合约交互的。

其实,我刚开始接触智能合约时,就在想,它为什么叫合约?

“智能”理解起来比较简单,智能是指它有代码,可以自动去执行一些事情。

那为什么要称之为“合约”?因为它是基于区块链。它的底层是以太坊区块链技术,拥有一旦部署便不能修改的特性,包括所有发生过的交易信息。

以太坊上有两种帐户:

一种叫外部帐户,外部帐户是由私钥控制的一类帐户,它和比特币上的帐户是一样的。如果你有私钥,就可以以这种帐户的身份在以太坊上发起交易。

第二种是合约帐户,合约帐户不能由私钥控制,那它是受什么控制的呢?它只能是你的合约在部署的时候,由合约代码逻辑去控制。比如说你的一个普通的帐户地址里边有一些ETH,如果我想要把它转出去,我用私钥发起一个转帐的交易就行了。

但是你如果想把一个合约帐户里的钱,也就是ETH转出去的话,一定是你的合约代码实现了相关的功能才可以。否则的话,这里面的ETH可能永远都转不出来。

以上简单说了一下我对智能合约的第一印象,第一印象是比较重要的。

现在说说智能合约的特别之处。

关于这一点,我想说的是,我们之所以要了解它的特别之处,是因为现在做智能合约审计时,你需要关注它的一些特点,即它跟之前代码的区别,而这个区别就很可能会有安全问题。

特别的运行规则

第一点,智能合约自身有几个特别的运行规则

第一,它执行代码要花费Gas,这对于我来说,刚刚接触的时候肯定是一个比较新的概念,Gas到底是什么东西?后来我简单地了解了一下,它就是代码需要的一种花费。代码写的越复杂,执行需要花费的Gas就越多。

就因为Gas的存在,所以,为了节省Gas,代码不会写得那么复杂,但如此会牺牲掉部分安全性能。

第二,如果调用者提供的Gas不足会发生什么呢?你执行调用的这个函数会回滚,这是Gas不足导致的后果,这个需要了解。

第三,登录者可以设置一个Gas price,Gas price的意思就是Gas的单价,也就是一个Gas等于多少ETH。如果你的Gas price设置较高的话,矿工就会优先处理你的交易。

我们知道在以太坊上,一个交易被最终确认的标准,就是它被矿工打包。这一点也会和后面谈到的一些安全问题有关。

第四,区块有一个Gas Limit,就是区块有一个Gas上限的限制。这是什么意思呢?假如你合约的代码,即某个函数如果写的特别复杂,那么它所需要的Gas特别多。假如多到超出了Gas Limit,这段代码永远都不会执行成功。以上是跟Gas有关的一些内容。

第二点是智能合约代码可以发送和接收以太币

这也是和我们之前看到的一些后端代码不是特别一样的地方,首先看一下接收,合约代码里有时候会有一个fall back函数。这个函数会在其他的地址给它的地址发送ETH的时候执行。

还有就是如果调用了合约里一个不存在的函数,比如说函数的名字没找到,或者函数的参数类型不匹配,就会执行fall back函数。假如fall back函数有payable标志的话,那么合约就可以接收调用者发过来的以太币。假如没有fall back函数,而你的合约提供一些其他的有payable标志的函数,并且其他合约也调用这个函数,你同样是可以接受以太币的。

假如fall back函数出现了一些异常,或者是fall back函数没有payable标志,其他的合约向你的合约地址发送以太币是会发送失败的。

这里谈到了常见的三种solidity语言里发送ETH的函数,transfer、send,还有call.value这三种方式。接着简单对比一下它们在发送失败时候的一些操作和一些情况。

我们看到transfer和send,它们在Gas使用上是有限制的,也就是它们只能传2300Gas进去。transfer在发送失败时会异常,并且回滚当前函数。而send和call只会返回一个force。

还有就是发币的方式可能有时候会被忽略。它是通过自毁函数的方式,向某个地址发送以太币。这是什么意思呢?一个合约里面如果有自毁函数的话,该自毁函数是可以传入一个地址参数的。那么它在自毁的时候,会把自己合约地址里剩下的以太币,发到你传的address里。

Solidity语言特性

说一下solidity语言里一些函数的调用方式,首先是call,这个应该是比较常见的,我把它叫做正常的调用。所谓“正常的调用”的意思是它是一个你用call调用的其他合约的函数,类似于调用一个web service的API一样,看起来比较正常。还有一种我觉得比较神奇的调用方式,这种调用方式不像是调函数,它类似于其他代码语言里import的操作,即把别人的代码加到自己的程序里面。

刚刚说到的是它运行的一些规则,现在再谈一下solidity语言本身的一些特性。

首先特别提一下函数的默认肯定性是public,因为智能合约里执行代码的一个基本的单元是函数。所以如果函数的防控出现问题的话,后果会很严重。

第二个是数值运算上的一些特性,其实前面也都提到了很多次溢出的问题。之前一些溢出的案例,像BEC,它造成的后果是很严重的。

我说一下除法上的一个问题,在早期版本solidity语言里执行除法,它的结果只取整数部分,小数点后面的部分就不要了。

现在先看一个小例子,比如这个函数,它的功能是让合约的拥有者可以用以太币回购他的代币。sellScale是兑换的比率,amount是你要回购的代币数量。

它是通过用amount除以sellScale得到应该花多少以太币去回收。假如amount小于sellScale,做除法之后,它的结果0点几,那么一取整就会变成0,导致的后果就是合约的拥有者他可以用0个ETH,即不花钱回收其他拥有者手里的代币。这是除法可能造成的问题。

第三个想说一下solidity中提供的抛出异常或者处理异常的一些方法,大概有三种。这三种在其他语言里也都有,比如说require,它往往是用在函数的开头,对该函数的输入做合规性的校验,比如在转帐的时候校验你的余额是不是充足。

assert在其他语言里也有,它是用来做币兑的判断。比如说assert1+1=2,如果1+1≠2的话,这个代码肯定是出问题了。它一般是放在函数的尾部。

revert则是直接抛出异常。

这三种异常,不管是require、assert触发的也好,还是直接revert的也好,它当前执行函数的代码都是会回滚的,这一点很重要。并且因为它的这个特性,我发现有一些意识流的操作,可能会避免一些新型的攻击方式,后面会谈到。

ERC20代币

第三个重点,其实作为一个黑客也好,作为一个安全工程师也好。你看一个新的东西,比如说智能合约,你最关注的肯定还是它有什么漏洞。你了解它的特别之处也是为了更好地去发现它里面的一些安全问题,所以重点在第三个内容。

我刚开始接触智能合约的时候,了解的只是网上公开的一些漏洞,BEC那些漏洞等等,它们都是ERC20的代币合约。所以最开始的时候我先去看了一下以太坊ERC20代币编写教程,它的官网有一个样例的代码。

我后来了解到其中的approve函数是有问题的,approve函数的功能前面也提到了,它允许你给其他帐户设置一个额度。这个函数一般会在什么场景下使用呢?你如果给其他人的外部地址转帐的话,其实没有必要用这个函数,transfer就可以了。它通常是用在我把我的代币付给另一个合约的时候。

如果你transfer给某人,他的代币可能就永远取不出来了。所以一般都是通过approve的方式,你要付款的合约给予额度,然后函数处理你要付给他的钱。

而approve函数有什么问题呢?这个函数很简单,它就是你传一个spend,即你要给别人配额的地址,然后传一个value值,即你要给别人多少钱。前面谈到,矿工是会优先处理Gas price高的交易。现在网上描述了这样一种安全风险,或者说攻击方式吧。

首先,假如用户A通过调用approve给用户B开了数量为N的配额,经过一段时间,A想把N修改成M, 但A的交易或者说发消息的请求,在链上都是可以被看到的,即便在被处理之前也是可以看到的。

那么B在看到A操作之后,他会以更高的Gas price发起一个交易,即通过调用合约里面的transferFrom函数,迅速地把之前给他的N个额度转走。然后A的approve,就是第二次调用,再次被矿工处理,那么B就又获得了M的额度,那么B可以再转出M。这样的效果就是B一共花了N+M的额度,即存在一个收入顺序上的攻击风险。

为什么以太坊官方的代币样例都会出现这个问题呢?想到这儿我就去看了一下ERC20代币的标准里面到底写了些什么内容,即EIP20。

简单说一下,它首先是规定了这三个重要函数的实现方式,一个是transfer,即转帐函数,一个是transferFrom,即刚刚提到那种场景的转帐,然后第三个就是approve。

标准中规定了这个函数的返回,按照规范它们要返回一个布尔类型的值,这是规范性上的要求之一。

第二个规定了他们在执行之后必须触发这两个事件,一个是transfer的事件,一个是approve的事件,这是前端会用到的,这是第二规范性要求。

我发现EIP20标准提到了刚刚说到的那种风险,它这里说的一个建议是前端在调用approve的时候,需要前端做一个限制。即我要把M改成N,或者N改成M的时候,我要先把N改成0,再从0改成M,这样的话就能规避刚刚说的那种风险。

但是它提到不建议在合约里做这样的限制,它是为了之前发布合约的兼容性考虑。但是它的标准底下给了一个链接,其中有一段代码。然后加了一行代码,做了刚刚说的那种限制,限制你的额度在修改之前是0。

所以它虽然建议不做限制,但是链接中还是给出了一种方案,说明它其实已经接受了这种防范的方式。这样的话,首先会增加这段代码所要花费的Gas,其实最开始也说到了,我们可能要在节省Gas和提升安全性之间找一个平衡。所以说在EIP20标准里,对这些安全问题还是有解释的。

再看一个,我们注意到transfer函数描述里有这样一句话,这个函数的数应该抛出异常,抛出一个异常,什么时候呢?即当它转帐的时候,如果帐户里代币的余额不够的话,应该抛出一个异常。它用的是一个比较温和的数,没有用must。这样的话,一些代币如果没有遵循这条建议的话,可能会有什么问题呢?这个建议是throw,如果没有throw呢?

ERC20代币假充值漏洞

这是之前网上披露的一种风险,叫假充值的漏洞。这个漏洞是什么意思呢?大家看两张图片上分别是两个transfer的实现方式,它们的逻辑都是差不多的。在转帐之前,首先对0地址做了一个判断。都是判断我的余额是否充足,如果充足的话,则进行先减后加的操作,上面是用了SafeMath,下面没有用。

余额不足的时候会有什么结果呢?底下这个通不过if,则会执行else,函数会直接返回一个force。而上面如果余额不足的话,会通不过require的校验。require的校验如果不是错误的话,那么这个函数就会throw,并且回滚掉,这就是两者之间的差别吧。也就是说上面这个函数它其实是遵循了EIP20标准里的这种规范性的实现,底下那个是没有的。

这样存在一个问题,什么问题呢?如果我们去in there sky上寻找相关的交易,我们发现用if else这种方法写的transfer函数在返回force以后,交易其中的data信息,它返回的仍然是一个success。为什么呢?因为这个函数在运行过程中没有抛出异常。那么这个data就是success。

这样的话,假如一些交易所在进行充值是否成功判断的话,仅仅根据是不是success去判断就会造成一些假充值的漏洞。那么具体怎么利用呢?比如说一个攻击者,他首先是往自己交易所里的代币地址去充值、去转帐。他转很多钱,然而他自己的代币地址里的钱是不够的,肯定就会转帐失败。

但是如果用if else这种方式写代币合约,就会得到success。假如交易所在判断你的充值是否成功时,仅仅判断它是不是success,如果是success,交易所就认为你充值成功了。这个时候你再发起提款的操作,从交易所的代币地址提款,交易所就会把它地址里的代币转回给你。但实际上你在充值的时候是没有任何损失的,然而你却可以把钱提出来。

这是一类风险,但是这种风险跟交易所在充值成功的校验上不是有很大关系,当然同时也是不能忽略的。这个代币合约其实是没有遵循EIP20标准建议的实现方式。所以说其实最佳安全实践很重要,这是我的一个小思考吧。

重入漏洞

刚刚说到最佳安全实践,我忽然发现其实还有很多可以描述的应用的场景。再看一个例子,我想大家对于重入漏洞或者说重入这种攻击方式应该是很熟悉了,因为太著名了嘛。这种攻击其实就是通过重入攻击去实现的,并且可以导致非常严重的后果。

在这里还是先简单说一下,withdraw函数是一个类似银行合约里的一种取款的操作,你通过这个函数可以把你之前存到合约地址里的以太币提出来。首先这个函数会判断你的余额够不够,如果够的话,它会通过call.value这种发送以太币的方式,去往你的地址里发送amount数量的以太币,然后它会把你在余额记录里边的余额给减掉。

看上去没有什么问题,但是下面这种方式,攻击者是在自己的恶意合约的fall back函数里,再次调用withdraw函数。因为刚刚提到了,在通过msg.send.call.value朝msg.send发送以太币时,它是会执行msg.send地址的fall back函数的。如果我在这个fall back函数里再次调用withdraw函数的话,由于balance的减少是在发币的最后面,所以这时候余额还没有减少,然后我就可以再次调用这个转帐函数,我可以继续把以太币给转出来。以上简单说一下重入的攻击方式。

再看一个具体的例子吧,这是我直接在CVR一篇文章中找的实验代码,这个就是刚刚描述的有重入风险的合约,ID money。它的withdraw函数就是用刚才那种方式实现的,其他的合约通过调用deposit函数往合约里面存钱。

这个是攻击合约,它主要干了这么几件事情。首先它有一个set victim函数,就是你在攻击之前设置你要攻击哪个存在重入漏洞的合约。然后step1、step2两个函数,就是攻击的两个步骤。你要攻击,第一个步骤肯定是先把攻击合约里边的一些钱,存到那个要被攻击的合约里面。

第二个步骤就是调用被攻击合约的withdraw函数去提款,然后在最底下的fall back再次重入,去调用withdraw函数。合约代码比较简单,然后大家可以看一下攻击的效果。

这个是在攻击之前,那个ID money合约里面,我存了有30个ether。首先是通过set victim设置一下被攻击合约地址,startAttack我传的参数是1,后面是18个0,就是1ether,因为这个单位就是V嘛,1ether就是10的18次方V,所以1后边是820。

它的攻击逻辑就是这个attack合约,先往ID money存1个以太,然后我每次往外提取0.5个以太。那么从打印记录来看,它确实是进行了很多次的转币操作。那么最后导致的后果是在攻击结束之后,这个ID money合约里,只剩下0.5个以太了。

这时候我之前的attack合约里实现了这么一种自毁函数,改变在自毁的时候把attack合约里的以太币发送到一个地址,现在攻击完成之后我发送一下,我会发现攻击者的帐户里马上就多了差不多30个以太币,说明这个攻击是成功了的。

对于这种重入的问题,我们现在是怎么解决的呢?首先第一个,建议你不要用call.value的方式,因为它是默认的。它是可以通过传三控制,但是默认的话它会把所有能用的Gas都传进去。这样的话,攻击者的fall back函数有足够的Gas去实施这种重入的调用。

现在大家应该都是用这种transfer去转帐,这样它限制在2300Gas,2300Gas是不够攻击者的fall back函数再次调用一个函数的,所以它从这个层面上规避了重入的风险。使用比较安全的转币函数是一种解决方法。

第二个我们应该遵循写代码的规范,就是checks-effects-interactions,什么意思呢?checks意思就是我在一个函数的开头,我应该首先要做输入的处理、检查,比如检查我的余额什么是不是够,这就是checks。

然后infix就是我接下来进行的动作,即infix对我合约本身有影响的一些操作。比如说刚刚提款的时候减去msg.send的balance的那个操作。

最后是interactions,就是和外部去交互,也就是说刚才那个发送以太币的操作,应该被放在函数的最后。

那么如果按照这种编码规范的话,确实是可以避免刚刚谈到的那种重入的问题。我在这两种方法后面打了两个问号,想表达什么呢?不是说我感觉这两种方法有问题,它没有任何的问题,我想表达的意思就是我觉得这两种方法可能都是在这种重入攻击出现之后,人们想到的一些方法。

比如说我用transfer转币,我用这种编码规范去编写。那我们有没有在重入攻击第一次出现之前,就通过某种意识流的操作,去规避这种风险呢?我想到了我在看以太坊官方ERC20代币实现样例时候的一段代码,就是它实现了transfer函数。然后它在这个transfer里面是做了一些什么样的操作呢?

它在转帐前首先是记录下了转帐和被转两个帐户的总量。然后在转完之后,通过assert的方式去做验证,即我转币前后两个帐户的和应该是相等的,这个应该是没有问题的。

当时我就在想,这么做有意义吗?感觉没有什么意义,我不知道这么做的意义在哪儿。

然而直到看到重入风险的时候,我突然意识到这好像是可以用上,怎么说呢?因为我刚刚做完那次重入攻击实验的时候,我通过balance of函数看了一下攻击者的合约的余额还有多少,我发现它变成了一个很大的值,很显然它是发生溢出了。因为那个代码的最后一行是直接用的减嘛,它没有用SafeMath之类的。

为什么它底下只用减呢?因为在上面它那个require是做了判断,它减的时候,它是认为amount肯定是小于balance msg.send的,所以在最后一行直接减。因为它没有预料到重入的风险,所以这个地方就发生了溢出。

这时候我就想,假如它这个代码在实现的时候,如果像以太坊代币官方实现那样,它的再生底下加一个assert,它像这样,它去校验转帐前后这个和是不是相等的话,这样会不会能够避免,就是规避掉重入的这种风险呢?然后我做了一下实验,就是加了一行代码,然后再次发起这种重入攻击,结果我发现这次重入失败了。

我查了一下余额,首先看记录,它是只有一次转出以太币的记录,0.5个。那么我整个攻击完成之后,我发现这个ID money合约里头还剩下35个以太,也就是说这是正常的,重入失败了。

再看一下这段代码,刚刚我最开始看到这段代码的时候,我觉得这种验证好像看不到它的意义。但是如果the door是这么写的,那么就可能会避免之前的那种攻击。

我们再看一下这个合约里还做了哪些实现,首先第一行它是校验了2是不是0地址,这是为了防止一些前端的失误。然后第二行是做了余额的校验,这实际上是一个下溢的校验。然后第三行require实际上是做了一个上溢的校验。

正常情况下,如果代币的总量是不会变的话,代币的总量一般是要小于2的256次方的。也就是说任意两个帐户的代币加起来,理论上是肯定不会发生溢出的,那这个地方为什么还要做这种校验呢?这种校验有什么意义呢?

之前我们做这种合约审计的客户,其实也一直在纠结这些问题,我给他们指出你这个地方少了一条校验,他们觉得这么做没意义。这样做我觉得体现的是一种思想,是什么呢?这个函数它是一个单元,我这个单元的安全性不应该去依赖其他的单元。

这个函数,虽然它的代币总量理论上是不会变的,这个地方是永远不会发生溢出的。但是这个函数作为一个单元,我要对自己单元的安全性去考虑,我需要做健壮性或者是安全性,我要做这种校验。你要是问我它到底什么时候会发生溢出,我真的不知道它什么时候会发生溢出。

我可以再多说一点,比如说你在做一个网站的时候,你看到这个网站随便传来一个畸形的urr之后,就报了一堆的错误,像什么500的页面就直接弹出来了,或者一些其他什么页面,你就会觉得这个网站做的很烂,你印象中这个网站就会存在很多的漏洞。那假如人家404、500都是自定义特别漂亮的页面,你觉得他们的网站可能实现得很好,感觉上漏洞会比较少。所以最佳安全实践其实很重要的,它在这里居然是可以起到防范0day的攻击方式的效果。

第三个我想谈一下ERC20代币里面一些代码逻辑的设计缺陷,就举一个例子,冻结帐户的绕过。这是之前做这种合约审计的时候发现的一个案例。理论上被冻结帐户数量应该不能再发生变化了,它这个类里边的transfer和transferFrom,在转帐之前都是先做过帐户是否处于冻结状态的校验。

但是跟传统的系统一样,它的入口不只一个,我发现它还提供了以太币和代币进行买卖,即兑换的的接口。那么你可以用以太币去买代币,也可以通过出售代币来获得以太币。

它的两个接口是通过直接采用增减balance,没有去调用transfer和transferFrom的实现。这样就使我通过这种方式就可以绕过。假如你把我的帐户冻结了,我通过这种方式就可以绕过。

函数访问控制绕过

现在谈一下函数的访问控制问题,因为刚刚谈到了,其实我觉得函数是智能合约里面一种单元的实现。那么一个函数往往能实现一个独立的功能。函数首先是类似语言中的一种修饰,比如说public、internal类似这样的修饰符去做访问控制上的限制,还有它可以通过自己定义modifier为一些函数增加自定义的访问控制。

如果函数的访问控制被绕过的话,产生的结果往往也是很严重的。那么用什么方式呢?首先是通过call注入,第二部分的时候提到了,它是solidity中的一种函数调用方式,对吧?那大家看这几行利用代码,类似call,它的意思是调用this地址的secret函数。

假如this是外部可控的,或者说call参数是外部可控的,那么就会造成一些问题。假如secret函数是一个比较关键的函数,它会执行一些比较关键的操作,它本来就有一个限制,require就只能是由合约自身调用。

假如这个infor函数里边存在call注入的风险,即这个data是外部可控的,这样的话攻击者通过精心构造一个data,让infor函数去调secret函数,那么msg.send就会变成this并通过require校验,最终执行secret operation。

首先还是看一个ERC20代币里面的问题,因为这一类的合约是目前见得最多的。这个还是官方的实现方式,就是approve and call,即在调用approve之后,通过send.call的方式去通知它。

这个是没有什么问题的,因为大家看到这个msg.send.,它只是传一个地址,这是不可控的,然后msg.send不存在刚刚那种data注入的风险。然而这个是有问题的实现的方式,它是直接把extra data传到了spend.call里。这样的话,spend是可控的,并且call第一个参数也是可控的。

如果是在一个代币合约中,肯定会有这种transfer函数。transfer函数肯定是根据msg.send去转帐,因为它只会减少msg.send的代币,然后给其他帐户增加代币。那这时候假如攻击者构造这么一条extra data,它就会直接通过approve and call函数去调用。这样的话,msg.send就是合约本身的地址,这样我就可以把合约本身这个地址中的代币给转出去。

ANT Token这个案例是一个实际场景中发生的案例,它在transferFrom的时候,是有这种call注入风险,就采用这种有风险的实现方式。就是receiving.call.value这一行,然后底下是攻击者怎么去利用它。

ANT使用了indoors防护控制的第三方的库,其中有合约属主这个设置,即合约owner,它只能是由合约本身地址去调用。那么通过刚刚那种描述的call注入的方式,攻击者就可以拿到合约owner的权限。

刚刚是call注入,那么通过delegate call也可以造成函数访问控制的突破。那么call注入一个关键点msg.sender,它会做相应的修改,即它的身份会变成合约本身。delicate call一个关键的利用点,就是代码A调用B,然后B去执行一个函数。它的上下文是A发起函数去执行B,然后让B通过这种delicate注入的方式去调用其他的函数。其他的函数在执行的时候,它的上下文是B合约,就相当于B合约直接从第三方加载了一段代码,然后过来执行。

这样的风险点其实是跟刚才类似的,首先address是有可能可控的,delicate call的参数也有可能。如果这两者都可控的话,我就可以让这个合约去调用以太坊中任意一个合约的任意一个函数,并加载到自己的地址去执行。这有点像我在渗透的时候找到了一个完全的代理。刚才call注入有点像反向代理的风险,比如说ssr,那么这个就像完整的代理,后果会更严重。

这里举一个简单的例子,调用攻击者自己写的合约attack,其中就一个操作,就是把owner设置一下。那么设置owner的上下文是在B合约里边,如果能够调用这个函数的话,B合约的owner就有可能被篡改掉。

这也是有过实际案例的,比如Parity钱包,它就存在delicate call的注入风险。由于msg.data是可控的,通过这么一串调用,当时的攻击者成功地获取了钱包的属主权限。

刚刚说了一下函数防护控制的问题,还有其他的什么问题呢?这些都是DSP TOP10里边列出来的问题。其中第10个我觉得比较神奇,它是Unknown,就是不知道。为什么把这些点列出来呢?我觉得也跟我前面谈到的一些想法一样。比如说重入攻击,这种攻击第一次发生之前,人们其实是不知道有这样的攻击方法存在的,也就没有做相应的防范,结果导致了很严重的后果。

所以它是因为以太坊智能合约安全之前没有太多人关注,现在大家可能都在关注,于是列了一个TOP10,并且把第10个风险写成Unknown。这也是我觉得为什么前面提到的transfer函数里,需要做单元安全上的考虑。虽然理论上认为一切正常的情况下,代币的总量是不会变的。但是如果有Unknown的风险出现,它的代币总量是不是就会变了呢?那么你在转帐之后,是不是就有可能会有溢出发生呢?这些大家都不知道。所以最佳安全条件很重要,这是今天第三遍说了。

最佳安全实践是什么呢?就是在黑客的眼里,或者说安全工程师的眼里,也不只是有漏洞。它其实也有一些对我们应该如何去实践安全、如何去做安全的一些思考吧。

本文分享自微信公众号 - 区块链大本营(blockchain_camp)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-08-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏FSociety

SQL中GROUP BY用法示例

GROUP BY我们可以先从字面上来理解,GROUP表示分组,BY后面写字段名,就表示根据哪个字段进行分组,如果有用Excel比较多的话,GROUP BY比较类...

5.2K20
来自专栏腾讯NEXT学位

今天我就说三句话

11620
来自专栏华章科技

穿越十年后看互联网+:家电行业的金矿在哪里?

现在市场上炒得火热的智能家居未来出路在何方?做智能家居的创业者应该注意哪些机会?传统家电厂商又到底如何借助互联网进行转型?本文以智能空调为例,用故事的形式,提前...

8310
来自专栏非著名程序员

「我真的没有改需求」

11910
来自专栏Ken的杂谈

【系统设置】CentOS 修改机器名

18030
来自专栏儿童编程

一张图理清《梅花易数》梗概

学《易经》的目的不一定是为了卜卦,但是了解卜卦绝对能够让你更好地了解易学。今天用一张思维导图对《梅花易数》的主要内容进行概括,希望能够给学友们提供帮助。

32140
来自专栏儿童编程

我不是算命先生,却对占卜有了疑惑——如何论证“占卜前提”的正确与否

事出有因,我对《周易》感兴趣了很多年。只是觉得特别有趣,断断续续学习了一些皮毛。这几天又偶然接触到了《梅花易数》,觉得很是精彩,将五行八卦天干地支都串联了起来。...

15310
来自专栏非著名程序员

这是对付产品经理的一副毒药,程序员慎入

程序员和产品经理的日常就像是一对天生的冤家,为了需求的实现,几乎天天在争吵。这不,就在昨天各大技术和产品群里一个程序员暴打产品经理的视频火了,被广泛传播。

12520
来自专栏haifeiWu与他朋友们的专栏

复杂业务下向Mysql导入30万条数据代码优化的踩坑记录

从毕业到现在第一次接触到超过30万条数据导入MySQL的场景(有点low),就是在顺丰公司接入我司EMM产品时需要将AD中的员工数据导入MySQL中,因此楼主负...

29740
来自专栏web前端教室

你可以从面试中学到什么?

讲一下我对面试的一些。。。“偏见”,哈哈,熟悉我的同学们一定要批判的读接下来的内容哈。

12200

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励