Porady i algorytmy

Odczytywanie i odszyfrowywanie hasła serwera Gadu-Gadu z pliku config.dat
Opublikowano 18.01.2008 r. w kategorii C++Builder.
Odsłon: 38604.

W niniejszym artykule chciałbym podzielić się z Wami wiedzą na temat odczytywania i odszyfrowywania hasła serwera z pliku konfiguracyjnego (config.dat). Wbrew pozorom implementacja tego zagadnienia nie jest wcale trudna.

Artykuł przeznaczony jest głównie dla programistów. Jeśli chcesz jesteś zainteresowany (-a) aplikacją do odzyskiwania haseł Gadu-Gadu - pobierz program GG Tools (dawniej Gadu-Gadu Password Recovery) lub skorzystaj z webGGT - webowej wersji GG Tools.

Część I: Przygotowania.

Przed przystąpieniem do jakichkolwiek działań warto wcześniej uzbroić się w dowolny hex-edytor, który pozwoli nam na podejrzenie zawartości pliku konfiguracyjnego. Być może komuś z Was, do zapisywania najważniejszych rzeczy, potrzebna będzie również kartka i długopis lub - jeśli ktoś woli - systemowy notatnik.
Jeśli wszystko już mamy gotowe, odpalamy IDE. Na początek przygotujemy sobie funkcję, która odpowiedzialna będzie za otworzenie, wczytanie i sprawdzenie poprawności pliku config.dat.

bool TForm1::OpenFile(AnsiString FileName) { }

Dodatkowo w pliku *.h deklarujemy dwie zmienne, które będą nam potrzebne później:

private: // User declarations char *buffer; int FileSize; bool OpenFile(AnsiString FileName);

Jak widać, funkcja OpenFile (nazwa może być oczywiście dowolna) jako parametr przyjmuje zmienną typu AnsiString (ścieżka do pliku konfiguracyjnego); typem zwracanym zaś jest wartość bool - dzięki niej dowiemy się, czy wczytanie pliku przebiegło pomyślnie. Nie jest to niezbędne do poprawnego odczytania hasła, lecz na pewno przydatne.

Aby otworzyć plik konfiguracyjny posłużymy się klasą TFileStream. Plik konfiguracyjny otwieramy tylko do odczytu i zabezpieczmy go, aby inne procesy (programy) nie mogły w tym czasie nic w nim zapisać. W praktyce wygląda to tak:

TFileStream *File; File = new TFileStream(FileName, fmOpenRead | fmShareDenyWrite); File->Seek(0, soFromBeginning); // dla pewności...

Plik został otworzony, ale przed wczytaniem jego zawartości do bufora warto jednak się chwilę zastanowić - czy na pewno jest sens wczytywania każdego pliku? Może warto najpierw byłoby upewnić się, że mamy do czynienia z plikiem konfiguracyjnym komunikatora Gadu-Gadu...? Pamiętajmy, że w ostatecznej wersji programu damy możliwość Użytkownikowi wskazania pliku config.dat, a ten, z ciekawości, złośliwości lub czystej niewiedzy może wskazać inny plik. Dlatego otwórzmy plik konfiguracyjny w hex-edytorze i sprawdźmy jaką ma budowę...
Po jego otwarciu powinniśmy zobaczyć coś takiego:

HEX | ASCII 63 66 67 31 00 | c f g 1 .

Dla nas, w tym momencie, najważniejszy jest początek pliku, a dokładniej mówiąc jego pierwsze pięć bajtów, czyli ciąg cfg1 + znak NULL (0x00 w hex). Znaki te (można je traktować jako nagłówek pliku) informują komunikator Gadu-Gadu, że dany plik jest prawidłowym plikiem konfiguracyjnym. Wykorzystajmy ten fakt w naszym programie.

char FileType[5]; File->Read(&FileType, sizeof(FileType)); if (AnsiString(FileType) != "cfg1") // plik jest nie prawidłowy else // plik jest prawidłowy

