Crackme .NET – Rozwiązania

Jakiś czas temu napisałem o konkursie na crackme dla .net zorganizowanym przez Krakowska Grupa Developerów .NET, dzisiaj z ciekawości tam zajrzałem i zobaczyłem, że jest tylko 1 rozwiązanie, więc postanowiłem zadziałać.

1. NemCrackMe # 1 by Pawlos

Standardowy schemat name/serial:

NemCrackMe # 1 by Pawlos

Szybka wrzutka do Reflectora i dekompilacja kodu po kliknięciu “Please Crack Me”:

cm1_check_serial

Mówiąc w skrócie, kod na powyższym obrazku, wywołuje jakąś magiczną funkcję (która jest dynamicznie tworzona) z 2 parametrami, którymi są teksty z kontrolek edycyjnych:

private void button1_Click(object sender, EventArgs e)
{
    Type type = this.tBuilder.CreateType();
    type.GetMethod("o").Invoke(Activator.CreateInstance(type), new object[] { this.textBox1.Text, this.textBox2.Text });
}

Jak nietrudno zgadnąć jest to właściwa metoda sprawdzająca poprawność hasła. Ok, tylko gdzie ona jest hmm? Bez większego doświadczenia w programowaniu w C#, po prostu kliknąłem w inne metody crackme, w tym w konstruktor formy i wszystko stało się jasne:

cm1_dynamic_code

Crackme generuje funkcję sprawdzającą poprawność wpisanego hasła w konstruktorze formy, funkcja generowana jest dynamicznie w pamięci wykorzystując składnię języka IL.

Składnia języka IL może nie jest jakaś szczególnie zawiła, ale zrozumienie tego kodu bez jakiegokolwiek doświadczenia może być trudne, idealnie byłoby gdybyśmy mogli skorzystać z Reflectora do zdekompilowania kodu.

Aby to zrobić, utworzyłem pomocniczy program, który tworzy assembly .net-we z powyższą procedurą i zapisuje je do pliku DLL, dzięki czemu można go wgrać do Reflectora i pokaże nam wszystko ładnie na tacy. Po dekompilacji otrzymałem taki algorytm:

public class MyDynamicType
{
    // Fields
    private int[] coes;
    private int num1;
    private int num2;

    // Methods
    public MyDynamicType();
    public void o(string, string);

// konstruktor
public MyDynamicType()
{
    this.num1 = 0x654321;
    this.num2 = 0x123456;
    this.coes = new int[] { 0x21, 12, 0x5d3, 110, 5, 0x610, 8 };
}

public void o(string text1, string text2)
{
    long num = 0x73L;
    int num2 = 1;
    foreach (char ch in text1)
    {
        num += Math.Abs((int) (ch * this.coes[num2++ % this.coes.Length]));
    }
    num += this.num1;
    num -= this.num2;
    if (text2 == num.ToString())
    {
        MessageBox.Show("Udało się", "NemCrackMe");
    }
}

}

Name: bartosz Password: 5779531

No i to by było na tyle. Użyte zabezpieczenie raczej nie jest dobrym przykładem, gdyż nawet bez większego doświadczenia można zrozumieć zasadę jego działania, myślę, że dodatkowym utrudnieniem byłaby obfuskacja instrukcji sprawdzających numer seryjny, choćby seria zmieniająca codeflow (swoją drogą to niezła baza do takich eksperymentów).

Generator kluczy oraz pomocniczy program tworzący assembly dostępny jest na końcu artykułu wraz z innymi rozwiązaniami.

Dzisiaj przeglądałem kolekcję wtyczek do Reflectora i jest tam plugin o nazwie ReflectionEmitLanguage, który z kodu IL generuje kod w HLL generujący dynamiczny kod IL wskazanej metody i chyba (głowy nie dam) właśnie to zostało użyte do wygenerowania funkcji sprawdzającej hasło w tym crackme.

2. PIJA CrackMe 1

Chyba najciekawsze crackme i zarazem stosujące dobre zabezpieczenia. Ale od początku:

PIJA CrackMe 1

Crackme wymaga wpisania numeru seryjnego i kodu aktywacyjnego, jednocześnie wyświetla kod identyfikacyjny (jak nietrudno zgadnąć, będzie to mieć coś wspólnego z identyfikatorem naszego sprzętu).

Szybka dekompilacja pod Reflectorem metody na naciśniecie buttona sprawdzającego:

