The attack consists of two main steps. The first step is to change the keeper and the second step is to withdraw the tokens (executing the unlock function). The second step has been fully analyzed. For the first step, Kevin ( https://twitter.com/kelvinfichter) has pointed out that the hash collision is one smart trick used by the hacker to invoke the
putCurEpochConPubKeyBytes function. However, why the attacker can have a valid transaction to make this call in the first place is still unknown.
In this blog, we use the malicious transaction from Ontology (0xf771ba610625d5a37b67d30bf2f8829703540c86ad76542802567caaffff280c) to illustrate the whole process.
In summary, we find that:
- The Ontology relayer does not have enough validation mechanisms for the transaction from the Ontology chain.
- The attacker can directly invoke the
EthCrossChainDatawithout going through the Ethereum relayer, as long as there is a valid block on the Poly chain.
- The hash collision as pointed out by Kevin (https://twitter.com/kelvinfichter/status/1425290462076747777)
Disclaimer: This blog contains the analysis results by our team, which are based on the publicly available source code and the on-chain transactions. Without the further information from Poly Network, we are not able to verify our result.
0x.1 Transactions and Contracts
Ontology transaction -> Ontology relayer -> Poly chain -> Ethereum relayer -> Ethereum
0x838bf9e95cb12dd76a54c9f9d2e3082eaf928270: EthCrossChainManager 0xcf2afe102057ba5c16f899271045a0a37fcb10f2: EthCrossChainData 0x250e76987d838a75310c34bf422ea9f1ac4cc906: LockProxy
0x2. Attack Flow
Take the attack occurred on Ethereum as an example. This is a
cross-chain attack involving three chains (and their corresponding relayers), i.e., Ontology Chain, Poly Chain and Ethereum.
The whole attack flow consists of three steps:
- the attacker first initiated a malicious transaction (0xf771ba610625d5a37b67d30bf2f8829703540c86ad76542802567caaffff280c) on Ontology Chain;
- the attacker then modified keeper’s public key stored in the
EthCrossChainDatacontract on Ethereum;
- the attacker finally crafted a malicious transaction to harvest crypto assets.
0x2.1 The First Step
The attacker first initiated a cross-chain transaction (0xf771ba610625d5a37b67d30bf2f8829703540c86ad76542802567caaffff280c) from Ontology, which includes a malicious payload:
You may notice that this payload contains a crafted function name (starting with
f1121318093 after conversion). This name is definitely meticulous, because the attacker would use it to invoke the
putCurEpochConPubKeyBytes function (see the
EthCrossChainData contract on Ethereum) by exploiting the hash collision of function signatures. Here we will not illustrate the details of hash collision stuff, because it has been discussed a lot .
After that, this transaction was successfully accepted by the Ontology Chain Relayer. Note that there does NOT exist any strict verification. As a result, it became a valid new transaction (0x1a72a0cf65e4c08bb8aab2c20da0085d7aee3dc69369651e2e08eb798497cc80) on Poly Chain.
The new transaction was then perceived and REJECTED by the Ethereum Relayer. Because the Ethereum Relayer verified the destination contract address (i.e.,
EthCrossChainData in this case), however, only
LockProxy would be allowed.
As such, the processing was terminated. However, the transaction with the malicious payload has been stored on Ploy Chain, which can be exploited to launch attack.
0x2.2 The Second Step
The attacker manually sent transaction to Ethereum by invoking the
verifyHeaderAndExecuteTx function of the
EthCrossChainManager contract. The malicious transaction data stored on Poly Chain was used as the input. As a valid Poly Chain transaction, it was able to bypass the verification (including the signatures and the merkle proof) in the
verifyHeaderAndExecuteTx function. After that, the
putCurEpochConPubKeyBytes function of the
EthCrossChainData contract was invoked to modify the original four keepers to a new one (i.e., 0xA87fB85A93Ca072Cd4e5F0D4f178Bc831Df8a00B) controlled by the attacker.
0x2.3 The Third Step
After the modification of the keeper, the attacker was able to directly call the
verifyHeaderAndExecuteTx function without using Poly Chain. Finally, the
unlock function of the
LockProxy contract was invoked to stole huge amount of digital assets from Ethereum. The detailed analysis can be found in our previous report .
Both Ontology and Ethereum relayers are implemented in Go. However, they lack of enough validation so that
- The attacker can construct a malicious transaction, which will be packed into the Poly chain
- The attacker can directly invoke the functions in the
EthCrossChainDatasmart contract on Ethereum
0x3.1 Ontology relayer blindly trusts the cross-chain transactions from Ontology
The ont_relayer( https://github.com/polynetwork/ont-relayer) is responsible for listening to cross-chain transactions from the Ontology chain and sending them to the Poly chain.
- Side means the Ontology chain; Alliance means the Poly chain
CrossChainContractAddressis the native smart contract (number 09) on the Ontology chain
The above figure shows that the Ontology relayer starts two routines to listen to cross-chain transactions from and to the Ontology chain, and the routine to check the status of the cross-chain transaction (line 71).
In the above figure, the Ontology relayer invokes the RPC interface exposed by the Ontology chain (line 215
GetSmartContractEventByBlock) to obtain events on the chain. From line 228 and 232, we can see that this routine only listens to the
makeFromOntProof event triggered by
In the above figure, when processing the cross-chain transaction, there are five checks. The first two are checking the RPC requests (check 1 and 4) to the Ontology chain and three checks for whether the parameters are null (check 2, 3 and 5). However, there does not exist any check for the semantics in the cross-chain transaction, i.e., whether the contract and method name are reasonable. At last, it sends the transaction to the Poly chain (line 183)
The Ontology relayer constructs and sends the transaction to the Poly chain using the RPC interface (line 164 — SendTransaction).
The function ProcessToAliiance Check AndRetry only checks whether the transaction has failed. If so, it will resend the transaction.
In summary, ont-relayer listens to all the
makeFromOntProof events triggered by
CrossChainContractAddress from the Ontology chain. Then the transaction will be sent to the Poly chain. Note that, the cross-chain transaction from anyone on the Ontology chain will trigger the
makeFromOntProof event, which results in being sent to the Poly chain.
0x3.2 Bypass Ethereum Relayer
Ethereum Relayer is responsible for listening transactions from the Poly chain and then sending the transaction to Ethereum.
Ethereum Relayer starts a Goroutine to monitor the Poly Chain;
It monitors the cross-chain transaction whose target is Ethereum (line 275–278). Then it checks whether the target contract (
ToContractAddress) is the one of the contracts that are configured in
config.TargetContracts. If not, the cross-chain transaction will not be sent to the target chain (Ethereum).
However, the attacker can directly interact with the target chain and invoke the function in
EthCrossChainManager. In another world, the check in the Ethereum relayer can be bypassed. As long as the malicious transaction has been packed on the poly chain (which is achieved in the previous step through the Ontology relayer), the attacker can directly interact with
EthCrossChainManager. During this process, the signature verification (
ECCUtils.verifySig) and merkle prove (
ECCUtils.merkleProve) can pass, since there is a valid transaction on the Poly chain.
By using the previous two methods, the attacker can successfully invoke the
ToContractAddress.method on Ethereum. Combining with the hash collision, the
putCurEpochConPubKeyBytes function is eventually invoked to change the keeper.
Yufeng Hu, Siwei Wu, Lei Wu, Yajin Zhou @ BlockSec
Twitter: BlockSec (@BlockSecTeam) / Twitter