Passive Decryption of Ethereum Peer-to-Peer Traffic

Ethereum, a popular cryptocurrency, utilizes a P2P flood network overlay protocol in order to propagate new transactions and state around the network. As has been shown in previous works^1, observing the propagation of transactions through the peer-to-peer network layer is often enough to deanonymize users of cryptocurrency networks. Additionally, attackers may run cryptocurrency nodes and monitor the messages on the P2P network to gain insight into transactions. On public blockchains such as Ethereum and Bitcoin, this is less of an issue since the ledger itself is accessible in plaintext. However, Ethereum makes an effort to encrypt the peer-to-peer layer of its stack in order to increase user privacy. Ethereum’s transport encryption protocol is known to have a serious flaw, which is still present in the current version of the protocol ^3. This blog post will show how to practically exploit the flaw in order to perform deanonymization attacks similar to the ones described in ^1 and ^2.

RLPx Protocol

The RLPx protocol, used in the current version of Ethereum, defines the transport protocol used to encapsulate Ethereum RPC messages. An overview of the protocol is available here. Like other cryptocurrencies, the network layer of Ethereum functions as a peer-to-peer flood network. The flood network relays information about transactions that should be added to the processing pool for eventual validation and inclusion inside blocks by miners. It also relays the current network state through block headers and blocks. RLPx defines a transport protocol which nominally provides confidentiality of network messages through cryptography.

Key Agreement

In short, Ethereum nodes use Elliptic Curve Diffie-Hellman to derive a shared secret for the RLPx session. This shared secret is then used with a key derivation function to derive a key for encryption and authentication. The derived keys are used by the peers to encrypt and authenticate network-layer messages.

Cipher Setup

The two peers use AES in Counter mode (AES-CTR) to encrypt their communications. They used the key material derived from the shared secret as the key, and an empty (all-zero) IV:

// NOTE: excerpt from the Geth source code

func newRLPXFrameRW(conn io.ReadWriter, s secrets) *rlpxFrameRW {
	macc, err := aes.NewCipher(s.MAC)
	if err != nil {
		panic("invalid MAC secret: " + err.Error())
	}
	encc, err := aes.NewCipher(s.AES)
	if err != nil {
		panic("invalid AES secret: " + err.Error())
	}
	// we use an all-zeroes IV for AES because the key used
	// for encryption is ephemeral.
	iv := make([]byte, encc.BlockSize()) // NOTE: all-zero IV for both initiator and receiver.
	return &rlpxFrameRW{
		aessecret: s.AES,
		conn:       conn,
		enc:        cipher.NewCTR(encc, iv),
		dec:        cipher.NewCTR(encc, iv),
		macCipher:  macc,
		egressMAC:  s.EgressMAC,
		ingressMAC: s.IngressMAC,
	}
}

Keystream Reuse

Counter mode is a commonly used and efficient construction which transforms a fixed-length permutation (a block cipher) into a stream cipher. This has several practical advantages from an engineering perspective: encryption and decryption is easily parallelizable, and random reads are performant. Counter mode works by XOR’ing the output of the keyed block permutation with the plaintext (for encryption) or the ciphertext (for decryption), where the input to the block cipher is the counter state. The output of the block cipher permutation, which is XORed with the ciphertext or the plaintext, is known as the "keystream".

A well-known property of Counter mode is that it is extremely important to not re-use a keystream for two different plaintexts. Doing so results in the exact same situation as that with a "two-time pad"; when you re-use a one-time pad key. The attacker can simply XOR the two ciphertexts and the keystream will cancel out, yielding the XOR of the underlying plaintexts.

In the case of RLPx, the "IV", which is the initial counter state, is set to zero for both the initiating peer and the receiving peer. Since the keystream is a function of the Key and the counter state, which is identical for both the receiving peer and the initiating peer, the peers will re-use the same keystream. This is the vulnerability noted in ^3, known but unfixed since 2015.

Practical Exploitation: A Deanonymization Attack

Now that we know the peers are re-using a keystream, we want to formulate a practical attack on the protocol. A few questions come to mind initially. What interesting protocol messages are there for a passive attacker? Do the peers reuse their keystream with every message that they send each other? How can we separate the individual plaintexts after learning the XOR of the two plaintexts?

First, let’s define the protocol messages. The interesting protocol messages to us, as a passive adversary looking to learn interesting things about the Ethereum network through p2p network traffic, is the core ETH protocol which handles things such as relaying transactions, new blocks, block headers, etc:

// eth protocol message codes
const (
	// Protocol messages belonging to eth/62
	StatusMsg          = 0x00
	NewBlockHashesMsg  = 0x01
	TxMsg              = 0x02
	GetBlockHeadersMsg = 0x03
	BlockHeadersMsg    = 0x04
	GetBlockBodiesMsg  = 0x05
	BlockBodiesMsg     = 0x06
	NewBlockMsg        = 0x07

	// Protocol messages belonging to eth/63
	GetNodeDataMsg = 0x0d
	NodeDataMsg    = 0x0e
	GetReceiptsMsg = 0x0f
	ReceiptsMsg    = 0x10
)

A useful deanonymization attack would be to target the TxMsg messages, since using known techniques^1, if we can learn the information inside transaction propagation graph of a transaction of interest, we can likely learn the details of the network node which initially broadcast the transaction.

We can then conceive of a simple attack:

  1. Assume a position on the network such that we can observe many Ethereum peer-to-peer connections.
  2. Compute the XOR of the ciphertext messages sent by the initiator and receiver for each connection.
  3. Compile a list of possible plaintexts, by compiling recent public blocks, block headers, and transactions into their correct message format.
  4. XOR the ciphertext messages to learn the XOR of the underlying plaintexts, then XOR with each possible known plaintext.
  5. Check if the output of 3. results in a valid transaction message. If it does, then you have learned that the initiator or receiver forwarded the transaction, revealing part of the transaction’s propagation graph.
  6. Repeat this process for every connection and every message.

This is a simple approach that exploits the fact that it is easy to compile a list of likely known plaintexts in this case, since the application is a blockchain network and the relayed transactions will very likely be recorded on the ledger in short order, assuming they are valid. Since the network is permissionless, an attacker could also actively inject known plaintexts which are then gossiped around the network, enabling them to decrypt more unknown plaintexts.

Conclusion

In this blog post, we show a known flaw in Ethereum’s peer-to-peer communication protocol RLPx. We exploit the flaw using techniques commonly employed against repeated Counter mode nonces. For some more practical experience exploiting the flaw noted in this post, repeated Counter mode nonces, check out Cryptopals Set 3, Challenges 19^4 and 20^5