private void enter_Click(object sender, EventArgs e)
{
    try
    {
        byte[] buffer3;
        string text = this.txtSerial.Text;

        // generuj klucz deszyfrujacy
        byte[] key = Utils.GetKey(this.regKey, text);

        // generuj wektor inicjujacy
        byte[] iV = Utils.GetIV();

        Rijndael rijndael = Rijndael.Create();
        rijndael.Padding = PaddingMode.Zeros;

        // otworz plik crackmelib.dll
        using (FileStream stream = File.Open("crackmelib.dll", FileMode.Open))
        {
            buffer3 = new byte[stream.Length];
            new CryptoStream(stream, rijndael.CreateDecryptor(key, iV), CryptoStreamMode.Read).Read(buffer3, 0, (int) stream.Length);
        }

        // wywolaj funkcje A() z odszyfrowanej (w pamieci) biblioteki crackmelib.dll
        ((IA) Assembly.Load(buffer3).CreateInstance("crackmelib.B")).A(this.txtActivationCode.Text);
    }
    catch
    {
        MessageBox.Show("Uuuups! Try again");
    }
}

Dla rozjaśnienia kolejne kroki:

  1. Generowany jest klucz deszyfrujący AES na podstawie “Activation request” i “Serial”
  2. Generowany jest wektor inicjujący dla AES
  3. Plik “crackmelib.dll” zostaje odszyfrowany w pamięci według ustawionych parametrów
  4. Odszyfrowana biblioteka “crackmelib.dll” zostaje załadowana do pamięci
  5. Z biblioteki “crackmelib.dll” zostaje wywołana funkcja A() z parametrem jakim jest kod kod aktywacyjny

Użycie wychwytywania wyjątków z użyciem try{} catch{} sugeruje, że albo klucz deszyfrujący będzie poprawny i nastąpi poprawne odszyfrowanie i załadowanie assembly z pliku “crackmelib.dll”, albo nastąpi wyjątek.

Activation request

Wyświetlany po uruchomieniu crackme identyfikator generowany jest w konstruktorze głównej formy crackme:

cm2_request_code

Za jego generację odpowiada biblioteka “CommonInterface.dll”:

public static string GetRequestCode()
{
    return (GetProcessorID() + GetBIOSSerialNumber());
}

private static string GetProcessorID()
{
    ManagementObjectSearcher searcher = new ManagementObjectSearcher("select * from Win32_Processor");
    foreach (ManagementObject obj2 in searcher.Get())
    {
        foreach (PropertyData data in obj2.Properties)
        {
            if (data.Name.ToUpper() == "PROCESSORID")
            {
                return data.Value.ToString();
            }
        }
    }
    return null;
}

private static string GetBIOSSerialNumber()
{
    ManagementObjectSearcher searcher = new ManagementObjectSearcher("select * from Win32_BIOS");
    foreach (ManagementObject obj2 in searcher.Get())
    {
        foreach (PropertyData data in obj2.Properties)
        {
            if (data.Name.ToUpper() == "SERIALNUMBER")
            {
                return data.Value.ToString();
            }
        }
    }
    return null;
}

Odczytywanie danych o procesorze i danych BIOS odbywa się poprzez klasy interfejsu WMI (przykłady użycia na CodeProject).

Wektor inicjujący AES

Wektor generowany jest w kodzie biblioteki “CommonInterface.dll”:

public static byte[] GetIV()
{
    foreach (object obj2 in Assembly.GetExecutingAssembly().Evidence)
    {
        if (obj2 is Hash)
        {
            return ((Hash) obj2).MD5;
        }
    }
    return new byte[0];
}

Kilka słów wyjaśnienia, metoda GetExecutingAssembly() zwraca assembly, w którym znajduje się aktualnie wykonywany kod, czyli w tym przypadku będzie to assembly “CommonInterface.dll”.

Następnie enumerowane są wszystkie elementy assembly określone jako Evidence, które są obiektem typu Hash. Może to troche skomplikowane, bo dokumentacja też tego jakoś prosto nie wyjaśnia, ale debugging wykazał, że obliczana jest po prostu suma MD5 z pliku “CommonInterface.dll”.

Czyli wektor inicjujący stanowi sumę kontrolną pliku crackme, jest to zapewne zabezpieczenie przed patchowaniem pliku, bo jakakolwiek zmiana, spowoduje wygenerowanie błędnego wektora inicjującego.

Klucz deszyfrujący AES

Klucz deszyfrujący generowany jest w funkcji GetKey() znajdującej się w tej samej bibliotece “CommonInterface.dll”:

