-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
107 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.0; | ||
|
||
import '../../src/EthernautCTF/MagicNumber.sol'; | ||
import '@forge-std/Test.sol'; | ||
import '@forge-std/console2.sol'; | ||
|
||
interface Solver { | ||
function whatIsTheMeaningOfLife() external pure returns (bytes32); | ||
} | ||
|
||
// The problem with this naive approach is that the deployed bytecode weights 119 bytes. | ||
// The goal is to have a contract that weights at most 10 bytes! | ||
// [93083] MagicNumberExploit::testNaiveExploit() | ||
// ├─ [23875] → new NaiveSolver@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f | ||
// │ └─ ← [Return] 119 bytes of code | ||
// ├─ [0] console::log("Naive solver deployed") [staticcall] | ||
contract NaiveSolver is Solver { | ||
function whatIsTheMeaningOfLife() public pure returns (bytes32) { | ||
return bytes32(uint256(42)); | ||
} | ||
} | ||
|
||
contract MagicNumberExploit is Test { | ||
MagicNumber target; | ||
address deployer = makeAddr('deployer'); | ||
|
||
function setUp() public { | ||
vm.startPrank(deployer); | ||
target = new MagicNumber(); | ||
console2.log('Target contract deployed'); | ||
vm.stopPrank(); | ||
} | ||
|
||
function testNaiveExploit() public { | ||
Solver solver = new NaiveSolver(); | ||
console2.log('Naive solver deployed'); | ||
|
||
target.setSolver(address(solver)); | ||
bytes32 result = Solver(target.solver()).whatIsTheMeaningOfLife(); | ||
console2.log( | ||
'What is the meaning of life? %s', | ||
vm.parseUint(vm.toString(result)) | ||
); | ||
} | ||
|
||
function testSmartExploit() public { | ||
// The size of the contract will be equal to its runtime size with is 10 bytes. | ||
// Indeed, we use 10 opcodes to return the number 42. | ||
// | ||
// Runtime code which returns 42. | ||
// 602a60005260206000F3 | ||
// 1. Store 42 to memory at offset 0. | ||
// PUSH1 0x2a (42 in hexadecimal) | ||
// PUSH1 0x00 | ||
// MSTORE | ||
// 2. Return 32 bytes from memory, starting at offset 0. | ||
// PUSH1 0x20 (32 in hexadecimal) | ||
// PUSH1 0x00 | ||
// RETURN | ||
|
||
// Creation code which returns the runtime code. | ||
// 69602a60005260206000F3600052600a6016F3 | ||
// 1. Store runtime code to memory at offset 0. | ||
// PUSH10 0x602a60005260206000F3 | ||
// PUSH1 0x00 | ||
// MSTORE | ||
// 2. Return 10 bytes from memory, starting at offset 22. | ||
// PUSH1 0x0a (10 in hexadecimal) | ||
// PUSH1 0x16 (22 in hexadecimal) | ||
// RETURN | ||
bytes memory bytecode = hex'69602a60005260206000F3600052600a6016F3'; | ||
|
||
address solver; | ||
assembly { | ||
// Create a new contract using the initialization code provided at the indicated offset in the memory. | ||
// create(value, offset, size) | ||
// - value: the amount of ether (in wei) to send to the new contract. | ||
// - offset: the memory location where the contract's initialization code begins. | ||
// - size: the size of the initialization code in bytes. | ||
|
||
// How to compute the offset of the bytecode? | ||
// To compute the offset for the contract bytcode, we use add(bytecode, 0x20) to skip the first | ||
// 32 bytes (0x20 in hexadecimal) which contain the length of the byte array. | ||
// Indeed, when declaring a dynamic-sized array, including the bytes types in Solidity, it is | ||
// stored in memory with a specific layout: | ||
// 1. The first 32 bytes (0x20 in decimal) contain the length of the byte array. | ||
// 2. The actual bytes data starts immeditately after these 32 bytes. | ||
|
||
// How to compute the size of the bytecode? | ||
// The creation code, 69602a60005260206000F3600052600a6016F3, is composed of 19 opcodes (or bytes). | ||
// And the hexadecimal value of 19 is 0x13, thus size will be 0x13. | ||
solver := create(0, add(bytecode, 0x20), 0x13) | ||
} | ||
assertNotEq(solver, address(0x0)); | ||
console2.log('Smart solver deployed'); | ||
|
||
target.setSolver(solver); | ||
bytes32 result = Solver(target.solver()).whatIsTheMeaningOfLife(); | ||
console2.log( | ||
'What is the meaning of life? %s', | ||
vm.parseUint(vm.toString(result)) | ||
); | ||
} | ||
} |