Ethereum signatures for hackers and auditors 101

Monethic.io
Coinmonks

--

In real world you can sign documents using your personal signature, which is assumed to be unique and proves that you support, acknowledge or commit something. The same can be done on ethereum blockchain and in solidity smart contracts — but using cryptography. In this article, we will briefly explain how signatures are applied in solidity and in its second part, what security vulnerabilities are related to them and how they can be exploited.

Signature basics

You probably know, that in order to create a valid transaction on Ethereum, it has to be signed, which means, in most cases, that the invoker uses own private key to perform a cryptographic operation on the transaction data, which causes it to produce a “signature”, which allows for confirming who signed it, but not for recovering the private key. It is basically as a private/public key cryptography used in GPG encrypted messaging. We will not dig into details on how these signatures work under the hood, but if you are interested we recommend to view these great articles [LINK1]

When the signed transaction is broadcasted to the network, user who broadcasts it pays the gas fee. Then, the transaction is being validated, and on that step one of things that network nodes do is check its validity by, among others, verifying its signature. In short, that process looks in following way:

Signing:

Signature = function_sign(private key, message)

Where in case of transaction, the message is RLP-encoded transaction data.

RLP encoding is another form of data serialization/encoding, you probably know other serialization types like protobuf or JSON etc. so it’s just a specific byte serialization format despite the scary name.

Validating signature:

Validation = function_validate(Signature, signed_message)

If Validation == public key of signer, then validation passed

Also, note that in solidity address is derived from public key, so we could verify the signer address this way too.

Sign everything!

Since the signing works well for proving responsibility for an action on the foundation level (The “chain layer”), why not use it a level above, so on smart contract layer? Basically, since the transaction issuer has a private key, he can also use this key to sign some string, or transfer to be executed later (imagine sigining a bank check and handing it over to someone — we will come back to that idea in a while)

However, some security concerns arise if you allow people to sign random things. Imagine an app that allows to sign some data in Solidity, for example, imagine something like a bank check.

Traditional signature of Bob

Normally, when dealing with financial stuff in real world, a person often has to sign some paperwork — contracts, checks, or permit other people (e.g. an attorney, accountant) to access own funds on one’s behalf. Similar logic is used in solidity, but before we explain how it is used, let’s stop a moment and think about an important security concern when signing something

In real world, you sign a paper, which usually has to be well understood before doing so. Since in solidity we have “smart” contracts and not paper ones, the data to be signed will usually be some stream of bytes.

Now thinking like an attacker… what if that stream of bytes will be an RLP encoded transaction data?

Then, the attacker could take such signed data and broadcast it as a transaction — and that transaction can be arbitrary, for example transferring ETH to the attacker. Such attack vector is by no means theoretical only because it happened and is happening. Example of such attack is also provided at the last paragraph of this article.

EIP 191 and 712

To avoid possible relays of signed data everywhere, two proposals have been invented. They propose an unified way of how to proceed with signed data in solidity, so it’s secure, functional and can be established as standard (so now if someone wants to implement signatures, then he should read these EIPs and implement such logic accordingly).

If you want to dig into details of them here are links to EIP 191 and EIP 712. But in short, EIP 191 addresses especially personal_sign operation on arbitrary data, implementing it in a such way, so this data cannot be passed later as a signed transaction. Moreover, EIP 712 extends the signing specification to establish a standard of signing structured data. In such operation, the end-user, who is the signer, will be able to see in plain-text what exactly is being signed when invoking the signing operation out of the user’s wallet.

EIP712 Signature example

You can read some detailed walkthroughs on how EIP712 is used in smart contracts, for example, this excellent article.

https://medium.com/coinmonks/eip712-a-full-stack-example-e12185b03d54

When signing, there are some components of the signature:

  • domain hash — uniquely identifies the application, so the signature will have an unique element.
  • message — the message (struct) to be hashed with keccak256

the whole thing is then hashed again and signed.

During signature verification:

  • we take an hashed struct (which structure we know because of application specific domain and data to be signed)
  • we take the signature (in v, r, s form — explained below)
  • using ecrecover() or similar function we get the signed address, and we can check the identity of the signer.

Another old solidity version, but simple snippet that shows the logic of ecrecover can be found here. We also recommend watching this or this video if you want to better understand the idea behind.

What is v,r,s?

The signature is in form of a 65-byte long string, however, these bytes are always later split to three components:

  • last one byte — v
  • first 32 bytes — r
  • second 32 bytes — s

v is the last byte, while r and s is simply the signature split in half. The v is so-called recovery ID. Without going deep into cryptography — it can have two values: 0 or 1, which corresponds to two halfs of Y axis and is related to how ECDSA signatures are calculated. For more details, you can also read this and this article.

By the way, for curious readers, there comes a question: since it can have two values, can there be two signatures valid for a r and s? Yes, unfortunately that’s what signature malleability is about. But it will be explained later on.

Further usage

Above example is a classic usage of signatures. However, there are already some well-known standards built on top of these. We won’t go much into details of them, but just to show you that understanding signatures is crucial in order to be able to audit some more complex solutions that uses them under the hood. Some of examples can be the following: ERC-20 Permit (EIP-2612) or Meta Transactions (EIP-2771)