public static byte[] GetKey(string reques, string serial)
{
    byte[] buffer = new byte[] {
        0, 5, 7, 0x59, 0x38, 0x22, 0x4e, 0x22, 0x38, 0x4e, 0x17, 0x2d, 9, 12, 0x38, 0x4e,
        0x22, 0x57, 90, 0x6f, 0x17, 0x4e, 0x62, 0x17, 0x38, 12, 0x5f, 0x2c, 0x43, 0x43, 12, 0x17
     };

    // pobierz 1 czesc identyfikatora sprzetowego (o dlugosci polowy buffer)
    byte[] bytes = Encoding.ASCII.GetBytes(reques.Substring(0, buffer.Length / 2));

    // numer seryjny z base64 zamien na bajty
    byte[] buffer3 = Convert.FromBase64String(serial);

    // klucz deszyfrujacy = pierwsza_czesc_hardware_id[] + numer_seryjny[]
    for (int i = 0; i < (buffer.Length / 2); i++)
    {
        buffer[i] = (byte) (bytes[i] + buffer3[i]);
    }

    return buffer;
}

O sile tego crackme świadczy właśnie ta powyższa procka, otóż cała reszta kodu znajdująca się w crackmelib.dll jest zaszyfrowana algorytmem AES z kluczem o długości 32 bajtów (256 bitów), klucz deszyfrujący tworzony jest z pary identyfikatora sprzętowego i numeru seryjnego (+reszta tablicy buffer) i musi być poprawny, inaczej odszfrowany kod będzie nieprawidłowy i nie będzie można załadować tego assembly.

Krótko mówiąć bez znajomości poprawnej pary identyfikator + serial nie można odszyfrować reszy kodu potrzebnego do pracy. Można próbować brute forcem znaleźć poprawną kombinację pierwszych 16 bajtów tablicy buffer (128 bitów), jednak jak przypuszczam taka próba zajęłaby więcej czasu niż czekanie przeze mnie na prezenty od świętego mikołaja za dobre zachowanie 😉

Schemat ten przypomina stosowane w exe-protektorach makra szyfrujące kod według danych licencyjnych, ma to swoje zalety i wady, ale z pewnością można powiedzieć, że to dobre zabezpieczenie z punktu widzenia łamiącego.

Po tej szybkiej analizie miałem już sobie odpuścić, jednak z ciekawości zajrzałem jeszcze raz na stronę konkursową i mój wzrok przykuło rozwiązanie własnie do tego crackme i temat na konkursowym forum, gdzie było zamieszczone ogłoszenie o umieszczeniu crackme oraz HA! autor podaje poprawną serię kodu identyfikatora sprzętowego, numeru seryjnego oraz kodu aktywacyjnego. No to teraz z górki :).

Mając te dane można stworzyć dekoder (w samym crackme nie można wpisać tego podanego identyfikatora sprzętowego), który wykorzystując podane parametry odszyfruje plik crackmelib.dll:

        public static void DecodeAssembly()
        {
            Assembly asmCrackme;
            string asmFile = "CommonInterface.dll";

            // sciezka, gdzie znajduje sie crackme
            string asmPath = Path.Combine(Directory.GetCurrentDirectory(), asmFile);

            Console.WriteLine("[i] Loading {0} assembly.", asmFile);

            // zaladuj assembly z pliku
            asmCrackme = Assembly.LoadFile(asmPath);

            if (asmCrackme == null)
            {
                Console.WriteLine("[!] Cannot load assembly!");
                return;
            }

            // dekoduj tak jak to robi crackme
            try
            {
                // zestaw poprawnych seriali podanych przez autora na forum konkursowym:
                // http://ms-groups.pl/kgd.net/Konkurs_CrackMe/Lists/Forum%20konkursowe/Flat.aspx?RootFolder=%2Fkgd%2Enet%2FKonkurs%5FCrackMe%2FLists%2FForum%20konkursowe%2FPIJA%20CrackMe%201&FolderCTID=0x0120020070660992493BC44DA19C94BED291A957
                // (inaczej pozostaje brute force 128 bitowej wartosci)
                string validRequest = "BFEBFBFF00010676BPSHY3J";
                string validSerial = "9gfHIoPHxMVLJzPk+QKHeg==";
                string validActivation = "+1XY8HQBb8KmSaMS/X8RUDafoZOIhHZb9bpgiYwW78Cpnz1j9WfdwpQ+5Yq8RU9l";

                byte[] buffer3;
                byte[] key = GetKey(validRequest, validSerial);
                byte[] iV = GetIV(asmCrackme);

                Console.WriteLine("[i] Decryption key:\r\n\r\n{0}", HexDump(key));
                Console.WriteLine("[i] Initialization vector:\r\n\r\n{0}", HexDump(iV));

                Rijndael rijndael = Rijndael.Create();
                rijndael.Padding = PaddingMode.Zeros;

                using (FileStream stream = File.Open("crackmelib.dll", FileMode.Open))
                {
                    buffer3 = new byte[stream.Length];
                    new CryptoStream(stream, rijndael.CreateDecryptor(key, iV), CryptoStreamMode.Read).Read(buffer3, 0, (int)stream.Length);
                }

                // zapisz zdekodowany plik
                File.WriteAllBytes("crackmelib.decoded.dll", buffer3);

                Console.WriteLine("[i] Assembly decoded successfully.");
            }
            catch
            {
                Console.WriteLine("[!] Cannot decode file (invalid keys?)!");
                return;
            }
        }

