Unfortunately on Tuesday 10th August there was the largest hack in DeFi history with $600 million in assets drained from Poly Network by an attack on their cross-chain bridges. This just shows how essential Insurance Services are in the DeFi ecosystem, and what an important job we are doing here at InsurAce.io.
Below is a detailed analysis written by twitter user @kelvinfichter on how this hack happened:
Poly has this contract called the “EthCrossChainManager”. It’s basically a privileged contract that has the right to trigger messages from another chain. It’s a pretty standard thing for cross-chain projects.
There’s this function verifyHeaderAndExecuteTx that anyone can call to execute a cross-chain transaction. Basically it (1) verifies that the block header is correct by checking signatures (seems the other chain was a poa sidechain or something) and… (cont. in the next tweet)
then (2) checks that the transaction was included within that block with a Merkle proof. Here’s the code, it’s pretty simple:
One of the last things the function does is call _executeCrossChainTx, which actually makes the call to the target contract. Here’s where the critical flaw sits. Poly checks that the target is a contract…
But Poly forgot to prevent users from calling a very important target… the EthCrossChainData contract:
Why is this target so important? It keeps track of the list of public keys that authenticate data coming from the other chain. If you can modify that list, you don’t even need to hack private keys. You just set the public keys to match your own private keys.
See here for where that list is tracked:
So someone realized that they could send an cross-chain message directly to the EthCrossChainData contract. What good does that do them? Well, guess which contracted owned the EthCrossChainData contract… yep. The EthCrossChainManager.
By sending this cross-chain message, the user could trick the EthCrossChainManager into calling the EthCrossChainData contract, passing the onlyOwner check. Now the user just had to craft the right data to be able to trigger the function that changes the public keys…
The only remaining challenge was to figure out how to make the EthCrossChainManager call the right function. Now comes a little bit of complexity around how Solidity picks which function you’re trying to call.
The first four bytes of transaction input data is called the “signature hash” or “sighash” for short. It’s a short piece of information that tells a Solidity contract what you’re trying to do.
The sighash of a function is calculated by taking the first four bytes of the hash of “<function name>(<function input types>)”. For example, the sighash of the ERC20 transfer function is the first four bytes of the hash of “transfer(address,uint256)”.
Poly’s contract was willing to call *any* contract. However, it would only call the contract function that corresponded to the following sighash:
Errr but wait… “_method” here was user input. All the attacker had to do to call the right function was figure out *some* value for “_method” that, when combined with those other values and hashed, had the same leading four bytes as the sighash of our target function.
With just a little bit of grinding, you can *easily* find some input that produces the right sighash. You don’t need to find a full hash collision, you’re only checking the first four bytes. So is this theory correct?
Well… here’s the actual sighash of the target function: >http://ethers.utils.id(\’putCurEpochConPubKeyBytes(bytes)\’).slice(0, 10) ‘0x41973cd9’
And the sighash that the attacker crafted… > http://ethers.utils.id(\’f1121318093(bytes,bytes,uint64)\’).slice(0, 10) ‘0x41973cd9’
Fantastic. No private key compromise required! Just craft the right data and boom… the contract will just hack itself!
One of the biggest design lessons that people need to take away from this is: if you have cross-chain relay contracts like this, MAKE SURE THAT THEY CAN’T BE USED TO CALL SPECIAL CONTRACTS. The EthCrossDomainManager shouldn’t have owned the EthCrossDomainData contract.
Separate concerns. If your contract absolutely need to have special privileges like this, make sure that users can’t use cross-chain messages to call those special contracts.
The only thing I need to confirm this 100% is the original message from the other chain. Unfortunately it seems that message was sent from the Ontology network and I need to understand more about how contracts/transactions work on that network to find the initiation tx.
From the above, it seems this could’ve been triggered from any network. My guess is Ontology was chosen deliberately to make the whole thing harder to trace. Idk for sure though.
Some additional important context here:
Poly is a cross-chain transaction project. Basically, they allow you to move assets /between/ different blockchains.
The basic mechanism used here is:
1. Deposit your assets into a “lock box” on one blockchain.
2. Some representation of those assets magically appear on the other blockchain.
The “lock box” will only ever release assets if it gets a message from a corresponding “lock box” on another blockchain basically asking it to “please give this user some funds”.
The “lock box” authenticates this message from the other blockchain by checking that it’s been signed by a group of people that Poly called “bookkeepers”.
The hacker figured out how to override the list of bookkeepers so that the hacker was now the /only/ bookkeeper.
This made it possible for the attacker to forge messages from the “lock box” on the other chain. The “lock box” on Ethereum suddenly got a message that said “please give the hacker all of the money”. It checked the signature attached to that message and it matched the bookkeeper!
But of course it matched the bookkeeper, the bookkeeper was the hacker now!
The original content can be viewed here: https://twitter.com/kelvinfichter/status/1425217046636371969