Wstęp
Historia stara jak świat. Macie grę, którą kupiliście 20 lat temu. Stary klaser na płyty kurzy się na półce i nie jest ruszany przez ostatnie 10 lat, bo steam ma wszystko czego potrzebujesz. Wtem jednego dnia stwierdzacie: "Ale bym zagrał w starego Need For Speeda". Odkurzasz klaser, znajdujesz płytę, wkładasz do zewnętrznego napędu, bo Twój komputer już dawno nie ma, odpalasz instalator a tam:

Pomysł
Jak to zrobić? Ano, jak wiadomo, każdy kod jest open source, jeżeli jesteśmy odpowiednio zmotywowani. Aplikacja, która nam się odpaliła i żąda od nas kodu, gdy ten kod podajemy, wie czy jest on poprawny czy nie. Więc na naszym komputerze, na naszym procesorze wykonuje się kod, który sprawdza czy kod jest właściwy. Skoro to jest prawda, to znaczy, że my mamy dostęp do tego algorytmu, więc możemy go podejrzeć i zobaczyć jak ten kod działa i co nam potrzeba, żeby się algorytm nad nami zlitował i puścił nasz kod, pozwalając nam zainstalować gierkę, którą chcemy.
Potrzebujemy plików
Napęd jest wolny. Strasznie wolny. Więc najfajniej byłoby, żeby zgrać sobie pliczki na dysk, żeby nam napęd nie buczał. Kopiujemy je na dysk. Ukazuje nam się lista plików:

AutoRun.exe oraz setup.exe. Oba mają ten sam efekt, czyli przenoszą nas do meny wyboru języka. Wybieramy więc jakikolwiek, przechodzimy dalej. Pojawia się opcja zrobienia różnych rzeczy, wybieramy instalację i bum. Pojawia się nam ekran kodu. Jesteśmy w dobrym miejscu.Jaki to proces?
Żeby zobaczyć co się dzieje, musimy sobie programik zdebugować, żeby zobaczyć co robi i dokąd nas będzie nosił, żeby znaleźć kod algorytmu, który nas interesuje. Odpalamy więc debugger, w naszym przypadku x64dbg, ale zagrałby dowolny. Ten jest po prostu mój ulubiony na ten czas. Aplikacja okazuje się być 32bitowa, więc trzeba odpalić odpowiedni wariant debuggera pod 32 bity właśnie. Podpinamy się, odpalamy i...



Od czego zacząć szukanie?
Wpisuję losowy kod. Niech to będą same litery 'a', przy okazji dostrzegając, że każda mała litera zamieniana jest przez program na wielką. Drobna wskazówka, ale jeżeli chcielibyśmy szukać bruteforcem, to nam się dziedzina odrobinę zmniejsza. Klikając jeszcze jak ciele zauważyłem, że znaki typu: -, +, _, też działają. Brute force więc może się okazać niefajny, ale z FIFY pamiętam, że kody były zazwyczaj literkowo-cyferkowe, więc w razie czego odrzucimy te znaki na testy. Wracając... Wpisujemy same "A" i...:

int MessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);, gdy sprawdzenie się nie powiedzie, więc tam się zatrzymamy debuggerem, by po call stacku dostać się do źródła. Debugger jednak pokaże nam tylko assembly, a ja bym chciał coś więcej zobaczyć, więc odpalimy sobie ten program w Ghidrze, żeby lepiej było widać co program robi, a debuggerem będziemy sobie go wykonywać. Tworzymy więc nowy projekt w ghidrze i do roboty.Ghidra na ratunek
Ustawiamy breakpoint na MessageBoxie, odpalamy program przez debugger i badamy call stack:

