智能合约安全分析与闪电贷利用
受众群体:
- 对智能合约感兴趣的
- 了解安全的
- 希望了解区块链和智能合约安全的
演讲稿:
大家好,今天分享的是智能合约安全分析与闪电贷攻击,我将从以下几个方面来讲解,可能里面有不对的地方,请多多指正。
首先我们知道区块链大致上可以分为以下几层:从最底层的数据层,这一块大部分来说是用的 LevelDB 数据库,然后是从中间的 P2P 协议层,以及最后的应用层,纵观这些范围来讲,其实每一个地方都存在着安全风险。不管是比特币,还是以太坊,在历史上都出现过很多的漏洞。
这里就给大家举两个例子,一个是比特币在 2018 年的 CVE,,可以造成拒绝式攻击,严重的话可以造成通胀,就是地址余额变多了,相当于是超发了,这个也算是比较严重的漏洞了,那它是怎么造成的呢?这里主要是在 CheckBlock() 函数,该函数在节点接收到新的区块时被调用。CheckTransaction() 函数对于传入的交易消息进行检测,其中包括了检测一笔交易是否发生双花。如果发现 utxo 被重复记录了两次,就会返回处理失败的信息。可以看出,这段检测代码在被 CheckBlock() 函数的调用过程中被认为是费时的,并通过将函数的第三个参数设置为 False 的方式,使其跳过。利用的话,如果通过 p2p 传播交易,就会被检查出来,但是如果是有一个恶意区块,里面的双花交易是没有经过内存池的,所以就会被绕过校验,从而形成一种增发的现象,并且也会造成 DoS,形成恐慌。
另一个例子的话,可能大家都有经历过,就是前段时间,以太坊出的那个关于 EVM 的漏洞,同样的话,攻击者如果构造一个恶意合约,可以将节点直接打崩溃,危害其实也很大的,配合上做空机制,攻击者同样也能赚到很多钱,那它是怎么造成的呢?大家可以看一下这块的代码,在 staticCall 的时候,被重复覆盖了数据。导致前后不一致,这个时候没有更新代码的节点就会崩溃。这里的 staticCall 其实只是读取一个合约,然后读取相关的信息,而攻击者提供的参数正好是一个预编译合约,0x4,这个合约也很简单,就是返回原有的内容。然后传递的参数中,正好输入和输出的产生了重叠,而 staticCall 处理的时候,是按指针处理的,正好影响到了输出,于是产生了不一致,就无法通过,实际造成了链的分叉。
不管是底层服务,还是中间的协议,应用层,基础设施,其实很需要注重安全的,尤其是那些和钱打交道的一些服务,比如说交易所,比如说钱包,很多攻击者,就一直盯着这些服务,如果是中心化的,那么就会更加的容易被攻击。之前交易所被攻击的太多了,数不过来,之前币安有一次都被盗了八千多个 BTC,讨论矿工去强行逆转那笔交易,当然最后没有实行,损失如果算起来的话,也很严重的。
不知道大家有没有想过一件事情,如果从一个地址或者公钥可以反推出私钥,或者是破解了这个私钥,那么不就可以把钱转走,或者通过量子计算机去破解,那就可以很有钱了。这也说明了密码学对于虚拟货币的重要性,基础的话基本上是数学的基本推理,前段时间,有人发了一个关于门限 ECDSA 的两种安全攻击,里面影响了 zcash,Binance,Anyswap 等用到这个协议的,这个发现大概价值 50 万美金,所以说,底层的安全影响也很大的。关于这个攻击就不给大家讲了,主要我也不会,不是密码学博士,里面要求数学和密码学的功底还是很高的。
区块链最直接的,跟我们联系最紧密的还是应用层,像一些 defi 项目,或者是 Dapp,在这两年经历了很多攻击,其中也不乏一些没有审计过的应用。在这个过程中,只有黑客是获利最大的,项目方尽可能地挽回损失然后跑路,直接造成投资者的损失。并且之前还出过很多比较简单的漏洞,造成几十亿的损失,之后会详细讲一下。
大家大致了解了区块链安全以后,接下来一起来看一下智能合约。
讲 solidity 的话,先得讲一下脚本语言在比特币上的实现,中本聪其实很早地就在比特币上留下了脚本语言的模型,利用 utxo,我们可以往里存放一些脚本,比方说,在某一高度的时候进行转账,或者是一个简单点的,2 + 4 = 6,在比特币中是怎么实现的呢,具体的代码的话,是 2 4 ADD 6 EQUAL ,执行的时候,是以栈的方式来实现的,栈的大小是有限的,先将 2 和 4 压栈,然后 ADD,就变成 6,再将 6 进行压栈,然后最后通过 EQUAL 就可以得到结果 True 了。但这种方式实现的脚本编程是非图灵完备的,复杂性受限,也没有循环等,执行的次数有限。
这个时候,以太坊横空出世,号称下一代智能合约和去中心化应用平台,就用了 solidity 语言,现在用这个实现的技术越来越多,从最早的 DAO,到 Dapp,再到 Defi 等。它实现了图灵完备,有循环,合约之间也可以互相调用,甚至可以跨链调用,这个时候就有一个问题,比方说有个合约,它写了一个死循环,然后又发了一个激活这个合约的交易,矿工受到打包执行这笔交易的时候,也就在跑这一段代码,那么就让矿工和节点一直卡在那里,为了解决这个问题呢,以太坊一开始就设计了 gas 这个概念,合约每进行一个操作,就消耗一定的 gas,然后设置一个 gas limit,不能超过这个上限,然后消耗的 gas 再乘以当时设置的 gas price 就是最终的手续费,由矿工打包。这块详细的可以参考文档。
那么以太坊合约是在哪里执行的,像 java 有 java 虚拟机,有些语言有运行时,以太坊这边的话,是在 evm 上执行的,这里画了一张图吧,就是代表 evm 的结构,evm 也是一种基于栈的虚拟机,在存储上主要分为几部分,一类是,code 和 storage,这种是持久化的,code 用来保存合约的二进制,storage 的话,用来保存合约执行的全局变量。其他还有就是 stack 用来保存局部变量,最多有 16 个。memory 的话,就类似之前讲的内存一样,存储临时数据。还有 evm 是如何执行的,这里不是基于寄存器的,通过不断地压栈和弹栈,然后去取内存的内容,通过程序计数器记录当前的指令,不断地执行。
在讲指令之前,我想先介绍一下这个东西,ABI,全名的话是,应用二进制接口,可以通俗的理解为合约的接口说明,接口文档。当合约被编译后,那么它的 abi 也就确定了。当合约被调用的时候,会指定函数名和参数,这个在 web3.py 里或者是其他调用合约的代码,是根据 abi 来找到对应的函数,然后执行,生成编译码,传到交易的 data 字段。
我们在算 gas 的时候,不可避免的要和这个打交道,opcode,大家学过汇编的时候,应该要学一些指令,像什么 JMP,EIP,ESP,在 evm 里也不例外,最多只能有 256 条指令,就对合约这块做了更多的优化,尽可能的简单,每条指令消耗多少 gas 也比较清楚。所以说,大致的流程是这样的,开发人员写 solidity 代码,然后编译器会编译成字节码的形式,同时生成 abi 文件,然后将字节码和 abi 文件打包成合约,然后发布到以太坊上,然后用户可以通过以太坊的浏览器,查看合约的信息。节点收到交易的时候,对应的就会部署这个合约,有交易如果要打到这个合约地址上时,也会执行这个合约,通过字节码的形式。
那么 evm 是如何执行的,这里一般和电脑上的软件一样,也会分静态和动态,静态的话,会直接去反编译字节码,这里举两种方式,一种是通过在线反编译器,网址的话是这个,可以直接对链上的合约进行反编译,https://ethervm.io/decompile ,另一种的话,使用一些常见的工具,加一些插件,比如说 IDA 神器,反编译完成之后,就可以看到指令的具体内容,这样就可以更加直观的看到合约的执行过程和数据流了。这样有什么好处呢,比如说有一个没有开源的合约,我们想要去找它有没有问题,通过这样的分析手段,了解它的执行流程,分析其可能存在的问题吧。
现在有人应该有个问题,我们如何收集链上的合约呢,总不能每次都要自己写吧,这里提几个思路,一种是查浏览器,有些浏览器会有个地方是合约列表,去爬这个列表,另一种的话是调用节点,通过 getCode 这个方法,传地址的参数,先爬一遍创建合约的地址,然后通过这个方法,获取合约的字节码什么的。但是这几种都有缺陷,现在合约有自毁的功能,比如有个合约不用了,然后它调一个 selfdestruct,那么这个合约就会自动被销毁,这个时候,我们就不能获取到这个合约的字节码了。还有一个思路就是修改节点,在相关地方插桩,hook 相关的地方,将合约信息保存下来,这样几天同步节点的时间就可以得到全部的合约了。
接下来的话,会讲一下,智能合约开发和分析当中,会遇到什么样的问题,之前有些机构排过智能合约风险前十大排名,可以简单了解一下,具体会详细的讲其中几个。
一个的话是溢出,这个可能有些人听过,在安全方面,溢出也很常见吧,在这里主要还是整形的溢出,分为上溢和下溢,上溢的话,给大家举个例子,比如说一个 4 位的数字,那么它就是从 0x0,到 0xFFFF,这个时候,如果 0x2 + 0xFFFF 如果不加以判断的话,就会造成一种溢出,或者是 0x01 - 0xFFFF,这个时候又会是另一种结果。这种也有实际例子,之前美图有一个链就是因为这个问题,损失了好几十亿,这种的话,挖掘需要判断输入的是否可控,传进的参数是否进行校验,是否使用了安全的库。
另一个是重入,之前 ETH 有一次很重要的分叉,就因为这个,分出了 ETC 这个币种,这个漏洞大致的原理是这样的,合约之间可以互相调用嘛,这里认为合约中所有的外部调用都是不安全的,都有可能存在重入漏洞。例如:如果外部调用的目标是一个攻击者可以控制的恶意的合约,那么当被攻击的合约在调用恶意合约的时候,攻击者可以执行恶意的逻辑然后再重新进入到被攻击合约的内部,通过这样的方式来发起一笔非预期的外部调用,从而影响被攻击合约正常的执行逻辑。
再给大家介绍一种情况,条件竞争。这个漏洞主要是这个 approve 函数,该函数的主要功能是授权给第三方让其代替当前账户转账给其他账户,但是在这个函数当中却存在“事务顺序依赖性问题”,假设有两个用户:用户A,用户B,首先用户A 通过调用 approve 函数允许用户B代其转账的数量为 N(N>0),经过一段时间后,用户A决定将 N 改为 M(M>0),所以再次调用 approve 函数;用户B在第二次调用被矿工处理之前迅速调用 transferFrom 函数转账 N 数量的 token;用户A对 approve 的第二次调用成功后,用户B便可再次获得M的转账额度,即用户B通过交易顺序攻击获得了 N+M 的转账额度。
其他的一些风险,没那么广泛,需要一定的利用条件,这里再简单列一下,有兴趣的话,下来可以继续讲一下。
在了解了这些安全问题以后,就会问了,我们怎么去利用他们,这里有几种利用方式,目前我还没有找到一些自动化利用框架,应该还处在科研阶段,一种是直接发交易,构造特殊的交易到这个合约地址上,另一种的话,是再部署一个恶意合约,这个合约去调用正常的合约,达到一些特殊的目的。这些可以通过起一个本地的测试链,加上一些合约部署工具来实现测试。
之前讲的基本上是以太坊平台的,其他平台其实或多或少地都有一定的问题,这里举几个攻击的例子,比如 BSC 上就遭遇过类似的攻击,按照原理上来说,这些风险都是差不多的。
休息时间。
接下来的话,就是讲一下闪电贷这个东西,我们知道有一个是原子性的东西,这个时候 Defi 出现了一种项目,如果你在同一个交易里完成了借款和还款的操作,那么是不用抵押东西的,也就是几乎没有成本,举个例子,比如说,有两家去中心化交易所有价差,这个时候我们怎么套利呢,比如一个交易所的交易对是另一个的一半价格,当然实际不会这么夸张,这个时候,我们可以走闪电贷,借点钱出来,在这两个交易所之间搬砖,先去这个交易所买点币,然后直接链上去另一个交易所卖掉,然后归还之前借的闪电贷,中间的收入减去手续费就是利润了。但这种现在比较少了,更多的是黑客进行攻击一些协议,之前这个协议就被攻击过,过程是这样的,首先从闪电贷借点 eth,然后去另一个借贷协议抵押一部分 eth 借到 wbtc,然后这个时候去 dZx 这里做保证金交易,开了一个 eth/btc 的做空,这个时候拿一部分钱去另一个交易所大量的用 eth 买入 btc,这个时候做空不是算亏损嘛,但是价格达到一个高点的时候,就在刚才的保证金交易进行抛售 btc,赚 eth,之前做空开了 5 倍,然后把钱还给闪电贷,中间的收入减去手续费就是利润了,这次赚了大概几十个 eth。
所以说闪电贷是一种致富思路和财富密码吧,如果发现某个 defi 协议有漏洞,就可以这样攻击,然后进行获利,这些协议都是开源的,这样的攻击也可以使得系统更加健全,之前的中心化金融就没有这么多应用了。
讲完这些问题以后,接下来讲一下,如何去分析出这些问题,当然有一种方式是找相关公司进行审计,花钱嘛,没有花钱办不到的事情,这里主要讲一下通过工具来进行分析的一些思路。这里主要分为几种,静态分析,动态,机器学习。
静态分析的话,目前业界主要是有两种思路,一种是生成控制流图,通过生成的控制流执行流,利用之前写的插件规则,校验对应的问题。缺点的话,就是需要维护规则,依赖生成的路径。
另一种的话是形式化验证,这个相对来说是有一定的技术门槛的。这里就不讲了。
动态分析的话,顾名思义,就是将这个合约跑起来,利用虚拟机提供的特性,进行模糊测试等。以及动态污点分析,其实也算是一种跟踪技术。这块实现业界还比较少,要么是论文,要么是公司产品。
机器学习。有一篇论文思路是通过 GNN 模型,来进行智能合约漏洞的检测,这种算是卷积神经网络和图的一种结合和升级。为什么要用这种,还是由于虚拟货币的一些特性,我们相互转账的行为,在分析上可以用图数据库的方式。具体可以看 《Smart Contract Vulnerability Detection Using Graph Neural Networks》这篇论文。总体来说,是将合约源代码特征化为合约图,然后规范化突出节点,用神经网络结合专家知识库进行检测。
最后一个内容的话,是如何去防御,这里简单提几点,一个是保持良好的编码习惯,尽可能地避免常见的漏洞,一个是找相关公司审计合约,然后就是多看之前人总结的经验,形成的一种官方库。
可能有些人想要了解一下,这个合约如何进行升级,比如临时发现了问题,想升级版本去解决,这个不像传统应用,直接发布到链上就不可更改的,但还是有一些升级的方式,目前升级的策略主要有以下两种,一种是数据分离,另一种是通过代理的方式。数据分离的意思是这样的,我们将逻辑和数据分开,部署成两个合约,然后升级的时候,数据合约修改逻辑合约的地址。代理的方式,主要是利用了 call 和 delegatecall 的上下文的不同,一个是上一个账户的,一个是外部账户的,在合约和账户中间,还会部署一个 proxy 代理合约,相当于传递数据给最终的合约,当合约升级的时候,proxy 合约的地址就会发生变化,指向升级后的合约。
未来的话,我觉得还是有很多可以值得思考的,但这里加密货币的特殊性,代码即法律,不像传统应用,会尽可能地隐藏代码,反调试,在智能合约领域,谁可以更快地找到或者避免这些问题,谁将更占优势。
谢谢大家。
具体 ppt 需要联系获取。