Po odpaleniu tego kodu, otrzymamy ładne assembly z ostatnim fragmentem crackme.

Analiza crackmelib.dll

Pamiętacie, po odszyfrowaniu tej biblioteki, wywoływana była metoda A() z parametrem, jakim był kod aktywacyjny:

cm2_crackmelib

Metoda na początku porównuje hash pliku “PIJA_Crackme.exe” z zakodowaną wartością i jeśli się nie pokrywają, kasowany jest plik crackmelib.dll, a następnie tworzona jest jego kopia z losową zawartością:

private void MessFile()
{
    int count = 0x1800;
    File.Delete("crackmelib.dll");
    FileStream stream = File.Create("crackmelib.dll");
    byte[] buffer = new byte[count];
    Random random = new Random();
    for (int i = 0; i < count; i++)
    {
        buffer[i] = (byte) random.Next(0xff);
    }
    stream.Write(buffer, 0, count);
    stream.Flush();
    stream.Close();
}

Jeśli ktoś zpatchuje główny plik crackme, nadpisana zostanie zaszyfrowana biblioteka sprawdzająca poprawność kodu aktywacyjnego i wszelkie następne próby rejestracji skończą się niepowodzeniem.

Keygen i pomocnicza aplikacja dekodująca znajduje się na końcu artykułu, jeszcze dla jasności:

cm2_cracked

Było to najlepsze crackme ze wszystkich, a użyte zabezpieczenia znakomicie sprawdzają się w warunkach bojowych,  wzajemnie sprawdzane sumy kontrolne są doskonałą metodą na ochrone przed patchowaniem (oczywiście bardziej ukryte), a zastosowany algorytm szyfrujący sprawia, że złamanie tak zabezpieczonej aplikacji bez poprawnego klucza jest praktycznie niemożliwe.

Na koniec jeszcze małe TheDailyWTF, które wyświetliło się podczas debugowania crackme pluginem Deblector (który i tak się do niczego raczej nie przydał):

cm2_zombie

Zombies ahead?

3. MKS CrackMe 1

Bardzo proste crackme, dodatkowo opublikowane razem z rozwiązaniem (WTF?) .

MKS CrackMe 1

W zasadzie, jedynym problemem jest to, że domyślnie wyłączony jest button do sprawdzania hasła:

private void Form1_Load(object sender, EventArgs e)
{
    this.button1.Enabled = false;
}

W rozwiązaniu zaproponowanym przez autora crackme (hehe nie moge ;)) użyty jest plugin Reflectora o nazwie Reflexil do spatchowania tego kodu, my jednak będziemy oryginalni i użyjemy IDA do znalezienia kodu wyłączającego button:

cm3_patch

Jak widać na obrazku powyżej, mamy tam instrukcje “ldc.i4.0” (flaga FALSE), która sprawia, że button jest wyłączany, trzeba tam wrzucić instrukcję “ldc.i4.1”, problemem może być znalezienie jej hexadecymalnego odpowiednika, ale wystarczy przewinąć deadlisting w IDA (albo zajrzeć na stronę MSDN z listą opcodów albo tutaj), żeby znaleźć jakiś inny kod, w którym ją znajdziemy, potem tylko szybki patch i button do sprawdzania hasła będzie włączony.

Algorytm sprawdzania hasła jest banalnie prosty:

private void button1_Click(object sender, EventArgs e)
{
    if ((this.textBox1.Text.Length > 0) && (Convert.ToBase64String(Encoding.Default.GetBytes(this.textBox1.Text)) == this.textBox2.Text))
    {
        MessageBox.Show("You are great", "Registration");
    }
    else
    {
        MessageBox.Show("You failed. Try again.", "Registration");
    }
}

Name: bartosz Password: YmFydG9zeg==

