Introduction | Signature Replay Vulnerability in Smart Contracts

Cryptographic signatures are the basic modules in a blockchain system. Signing a transaction with the corresponding private key can associate the transaction initiator with a specific account. Without this feature, the blockchain accounting will not work properly.

Many smart contracts deployed on Ethereum also have the ability to directly verify digital signatures so that one or more verifiers can authorize operations by submitting an offline created signature, even a signature generated by another smart contract. This verification is typically used for multi-signature cold wallets or voting contracts to submit various signatures or delegate authorizations together.

A common vulnerability in such an implementation is a signature replay attack. In Cryptonics' smart contract audit of an important project, we encountered an interesting example of this problem. In this article, we will use this example to illustrate how signature verification is wrong in smart contracts.

Vulnerabilities related to signature verification are usually caused by misunderstanding the underlying cryptographic principles and the purpose of the signature. So, before we get to know this specific vulnerability in detail, let's take a quick look at how cryptographic signatures work.

Cryptographic signature

Most cryptographic signature systems are based on public-private key pairs. The private key can sign the data and the signature can be verified by the corresponding public key. As its name implies, a user's public key is public, and the private key must be kept secret.

Encrypting the data can achieve two important attributes:

  • Data signer identifiability is achieved by restoring the signer's public key.
  • The complete verifiability of the data means that the signature can be used to prove that the data has not been modified since it was signed.

Although these are very powerful attributes, it is important to note that the signed data itself does not provide additional protection. Signature does not guarantee the uniqueness of a message, nor does it guarantee that the signer is the sender itself. Of course, the cryptographic signature can be used to confirm the relevant facts, but the application must also perform the necessary checks. We can investigate the above facts in the Ethereum Smart Contract.

Signature verification in Ethereum

Like the Bitcoin, Ethereum uses the Elliptic Curve Digital Signature Algorithm (ECDSA) and the secp256k1 curve. Smart contracts can access the built-in ECDSA signature verification algorithm through the system method ecrecover. The following example shows the usage of this function:

  Address signer = ecrecover(msgHash, v, r, s); 

The input parameters for this method are the signature values ​​v, r and s, and the keccak256 hash of the signature data. It can verify the integrity of the data, that is, confirm that the digital signature corresponds to the hash value of the data, and can restore the signer's Ethereum address from the signature (the Ethereum address is derived from the public key).

Any additional checks, whether checking whether the signature address is the correct address or checking whether the signed message is unique, must be manually added to the smart contract .

It is often misunderstood the functionality of ecrecover and then exploited security vulnerabilities.

Signature replay vulnerability

Code example

Let's take a look at the vulnerabilities we found in recent contract audits:

  Function unlock( address _to, uint256 _amount, uint8[] _v, bytes32[] _r, bytes32[] _s) external{ require(_v.length >= 5); bytes32 hashData = keccak256(_to, _amount); for (uint i = 0; i < _v.length; i++) { address recAddr = ecrecover(hashData, _v[i], _r[i], _s[i]); require(_isValidator(recAddr)); } to.transfer(_amount) ;} 

The above code is a simplified version of the code we audited, and to keep the code short and easy to understand, it only retains the most basic information. But the loopholes were completely preserved.

The audited contract is part of a cross-chain bridge that allows digital assets to be moved from one blockchain to another. When Ethereum is locked in the Ethereum Smart Contract, the corresponding asset is created on the other chain. The unlock function releases the previously locked Ethercoin when the asset is locked or destroyed on another chain. To achieve this effect, cross-chain repeaters can submit a series of certifier signatures, an unlocked amount, and a destination address. This function requires at least five signatures to unlock the required amount and pass the funds to the recipient. The internal _isValidator function (which omits the implementation for simplicity) checks that an address does not have a certifier identity.

Attack scenario

The problem with the above code is in the message that the verified person signed the name with the ECDSA algorithm. This message only contains the address of the recipient and the amount that needs to be unlocked. In this message, there is nothing to prevent the same signature from being reused multiple times . Imagine the following scenario:

  • Bob's asset equivalent to 10ETH on another chain connected to Ethereum was passed back to the Ethereum chain via the bridge.
  • Alice is a repeater that handles cross-chain transactions. She collects the necessary certifier signatures, locks the corresponding number of assets on the connected chain, and calls the unlock function to release 10ETH from the contract to Bob.
  • A transaction containing a series of signature values ​​can be read publicly on the blockchain.
  • Bob can now copy the sequence of signature values ​​and submit an identical unlock function call request. This unlocked operation can be successful again, causing another 10 ETH to be sent to Bob.
  • Bob was able to repeat this process until the Ethereum in the smart contract was exhausted.


Improvement means

The above situation is called a signature replay attack. This attack can be successful because we are unable to verify the uniqueness of the signed message or whether it was used before.

An easy way to prevent such attacks is to include a message sequence number or nonce in the signed data . The revised version of the above code is as follows:

  Public uint256 nonce; function unlock( address _to, uint256 _amount, uint256 _nonce, uint8[] _v, bytes32[] _r, bytes32[] _s) external{ require(_v.length >= 5); require(_nonce == nonce++) ; bytes32 hashData = keccak256(_to, _amount, _nonce); for (uint i = 0; i < _v.length; i++) { address recAddr = ecrecover(hashData, _v[i], _r[i], _s[i] ); require(_isValidator(recAddr)); } to.transfer(_amount);} 

This code now requires that each successful unlock call contain a serial number. Because the message contains a unique number, the signature required for each successful call is unique. This means that previously observed messages are useless to the attacker because replay will fail.

The best mode for signature verification

The above example is just one example, demonstrating that there is no guarantee that a unique signature will be played back. In most scenarios, it is important to ensure that signatures are uniquely matched to each call to prevent replay attacks.

However, this code is not perfect. It does not follow best practices for signature verification. The reason is that it does not check the plasticity signature, we should check if the s value as part of the accepted signature is in the lower range. The recommended procedure for using the ecrecover function can be found in Open Zeppelin's excellent ECDSA library. In fact, it's always a good idea to develop on community-audited code, such as Open Zeppelin.

Original link:


Author: Stefan Beyer

Translation & Proofreading: TrumanW & Ajian

This article is authored by the author to translate and republish EthFans.

(This article is from the EthFans of Ethereum fans, and it is strictly forbidden to reprint without the permission of the author.