0x41), więc jesteśmy niedaleko. Na stosie widzimy parametry oraz powrotne adresy. Debugger pokazuje, nam ułatwiając, które adresy są w kodzie, więc widzimy skąd przychodzimy po ten message box. Widzimy, że drugi pokazuje nam na jakieś dane, więc to na pewno nie jest kod, ale pierwszy już nie. Kopiujemy adres, wskakujemy tam w ghidrze i patrzymy co się dzieje:
_sprintf , który ma pięć %s i parametry, co są koło siebie na stosie co 4 bajty. Wygląda jak nasze wejście, które z resztą widzieliśmy na stosie już wcześniej. Druga sprawa, że jest jakaś zmienna, która ewidentnie jak jest większa niż dwa, to wywołuje CDialog::EndDialog(), oraz jak wynik jakiejś tajemniczej funkcji jest prawdą, to też. Z dokumentacji wiemy, że CDialog::EndDialog() zamyka okienko, więc jeżeli wynik tajemniczej funkcji, zamyka okienko, oraz jeżeli jakaś zmienna ma więcej niż 2 to też się zamyka to... Z testów organoleptycznych dostrzegamy, że jak się wpisze kod źle więcej niż trzy razy, to się nam wyświetla wiadomość, że zły kod i nara, i okienko się zamyka. W kodzie widać, że przed tym zamknięciem dzieje się coś jeszcze, ale to jeszcze, jak się tam w ghidrze wklikamy to zauważymy, że te dwie funkcje zapisują coś do rejestru, oraz wyświetlają MessageBoxa z parameterem 0x10, MB_ICONHAND, czyli ikonka z krzyżykiem. Jak wpiszemy źle kod to nam pokaże błąd z czerwonym krzyżykiem. Więc jesteśmy zasadniczo w domu. Wnioski:DAT_0042f648 to ewidentnie licznik błędówFUN_004182bc - to wrapper na MessageBoxA() FUN_00404cb0 - miesza coś w rejestrze FUN_004055c0 - też, ale tylko jak tajemnicza funkcja zwróci prawdęFUN_00405cb0 - to tajemnicza funkcja, która coś sprawdzaFUN_00405cb0 sprawdza nam, czy mamy poprawny kod. Wjedźmy tam w ghidrze:
thiscall, na razie nieisotne jaki, bo widzimy, że on robi tylko proxy dla innego wywołania. Wskoczmy więc do FUN_00405bf0:

bVar3 jest zupełnie bezużyteczna. Ostatnia linia, jak sobie sprawdzimy, kiedy ten return daje nam true, wychodzi na to, że nigdy. Jaki bVar3 by nie był, to wyrażenie jest fałszem. Więc skoro kod tam dojdzie, znaczy, że nasz kod jest niepoprawny. bVar1 wyrzuca nas z pętli, a więc przekierowuje do returna, który zawsze jest fałszem, jeżeli jest różna od tego na co wskazuje pbVar2. bVar1 jest brana z bufora param_1, a więc z naszego kodu. a pbVar2 jest wskaźnikiem na lokalny bufor local_20, więc nasz kod, musi być dokładnie taki sam jak bufor local_20.local_20 jest wrzucony do drugiej funkcji i ona coś z nim robi. bVar1 jest 0, czyli defacto jeżeli kod się skończy, to funkcja zwraca true, czyli jak pętla dojdzie do momentu, w którym string się skończy, to znaczy, że nie wywaliła się wcześniej, to znaczy że kod jest oklocal_20, bo kod oczekuje, że nasz kod będzie dokładnie taki, jak ten. Więc jeżeli wykopiemy to co jest w tym buforze, to mamy nasz kod.Poor mans keygen
FUN_00405a80:
esi, czyli tam gdzie tymczasowo mamy wskaźnik do local_20. Mamy tam wpisany kod, jaki program chce, żeby się utworzył. Wpiszmy ten kod do programu:
No i działa. Teraz możemy generować klucz dowolną ilość razy, zatrzymując się debuggerem w tym dokładnie miejscu, wpisując dowolny kod i sprawdzając, czego program się spodziewał, że dostanie. Brać to czego się spodziewa i po robocie. Ale gdzie tu zabawa? Sprawdźmy czy da się to zrobić w taki sposób, żeby móc to zrobić łatwiej, bez tych wszystkich debuggerów.
Robota właściwa
local_20, jedyna funkcja którą musimy zrozumieć i odtworzyć, to FUN_00405bf0. Jak ją możemy odtworzyć? Np. na pałę skopiować z ghidry do pliku i próbować go kompilować, poprawiając i doprowadzić do sytuacji, w której się skompiluje i zacznie działać. No to do dzieła. Kopiujemy wszystko co ma FUN_00405bf0 oraz wszystko czego mu brakuje. Szybko orientujemy się, że brakuje nam 3 rzeczy: DAT_0042fab8, DAT_0042dcd8, DAT_0042dcd8. No to skok w ghidrze do pierwszego:

