Skip to content

Instantly share code, notes, and snippets.

@yajin
Created August 11, 2021 18:32
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yajin/0f1a7acfd54adce02422298a1dea8d89 to your computer and use it in GitHub Desktop.
Save yajin/0f1a7acfd54adce02422298a1dea8d89 to your computer and use it in GitHub Desktop.

The Further Analysis of the Poly Network Attack

By BlockSec

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 putCurEpochConPubKeyBytes function in EthCrossChainData without 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

Attack Flow

Ontology transaction -> Ontology relayer -> Poly chain -> Ethereum relayer -> Ethereum

Ethereum

0x838bf9e95cb12dd76a54c9f9d2e3082eaf928270: EthCrossChainManager
0xcf2afe102057ba5c16f899271045a0a37fcb10f2: EthCrossChainData
0x250e76987d838a75310c34bf422ea9f1ac4cc906: LockProxy
 
Transaction: 0xb1f70464bd95b774c6ce60fc706eb5f9e35cb5f06e6cfe7c17dcda46ffd59581

Ontology

Transaction: 0xf771ba610625d5a37b67d30bf2f8829703540c86ad76542802567caaffff280c

Poly

Transaction: 0x1a72a0cf65e4c08bb8aab2c20da0085d7aee3dc69369651e2e08eb798497cc80

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:

  1. the attacker first initiated a malicious transaction (0xf771ba610625d5a37b67d30bf2f8829703540c86ad76542802567caaffff280c) on Ontology Chain;
  2. the attacker then modified keeper's public key stored in the EthCrossChainData contract on Ethereum;
  3. 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 6631, i.e., 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 [2].

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 [1].

0x3. Relayer

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 EthCrossChainData smart 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
  • CrossChainContractAddress is 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 CrossChainContractAddress.

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 ProcessToAliianceCheckAndRetry 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.

Credits

Yufeng Hu, Siwei Wu, Lei Wu, Yajin Zhou @ BlockSec

@zb3
Copy link

zb3 commented Sep 5, 2021

Let me answer my own questions, for anyone interested... note this might not be 100% correct.
Questions I had:

  • why couldn't the attacker call unlock directly?
  • why was a fix on the ethereum side sufficient?

So, the ontology relayer doesn't check anything because the actual authentication is the contract that calls 0x09.createCrossChainTx. 0x09 native contract on ontology creates the MakeTxParam but writes the caller address to the FromContractAddress field. Poly just verifies whether the field was correctly relayed, but it doesn't need to authenticate anything beyond that... at least as long as we call LockProxy.

This field is checked in the LockProxy contract:

        require(fromContractAddr.length != 0, "from proxy contract address cannot be empty");
        require(Utils.equalStorage(proxyHashMap[fromChainId], fromContractAddr), "From Proxy contract address error!");

That's why it wasn't possible to call unlock directly.

Now, the above also means that as long as only this contract is executed, the attacker can't do anything without being able to spoof fromContractAddr. But that can't be done as explained above.

Was the fix on the ethereum side sufficient, given FromContractAddress and CrossChainID are the only fields the attacker doesn't control? Time will tell...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment