On March 21, 2024, the SSS Token (Super Sushi Samurai) was exploited due to a contract flaw. A logic error in the token contract allowed the attacker to arbitrarily increase the SSS Token balance of a specified account, resulting in a loss of over 1,310 ETH (approximately $4.6 million) for the project.
Less than a week after the SSS Token attack, another larger attack occurred on Blast, targeting the Munchables project. The attacker made off with 17,413.96 ETH, amounting to approximately $62.5 million. Half an hour after this attack transaction, 73.49 WETH from the project's contract was also stolen and transferred to another address by the hacker. At that time, the project's contract address still held 7,276 WETH, 7,758,267 USDB, and 4 ETH, all of which were at risk of falling into the hacker's hands. The hacker had the ability to take all the funds of the entire project, exposing a total of approximately $97 million to risk.
Following the breach, onchain detective @zachxbt identified North Korean hackers as the primary perpetrators.
Due to the significant losses suffered by users in this attack, we immediately initiated our own onchain investigation. Let's delve deeper into how these North Korean hackers executed an attack nearing a hundred million dollars.
The Victim's Statement: March 26, 2024, at 21:37 [UTC+0] (5 minutes after the attack), Munchables publicly confirmed the incident on X.
The Crime Scene: The compromised contract (0x29958E8E4d8a9899CF1a0aba5883DBc7699a5E1F) is a proxy contract that held users' staked funds.
We can see that the attacker invoked the unlock function of the staking contract, passed all permission checks, and transferred all the ETH in the contract to Attacker Address 1 (0x6E8836F050A315611208A5CD7e228701563D09c5).
It appears that the attacker invoked an unlock function, similar to a withdraw action, and withdrew most of the ETH from the compromised contract (0x29..1F).
The unlock function in the compromised contract (0x29..1F) has two relevant checks. Let's examine them one by one.
First, we found that in the process of verifying permissions, the isRegistered method of contract (0x16..A0) was called to check whether the current msg.sender, which in this case is Hacker Address 1 (0x6E8836F050A315611208A5CD7e228701563D09c5), has already been registered:
The answer is: True.
This involves contract (0x16..A0) and its corresponding latest logic contract (0xe74..f1).
At March 24, 2024, at 08:39 [UTC+0] (two days before the attack), the Implementation contract corresponding to contract (0x16..A0) was upgraded.
The implementation contract was updated to 0xe7..f1.
The original implementation contract address can be seen here, which is 0x9e..CD.
At this point, we suspect that the hacker updated the implementation contract of the proxy contract, changing it from the original 0x9e..CD to the malicious 0xe7..f1, thereby bypassing the permission verification.
Luckily, in Web3, there is no need for guesswork or relying on others' words. If you have the technical skills, you can verify the answers yourself.
By comparing the two contracts (which are not open-sourced), we can observe some obvious differences between the original 0x9e..CD contract and the updated 0xe7..f1 contract.
The implementation of the initialize function in the 0xe7..f1 contract is as follows:
The implementation of the initialize function in the 0x9e..CD contract is as follows:
As we can see, in the original implementation contract (0x9e..CD), the attacker's address (0x6e..c5) was registered, along with two other attacker addresses 0xc5..0d and 0xbf..87. Additionally, their field0 was set to the block time at initialization. The use of field0 will be explained later.
Contrary to our initial speculation, the implementation contract with the backdoor was actually the original one, and the later updated contract turned out to be normal!
Wait, this update occurred on [UTC+0] March 24, 2024, at 08:39 (two days before the attack), which means that before this incident, the implementation contract had already been changed to one without a backdoor. So why was the attacker still able to carry out the attack afterward?
This is because of the delegatecall, which means that the actual state storage update is in contract (0x16..A0). This also means that even after the implementation contract was updated to the logic contract 0xe7..f1 without the backdoor, the slot changed in contract (0x16..A0) would not be restored.
Let's verify this:
As we can see, the slot corresponding to contract (0x16..A0) does have a value.
This allows the attacker to pass the verification in the isRegistered method:
The attacker later replaced the backdoor contract with a normal contract to cover their tracks. However, by that time, the backdoor had already been planted.
Additionally, in the unlock process, there is a second verification: a check for the lock duration is performed to ensure that the locked assets cannot be transferred before the lock period expires.
The attacker needs to ensure that the block time when unlock is called is greater than the required lock expiration time (field3).
This verification involves the compromised contract (0x29..1F) and its corresponding implementation contract 0xf563Ce437E3aB8e0B79585dF5122700FBc42aFcd.
In a transaction on March 21, 2024, at 11:54 [UTC+0] (five days before the attack), we see that the original implementation contract for the compromised contract (0x29..1F) was 0x91..11.
However, just four minutes later, it was upgraded to 0xf5..cd.
Let's compare the two contracts. We can see that, similarly to before, the attacker tampered with the initialize function in both contracts. The implementation of the initialize function in the 0xf5...cd contract is as follows:
The implementation of the initialize function in the 0x91...11 contract is as follows:
The attacker manipulated the ETH amount and unlock time in the contract, then reverted it to its original form to obscure their actions. This made it difficult for the project team and us as security researchers to identify the breach, especially since the contracts were not open-source, adding another layer of complexity to uncovering the core issue.
We've delved into how the attacker conducted a transaction with 17,413 ETH. Our analysis highlighted three embedded addresses within the contract.
- 0x6e...c5 (Attacker Address 1)
- 0xc5...0d (Attacker Address 2)
- 0xbf...87 (Attacker Address 3)
While we initially focused on the first, the roles and activities of the latter two addresses remain undisclosed. Moreover, the functions of the address(0), _dodoApproveAddress, and _uniswapV3Factory parameters within the contract's code are still unclear, suggesting there's more to uncover regarding the contract's inner workings and the full scope of the attack.
The Second Crime Scene: Let's take a look at what Attacker Address 3 (0xbf...87) did. They used the same method to steal 73.49 WETH.
Furthermore, the source address for the attack gas (0x97...de) provided fees to both 0xc5...0d (Attacker Address 2) and 0xbf...87 (Attacker Address 3).
The source of the 0.1 ETH from the attack gas source address (0x97..de) can be traced back to owlto.finance (a cross-chain bridge).
After receiving fees, the second attacker address didn't launch an attack, yet played their part in the operation, as we’ll soon see.
Post-event analysis revealed the compromised contract (0x29..1F) held substantial assets beyond the initial theft, including over 7,000 WETH and more than 7 million USDB.
And here’s the rescue transaction.
Originally, the attacker intended to steal these assets. We can see that the address 0xc5...0d (Attacker Address 2) originally intended to steal USDB.
The _dodoApproveAddress here is 0x0000000000000000000000004300000000000000000000000000000000000003:
Which is the address for USDB:
Address 0xbf..87 (Attacker Address 3) was used to steal WETH:
The _uniswapV3Factory here is 0x0000000000000000000000004300000000000000000000000000000000000004
Which is the address for WETH:
Address 0x6e...c5 (Attacker Address 1) was responsible for stealing address(0), which is the native asset ETH.
The attacker could steal the corresponding assets through the following logic by setting field0:
Why Didn't They Steal Everything?
In theory, the attacker could have stolen all the remaining assets, including the remaining WETH and USDB.
Address 0xbf..87 (Attacker Address 3) only stole 73.49 WETH, but they could have taken all 7,350 WETH. They could also have utilized address 0xc5..0d (Attacker Address 2) to take all 7,758,267 USDB. Why they stopped after taking just a small amount of WETH is unclear.
Why Didn't They Transfer the 17,413 ETH to the Ethereum Mainnet?
As is well-known, Blast has the ability to intercept these ETH through a centralized mechanism, ensuring they remain permanently within the network and avoiding any substantial user losses. However, once these ETH enter the Ethereum mainnet, there is no way to intercept them.
While the Blast's official bridge imposes no transfer limits but enforces a 14-day withdrawal period, third-party bridges facilitate quicker transactions, raising questions about the attacker's initial hesitancy to transfer assets.
In fact, the attacker initiated cross-chain transfers within two minutes of the attack:
The funds arrived on the Ethereum mainnet within 20 seconds. In theory, the attacker could continuously carry out cross-chain transfers, moving a large amount of ETH across before any manual intervention by the bridge operators.
The restriction to transferring only 3 ETH at a time is attributed to the liquidity limitations set by the cross-chain bridge, affecting the volume that can be moved between networks. Transferring from Blast to Ethereum involves:
Another cross-chain bridge supporting Blast offers even less:
After this transaction, the attacker did not continue with any further cross-chain operations. The reason for this is unknown. It appears that they may not have been adequately prepared for withdrawing funds from Blast.
Following the attack, the community rallied.
Ultimately, due to collective efforts from the broader community, the attackers, potentially fearing exposure, returned all stolen assets by providing the Munchables team with the private keys for the associated addresses. Subsequently, the team executed a rescue operation, securely transferring the retrieved funds to a multi-signature contract.