Analysis of the Orbit Chain Exploit

8 min read

Learn how Orbit Chain was exploited, resulting in a loss of assets worth $81.6 million.

TL;DR#

On December 31, 2023, Orbit Chain was exploited across a series of transactions, which resulted in a loss of funds worth approximately $81.6 million. 

Introduction to Orbit Chain#

Orbit is a cross-chain bridge platform.

Vulnerability Assessment#

The root cause of the exploit appears to be the misuse of valid signatures for unauthorized transactions. The exploiter was likely able to create fake signatures for a withdrawal transaction by compromising the private keys of the owner.

Attack Steps#

ETH:
The address tagged as Orbit Bridge Exploiter 4 first drained 0.004 ETH on 06:14:11 PM UTC from the Orbit Chain ETH vault. This was followed by another transaction at 07:45:59 PM UTC in which 0.000137 ETH was drained.

Roughly, after one and a half hours after the second transaction, the vault was drained of approximately 9500 ETH.

USDT:
The address tagged as Orbit Bridge Exploiter 5 first drained 9.71 USDT on 06:30:11 PM UTC from the Orbit Chain ETH vault. This was followed by yet another transaction at 07:51:35 PM UTC for the same value.

Roughly, after one and a half hours after the second transaction, the vault was drained of approximately $30 million worth of USDT.

USDC:

The address tagged as Orbit Bridge Exploiter 1 first drained 3.92 USDC on 08:04:23 PM UTC from the Orbit Chain ETH vault. Roughly after one hour and 20 minutes, the vault was drained of approximately $10 million USDC.

DAI:
The address tagged as Orbit Bridge Exploiter 3 first drained 1.322 DAI on 08:22:59 PM UTC from the Orbit Chain ETH vault. Roughly after 35 minutes of this, the vault was drained of approximately 10 million DAI.

WBTC:
The address tagged as Orbit Bridge Exploiter 2 first drained 0.012 WBTC on 08:40:35 PM UTC from the Orbit Chain ETH vault. Roughly after 30 minutes, the vault was drained of approximately 230.879 WBTC.

Dispersed Assets Holders:
The stolen funds are held at the following addresses -
0x009b60aab8e64c8f5fe449bd96fa78b1a7fffcc5 - $21,703,809
0x157a409c2bfff38209a32e55d3eac1bfc93dd664 - $4,999,328
0x3a886a63c768665a9830886e608d6f9dc6b4f730 - $10,001,788
0x589257e07e11e761f31956d54b2323f63ee36b7d - $9,869,515
0x5e22cb028865d6a93080d7ab42d2fe9a0e8dc085 - $9,713,789
0x817bb1761b715a08a9142f99fa7d0ccf73f4c0ef - $4,999,443
0x9ca536d01b9e78dd30de9d7457867f8898634049 - $2,279
0xa70f8917a957757f5505a5535df1591c54f65b9d - $2,281
0xd283fa3bd85887725c8982f539cc404a450f7fd9 - $9,135,360
0xdadfa3ccd40fc3d5a0164c6f9444f60163ccbf3b - $2,296
0xf49de491e1c0d84a0e0bd2d57a841825fcf179fd - $10,683,841

Withdraw Logic#

In all of these attack transactions, one entity remains constant which is the `bytes32s` parameter `50F408F4B0FB17BF4F5143DE4BD95802410D00422F008E9DEEF06FB101A0F060` as viewed from the event logs in the withdraw function.

undefined

Let’s attempt to decipher the withdraw function from the vulnerable contract

