赞
踩
also known as or related to race to empty, recursive call vulnerability, call to the unknown
This exploit was missed in review so many times by so many different people: reviewers tend to review functions one at a time, and assume that calls to secure subroutines will operate securely and as intended.
Phil Daian
The Reentrancy attack, probably the most famous Ethereum vulnerability, surprised everyone when discovered for the first time. It was first unveiled during a multimillion dollar heist which led to a hard fork of Ethereum. Reentrancy occurs when external contract calls are allowed to make new calls to the calling contract before the initial execution is complete. For a function, this means that the contract state may change in the middle of its execution as a result of a call to an untrusted contract or the use of a low level function with an external address.
Loss: estimated at 3.5M ETH (~50M USD at the time)
Timeline of discovery:
Real World Impact:
Example:
withdraw()
function.withdraw()
function to retrieve its entire balance.call.value(amount)()
low level function to send the ether to the malicious contract before updating the balance of the malicious contract.fallback()
function that accepts the funds and then calls back into the victim contract's withdraw()
function.Code Example:
The following function contains a function vulnerable to a reentrancy attack. When the low level call()
function sends ether to the msg.sender
address, it becomes vulnerable; if the address is a smart contract, the payment will trigger its fallback function with what's left of the transaction gas:
- function withdraw(uint _amount) {
- require(balances[msg.sender] >= _amount);
- msg.sender.call.value(_amount)();
- balances[msg.sender] -= _amount;
- }
Additional Resources:
It was possible to turn the Parity Wallet library contract into a regular multi-sig wallet and become an owner of it by calling the initWallet function.
Parity
Access Control issues are common in all programs, not just smart contracts. In fact, it's number 5 on the OWASP top 10. One usually accesses a contract's functionality through its public or external functions. While insecure visibility settings give attackers straightforward ways to access a contract's private values or logic, access control bypasses are sometimes more subtle. These vulnerabilities can occur when contracts use the deprecated tx.origin
to validate callers, handle large authorization logic with lengthy require
and make reckless use of delegatecall
in proxy libraries or proxy contracts.
Loss: estimated at 150,000 ETH (~30M USD at the time)
Real World Impact:
Example:
Code Example:
In the following example, the contract's initialization function sets the caller of the function as its owner. However, the logic is detached from the contract's constructor, and it does not keep track of the fact that it has already been called.
- function initContract() public {
- owner = msg.sender;
- }
In the Parity multi-sig wallet, this initialization function was detached from the wallets themselves and defined in a "library" contract. Users were expected to initialize their own wallet by calling the library's function via a delegateCall
. Unfortunately, as in our example, the function did not check if the wallet had already been initialized. Worse, since the library was a smart contract, anyone could initialize the library itself and call for its destruction.
Additional Resources:
also known as integer overflow and integer underflow
An overflow condition gives incorrect results and, particularly if the possibility has not been anticipated, can compromise a program’s reliability and security.
Jules Dourlens
Integer overflows and underflows are not a new class of vulnerability, but they are especially dangerous in smart contracts, where unsigned integers are prevalent and most developers are used to simple int
types (which are often just signed integers). If overflows occur, many benign-seeming codepaths become vectors for theft or denial of service.
Real World Impact:
Example:
withdraw()
function allows you to retrieve ether donated to the contract as long as your balance remains positive after the operation.withdraw()
function check's result is always a positive amount, allowing the attacker to withdraw more than allowed. The resulting balance underflows and becomes an order of magnitude larger than it should be.Code Example:
The most straightforward example is a function that does not check for integer underflow, allowing you to withdraw an infinite amount of tokens:
- function withdraw(uint _amount) {
- require(balances[msg.sender] - _amount > 0);
- msg.sender.transfer(_amount);
- balances[msg.sender] -= _amount;
- }
The second example (spotted during the Underhanded Solidity Coding Contest) is an off-by-one error facilitated by the fact that an array's length is represented by an unsigned integer:
- function popArrayOfThings() {
- require(arrayOfThings.length >= 0);
- arrayOfThings.length--;
- }
The third example is a variant of the first example, where the result of arithmetic on two unsigned integers is an unsigned integer:
- function votes(uint postId, uint upvote, uint downvotes) {
- if (upvote - downvote < 0) {
- deletePost(postId)
- }
- }
The fourth example features the soon-to-be-deprecated var
keyword. Because var
will change itself to the smallest type needed to contain the assigned value, it will become an uint8
to hold the value 0. If the loop is meant to iterate more than 255 times, it will never reach that number and will stop when the execution runs out of gas:
- for (var i = 0; i < somethingLarge; i ++) {
- // ...
- }
Additional Resources:
also known as or related to silent failing sends, unchecked-send
The use of low level "call" should be avoided whenever possible. It can lead to unexpected behavior if return values are not handled properly.
Remix
One of the deeper features of Solidity are the low level functions call()
, callcode()
, delegatecall()
and send()
. Their behavior in accounting for errors is quite different from other Solidity functions, as they will not propagate (or bubble up) and will not lead to a total reversion of the current execution. Instead, they will return a boolean value set to false
, and the code will continue to run. This can surprise developers and, if the return value of such low-level calls are not checked, can lead to fail-opens and other unwanted outcomes. Remember, send can fail!
Real World Impact:
Code Example:
The following code is an example of what can go wrong when one forgets to check the return value of send()
. If the call is used to send ether to a smart contract that does not accept them (e.g. because it does not have a payable fallback function), the EVM will replace its return value with false
. Since the return value is not checked in our example, the function's changes to the contract state will not be reverted, and the etherLeft
variable will end up tracking an incorrect value:
- function withdraw(uint256 _amount) public {
- require(balances[msg.sender] >= _amount);
- balances[msg.sender] -= _amount;
- etherLeft -= _amount;
- msg.sender.send(_amount);
- }
Additional Resources:
including gas limit reached, unexpected throw, unexpected kill, access control breached
I accidentally killed it.
devops199 on the Parity multi-sig wallet
Denial of service is deadly in the world of Ethereum: while other types of applications can eventually recover, smart contracts can be taken offline forever by just one of these attacks. Many ways lead to denials of service, including maliciously behaving when being the recipient of a transaction, artificially increasing the gas necessary to compute a function, abusing access controls to access private components of smart contracts, taking advantage of mixups and negligence, etc. This class of attack includes many different variants and will probably see a lot of development in the years to come.
Loss: estimated at 514,874 ETH (~300M USD at the time)
Real World Impact:
Example:
bid(uint object)
function with the desired amount of ether. The auction contract will store the ether in escrow until the object's owner accepts the bid or the initial bidder cancels it. This means that the auction contract must hold the full value of any unresolved bid in its balance.withdraw(uint amount)
function which allows admins to retrieve funds from the contract. As the function sends the amount
to a hardcoded address, the developers have decided to make the function public.Code Example:
In the following example (inspired by King of the Ether) a function of a game contract allows you to become the president if you publicly bribe the previous one. Unfortunately, if the previous president is a smart contract and causes reversion on payment, the transfer of power will fail and the malicious smart contract will remain president forever. Sounds like a dictatorship to me:
- function becomePresident() payable {
- require(msg.value >= price); // must pay the price to become president
- president.transfer(price); // we pay the previous president
- president = msg.sender; // we crown the new president
- price = price * 2; // we double the price to become president
- }
In this second example, a caller can decide who the next function call will reward. Because of the expensive instructions in the for
loop, an attacker can introduce a number too large to iterate on (due to gas block limitations in Ethereum) which will effectively block the function from functioning.
- function selectNextWinners(uint256 _largestWinner) {
- for(uint256 i = 0; i < largestWinner, i++) {
- // heavy code
- }
- largestWinner = _largestWinner;
- }
Additional Resources:
also known as nothing is secret
The contract had insufficient validation of the block.number age, which resulted in 400 ETH being lost to an unknown player who waited for 256 blocks before revealing the predictable winning number.
Arseny Reutov
Randomness is hard to get right in Ethereum. While Solidity offers functions and variables that can access apparently hard-to-predict values, they are generally either more public than they seem or subject to miners' influence. Because these sources of randomness are to an extent predictable, malicious users can generally replicate it and attack the function relying on its unpredictablility.
Loss: more than 400 ETH
Real World Impact:
Example:
Code Example:
In this first example, a private seed
is used in combination with an iteration
number and the keccak256
hash function to determine if the caller wins. Even though the seed
is private
, it must have been set via a transaction at some point in time and thus is visible on the blockchain.
- uint256 private seed;
-
- function play() public payable {
- require(msg.value >= 1 ether);
- iteration++;
- uint randomNumber = uint(keccak256(seed + iteration));
- if (randomNumber % 2 == 0) {
- msg.sender.transfer(this.balance);
- }
- }
In this second example, block.blockhash
is being used to generate a random number. This hash is unknown if the blockNumber
is set to the current block.number
(for obvious reasons), and is thus set to 0
. In the case where the blockNumber
is set to more than 256 blocks in the past, it will always be zero. Finally, if it is set to a previous block number that is not too old, another smart contract can access the same number and call the game contract as part of the same transaction.
- function play() public payable {
- require(msg.value >= 1 ether);
- if (block.blockhash(blockNumber) % 2 == 0) {
- msg.sender.transfer(this.balance);
- }
- }
Additional Resources:
also known as time-of-check vs time-of-use (TOCTOU), race condition, transaction ordering dependence (TOD)
Turns out, all it takes is about 150 lines of Python to get a working front-running algorithm.
Ivan Bogatyy
Since miners always get rewarded via gas fees for running code on behalf of externally owned addresses (EOA), users can specify higher fees to have their transactions mined more quickly. Since the Ethereum blockchain is public, everyone can see the contents of others' pending transactions. This means if a given user is revealing the solution to a puzzle or other valuable secret, a malicious user can steal the solution and copy their transaction with higher fees to preempt the original solution. If developers of smart contracts are not careful, this situation can lead to practical and devastating front-running attacks.
Real World Impact:
Example:
N = prime1 x prime2
).submitSolution()
public function with the right prime1
and prime2
rewards the caller.Additional Resources:
also known as timestamp dependence
If a miner holds a stake on a contract, he could gain an advantage by choosing a suitable timestamp for a block he is mining.
Nicola Atzei, Massimo Bartoletti and Tiziana Cimoli
From locking a token sale to unlocking funds at a specific time for a game, contracts sometimes need to rely on the current time. This is usually done via block.timestamp
or its alias now
in Solidity. But where does that value come from? From the miners! Because a transaction's miner has leeway in reporting the time at which the mining occurred, good smart contracts will avoid relying strongly on the time advertised. Note that block.timestamp
is also sometimes (mis)used in the generation of random numbers as is discussed in #6. Bad Randomness.
Real World Impact:
Example:
Code Example:
The following function only accepts calls that come after a specific date. Since miners can influence their block's timestamp (to a certain extent), they can attempt to mine a block containing their transaction with a block timestamp set in the future. If it is close enough, it will be accepted on the network and the transaction will give the miner ether before any other player could have attempted to win the game:
- function play() public {
- require(now > 1521763200 && neverPlayed == true);
- neverPlayed = false;
- msg.sender.transfer(1500 ether);
- }
Additional Resources:
also known as or related to off-chain issues, client vulnerabilities
The service preparing the data for token transfers assumed that users will input 20-byte long addresses, but the length of the addresses was not actually checked.
Paweł Bylica
Short address attacks are a side-effect of the EVM itself accepting incorrectly padded arguments. Attackers can exploit this by using specially-crafted addresses to make poorly coded clients encode arguments incorrectly before including them in transactions. Is this an EVM issue or a client issue? Should it be fixed in smart contracts instead? While everyone has a different opinion, the fact is that a great deal of ether could be directly impacted by this issue. While this vulnerability has yet to be exploited in the wild, it is a good demonstration of problems arising from the interaction between clients and the Ethereum blockchain. Other off-chain issues exist: an important one is the Ethereum ecosystem's deep trust in specific Javascript front ends, browser plugins and public nodes. An infamous off-chain exploit was used in the hack of the Coindash ICO that modified the company's Ethereum address on their webpage to trick participants into sending ethers to the attacker's address.
Timeline of discovery:
Date | Event |
---|---|
April 6, 2017 | How to Find $10M Just by Reading the Blockchain |
Real World Impact:
Example:
transfer(address _to, uint256 _amount)
function with padded arguments: it prepends the address (of an expected 20-byte length) with 12 zero bytes to make it 32-byte long0x3bdde1e9fbaef2579dd63e2abbf0be445ab93f00
) asks Alice to transfer him 20 tokens. He maliciously gives her his address truncated to remove the trailing zeroes.0x3bdde1e9fbaef2579dd63e2abbf0be445ab93f
)._amount
argument._amount
argument. Effectively transfering 256 times more tokens than thought.Additional Resources:
We believe more security audits or more tests would have made no difference. The main problem was that reviewers did not know what to look for.
Christoph Jentzsch
Ethereum is still in its infancy. The main language used to develop smart contracts, Solidity, has yet to reach a stable version and the ecosystem's tools are still experimental. Some of the most damaging smart contract vulnerabilities surprised everyone, and there is no reason to believe there will not be another one that will be equally unexpected or equally destructive. As long as investors decide to place large amounts of money on complex but lightly-audited code, we will continue to see new discoveries leading to dire consequences. Methods of formally verifying smart contracts are not yet mature, but they seem to hold great promise as ways past today's shaky status quo. As new classes of vulnerabilities continue to be found, developers will need to stay on their feet, and new tools will need to be developed to find them before the bad guys do. This top 10 will likely evolve rapidly until smart contract development reaches a state of steadiness and maturity.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。