W tym poście chciałbym opisać rozwiązanie zadania RPS z picoCTF.
Działanie gry papier kamień nożyce uruchomionego na serwerze:
- Welcome challenger to the game of Rock, Paper, Scissors
- For anyone that beats me 5 times in a row, I will offer up a flag I found
- Are you ready?
- Type '1' to play a game
- Type '2' to exit the program
- 1
- 1
- Please make your selection (rock/paper/scissors):
- paper
- paper
- You played: paper
- The computer played: paper
- Seems like you didn't win this time. Play again?
- Type '1' to play a game
- Type '2' to exit the program
- 1
- 1
- Please make your selection (rock/paper/scissors):
- rock
- rock
- You played: rock
- The computer played: rock
- Seems like you didn't win this time. Play again?
- Type '1' to play a game
- Type '2' to exit the program
Kod programu dostarczony do wywania:
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdbool.h>
- #include <string.h>
- #include <time.h>
- #include <unistd.h>
- #include <sys/time.h>
- #include <sys/types.h>
- #define WAIT 60
- static const char* flag = "[REDACTED]";
- char* hands[3] = {"rock", "paper", "scissors"};
- char* loses[3] = {"paper", "scissors", "rock"};
- int wins = 0;
- int tgetinput(char *input, unsigned int l)
- {
- fd_set input_set;
- struct timeval timeout;
- int ready_for_reading = 0;
- int read_bytes = 0;
- if( l <= 0 )
- {
- printf("'l' for tgetinput must be greater than 0\n");
- return -2;
- }
- /* Empty the FD Set */
- FD_ZERO(&input_set );
- /* Listen to the input descriptor */
- FD_SET(STDIN_FILENO, &input_set);
- /* Waiting for some seconds */
- timeout.tv_sec = WAIT; // WAIT seconds
- timeout.tv_usec = 0; // 0 milliseconds
- /* Listening for input stream for any activity */
- ready_for_reading = select(1, &input_set, NULL, NULL, &timeout);
- /* Here, first parameter is number of FDs in the set,
- * second is our FD set for reading,
- * third is the FD set in which any write activity needs to updated,
- * which is not required in this case.
- * Fourth is timeout
- */
- if (ready_for_reading == -1) {
- /* Some error has occured in input */
- printf("Unable to read your input\n");
- return -1;
- }
- if (ready_for_reading) {
- read_bytes = read(0, input, l-1);
- if(input[read_bytes-1]=='\n'){
- --read_bytes;
- input[read_bytes]='\0';
- }
- if(read_bytes==0){
- printf("No data given.\n");
- return -4;
- } else {
- return 0;
- }
- } else {
- printf("Timed out waiting for user input. Press Ctrl-C to disconnect\n");
- return -3;
- }
- return 0;
- }
- bool play () {
- char player_turn[100];
- srand(time(0));
- int r;
- printf("Please make your selection (rock/paper/scissors):\n");
- r = tgetinput(player_turn, 100);
- // Timeout on user input
- if(r == -3)
- {
- printf("Goodbye!\n");
- exit(0);
- }
- int computer_turn = rand() % 3;
- printf("You played: %s\n", player_turn);
- printf("The computer played: %s\n", hands[computer_turn]);
- if (strstr(player_turn, loses[computer_turn])) {
- puts("You win! Play again?");
- return true;
- } else {
- puts("Seems like you didn't win this time. Play again?");
- return false;
- }
- }
- int main () {
- char input[3] = {'\0'};
- int command;
- int r;
- puts("Welcome challenger to the game of Rock, Paper, Scissors");
- puts("For anyone that beats me 5 times in a row, I will offer up a flag I found");
- puts("Are you ready?");
- while (true) {
- puts("Type '1' to play a game");
- puts("Type '2' to exit the program");
- r = tgetinput(input, 3);
- // Timeout on user input
- if(r == -3)
- {
- printf("Goodbye!\n");
- exit(0);
- }
- if ((command = strtol(input, NULL, 10)) == 0) {
- puts("Please put in a valid number");
- } else if (command == 1) {
- printf("\n\n");
- if (play()) {
- wins++;
- } else {
- wins = 0;
- }
- if (wins >= 5) {
- puts("Congrats, here's the flag!");
- puts(flag);
- }
- } else if (command == 2) {
- return 0;
- } else {
- puts("Please type either 1 or 2");
- }
- }
- return 0;
- }
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.
- if (strstr(player_turn, loses[computer_turn])) {
- puts("You win! Play again?");
- return true;
- } else {
- puts("Seems like you didn't win this time. Play again?");
- return false;
- }
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:
- int main()
- {
- // Take any two strings
- char s1[] = "rock";
- char s2[] = "paper";
- char* p;
- // Find first occurrence of s2 in s1
- p = strstr(s1, s2);
- // Prints the result
- if (p) {
- printf("Wygrana\n");
- }
- else{
- printf("Przegrana\n");
- }
- return 0;
- }
Teraz wprowadzę wszystkie dane za jednym razem:
- #include <stdio.h>
- #include <string.h>
- int main()
- {
- // Take any two strings
- char s1[] = "rockpaperscissorsrrggrdwee";
- char s2[] = "paper";
- char* p;
- // Find first occurrence of s2 in s1
- p = strstr(s1, s2);
- // Prints the result
- if (p) {
- printf("Wygrana\n");
- }
- else{
- printf("Przegrana\n");
- }
- return 0;
- }
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ę:
- Please make your selection (rock/paper/scissors):
- rockpaperscissors
- rockpaperscissors
- You played: rockpaperscissors
- The computer played: paper
- You win! Play again?
- Type '1' to play a game
- Type '2' to exit the program
- 1
- 1
- Please make your selection (rock/paper/scissors):
- rockpaperscissors
- rockpaperscissors
- You played: rockpaperscissors
- The computer played: rock
- You win! Play again?
- Type '1' to play a game
- Type '2' to exit the program
- 1
- 1
- Please make your selection (rock/paper/scissors):
- rockpaperscissorsgyyfesgyegygfseyj
- rockpaperscissorsgyyfesgyegygfseyj
- You played: rockpaperscissorsgyyfesgyegygfseyj
- The computer played: paper
- You win! Play again?
- Congrats, here's the flag!
- picoCTF{xxxxxxxxxxxxxxxxxxxxxxxxxx}
- Type '1' to play a game
- Type '2' to exit the program
Można to także trochę zautomatyzować, np przez wysłanie do serwera skryptu w c:
- nano rpc_test.c
- //---------------------- #include <stdio.h>
- #include <stdint.h>
- int main() {
- for(uint8_t i=0; i<5;i++) {
- printf("1\n");
- printf("rockpaperscissors\n");
- }
- return 0;
- }
- //----------------------
- gcc -o rpc_test rcp_test.c
- ./rps_test | nc saturn.picoctf.net <port>
Czy tak bezpośredni z konsoli:
- echo -e "1\nrockpaperscissors\n1\nrockpaperscissors\n1\nrockpaperscissors\n1\nrockpaperscissors\n1\nrockpaperscissors\n" | nc saturn.picoctf.net <port>
- //----------------------------------------
- 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.
- #include <string.h>
- #include <stdio.h>
- #include <stdbool.h>
- char* custom_strstr(char* s1, char* s2, const int s2_size) {
- if (!s1 || !s2) {
- return NULL;
- }
- for(int i=0; i<(s2_size); i++)
- {
- if(s1[i] != s2[i]) {
- return NULL;
- }
- }
- return &s1[0];
- }
- int main() {
- char s1[] = "rockpaperscissorsrrggrdwee";
- char s2[] = "rock";
- char* p;
- p = custom_strstr(s1, s2, sizeof(s2));
- if (p) {
- printf("Wygrana\n");
- } else {
- printf("Przegrana\n");
- }
- return 0;
- }
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.