char[] i wrzucić do naszego kodu. A więc ile? Z tego fragmentu, gdzie tam pamięć jest rzeczywiście używana:
*(uint *)(&DAT_0042fab8 + ((local_40[iVar8] ^ param_3) & 0xff) * 4); widać że maksymalny offset do którego sięgamy od adresu tej pamięci jest zawsze andowany z 0xff. Czyli ta tablica ma maksymalnie 256 elementów, które są dla nas wartościowe. Dodatkowo, jako że ten offset jest mnożony razy 4 później, to znaczy że tych elementów jest ostatecznie 1024. To jest później rzucane na uint*, więc efektywnie jest to tablica jakichś uintów. Ale nas to nie obchodzi na tym etapie. Nam wystarczą bajty, bez większej analizy. Wracamy więc do debuggera, kopiujemy z dumpa 1024 bajty i konstruujemy z tego tablicę żeby nam się ładnie skompilowała. Teraz wracamy do DAT_0042dcd8. Hyc do ghidry:
FUN_004059d0 ma jeszcze pare zmiennych z danych, które trzeba wyekstrahować. Robimy to dokładnie tak samo jak poprzednio, bo okazuje się że to są pojedyncze bajty. Więc pracowicie wyciągamy wszystkie te dane w ghidrze. Z bardziej nieoczywistych problemów, których nie da się w prosty sposób zamienić z ghidrakodu do kompilowalnego kodu, możemy się zatrzymać jeszcze na tym (z funkcji FUN_00405a80):

local_40 do edi, a potem do dekrementuje. Więc ghidra nie jest w stanie strybić za bardzo co to jest, więc wrzuca jakiś adres, który uznaje za właściwy. A tak naprawdę, to jest po prostu local_40 - 1, i jako że pętla od razu inkrementuje ten adres, to znaczy, że on korzysta sobie po prostu z local_40. Więc to dziwne przypisanie należy zamienić na pbVar6 = &local_40[-1];. W drugiej instancji tego dziwacznego adresu mamy dokładnie to samo.
13 bajtów. Później do jakiejś niezdefiniowanej zmiennej zapisuje się zero. Jeżeli jednak przyjrzymy się funkcji FUN_004059d0, zobaczymy, że bufor tam przekazany jako parametr (adres jako int, ale do tego wrócimy), robi przypisanie aż 0x14, czyli 20 bajtów za bazowym adresem. Widać to w ostatniej linii:
21 bajtów. Skąd więc to 13? Ghidra przyjęła, jako że z jakiegoś dziwnego powodu, funkcja zapisuje sobie za 0x2d, czyli 45 bajtów za początek stosu, 0, to znaczy, że musi to być jakaś zmienna, ale nie była w stanie określić jej rozmiaru. My wiemy jednak, że bufor jaki wchodzi do funkcji musi mieć przynajmniej 21. Więc ten poprzedni bufor ma przynajmniej 21 bajtów. Skąd ten pomysł, można jeszcze wywnioskować z assembly:
0x2d wziąłem sobie z ghidry, bo tak się rozkodowuje local_33 offset. Widzimy, że na stosie na offsecie 0x18 znajduje się local_40, a na 0x3c local_20. Więc pomiędzy nimi jest 36 bajtów. Jak dodamy do tego ten pomiędzy nimi PUSH EAX to mamy 4 bonusowe, więc realnie pomiędzy nimi są 32 bajty. Więc widzimy, że local_20 ma zarezerwowane dla siebie32 bajty tak naprawdę. Start tej zmiennej na stosie to 0x18, a przypisanie zera wjeżdża na offset 0x2d, więc 0x18 + dwa wciśnięte międzyczasie na stos rejestry, czyli 8, to 0x20, no wiec żeby osiągnąć 0x2d to trzeba dodać 13. Więc to przypisanie jest na offset 13 zmiennej local_40, czyli de facto local_40[13] = 0.FUN_00405a80, FUN_00405780, FUN_004059d0, przyjmują za pierwszy argument int, mimo, że z kodu wynika, że przyjmuje ona bufory ze stosu, które są char*, więc bezpiecznie jest założyć i w tym przypadku można też zweryfikować, że ghidra po prostu po rozmiarze sobie sklasyfikowała tę zmienną i wstawiła sobie inta. Można to szybko poprawić i po robocie.
W kluczu masz literówkę, ale mogę się mylić.