Knowsec Blockchain Lab | In-depth understanding of denial of service vulnerabilities
As DoS attacks become more rampant and more serious, how can contract developers fix bugs? How best to prevent such attacks?
1. Introduction
DoS is short for Denial of Service. Any interference with a Service that reduces or loses its availability is called a Denial of Service. Simply put, normal service requests that a user needs cannot be processed by the system. For example, when a computer system crashes or its bandwidth is exhausted or its hard disk is filled up so that it cannot provide normal service, it constitutes a DoS.
Denial-of-service attacks: Attacks that cause DoS are called DoS attacks. The purpose is to disable the computer or network from providing normal service.
On the Internet, DoS attacks can be roughly divided into three categories: the use of software implementation defects; Exploit loopholes in the protocol; Use of resources to suppress. In the blockchain, DoS attacks disrupt, suspend, or freeze the execution of a normal contract, or even the logic of the contract itself.
2. Overview of vulnerabilities
In Solidity, a DoS vulnerability can be simply understood as “unrecoverable malicious manipulation or uncontrolled unlimited resource consumption”, i.e. a DoS attack on an Ethereum contract that could result in massive consumption of Ether and Gas and, worse, render the original contract code logic unworkable.
For example, there are three checkout points in the supermarket. Normally, people queue up to pay by scanning the code at the checkout point. However, one day, there was an Internet problem, and all the customers at the checkout point failed to scan the code and pay. Or, when paying, customers deliberately make trouble, so that the following customers can not pay, which will also lead to the supermarket can not operate. We can see that there are internal ones, that can cause the DoS attacks.
In smart contracts, the same is true. By consuming the resources of the contract, the attacker can make the user temporarily quit the non-operable contract, or even permanently quit the contract, thus locking the Ether in the attacked contract.
3. Vulnerability analysis
There are three types of DoS attacks in smart contracts:
- Manipulate a map or array loop externally.
- Owner operation.
- Progress status based on external calls.
3.1 Externally manipulate a map or array loop
This situation is generally due to the fact that the mapping or array loop can be manipulated externally by others, and since the length of the mapping or array loop is not limited, resulting in massive consumption of Ether and Gas, finally, the smart contract is temporarily or permanently inoperable. In smart contracts, it is common for the ‘owner’ of the contract and its investors to appear when the ‘token’ is allocated, as in the ‘distribute ()’ function in the following contract.
contract DistributeTokens {
address public owner; // Contract owner
address[] investors; // Investor array
uint[] investorTokens; // The number of tokens received by each investor //... Omit related functions, including transferToken ()
function invest() public payable { // invest
investors.push(msg.sender);
investorTokens.push(msg.value * 5); // 5 times value
}
function distribute() public { // distribute
require(msg.sender == owner); // Only the contract owner can operate
for(uint i = 0; i < investors.length; i++) {
// Where transferToken(to,amount) transfers "amount" to address "to"
transferToken(investors[i], investorTokens[i]);
}
}
In the above code snippet, we can see that the `distribute()` function will traverse the investor array, but the loop traversal array of the contract can be artificially expanded by outsiders. If an attacker wants to attack this contract, Then he can create multiple accounts to join the investor’s array, so that the investor data becomes very large, so that the amount of gas required to loop through the array exceeds the upper limit of the block gas amount. At this time, the `distribute()` function will unable to operate normally, this will cause a DoS attack of the contract.
In this case, the contract should not bulk operate on a map or a circular array that can be manipulated by an external user. It is recommended to use fetch mode instead of send mode, that is, each investor can get their money back by using ‘withdrawFunds ()’.
If contracts must be transferred by traversing a variable-length array, it is best to estimate how many blocks and how many transactions are required to complete them, thus limiting the array’s length, you must also be able to track where you are going so you can recover from there if the operation fails. As shown in the following code, you must ensure that other ongoing transactions do not make any errors before the next execution of ‘payOut ()’.
struct Payee {
address addr;
uint256 value;
}
Payee payees[];
uint256 nextPayeeIndex;
function payOut() {
uint256 i = nextPayeeIndex;
while (i < payees.length && msg.gas > 200000) {
payees[i].addr.send(payees[i].value);
i++;
}
nextPayeeIndex = i;
}
3.2 Owner action
In token contracts, there is usually an owner account, also known as the contract owner account, which has the right to open/suspend transactions. If the owner's address is lost, the whole token contract cannot be operated, resulting in non-subjective DoS attacks.
bool public isFinalized = false;
address public owner; // Contract owner
function finalize() public {
require(msg.sender == owner);
isFinalized == true;
}
// ... Some extra ICO features
// Rewrite the transfer function to check isFinalized
function transfer(address _to, uint _value) returns (bool) {
require(isFinalized);
super.transfer(_to,_value)
}
In the above contract, the entire operation of the token system only depends on one address, which is the owner's address. After the END of THE ICO, if the privileged user loses, his/her private key may become inactive. Then, the function of ‘Finalize ()’ cannot be called to open the transaction, and the user cannot send tokens all the time. The contract will not work properly.
In view of the above situation, the contract should not make the entire token system only depend on one owner address, you can set multiple user addresses, you can also set the suspension of trading time, exceed the time, or meet a certain condition to open the transaction so that the whole token system will not be attacked by DoS. The following code can be used as a reference to prevent DoS attacks caused by owner actions.
require(msg.sender == owner || now > unlockTime)
3.3 Progress status based on external calls
If the smart contract state changes depending on the result of the execution of external functions, but there is no protection against the failure of execution, then if the external call fails or is rejected for external reasons, DoS attacks may occur. For example, if a user creates a contract that doesn’t accept Ethereum (non-payable) if a normal contract needs to send Ethereum to a contract that doesn’t accept Ethereum in order to reach a new state, the contract will be rejected and won’t reach the new state.
pragma solidity ^0.4.22;
contract Auction {
address public currentLeader; // Current bidder
uint256 public highestBid; // The highest bid
function bid() public payable {
require(msg.value > highestBid); // The transaction carries more Ether than the current highestBid
require(currentLeader.send(highestBid)); // Return the current highestBid to the current bidder currentLeader
currentLeader = msg.sender; // Set the new bidder to the message caller msg.sender
highestBid = msg.value; // Set the new highest bidding price to msg.value
}
}
If the user executes the ‘bid’ function with more ether than the current ‘highestBid’, then highestBid’s ether will be returned to the current bidder ‘currentLeader’.
The new current bidder is then set as the calling user, and ‘highestBid’ is also set as the Ethereum that the user carries when initiating the transaction. The contract code looks fine, but bidding through the contract becomes problematic when a malicious attacker deploys the following attack contract.
pragma solidity ^0.4.22;
interface Auction{ // Sets the original contract interface to call the function
function bid() external payable;
}
contract POC {
address owner;
Auction auInstance;
constructor() public {
owner = msg.sender;
}
modifier onlyOwner() {
require(owner==msg.sender);
_;
}
function setInstance(address addr) public onlyOwner { // Point to the original contract address
auInstance = Auction(addr);
}
function attack() public onlyOwner {
auInstance.bid.value(msg.value)();
}
function() external payable{
revert();
}
}
The attacker uses the attack contract to call the bid() function to transfer money to the bid contract and become the new bidder ‘currentLeader’. Then the new bid() function is executed to bid.
When performing ‘require(currentLeader.send(highestBid))’ ‘return Ethereum,’ the ‘fallback()’ function that attacks the contract does’ revert() ‘and cannot accept Ethereum, causing it to remain false. All the other bidders would lose and eventually attack the contract and win the auction with the lower Ethereum.
In view of the above situation, if the result of the external function call needs to be processed before entering the new state, it must be considered that the external call may fail all the time, or time-based operations can be added to prevent the external function call from failing to meet the require judgment.
4. Relevant cases
4.1 Demo case
DoS attacks will be demonstrated in detail and an example will be attached.
The following contract code is modified according to the third point in vulnerability analysis based on the progress of the external call. The normal operation logic is that any bid higher than the current contract price can become the new president.
The deposit in the contract will also be transferred to the previous president through the transfer() function. There is no problem with that, but Ethereum has two types of accounts, external accounts, and contract accounts.
If you launch becomePresident() and call an external account that’s normal, but if you launch becomePresident() and call a contract account, And it maliciously used ‘revert()’ in the ‘fallback’ function of the contract account, so the ‘fallback’ function would be triggered when other users returned ether to the contract account when they launched ‘becomePresident’. The logic in ‘becomePresident()’ cannot become the new ‘president’ anymore.
So let’s take a look at the contract code in question. Here we set the contract code to PresidentOfCountry. Sol:
pragma solidity ^0.4.19;
contract PresidentOfCountry {
address public president; // the president's address
uint256 price; // offer
function PresidentOfCountry(uint256 _price) { // Constructor, set the initial price
require(_price > 0);
price = _price; // Set the initial price
}
function becomePresident() payable { // becomePresident
require(msg.value > price); // The amount of Ether paid must be greater than the current president's competition fee
president.transfer(price); // Return ether to the previous president
president = msg.sender; // Set the new president as the competitive user
price = price; // Set the latest competitive price
}
}
Before writing the attack contract, let’s take a look at the two account types of smart contracts and the fallback function.
There are two account types in Ethereum:
- The external owned accounts, or user accounts, are controlled by a private key.
- Contract Accounts, executable code, and private state, controlled by contract code.
Fallback function: a fallback function is the one and only unnamed function in each contract that has no arguments and returns no value, as shown below:
function() public payable{
...
}
The fallback function is executed in the following cases:
- No function was matched when calling the contract;
- No data is transmitted;
- Smart contract receives Ethereum (in order to accept Ethereum, the ‘fallback’ function must be marked ‘payable’).
Let’s write the attack contract. There are two main points, one is the external call ‘becomePresident’, and the other is the use of ‘revert’ in the fallback function.
pragma solidity ^0.4.19;
import "./PresidentOfCountry.sol";
contract Attack {
function Attack(address _target) payable { // Constructor that sets the destination contract address and makes the external call becomePresident with call
_target.call.value(msg.value)(bytes4(keccak256("becomePresident()")));
}
function () payable { // The revert function returns an error using revert
revert();
}
}
In ` Remix ` debugging in view the results, the first to use account (0 x5b38da6a701c568545dcfcb03fcb875f56beddc4) to set initial price competition and deployment vulnerabilities contract code ` PresidentOfCountry. Sol `.
Contract address 0 xd9145cce52d386f254917e481eb44e9943f39138 after deployment good, later need to use when deployed against contract.
Click on ‘President’ to view the addresses of current competitors.
Use account (0 x5b38da6a701c568545dcfcb03fcb875f56beddc4) call `becomePresident` and carry `1 eth`, performed successfully and then click `President` view, It turns out that the new presidential address has been changed to account 0X5B.
At this time have an attacker (0 xab8483f64d9c6d1ecf9b849ae677dd3315835cb2) wrote an attacker contract `Attack. Sol`, carrying 2 eth (because the new President must be greater than the current President’s competitive price, Current of `1 eth`) and set the `_target` for `PresidentOfCountry` contract address (0 xd9145cce52d386f254917e481eb44e9943f39138) for deployment.
After been deployed attack address 0 xa131ad247055fd2e2aa8b156a11bdec81b9ead95 contract, and click `President` view the new President’s address, found that it was already against the address of the contract.
After President position, if there are other users who want to competition requires greater than `2 eth` ` price to call `becomePresident` function, there is a user (0 xca35b7d915458ef540ade6068dfe2f44e8fa733c) the President wants to go to the competition, Took ‘3 ETH’ to call ‘becomePresident’, found an error and backoff, clicked president to find the address of the president or attack the contract, no matter who used how much ether to call ‘becomePresident’, the result was failed.
The contract cannot operate normally, which indicates that the contract has been attacked by DoS.
4.2 Real Cases
The following code is an example of a DoS attack in an actual contract, with only the key code written and the relevant changes made.
As you can see, the key code of the contract is for a withdrawal operation, but in a withdrawal, there is a judgment that whether the amount to be withdrawn is equal to the amount the user has deposited in the contract, rather than greater than or equal to. Then it may happen that when the user wants to request the ‘amount’ of tokens, The ‘balances[msg.sender]’ changes due to various reasons (transfer by others, reward allocation, etc.), and even the user does not want to withdraw all the money. Balances [msg.sender] == amount; does not hold, which causes a temporary DoS attack.
...
function withdraw(uint256 amount) public { // withdrawal amount
require(balances[msg.sender] == amount); // check whether the amount to be withdrawn is equal to the customer’s deposit in the contract
balances[msg.sender] -= amount; // modify the status variable for the deposit in the contract
msg.sender.transfer(amount); // transfer to user account
}
The way to modify it is to modify the judgment condition `require(balances[msg.sender] == amount);` to `require(balances[msg.sender] >= amount);`.
4.3 Historical Cases
Historically, there was a denial of service attack during the “Turbulent Age” of the game KotET(King OG the Ether Throne) from 6 to 8 February 2016, which resulted in some character compensation and unreceived money not being returned to the player’s wallet.
In June of that year, the GovernMental Contract was also hit by a DoS attack, when 1,100 Ether tokens were obtained using 2.5 million gas transactions that exceeded the contract’s gas ceiling, resulting in the suspension of the transaction.
Related DoS attacks such as Fomo 3D.
5. Solution
From the above explanation, we can find that DoS attacks have a very serious impact on smart contracts. Therefore, for DoS attacks, contract developers should make corresponding code modifications for the three situations mentioned in the vulnerability analysis above.
For example, for external operation mapping or array loop, need to limit the length; For the owner operation, the non-uniqueness of the contract should be considered, and the whole business should not be paralyzed because of a certain permission account. Based on the external call progress state need to except the handling of function calls, without any harm in general internal function calls, if the call is a failure will be back, and the external call is uncertain, we don’t know what the external caller wants to do if being attacked by the attacker, it may cause serious consequences.
Specific performance is malicious return execution error, resulting in normal code cannot be executed, resulting in DoS attacks, so for this kind of developers should add a processing mechanism for abnormal function execution.
In general, contract developers need to consider the logic and rigor of contract code in order to better prevent DoS attacks.
6. References
- [DoS attacks (hacker attack is one of the means) _ baidu encyclopedia (baidu.com)] (https://baike.baidu.com/item/ denial of service attack / 421896? Fr = Aladdin)
- [Ethereum smart Contract security introduction to understanding (ii) (rickgray.me)](http://rickgray.me/2018/05/26/ethereum-smart-contracts-vulnerabilities-review-part2/#5-Denial-of-Service- Denial of service)
- Smart Contract Security Analysis and Audit Guide