// Fix Data Info
///@param bytes32s [0]:govId, [1]:txHash
///@param uints [0]:amount, [1]:decimals
function withdraw(
  address hubContract,
  string memory fromChain,
  bytes memory fromAddr,
  bytes memory toAddr,
  bytes memory token,
  bytes32[] memory bytes32s,
  uint[] memory uints,
  uint8[] memory v,
  bytes32[] memory r,
  bytes32[] memory s
) public onlyActivated {
  require(bytes32s.length >= 1);
  require(bytes32s[0] == sha256(abi.encodePacked(hubContract, chain, address(this))));
  require(uints.length >= 2);
  require(isValidChain[getChainId(fromChain)]);

  bytes32 whash = sha256(abi.encodePacked(hubContract, fromChain, chain, fromAddr, toAddr, token, bytes32s, uints));

  require(!isUsedWithdrawal[whash]);
  isUsedWithdrawal[whash] = true;

  uint validatorCount = _validate(whash, v, r, s);
  require(validatorCount >= required);

  address payable _toAddr = bytesToAddress(toAddr);
  address tokenAddress = bytesToAddress(token);
  if (tokenAddress == address(0)) {
    if (!_toAddr.send(uints[0])) revert();
  } else {
    if (tokenAddress == tetherAddress) {
      TIERC20(tokenAddress).transfer(_toAddr, uints[0]);
    } else {
      if (!IERC20(tokenAddress).transfer(_toAddr, uints[0])) revert();
    }
  }

  emit Withdraw(hubContract, fromChain, chain, fromAddr, toAddr, token, bytes32s, uints);
}

The function begins by checking the length of the `bytes32s`. In the second part of the validation, it is checking if the first element of the `bytes32s` array equals the SHA-256 hash of the concatenated hubContract, chain, and contract address.

require(bytes32s[0] == sha256(abi.encodePacked(hubContract, chain, address(this))));

This above line is crucial because it serves as a critical validation check. Given that the `bytes32s[0]` value remained constant across all the exploited transactions, and assuming these transactions passed this `require` check, it suggests that the hubContract, chain, and contract address values remained consistent across these transactions.

This is expected behavior, as these parameters typically remain constant for a given deployment of a smart contract in a specific environment or for a specific use case.

Now, as the exploit did not manipulate the `bytes32s[0]` value directly, nor did it tamper with the hubContract, chain, or the contract's address, the attackers must have manipulated other aspects of the transaction data to bypass security checks.

The function also verifies the validity of the fromChain using `isValidChain[getChainId(fromChain)]`. This ensures that the withdrawal is being made from a recognized chain.

`whash` generation:
A hash `whash` is generated using key transaction data, including `hubContract`, `fromChain`, `chain`, `fromAddr`, `toAddr`, `token`, `bytes32s`, and `uints`.

bytes32 whash = sha256(abi.encodePacked(hubContract, fromChain, chain, fromAddr, toAddr, token, bytes32s, uints));

This line creates a hash of the concatenated inputs, intended to uniquely identify each withdrawal request. The subsequent check,

require(!isUsedWithdrawal[whash]);
isUsedWithdrawal[whash] = true;

aims to prevent the same withdrawal hash from being used more than once, and helps to prevent replay attacks.

Validator Count:
The `_validate` function checks the signatures (`v`, `r`, `s`) against the withdrawal hash. The number of valid signatures must meet a certain threshold (`required`).

uint validatorCount = _validate(whash, v, r, s);
require(validatorCount >= required);

This part ensures that the withdrawal has been approved by a sufficient number of validators. However, if the whash can be manipulated while still getting valid signatures from validators, this check might not be sufficient to prevent fraud.

Token Transfer:
The withdraw function then transfers the specified amount `uints[0]` to the target address `_toAddr`, with special case handling for the USDT token. Finally, it emits a `Withdraw` event with all relevant transaction details.

The Issue:
However, the function does not validate the token parameter directly against a list of authorized tokens or any other criteria that would ensure its legitimacy. This means that a potential exploiter can specify any token address.

Also, the function does not validate whether the `msg.sender` or caller has the right to withdraw the specified token and amount. Meaning, if the contract relies solely on this function for withdrawals, without additional checks elsewhere, it could be vulnerable to unauthorized withdrawals.

Validation Logic#

As seen from the withdraw function logic, the `validatorCount` has to be greater than or equal to the `required` index, thus it ensures that the transaction only proceeds if there are enough valid signatures.

