SlockDotIt의 신제품 ECLocker는 IoT 게이트 잠금 장치와 Solidity 스마트 컨트랙트를 통합한 것으로, 인증에 이더리움 ECDSA를 사용합니다.
유효한 서명이 잠금 장치로 전송되면, 시스템은 Open 이벤트를 발생시키고, 이에 따라 권한이 부여된 컨트롤러가 문을 열 수 있게 됩니다.
SlockDotIt은 출시 전에 이 제품의 보안을 평가해 달라고 당신을 고용했습니다.
👉 당신은 이 시스템을 누구나 문을 열 수 있도록 (즉, 권한 우회) 공격할 수 있겠습니까?
요약: 누구나 ECLocker의 open함수를 호출해 누구나 Open 이벤트를 발생시킬 수 있어야 한다
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "../lib/openzeppelin-contracts/contracts/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;
}
}
function open(uint8 v, bytes32 r, bytes32 s) external {
address add = _isValidSignature(v, r, s);
emit Open(add, block.timestamp);
}
일단 누구나 open() 함수를 호출할 수 있어야 하므로 open() 부터 살펴보겠다
open() 함수는 _isValidSignature() 함수를 호출하고 Open 기록을 남기고 끝.
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;
}
_isValidSignature() 를 살펴보면 ecrecover(msgHash, v, r, s) 를 사용하여 address를 복구하는데 해당 함수는 서명자의 주소를 복구할 수 없거나 실행에 필요한 가스가 부족할 경우 revert가 발생하는 것이 아니라 어떠한 데이터도 반환하지 않습니다.
→ 결과적으로 기본으로 저장되어있는 0x00이 _address 변수에 들어가게 된다.
그리고 위 함수를 우회하려면 아래 2가지 require 문을 우회해야한다.