Crackme może nie jest oryginalne, ale naprowadziło mnie na plugin Reflexil, który przyda się w przyszłości (pozwala na manipulację .net-owych assemblies i zapisywanie zmian do plików), tak więc zawsze jakis plus 🙂

4. SimpleCrackMe

Chyba najprostsze crackme z całej listy (w wersji widnieje nazwa firmy Comarch).

SimpleCrackMe

Do złamania wystarczy zmusić crackme do włączenia buttona. Jak to zrobić? Posłużymy się opisanym wyżej pluginem Reflexil. Kod wyłączający buttona zwykle znajduje się w procedurach ładowania okna lub inicjowania komponentów.

Po jego znalezieniu, z menu Tools, wybieramy Reflexil i w prawym panelu, na dole, pojawia się okno Reflexila z kodem IL bieżącej procedury:

cm4_patch

Mamy podobną sytuację jak w przypadku poprzedniego crackme. Należy zmodyfikować flagę FALSE, która wykorzystana jest do wyłączenia buttona. Aby to zrobić, klikamy prawym przyciskiem myszki na zaznaczony opcode i wybieramy EDIT, wyświetli się nam okno z opcją podmiany instrukcji:

cm4_edit_op

Na liście combo znajdują się wszystkie dostępne instrukcje, wybieramy więc “ldc.i4.1” (dokładnie tak jak w poprzednim crackme). Teraz, aby zapisać zmiany, należy kliknąć w drzewku Reflectora ikonkę modułu SimpleCrackme.exe i w oknie Reflexila pojawi się opcja zapisu:

cm4_save

Tutaj wystąpuje jeden problem. Otóż crackme jest podpisane cyfrowo (tzw. StrongNamed Assemblies) i zapisanie zmodyfikowanej wersji crackme spowoduje jego zawieszenie podczas uruchamiania.

Przy próbie zapisu zmodyfikowanego crackme, Reflexil pozwala wybrać co z tym fantem zrobić:

cm4_signed

Z dostępnych opcji należy wybrać opcję usunięcia sygnatury strong name a następnie wybrać “Cancel and keep it delay signed”, albo jak komuś się chce, to można się pobawić w ponowne podpisywanie, własnym kluczem. Nie jestem do końca obeznany z tematem podpisywania assemblies, więc jeśli popełniłem jakiś błąd, skorygujcie mnie.

W każdym razie, po tych zmianach crackme się ładnie uruchamia i można klikać w button:

cm4_cracked

Finał

Na tle wszystkich crackme, wyróżniło się crackme PIJA CrackMe 1. Wniosek jaki mi się jednak nasuwa jest taki, że jakiekolwiek zabezpieczenia .net nie mają sensu bez zastosowania obfuscatorów (i to dobrych).

Keygeny

Paczka z kodami dodatkowych narzędzi i generatorów kluczy:

konkurs_dotnet_crackme.zip (4.51 MB)

ESET crackme

ESETESET vs ME

Na tegorocznej edycji konferencji CONFidence 2009 ukazało się CrackMe przygotowane przez dzielnych pracowników polskiego oddziału firmy antywirusowej ESET.

Jako, że nie uczeszczam na tego typu zloty, dowiedziałem się o tym CrackMe ze wpisu na blogu Gynvaela, który opisał metodę jego złamania.

Zachęcony usunięciem moich negatywnych komentarzy z blogu Gynvaela, dotyczących metod zabezpieczeń zastosowanych w CrackMe, postanowiłem w ramach rozrywki i jednocześnie zemsty za cenzurę, opublikować moją analizę i jednocześnie dekompilację CrackMe.

Opis ogólny

CrackMe zostało stworzone w assemblerze pod MASMem w wersji 5 (czyli dość starej), celem jest odnalezienie właściwego hasła (samego, bez pary name/serial).

ESET crackme

Użyte zabezpieczenia

Największym zabezpieczeniem CrackMe jest jego niekompatybilność ze wszystkimi innymi wersjami systemu Windows oprócz Windows XP. Na starszych wersjach systemu Windows nawet nie ma co go odpalać (brak mechanizmu obsługującego TLS Callbacks), Windows 2000 wisi, a na nowszych występuje problem z prawami administracyjnymi. Ale może od początku.

Dodatkowym zabezpieczeniem są debug messages, które zostawił programista (zapewne dla zmyłki), choć jak czytam na blogu Gyna, po prostu ich chłopaczyna zapomniał usunąć — biedne, zagubione dziecko.

TLS Callbacks