Key points

So to sum up, signatures:

  • are derived from private key, without need to disclosing it
  • are verifable based on signer address
  • on blockchain layer, can be used to sign transactions
  • on smart contract layer, can be used to identify and further authorize users

Let’s now go to another part of this article to try to threat model the signatures logic.

Attacking signatures

Signatures can be a subject to numerous attacks. Below you can read about types of vulnerabilities related to signatures.

Improper implementation

Improperi implementation is a generic name for any vulnerability that is a result of implementing the signature logic in wrong way, leading for example to logic bugs. The below code is from EthernautDAO CTF 7 — Switch. In order not to spoil your fun, we are not disclosing what is wrong here, but instead, we post link to a walkthrough below the code section in case you want to learn example type of wrong implementation.

/**
*Submitted for verification at Etherscan.io on 2022-08-13
*/
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
/**
* @title Claim ownership of the contract below to complete this level
* @dev Implement one time hackable smart contract (Switch)
*/
contract Switch {
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}
constructor() {
owner = msg.sender;
}
// Changes the ownership of the contract. Can only be called by the owner
function changeOwnership(address _owner) public onlyOwner {
owner = _owner;
}
// Allows the owner to delegate the change of ownership to a different address by providing the owner's signature
function changeOwnership(
uint8 v,
bytes32 r,
bytes32 s
) public {
require(ecrecover(generateHash(owner), v, r, s) != address(0), "signer is not the owner");
owner = msg.sender;
}
// Generates a hash compatible with EIP-191 signatures
function generateHash(address _addr) private pure returns (bytes32) {
bytes32 addressHash = keccak256(abi.encodePacked(_addr));
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", addressHash));
}
}

The solution to above challenge is available here.

Signature replay

Let’s say, Bob signed an approval for Alice, so Alice can use some of Bob’s fund on a DApp they both use. How to make sure that Alice will not reuse the signed approval multiple times?

The solution is that the application accepting signatures should also validate Nonces when considering if a signature is valid, or implement any other logic to mark “used” signatures. While you can find a full example of how both vulnerable and fixed code looks like here, below we post a code snippet from solidity by example to show a secure function, which apart from checking signatures, also checks the nonces.

function transfer(
address _to,
uint _amount,
uint _nonce,
bytes[2] memory _sigs
) external {
bytes32 txHash = getTxHash(_to, _amount, _nonce);
require(!executed[txHash], "tx executed");
require(_checkSigs(_sigs, txHash), "invalid sig");

executed[txHash] = true;

(bool sent, ) = _to.call{value: _amount}("");
require(sent, "Failed to send Ether");
}

In above code you can see that this check

require(!executed[txHash], "tx executed");

makes sure that the signature is not reused. The full code is available here.

Signature malleability

Term “malleability” refers to any situation, where the signature can be crafted. This is a critical issue, since it allows for acting of someone else’s behalf. However, there are very few major public exploits known, one that is especially worth attention is Openzeppelin ECDSA signature malleability, which is already fixed in latest versions of OpenZeppelin contracts. The vulnerability existed, because it was possible to submit en EIP-2098 compact signature to bypass signature check.

In theory, ecrecover() function as a solution that verifies signatures can also be vulnerable, because it is valid for both two existing v values, 27 and 28. However, it is not the case when the nonce is checked externally, because even on crafting the signature, the nonce check might kick in.

Instead of ecrecover(), OpenZeppelin’s ECDSA library should be used.

You can also take a look at these more advanced articles if you want to research malleability in-depth:

Lack of ecrecover() result check

ecrecover() returns 0 on error. That means, if we try to obtain signer address via ecrecover(), but the signature verification fails, the result will still be zero. Considering a zero can also be an address, this may result in unpredictable behavior later. Having this in mind, ecrecover() result should never be checked against true or false, always against 0 or not.

Phishing for signatures

The last attack vector, which is not used by auditors but rather by blackhats, is phishing for signatures. This is something that probably very few users are aware of, but the “sign” popup of metamask appearing should immediately bring up the caution of anyone. Would you sign below message?

As of time of writing this article, metamask have several methods for signing text in its underlying API. As in the beginning of this article, it might be possible to try to phish users to sign some arbitrary data, even a sequence of bytes that can be a valid transaction. When signing something with the eth_sign method, which allows for this, metamask shows a big red text:

You can read more about this type of scam here. Another type of scam is requesting a signature request on a request of type approve, or other in-contract allowance type, for example, signing approveForAll() in favour of attacker might allow him to drain all NFTs off the wallet. If you want to learn more about this type of scams, you can also take a look at this article. Since we’re dedicated mostly to auditing, we will not cover these examples in detail.

However, we would like to remind everyone to keep in mind that a “sign” button in the wallet might be equivalent to putting a wet-ink signature on a formal document. In short: use it wisely!

References

New to trading? Try crypto trading bots or copy trading on best crypto exchanges

--

--

Monethic.io
Coinmonks

We are providing security services for smart contracts & web3. Find us on twitter https://twitter.com/Monethic_io