piątek, 3 stycznia 2025

PicoCTF - RPS

W tym poście chciałbym opisać rozwiązanie zadania RPS z picoCTF.


Działanie gry papier kamień nożyce uruchomionego na serwerze:

  1. Welcome challenger to the game of Rock, Paper, Scissors
  2. For anyone that beats me 5 times in a row, I will offer up a flag I found
  3. Are you ready?
  4. Type '1' to play a game
  5. Type '2' to exit the program
  6. 1
  7. 1
  8.  
  9.  
  10. Please make your selection (rock/paper/scissors):
  11. paper
  12. paper
  13. You played: paper
  14. The computer played: paper
  15. Seems like you didn't win this time. Play again?
  16. Type '1' to play a game
  17. Type '2' to exit the program
  18. 1
  19. 1
  20.  
  21.  
  22. Please make your selection (rock/paper/scissors):
  23. rock
  24. rock
  25. You played: rock
  26. The computer played: rock
  27. Seems like you didn't win this time. Play again?
  28. Type '1' to play a game
  29. Type '2' to exit the program

Kod programu dostarczony do wywania:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <stdbool.h>
  4. #include <string.h>
  5. #include <time.h>
  6. #include <unistd.h>
  7. #include <sys/time.h>
  8. #include <sys/types.h>
  9.  
  10.  
  11. #define WAIT 60
  12. static const char* flag = "[REDACTED]";
  13.  
  14. char* hands[3] = {"rock", "paper", "scissors"};
  15. char* loses[3] = {"paper", "scissors", "rock"};
  16. int wins = 0;
  17.  
  18.  
  19.  
  20. int tgetinput(char *input, unsigned int l)
  21. {
  22.     fd_set          input_set;
  23.     struct timeval  timeout;
  24.     int             ready_for_reading = 0;
  25.     int             read_bytes = 0;
  26.    
  27.     if( l <= 0 )
  28.     {
  29.       printf("'l' for tgetinput must be greater than 0\n");
  30.       return -2;
  31.     }
  32.    
  33.    
  34.     /* Empty the FD Set */
  35.     FD_ZERO(&input_set );
  36.     /* Listen to the input descriptor */
  37.     FD_SET(STDIN_FILENO, &input_set);
  38.  
  39.     /* Waiting for some seconds */
  40.     timeout.tv_sec = WAIT;    // WAIT seconds
  41.     timeout.tv_usec = 0;    // 0 milliseconds
  42.  
  43.     /* Listening for input stream for any activity */
  44.     ready_for_reading = select(1, &input_set, NULL, NULL, &timeout);
  45.     /* Here, first parameter is number of FDs in the set,
  46.      * second is our FD set for reading,
  47.      * third is the FD set in which any write activity needs to updated,
  48.      * which is not required in this case.
  49.      * Fourth is timeout
  50.      */
  51.  
  52.     if (ready_for_reading == -1) {
  53.         /* Some error has occured in input */
  54.         printf("Unable to read your input\n");
  55.         return -1;
  56.     }
  57.  
  58.     if (ready_for_reading) {
  59.         read_bytes = read(0, input, l-1);
  60.         if(input[read_bytes-1]=='\n'){
  61.         --read_bytes;
  62.         input[read_bytes]='\0';
  63.         }
  64.         if(read_bytes==0){
  65.             printf("No data given.\n");
  66.             return -4;
  67.         } else {
  68.             return 0;
  69.         }
  70.     } else {
  71.         printf("Timed out waiting for user input. Press Ctrl-C to disconnect\n");
  72.         return -3;
  73.     }
  74.  
  75.     return 0;
  76. }
  77.  
  78.  
  79. bool play () {
  80.   char player_turn[100];
  81.   srand(time(0));
  82.   int r;
  83.  
  84.   printf("Please make your selection (rock/paper/scissors):\n");
  85.   r = tgetinput(player_turn, 100);
  86.   // Timeout on user input
  87.   if(r == -3)
  88.   {
  89.     printf("Goodbye!\n");
  90.     exit(0);
  91.   }
  92.  
  93.   int computer_turn = rand() % 3;
  94.   printf("You played: %s\n", player_turn);
  95.   printf("The computer played: %s\n", hands[computer_turn]);
  96.  
  97.   if (strstr(player_turn, loses[computer_turn])) {
  98.     puts("You win! Play again?");
  99.     return true;
  100.   } else {
  101.     puts("Seems like you didn't win this time. Play again?");
  102.     return false;
  103.   }
  104. }
  105.  
  106.  
  107. int main () {
  108.   char input[3] = {'\0'};
  109.   int command;
  110.   int r;
  111.  
  112.   puts("Welcome challenger to the game of Rock, Paper, Scissors");
  113.   puts("For anyone that beats me 5 times in a row, I will offer up a flag I found");
  114.   puts("Are you ready?");
  115.  
  116.   while (true) {
  117.     puts("Type '1' to play a game");
  118.     puts("Type '2' to exit the program");
  119.     r = tgetinput(input, 3);
  120.     // Timeout on user input
  121.     if(r == -3)
  122.     {
  123.       printf("Goodbye!\n");
  124.       exit(0);
  125.     }
  126.    
  127.     if ((command = strtol(input, NULL, 10)) == 0) {
  128.       puts("Please put in a valid number");
  129.     } else if (command == 1) {
  130.       printf("\n\n");
  131.       if (play()) {
  132.         wins++;
  133.       } else {
  134.         wins = 0;
  135.       }
  136.  
  137.       if (wins >= 5) {
  138.         puts("Congrats, here's the flag!");
  139.         puts(flag);
  140.       }
  141.     } else if (command == 2) {
  142.       return 0;
  143.     } else {
  144.       puts("Please type either 1 or 2");
  145.     }
  146.   }
  147.  
  148.   return 0;
  149. }

