Ok here's how the Poly Network hack actually worked. If I'm reading the contracts correctly, it's pretty genius.
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: #L127" target="_blank" rel="noopener" onclick="event.stopPropagation()">github.com
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... #L185" target="_blank" rel="noopener" onclick="event.stopPropagation()">github.com
But Poly forgot to prevent users from calling a very important target... the EthCrossChainData contract: github.com.
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: #L22" target="_blank" rel="noopener" onclick="event.stopPropagation()">github.com
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...
Link to that function here: #L45" target="_blank" rel="noopener" onclick="event.stopPropagation()">github.com
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. 1/n
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)".
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:
>ethers.utils.id('putCurEpochConPubKeyBytes(bytes)').slice(0, 10)
'0x41973cd9'
>ethers.utils.id('putCurEpochConPubKeyBytes(bytes)').slice(0, 10)
'0x41973cd9'
And the sighash that the attacker crafted...
> ethers.utils.id('f1121318093(bytes,bytes,uint64)').slice(0, 10)
'0x41973cd9'
> 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.
This also needs to become a regular thing that auditors of cross-chain projects check for. Just make it part of the checklist for any x-chain project, please.
For some additional context: once you have control over the signing keys, you can send any cross-chain messages you want. All the attacker had to do was send a message that would transfer themselves all of the ETH locked in the Ethereum deposit contract: #L102" target="_blank" rel="noopener" onclick="event.stopPropagation()">github.com
Note the following check, which normally prevents this sort of attack by verifying that the address that sent the message is a special contract address: #L106" target="_blank" rel="noopener" onclick="event.stopPropagation()">github.com
Since the attacker could sign off on whatever they wanted at this point, they could make it look like the deposit/withdrawal contract on the other chain was allowing the attacker to withdraw everything
And just for good measure, here's the function to trigger a cross-chain message: #L91" target="_blank" rel="noopener" onclick="event.stopPropagation()">github.com
Nothing here prevents you from sending a message to the EthCrossChainData contract.
Nothing here prevents you from sending a message to the EthCrossChainData contract.
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.
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.
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!
Anyway, that's the lighter version of this story.
Looks like someone found it!
Loading suggestions...