function _validate(bytes32 whash, uint8[] memory v, bytes32[] memory r, bytes32[] memory s) private view returns (uint) {
  uint validatorCount = 0;
  address[] memory vaList = new address[](owners.length);

  uint i = 0;
  uint j = 0;

  for (i; i < v.length; i++) {
    address va = ecrecover(whash, v[i], r[i], s[i]);
    if (isOwner[va]) {
      for (j = 0; j < validatorCount; j++) {
        require(vaList[j] != va);
      }

      vaList[validatorCount] = va;
      validatorCount += 1;
    }
  }

  return validatorCount;
}

However, there is a lack of direct association between signatures and withdrawal details. The `_validate` function verifies the validity of the signatures in terms of them coming from recognized owners but does not ensure that these signatures are directly tied to the transaction details in `whash`.

This means that, if an attacker gains access to the private key of even one of the owners, they could generate valid signatures for any transaction. Since `_validate` doesn't check the association of the signature with specific transaction details, these signatures would be considered valid, potentially leading to unauthorized transactions.

Attack Logic#

  • The attackers possibly gained access to at least one private key of a recognized validator (owner) in the Orbit Chain.

  • With this access, they could generate valid signatures for transactions, exploiting the `_validate` function's inability to link these signatures directly to the specific details of the transaction (encapsulated in whash).

  •  Since the _validate function only verified the authenticity of signatures (not their association with the transaction details), these crafted transactions passed the validation checks.
     
  • The attackers executed these transactions, resulting in the withdrawal of large sums of various assets from the vaults.

Aftermath#

The orbit exchange team sent an on-chain message to the attacker, stating that they are open to suggestions and ready to listen.

Solution#

To address each scenario in the Orbit Chain incident, we need to consider various aspects of blockchain security, smart contract design, and operational procedures. The primary focus should be on enhancing the integrity of the validation process, ensuring the security of private keys, and implementing robust fail-safes against unauthorized transactions.

Starting with the misuse of valid signatures for unauthorized transactions, the core issue is the validation mechanism's inability to associate signatures with specific transaction details. A potential solution is to implement multi-factor transaction validation. This approach could involve linking each signature to specific transaction parameters and requiring a secondary confirmation for each transaction. This secondary confirmation could be an off-chain verification process or a multi-signature requirement involving several validators. Such a system would ensure that even if one private key is compromised, the attacker cannot authorize transactions single-handedly.

Regarding the vulnerability in the withdrawal function, the contract should include stringent checks for token addresses and withdrawal rights. This can be achieved by maintaining a whitelist of approved tokens and validating each withdrawal request against this list. Additionally, incorporating role-based access control (RBAC) would ensure that only authorized entities can initiate withdrawal requests. RBAC can be implemented by mapping addresses to roles and verifying these roles before processing any withdrawal.

The smart contract should also include a mechanism to detect and flag unusual transaction patterns. Anomalies such as multiple large withdrawals in a short period or withdrawals of amounts significantly higher than average should trigger an automatic pause in transactions, followed by a manual review. This kind of anomaly detection system acts as an early warning mechanism to prevent large-scale losses.

Furthermore, improving key management practices is essential. The use of hardware security modules (HSMs) for storing private keys can significantly reduce the risk of key compromise. HSMs provide a highly secure environment for key generation, management, and storage, thereby safeguarding against external breaches.

Despite rigorous security measures, completely eliminating the risk of exploitation remains an elusive target. This inherent uncertainty in the blockchain space highlights the vital role we play at Neptune Mutual. By establishing a dedicated cover pool in our marketplace, the impacts of exploits, like the one suffered by Orbit Chain, could have been significantly mitigated. Our services provide users with a layer of reassurance, effectively diminishing the financial or digital asset losses due to smart contract vulnerabilities, thanks to our distinctive parametric-based policies.

At Neptune Mutual, we offer a unique advantage for those who opt for our parametric cover policies. There's no need for users to present proof of loss to receive a payout. Once an incident is verified and conclusively resolved through our incident resolution system, affected users can promptly claim their payouts. This streamlined approach ensures that users are not bogged down by lengthy and complicated claims processes, which is often a pain point in traditional insurance models.

We have extended our marketplace across several prominent blockchain networks, including EthereumArbitrum, and the BNB chain. This expansive coverage allows us to deliver insurance solutions to a diverse array of DeFi users.

Reference Source SlowMist

By

Tags