Aby odczytać flagę należy uzyskać 5 zwycięstw. 

Interackja użytkownika z programem przebiega w funkcji main w pętli while. Gdzie możemy wprowadzić cyfrę 1 lub 2 w celu rozpoczęcia lub zakończenia gry. Po kliknięciu cyfry 1 przechodzimy do funkcji play(). W niej wybrane zagranie (rock/papper/scissors) jest analizowane w funckji strstr. Ta funkcja w sposób jaki jest zaimplementowana w tym programie pozwoli na wygrywanie w grze za każdym razem. 

  1.   if (strstr(player_turn, loses[computer_turn])) {
  2.     puts("You win! Play again?");
  3.     return true;
  4.   } else {
  5.     puts("Seems like you didn't win this time. Play again?");
  6.     return false;
  7.   }

W zmiennej player_turn znajduje się to co wpiszemy z klawiatury, natomiast w zmiennej loses[computer_turn] będzie znajdowała się jedna z wartości wylowana z tablicy. Wobec tego jeśli w player_turn będzie znajdował się odpowiedni string to uda się wygrać. Powyższa implementacja strstr sprawdza czy dany ciąg znaków znajduje się w zmiennej player_turn i zwróci wskaźnik do jego początku. Wartość zwraca będzie wynosiła zero jeśli nie znajdzie dopasowania.

Poniżej przykład użycia funkcji w programie testowym:

  1. int main()
  2. {
  3.     // Take any two strings
  4.     char s1[] = "rock";
  5.     char s2[] = "paper";
  6.     char* p;
  7.  
  8.     // Find first occurrence of s2 in s1
  9.     p = strstr(s1, s2);
  10.  
  11.     // Prints the result
  12.     if (p) {
  13.         printf("Wygrana\n");
  14.     }
  15.     else{
  16.         printf("Przegrana\n");
  17.     }
  18.    
  19.     return 0;
  20. }

Teraz wprowadzę wszystkie dane za jednym razem:

  1. #include <stdio.h>
  2. #include <string.h>
  3.  
  4. int main()
  5. {
  6.     // Take any two strings
  7.     char s1[] = "rockpaperscissorsrrggrdwee";
  8.     char s2[] = "paper";
  9.     char* p;
  10.  
  11.     // Find first occurrence of s2 in s1
  12.     p = strstr(s1, s2);
  13.  
  14.     // Prints the result
  15.     if (p) {
  16.         printf("Wygrana\n");
  17.     }
  18.     else{
  19.         printf("Przegrana\n");
  20.     }
  21.    
  22.     return 0;
  23. }

Dzięki tekiemu wprowadzeniu danych wygrana nastąpi za każdym razem. Ponieważ szukany string zawsze będzie znajdował się w zmiennej. 