Teoretycznie sprawę poprawności pliku mamy z głowy - możemy wyświetlić MessageBox informujący o nieprawidłowym typie pliku i wyjść z funkcji. Z tym, że - jak wiemy - teoria a praktyka to dwie różne rzeczy. W praktyce bowiem może zdarzyć się tak, że jakiś program (patch) zmodyfikuje działanie Gadu-Gadu tak, aby wczytywał (i tym samym traktował jako poprawne) pliki z innym nagłówkiem, np. pstg + znak NULL. W ten właśnie sposób działa Anty-Dekoder Gadu-Gadu stworzony przez Urbiego. Nie będziemy się tym zajmować, ale warto o tym pamiętać i trzeba to uwzględnić w pisanej aplikacji.

// Sprawdzamy, czy otwarty plik jest prawidłowym // plikiem konfiguracyjnym... if (AnsiString(FileType) != "cfg1") { // Nie jest to prawidłowy plik config.dat - poinformuj // Użytkownika i zapytaj czy chce kontynuować if (MessageBox(Handle, "Wybrany plik nie jest prawidłowym plikiem " "konfiguracyjnym Gadu-Gadu.\nCzy mimo to chcesz kontynuować " "sprawdzanie pliku?", "Uwaga", MB_YESNO | MB_ICONWARNING) == ID_NO) { delete File; // Sprzątamy po sobie... return false; // Próba odzyskania hasła nie powiodła się } }

Teraz już pozostaje nam tylko wczytanie zawartości pliku do bufora. Zadania to realizujemy w następujący sposób:

FileSize = File->Size; // pobieramy rozmiar pliku (w bajtach) buffer = new char[FileSize]; // tworzymy i ustalamy pojemność bufora File->Read(buffer, FileSize); // wczytujemy zawartość pliku delete File; // zwalniamy plik (nie będzie on już potrzebny) return true;

Tym samym dotarliśmy do końca przygotowań - w buforze mamy już plik konfiguracyjny, z którego pozostaje nam tylko odczytanie hasła serwera. Poniżej podsumowanie tego, co do tej pory zrobiliśmy (z listingu zostały usunięte komentarz oraz blok try... catch, w celu zwiększenia czytelności kodu):

bool TForm1::OpenFile(AnsiString FileName) { TFileStream *File; File = new TFileStream(FileName, fmOpenRead | fmShareDenyWrite); File->Seek(0, soFromBeginning); // dla pewności... char FileType[5]; File->Read(&FileType, sizeof(FileType)); if (AnsiString(FileType) != "cfg1") { if (MessageBox(Handle, "Wybrany plik nie jest prawidłowym plikiem " "konfiguracyjnym Gadu-Gadu.\nCzy mimo to chcesz kontynuować " "sprawdzanie pliku?", "Uwaga", MB_YESNO | MB_ICONWARNING) == ID_NO) { delete File; return false; } } FileSize = File->Size; buffer = new char[FileSize]; File->Read(buffer, FileSize); delete File; return true; }


Część II: Odczytywanie zaszyfrowanego hasła serwera Gadu-Gadu.

Przygotowania mamy już za sobą, więc możemy się zabrać za "poszukiwanie" hasła. Ze względu na to, że plik config.dat jest plikiem amorficznym (nie mającym stałej, określonej budowy), musimy przeszukać całą jego zawartość w poszukiwaniu hasła. Otwieramy zatem ponownie hex-edytor i szukamy czegoś, co może naprowadzić nas na miejsce, w którym zapisane jest hasło. W pliku config.dat występują przynajmniej dwa ciągi, których brzmienie sugeruje wystąpienie jakiegoś hasła - są to Password2 oraz passwordstr (tutaj pomijam EraOmnixPwd i EraOmnixPwd2, których znaczenie jest oczywiste). Nas w tej chwili interesować będzie pierwszy ciąg, czyli Password2 - zaraz po nim zapisane jest hasło serwera. Zainteresowanym podpowiem, że drugi ciąg oznacza miejsce występowania hasła profilu, a hasła i loginy Ery Omnix nie są wcale szyfrowane(!). Fakt ten możemy sprawdzić wpisując login i hasło bramki Omnix w ustawieniach komunikatora Gadu-Gadu, a następnie otwierając plik konfiguracyjny (nawet w systemowym Notatniku!) - wprowadzone dane zapisane są zaraz po frazach EraOmnixLogin i EraOmnixPwd. Śmiesznie, co? Ale dobra; jest jak jest, więc zostawmy ten temat i wróćmy do hasła serwera.
W hex-edytorze miejsce, po którym znajduje się zakodowane hasło wygląda to tak:

HEX | ASCII 00 50 61 73 73 77 6F 72 64 32 00 02 | . P a s s w o r d 2 . .

Zwróćmy szczególną uwagę, na to, że przed ciągiem Password2 występuje jeden znak NULL oraz znaki 0x00 i 0x02 występujące po nim. Znaki występujące po 0x02 to zakodowane hasło serwera. Jeśli po nim znajduje się znak 0x00, oznacza to, że hasło nie jest zapisane w pliku konfiguracyjnym i nie jest możliwe jego odzyskanie tą metodą. Identyczną sytuację mamy również wtedy, gdy w pliku config.dat ciąg Password2 w ogóle nie występuje. Wiemy już kiedy hasło serwera nie jest zapisane w pliku konfiguracyjnym; zobaczmy, więc jak przedstawia się sytuacja, gdy hasło jest zapisane.

HEX | ASCII 00 02 49 47 42 47 44 48 4D 47 50 47 00 | . . I G B G D H M G P G .

Dwa pierwsze znaki to 0x00 i 0x02 stojące za ciągiem Password2, o których pisałem wyżej. Po nich znajduje się pierwszy znak zakodowanego hasła serwera. Oczywiście u Was znaki te będą się różnić (no, chyba, że używane przez Was hasło serwera to "haslo" tak jak w tym przykładzie ;) ). Tutaj istotne jest to, że: po pierwsze - liczba znaków hasła serwera jest zawsze parzysta, po drugie: po ostatnim znaku występuje znak NULL informujący o końcu ciągu.