CrackMe po uruchomieniu wykorzystuje funkcje TLS Callbacks do pobrania adresów swoich funkcji API. Są one wywoływane przez loadera Windows przed wejśćiem w entrypoint, mechanizm ten jest wykorzystywany m.in. przez protektor ExeCryptor. Adresy funkcji API (tylko z biblioteki KERNEL32.dll) pobierane są poprzez liczenie sum kontrolnych wszystkich eksportowanych funkcji z KERNEL32.dll i porównywanie ich z zapisanymi sumami funkcji wykorzystywanych przez CrackMe.

Procedura obliczająca sumę kontrolną wydała mi się na tyle charakterystyczna, że od razu pomyślałem, żeby sprawdzić z jakiego wirusa została podprowadzona przez sprytnego kodera ESET. Po krótkim poszukiwaniu wśród archiwów magazynów virii, poniżej przedstawiam procedurę z CrackMe oraz 2 procedury CRC32 autorstwa Vecny, znanego twórcy wirusów z grupy 29A:

ESET crackme CRC32

Zmieniona została jedynie wartość inicjalizacyjna CRC_POLY. Wnioski co do korzystania z kodów wirusów w CrackMe firmy antywirusowej pozostawiam czytelnikom.

Tracer

Po pobraniu adresów funkcji API, CrackMe uruchamia kopię swojego procesu w trybie debug (na wzór protektora Armadillo) i oczekuje na wyjątek.

ESET crackme tracer

Sprawdzanie hasła

Uruchomienie kopii swojego procesu w trybie debug wykorzystywane jest w procedurze sprawdzającej hasło, gdzie wpisany tekst jest szyfrowany a następnie porównywany z zaszyfrowanym wzorem poprawnego hasła.

