Wormhole is a message-passing protocol enabling the transfer of tokenized assets between supported chains. In other words, it allows one to send ETH from Ethereum and receive a wrapped wormhole ETH (wETH) on Solana. Thanks to that, developers can also create cross-chain dApps.

The basic principle of operation is that each supported chain has a wormhole contract that emits or processes wormhole messages. The system relies on so-called ‘Guardians‘, a distributed set of nodes that monitor the state of transactions and after consensus sign the messages emitted by the wormhole contracts. When a user sends ETH to Solana, the system locks the tokens into a contract on the source chain (Ethereum) and when the Guardian nodes approve that it happened, they sign the message, and another contract on the destination chain is called to issue the parallel ‘wrapped’ token.

What happened

In February 2022, an attacker stole assets worth roughly $338M (at the time of the exploit) by circumventing the signing process and minting 120k wrapped ETH tokens on Solana without locking an equivalent on Ethereum. Even though the Wormhole team sent a message to the hacker offering a white hat agreement, there was no response. The hacker used 93,750 of the minted wrapped ETH to redeem back to equivalent ETH tokens on Ethereum, thus draining the money from Ethereum liquidity pool. The rest of the tokens were sold for SOL and USDC, as seen in the hacker’s wallet. Wormhole’s investor, Jump Crypto replenished all 120k ETH, virtually bailing Wormhole out.

Exploit Details

The Wormhole project is open-source, and the public repository is accessible to anyone. Interestingly enough, the hack happened only a few hours after a critical bug was fixed and committed to the public repository and before a new version of the program was deployed.

The bug’s root cause was a deprecated function

load_instruction_at 

being used during the Wormhole signature verification. The way to build custom instructions that ‘do’ signature verification is by sending a transaction made of (at least) two instructions and checking that the native program instruction was sent. As described in the secp256k1_instruction.rs documentation, it is necessary to:

  • Load the secp256k1 instruction data with
load_instruction_at_checked or get_instruction_relative;
  • Check that the
secp256k1

program ID is equal to

secp256k1_program::ID

so that the signature verification cannot be faked with a malicious program;

  • Check that the public keys and messages are the expected values per the program’s requirements.

Unfortunately, Wormhole’s implementation of the signature verification did not use the suggested

load_instruction_at_checked 

function and instead used a deprecated version

load_instruction_at. 

This function does not check that the origin of data to deserialize is actually from the sysvar account and not a fake account. That way, the attacker was able to:

  • Create an account 2tHS1cXX2h1KBEaadprqELJ6sV9wLoaSdX68FqsrrZRd with
secp256k1 

instruction data earlier within different transaction;

  • Call
verify_certificate

where he replaced the sysvar::instructions account with the malicious account created in the first step.

Signature verification normally requires two instructions within one transaction:

secp256k1 and verify_certificate. 

However, the attacker managed to call only the single

verify_certificates 

instruction and fake the instruction input data, so the

verify_certificates

did not recognize it was not called together with the

secp256k1 

instruction and passed the verification. You can check the transaction yourself. Once the verification was complete, it was possible to call the usual instruction

post_vaa

to obtain the so-called ‘validator action approval’ and subsequently complete_wrapped instruction to mint the 120,000 ETH.

In simple words, the Wormhole hack was quite sophisticated on one side, but after all, the root cause was a missing ownership check on one account, which allowed the hacker to fake that he had money on his account on one blockchain. And due to the lack of checks, he managed to mint the money on another blockchain. 

Reference