Dobra, wiemy już co i jak, więc możemy przejść do drugiego etapu, czyli do wdrożenia. Najpierw - tak jak w części I - przygotujemy szkielet funkcji:

AnsiString TForm1::GetServerPassword() { } private: // User declarations AnsiString GetServerPassword();

Konstrukcja funkcji jest bardzo prosta - zwraca ona typ AnsiString (odkodowane już hasło), nie przyjmując przy tym żadnych parametrów. Jeśli ktoś zechce może ją zmodyfikować tak, aby jako parametr przyjmowała bufor z zawartością pliku, dzięki czemu nie trzeba będzie deklarować zmiennych char* buffer oraz int FileSize jako zmiennych globalnych w sekcji private pliku nagłówkowego (*.h). Następny krok jest również bardzo prosty - stworzymy w nim pętlę for, w której poszukiwać będziemy w buforze ciągu Password2, po którym - jak już wiemy - zapisane jest hasło serwera. Implementacja tego kroku przedstawia się następująco:

AnsiString TForm1::GetServerPassword() { AnsiString ServerPassword; for (int Pos = 11; Pos < FileSize; Pos++) { if ((buffer[Pos] == '\x02') && (buffer[Pos-1] == '\x00') && (buffer[Pos-2] == '2') && (buffer[Pos-3] == 'd')) { // ... } } }

Wygląda skomplikowanie? W żadnym wypadku! Wbrew pozorom pętla jest bardzo prosta. W pętli tej poszukujemy takiego miejsca w pliku, aby znak numer Pos w buforze był znakiem 0x02, a trzy poprzednie (liczone od Pos) odpowiednio 0x00, 2 i d. Gdybyśmy pociągnęli to dalej kolenjymi znakami byłyby r, o, w, s, s, a, P oraz 0x00 :-) Krótko mówiąc, jest to ciąg Password2 z tym, że... pisany od tyłu :-) Dzięki takiemu zapisowi, łatwiej będzie nam ustalić pozycję hasła w pliku, która wynosi Pos + 1. Proste, prawda? Przypominam, że hasło jest zakończone znakiem 0x00, więc miejsce, w którym hasło się kończy to Pos + 1 + pierwszy napotkany znak NULL. AnsiString TForm1::GetServerPassword() { AnsiString ServerPassword; for (int Pos = 11; Pos < FileSize; Pos++) { if ((buffer[Pos] == '\x02') && (buffer[Pos-1] == '\x00') && (buffer[Pos-2] == '2') && (buffer[Pos-3] == 'd')) { AnsiString Temp; // zmienna tymczasowa int PassEnd = Pos + 1; while (buffer[PassEnd] != '\x00') { Temp += buffer[PassEnd]; PassEnd++; } if (!Temp.IsEmpty() && Temp != '\x00') ServerPassword = EncodeServerPassword(Temp); break; // znaleźliśmy hasło, więc opuszczmy pętlę } } delete buffer; // sprzątamy po sobie... return ServerPassword }

Co się tutaj dzieje? Najpierw deklarujemy tymczasową zmienną pomocniczą o nazwie Temp, następnie zmienną PassEnd, która wskazuje nam koniec miejsce w którym zakodowane hasło się kończy. Jednocześnie zmienna ta na samym początku wskazuje nam początek hasła (stąd wartość początkowa Pos + 1). W warunku pętli while sprawdzamy, czy napotkanym znakiem jest 0x00; jeśli tak - wychodzimy z pętli, jeśli nie, oznacza to, że znak ten jest częścią hasła i dodajemy go do zmiennej Temp. Po wyjściu z pętli upewniamy się, że ciąg, który pobraliśmy nie jest ciągiem pustym lub nie zawiera samego znaku NULL, co oznaczałoby, że hasło nie jest zapisane w pliku. Jeśli warunek ten jest spełniony możemy przejść odszyfrować zakodowane hasło funkcją EncodeServerPassword().

Część III: Odszyfrowywanie zakodowanego hasła serwera.

Tym samym dotarliśmy do najważniejszej części artykułu. Na nic, bowiem, zda się otworzenie i wczytanie pliku konfiguracyjnego, a nawet pobranie z niego zakodowanego hasła, jeśli nie przedstawimy go Użytkownikowi w czytelnej (tj. odszyfrowanej) postaci. W internecie spotkałem się z wieloma stronami poświęconymi odczytywaniu zakodowanego hasła serwera. Niektóre z nich proponują użycie algorytmu, inne z kolei - podstawianie znaków, np.: BG = a; BE = A CG = b; CE = B DG = c; DE = C EG = d; EE = D FG = e; FE = E GG = f; GE = F

i tak dalej... Algorytm szyfrowania hasła serwera jest na tyle prosty, że można je odkodować metodą podstawiania - wystarczy tylko cierpliwość. Ja jednak chciałbym przedstawić nieco inny sposób odkodowania hasła serwera. Oto kod:

AnsiString TForm1::EncodeServerPassword(AnsiString Password) { AnsiString EncodedPassword; int i = 0; int x, y, z; do { i++; x = int(Password[i]) - 65; i++; y = int(Password[i]) - 65; z = y * 16 + x; EncodedPassword += char(z); } while (i != Password.Length()); return EncodedPassword; } Oczywiście, w pliku nagłówkowym należy dopisać: private: // User declarations AnsiString EncodeServerPassword(AnsiString Password); Powyższy kod (w nieco rozszerzonej formie) odpowiedzialny jest za odszyfrowywanie haseł serwera w programie GG Tools.

Część IV: Podsumowanie.

Nadszedł czas na zebranie wszystkich informacji i przetestowanie programu. Do tego celu potrzebne będzie nam jeden komponent TEdit, TOpenDialog oraz TButton. Jeśli ktoś zechce może dodać etykiety TLabel opisujące poszczególne kontrolki. Klikamy dwukrotnie na TButton i uzupełniamy funkcję: void __fastcall TForm1::Button1Click(TObject *Sender) { if (OpenDialog1->Execute()) if (OpenFile(OpenDialog1->FileName)) { // deklarujemy zmienną pomocniczą AnsiString Password = GetServerPassword(); if (Password.IsEmpty() == false) Edit1->Text = Password; else Edit1->Text = "Hasła nie ma w pliku!"; } }

Tym samym dotarliśmy do końca artykułu. Mam nadzieję, że pomogłem Wam zrozumieć gdzie w pliku konfiguracyjnym Gadu-Gadu trzyma zakodowane hasło, w jaki sposób się do niego dostać i jak je odszyfrować. Jeśli jeszcze coś nie do końca jest dla Was jasne proszę o kontakt.

W następnym artykule opiszę w jaki sposób z pliku konfiguracyjnego możemy odczytać numer Gadu-Gadu, a także jak odszyfrować hasło profilu. Stay tuned! :-)