Teraz wracamy do programu na serwerze by odczytać flagę:

  1. Please make your selection (rock/paper/scissors):
  2. rockpaperscissors
  3. rockpaperscissors
  4. You played: rockpaperscissors
  5. The computer played: paper
  6. You win! Play again?
  7. Type '1' to play a game
  8. Type '2' to exit the program
  9. 1
  10. 1
  11.  
  12.  
  13. Please make your selection (rock/paper/scissors):
  14. rockpaperscissors    
  15. rockpaperscissors
  16. You played: rockpaperscissors
  17. The computer played: rock
  18. You win! Play again?
  19. Type '1' to play a game
  20. Type '2' to exit the program
  21. 1
  22. 1
  23.  
  24.  
  25. Please make your selection (rock/paper/scissors):
  26. rockpaperscissorsgyyfesgyegygfseyj
  27. rockpaperscissorsgyyfesgyegygfseyj
  28. You played: rockpaperscissorsgyyfesgyegygfseyj
  29. The computer played: paper
  30. You win! Play again?
  31. Congrats, here's the flag!
  32. picoCTF{xxxxxxxxxxxxxxxxxxxxxxxxxx}
  33. Type '1' to play a game
  34. Type '2' to exit the program

Można to także trochę zautomatyzować, np przez wysłanie do serwera skryptu w c:

  1. nano rpc_test.c
  2. //----------------------                                                                         #include <stdio.h>
  3. #include <stdint.h>
  4.  
  5. int main() {
  6.         for(uint8_t i=0; i<5;i++) {
  7.                 printf("1\n");
  8.                 printf("rockpaperscissors\n");
  9.         }
  10.         return 0;
  11. }
  12. //----------------------  
  13. gcc -o rpc_test rcp_test.c
  14. ./rps_test | nc saturn.picoctf.net <port>

Czy tak bezpośredni z konsoli:

  1. echo -e "1\nrockpaperscissors\n1\nrockpaperscissors\n1\nrockpaperscissors\n1\nrockpaperscissors\n1\nrockpaperscissors\n" | nc saturn.picoctf.net <port>
  2. //----------------------------------------
  3. printf "1\nrockpaperscissors\n1\nrockpaperscissors\n1\nrockpaperscissors\n1\nrockpaperscissors\n1\nrockpaperscissors\n" | nc saturn.picoctf.net <port>

Poniżej pokaże sposoby w jaki można poprawić program aby nie można było już go tak oszukać w powyższy sposób. 

Na samym początku można zastosować ograniczenie danych wejściowych. W przypadku opisanego programu dane zapisywane są w tablicy player_turn[100]. Tutaj największy string jakiego można się spodziewać to scissors. Wobec tego zastosowanie tablicy wielkości 10 byłoby wystarczające, i nie pozwoliło na zastosowanie powyższego sposobu na wygranie.

Kolejnym sposobem jest manualne sprawdzanie zgodności każdego znaku ze sobą przez przygotowanie włanej funkcji porównującej. 

  1. #include <string.h>
  2. #include <stdio.h>
  3. #include <stdbool.h>
  4.  
  5. char* custom_strstr(char* s1, char* s2, const int s2_size) {
  6.     if (!s1 || !s2) {
  7.         return NULL;
  8.     }
  9.    
  10.     for(int i=0; i<(s2_size); i++)
  11.     {
  12.         if(s1[i] != s2[i]) {
  13.             return NULL;
  14.         }
  15.     }
  16.    
  17.     return &s1[0];
  18. }
  19.  
  20. int main() {
  21.     char s1[] = "rockpaperscissorsrrggrdwee";
  22.     char s2[] = "rock";
  23.     char* p;
  24.    
  25.     p = custom_strstr(s1, s2, sizeof(s2));
  26.  
  27.     if (p) {
  28.         printf("Wygrana\n");
  29.     } else {
  30.         printf("Przegrana\n");
  31.     }
  32.  
  33.     return 0;
  34. }

Porówuje tutaj poprawność danych wejściowych względem danych wylosowanych. Jeśli wylosowane są inne niż przewidywane i mają nadmiarowe znaki i nie znaczynają się od pierwszej pozycji to je odrzucam.