Tutaj wchodzi do gry 2 kopia procesu, w pętli debugującej oczekuje na wyjątek. Procedura szyfrowania wpisanego hasła, przed przystąpieniem do samego szyfrowania ustawia flagę TRAP FLAG w rejestrze flag (używając instrukcji:

pushfd ; zapamiętaj stan flag na stosie
xor     dword ptr ds:[esp], 100000000b ; ustaw flagę TRAP
popfd ; przywróć stan flag ze stosu

ESET crackme trapflag

Ustawienie flagi TRAP FLAG sprawi, że wykonanie każdej następnej instrukcji spowoduję wyjątek single step exception (EXCEPTION_SINGLE_STEP) i kontrola zostanie przekazana do pętli debugującej.

O co w tym chodzi? Szyfrowanie wpisanego hasła wykorzystuje serię instrukcji ADD, SUB, XOR i LEA. Jednak przy ustawionej fladze śledzenia krokowego, przed wykonaniem kolejnych instrukcji szyfrujących, kontrola przekazywana jest do pętli debugującej, która sprawdza jaka ma być aktualnie wykonana instrukcja szyfrująca i w zależności co to będzie, dodatkowo modyfikuje wartość przed zaszyfrowaniem.

Wykrywane są 3 rodzaje instrukcji szyfrujących i przed ich wykonaniem modyfikowany jest rejestr EAX (w którym znajduje się fragment wpisanego hasła):

  • XOR EAX, IMM32 -> EAX = EAX – 2
  • SUB EAX,IMM32 -> EAX = EAX + 1
  • ADD EAX,IMM32 -> EAX = EAX ^ 0x10101010

Cała pętla szyfrująca jest na bieżąco śledzona przez drugą kopię programu (cały czas odświeżana jest flaga TRAP FLAG), aż do momentu napotkania specjalnego markera (4 x NOP), który przerywa proces śledzenia, co oznacza, że całe hasło zostało zaszyfrowane, po czym jest porównywane z wzorcem oczekiwanego hasła i na tej podstawie stwierdzana jest jego poprawność.

Odtworzenie poprawnego hasła wymaga odwrócenia kolejności instrukcji szyfrujących oraz dodania dodatkowych instrukcji modyfikujących stan rejestru EAX (symulacja pętli debugującej), całość klepnąłem w PHP i jest dostępna na końcu.

Poprawne hasło to “You talkin’ to me?”.

Bug

CrackMe posiada również buga (feature?), który dzięki sprytnym panom z Microsoft jest poprawnie rozpoznawany przez Windows i nie powoduje zwiechy:

ESET crackme bug

Dekompilacja

Dekompilacja crackme + dekoder i oryginalne 2 wersje crackme:

eset_crackme.zip (102 kB)

Fin

Wklepując w google frazę “ESET Polska” wyskakuje m.in. strona zapowiadająca:

ESET w Polsce: zatrudnimy wszystkich najlepszych specjalistów

Jeśli u was rzeczywiście pracują sami najlepsi, to czy całą resztę macie za idiotów?

PS.
I jeszcze krótka wiadomość dla panów z ESET, którzy odgrażali się, że zrobią mi CrackMe z jakimś RSA64 i zakazem patchowania:

Zjadam was na śniadanie

Jak widzicie, takie mózgi jak wy zjadam na śniadanie 😛

HASP Envelope

Sklonowany klucz HASPJakiś czas temu znajomy poprosił mnie o usunięcie zabezpieczenia HASP Envelope z aplikacji. HASP Envelope to rodzaj exe-protectora, który powoduje, że nie moża uruchomić aplikacji bez włożonego klucza sprzętowego (tzw. dongle) lub tak jak na obrazku, jego klona.

Opcje zabezpieczające systemu HASP Envelope może nie należą do najbardziej zaawansowanych, jakie obecnie są używane na rynku (chociaż jeśli chcemy śledzić kod z OllyDbg, musimy wesprzeć się pluginem ukrywającym jego obecność, najlepiej PhantOm Plugin), jednak utknąłem w pewnym momencie na zabezpieczeniu tabeli importów, które wykorzystuje technikę API redirectingu.

API redirecting w wydaniu firmy Alladin jest jednym z najbardziej uciążliwych w śledzeniu jakie widziałem, większość procedur wypełniających tabelę importów jest zmutowana albo poddana obfuskacji, co sprawia, że codeflow jest nielinearny i ciężki do śledzenia.

Piszę o procedurach wypełniających tabelę importów, bo jest to jedna z moich ulubionych metod uzyskiwania oryginalej tabeli importów, poprzez zpatchowanie procedur wypełniających importy (udało mi się tą metodą otworzyć zaledwie garstkę funkcji).

Na pewno ktoś pomyślał “Dlaczego nie skorzystałeś z ImpRec-a?”, programiści Alladina byli na tyle sprytni, że kod przekierowanych funkcji przechodzi przez masę równie zagmatwanych funkcji oraz wykorzystuje ciekawe mechanizmy jak kopiowanie stanu stosu przed bezpośrednim wywołaniem danej funkcji (czytaj protector musiał znać każdą API i ilość jej parametrów), co skutecznie blokuje wszystkie metody śledzenia wykorzystywane przez rebuilder ImpRec.

Trochę podłamany nieskutecznością standardowych metod, zacząłem kombinować jak do tego podejść. Po wielu próbach postanowiłem wykorzystać proste fakty:

  • znajomość położenia tabeli importów w pliku wykonywalnym
  • każde wywołanie funkcji API w końcu do niej doprowadzi

Tracing z wykorzystaniem ImpRec-a nie dał rezultatów, co nie znaczy, że ta metoda była nieskuteczna, jedynie jej implementacja.

Postanowiłem zbudować własnego tracera w oparciu o język skryptowy ODbgScript przeznaczony oczywiście dla debuggera OllyDbg. Idea jego działania jest następująca:

  • pobierane są kolejne adresy funkcji z tabeli importów
  • zachowywany jest stan rejestrów wejściowych
  • uruchamiane jest śledzenie warunkowe
  • sprawdzane są warunki
  • jeśli warunki się zgadzają, jesteśmy w kodzie funkcji API
  • jej adres jest umieszczany w tabeli importów
  • skok do początku

W trakcie pisania skryptu wyszło kilka faktów zabezpieczenia, wyżej wspomniane kopiowanie parametrów stosu w inne miejsce pamięci, co spowodowało, że tracer nie był w stanie rozpoznać, że jest w kodzie funkcji API, gdyż stan rejestru ESP przed wywołaniem funkcji i w jej kodzie był inny (takie warunki początkowo ustaliłem).

Metoda z kopiowaniem stanu stosu skutecznie uniemożliwia sprawdzanie adresu ESP, jednak stan stosu musi być zachowany dla poprawności wywoływanych funkcji, stąd nowym warunkiem było wprowadzenie na stos 2 fałszywych parametrów, można powiedzieć markerów,  które każdorazowo były sprawdzane, gdy wskaźnik kodu po śledzeniu warunkowym znajdował się poza przestrzenią adresową aplikacji (czyli w kodzie innej biblioteki).

;
; odbudowa tabeli IAT w aplikacjach
; zabezpieczonych HASP Envelope
;
  var     safe_eax
  var     safe_ecx
  var     safe_edx
  var     safe_esi
  var     safe_edi
  var     safe_ebx
  var     safe_esp
  var     safe_ebp

  var     iat_entry
  var     iat_entry_ptr
  var     iat_dword
  var     x
  var     y

  bphwcall                                ; usun bp na hardware id
  bc                                      ; i wszystkie normalne bp

  bphws   06688AF,"x"                     ; hw bp na OEP

  run

; tu jestesmy w OEP

; zachowaj stan rejestrow do call-a
  mov     safe_eax,eax
  mov     safe_ecx,ecx
  mov     safe_edx,edx
  mov     safe_esi,esi
  mov     safe_edi,edi
  mov     safe_ebx,ebx
  mov     safe_esp,esp
  mov     safe_ebp,ebp

; poczatek tabeli importow
  mov     iat_entry,0685000

resolve_next_api:

; kilka funkcji WinAPI musi być pominietych
  cmp     iat_entry,0685108               ; DecodePointer?
  je      resolve_api_skip
  cmp     iat_entry,06851AC
  je      resolve_api_skip

  mov     iat_entry_ptr,[iat_entry]
  cmp     iat_entry_ptr,0
  je      resolve_api_skip

  cmp     iat_entry_ptr,50000000
  je      resolve_api_skip

; czy to instrukcja CALL (1 instrukcja api redirectingu)?
  mov     iat_dword,[iat_entry_ptr]
  and     iat_dword,0FF
  cmp     iat_dword,0E8
  jne     resolve_api_skip

; zachowaj na stosie fałszywe markery
  exec
          push    0AAAAAABB
          push    066AA9F
  ende

  mov     eip,iat_entry_ptr

; run trace into

trace_again:
  ticnd   "eip > 50000000"                ; przerwij śledzenie
                                          ; jesli EIP > 50000000

  cmp     eip,07C80929C                   ; GetTickCount
  je      trace_skip
  cmp     eip,07C813093                   ; IsDebuggerPresent
  je      trace_skip

; sprawdz czy na stosie znajduje sie markery
  mov     y,esp
  add     y,4
  cmp     [y],0AAAAAABB
  je      trace_hit

trace_skip:

  rtu
  jmp     trace_again

trace_hit:

; pause
  bphwcall

  log     eip

; wstaw oryginalny adres funkcji API
  mov     [iat_entry],eip

; przywroc stan rejestrow
  mov     eax,safe_eax
  mov     ecx,safe_ecx
  mov     edx,safe_edx
  mov     esi,safe_esi
  mov     edi,safe_edi
  mov     ebx,safe_ebx
  mov     esp,safe_esp
  mov     ebp,safe_ebp

  jmp     resolve_api_next

resolve_api_skip:

; pause

resolve_api_next:

  add     iat_entry,4
  cmp     iat_entry,685340
  jb      resolve_next_api

resolve_api_exit

  ret

Skrypt nie jest idealny, gdyż kilka funkcji API śledzonych tą metodą powoduje zawieszenie debuggera (należy je ręcznie dodać do omijanych pozycji z tabeli IAT), dodane także musiały być metody sprawdzające czy wskaźnik kodu nie znajduje się w funkcjach WinAPI wykorzystywanych w kodzie API redirectingu (wskutek czego ich adresów nie ma w odbudowanej tabeli IAT i trzeba ręcznie je skorygować).

To by było na tyle, jeśli ktoś zna jakąś ciekawszą metodę na odbudowę tabeli importów w plikach zabezpieczonych HASP Envelope chętnie wysłucham waszych komentarzy.

ReverseCraft ruszył – videokurs dla Reverserów!

Redakcja UW-Team.org znana z publikacji w internecie darmowych kursów poruszających tematykę bezpieczeństwa webowego, programowania w językach PHP i JavaScript, opublikowała wczorajszego dnia (22.04.2009) na swoim portalu, nową serie Videoartów poświeconą Reverse Engineeringowi.

Kurs pod nazwą ‘ReverseCraft’ prowadzony jest przez programistę i reversera – Gynvaela Coldwinda. Autor w swych filmach prezentuje, w jaki sposób, krok po kroku, przeanalizować nieznaną dla nas na początku aplikację i jak zmodyfikować jej kod.

Seria tutoriali podzielona jest na sezony – każdy z nich dotyczy innej
aplikacji i tematyki. Obecnie do pobrania na stronie znajduje się pilotażowy (trwający blisko 40 minut) odcinek pierwszego sezonu. Kolejne odcinki z tej serii, publikowane będą w 4-5 dniowych odstępach.

http://www.uw-team.org/video_reverse.html