ERC20的ProxyOverflow漏洞造成影响广泛,本文将对其攻击方法进行分析,以便于智能合约发布者提高自身代码安全性以及其他研究人员进行测试。本文选择传播广泛、影响恶劣的SMT漏洞(CVE-2018–10376)作为样本进行分析,文中所涉及的代码截图均来自于SMT代码。由于目前各大交易平台已经将ERC20协议的数字货币交易叫停,本文的发布不会对这些货币带来直接影响。
1 ERC20货币及transferProxy函数
1.1 ERC20货币简介
基于ERC20协议的数字货币(以下简称为ERC20货币)实际上是以太坊上运行的智能合约,合约中对于每个账户拥有的货币数目是通过 账户地址→货币数 的映射关系进行的记录:
mapping (address => uint256) balances
ERC20货币的拥有者要想进行货币交易、余额查询等操作时,需要向智能合约对应的地址发送消息,声明调用的函数和相应参数。这一消息将会矿机接收,并执行智能合约中相应的函数代码。在这一过程中,消息发送者需要向挖矿成功的矿机支付相应的报酬。这笔报酬在以太坊中被称作gas,其支付货币为以太币。也就是说,ERC20的货币拥有者要想发送货币交易消息,就需要拥有一定数量的以太币。
然而,ERC20货币拥有者并不一定拥有以太币。为了满足他们发起货币交易的需求,ERC20 协议提供了transferProxy函数。利用该函数,ERC20货币拥有者可以签署一个交易消息,并交由拥有以太币的第三方节点将其发送到以太坊上。消息的发送者会从拥有者那里获取一定数量的ERC20货币作为其发送消息的代理费用。
1.2 transferProxy函数代码分析
SMT的transferProxy函数代码如下图所示:
该函数的各个参数解释如下,该函数代码逻辑较为简单,此处不做赘述。
- address _from:ERC20 货币的拥有者和交易的发起者;
- address _to:货币交易中的接收者;
- uint256 _value:货币交易的数额;
- uint256 _feeSmt:交易信息发送者(即函数中msg.sender)收取的代理费用;
- uint _v,bytes32 _r,bytes32 _s:交易发起者(即_from)生成的签名数据。
需注意的是,代码215行中的transferAllowed(_from)是transferProxy()运行前必会运行的验证函数。该函数代码如下:
代码117行中的exclude为映射结构,仅合约的创建者将为设置为True,其他地址默认均为False。
代码118行判定transferEnabled标志符是否为true,该标志只能通过enableTransfer函数设定,且该函数只能被合约创建者调用,该函数的作用是使得ERC20合约的交易过程可控,这也是SMT等货币出现问题时能够在后续中止交易的原因:
代码119-121行对于交易发送者(即_from)帐号是否被锁定进行了检查,lockFlag和locked都只能被合约创建者所控制:
综上所述,只有整个合约在允许交易且攻击者帐号未被锁定的情况下,攻击者才能真正调用transferProxy函数。在参数处理过程中发生漏洞的原因可参见我们之前的分析文章:https://weibo.com/ttarticle/p/show?id=2309404232782242012923。
2 攻击重现
为了重现攻击,我们选择了基于go语言编写的以太坊客户端geth进行以太坊私有网络的部署。为了便于实现可编程的自动化交互,我们选择了Web3.py作为与以太坊节点交互的中间件。
2.1 漏洞验证环境的搭建
S1. 从链接页面下载SMT智能合约源码;
S2. 创建两台Linux虚拟机;
S3. 准备Python运行环境,在两台虚拟机上安装python3,并利用pip安装web3、py-solc、hexbytes、attrdict;
S4. 准备合约编译环境,在两台虚拟机上安装智能合约代码编译器solc,参考链接;
S5. 在两台虚拟机上搭建以太坊私有网络,可参考链接,其中:
— 1) 节点1用于发布SMT合约代码,为其创建以太坊账户并分配一定数量以太币,启动挖矿;
— 2) 节点2用于部署攻击代码,创建两个以太坊账户,分别作为transferProxy中的from账户(转账消息签署者,记为Signer)和transferProxy调用者(即转账消息的发送者,记为Sender),为Sender分配一定数量以太币,并启动挖矿。
2.2 SMT智能合约发布
在节点1上,利用deploy_SMT.py脚本中的代码实现SMT智能合约的一键部署。
关于执行前的配置的介绍:
1) sol_path,代表合约代码路径;
2) account,代表用于发布合约的账户,如1.2所示,只有该账户才能调用部署好的智能合约函数,进行控制交易开启和关闭,维护被锁账户列表等操作;
3) pin,用于解锁account的密码。
关于执行过程与结果的分析:
1) tx_receipt,该变量用于获取部署智能合约(23行)和发送启动交易消息(35行)的结果,当这两行代码被调用后,以太坊网络中会发布相应的消息,只有在下一个区块被挖掘出来后,tx_receipt才能获取非空的结果;
2) contract_address,代表该合约被顺利部署到以太坊网络后的合约地址,其他节点要想调用合约代码,需要获知该地址以便发送函数调用消息。
合约代码部署结果的截图如下:
2.3 ProxyOverflow漏洞攻击
在节点2上,利用test_SMT.py脚本中的代码可实现针对SMT合约的一键攻击。
关于执行前的配置的介绍:
1) contract_address,来自2.2中SMT部署完成后的输出值;
2) sol_path,代表合约代码路径;
3) signer,交易信息的签署者,也将作为调用transferProxy时的_from和_to的实参;
4) sender,交易信息的发送者,需要拥有一定数量以太坊以支付gas费用;
5) signer_pin,signer的密钥解锁口令,以便对交易信息进行签名;
6) sender_pin,sender的密钥解锁口令,以便解锁sender账户,支付gas费用;
7) value,代表发生交易的金额;
8) fee,代表支付给sender的代理费用;
9) signer_key_path,代表signer的密钥文件路径。
关于执行过程与结果的分析:
1) 30-35行,基于目标智能合约地址和代码,创建智能合约对象;
2) 37-38行,获取并打印sender和signer在攻击前的SMT币数目;
3) 40-43行,获取signer现有的nonce的值,并将其扩充为64字符的字符串;
4) 46-62行,构建要进行签名的数据的Hash值,获取signer的私有密钥,并对Hash值进行签名,获得签名数据s,r,v;
5) 63-77行,构造transferProxy函数调用参数,进行函数调用,并获取交易回执;
6) 79-80行,获取并打印sender和signer在攻击后的SMT币数目。
攻击结果的截图如下: