Rozwiązanie do CC-Team Arigo Crackme8

[English version]

Wstęp
Ostatnio miałam trochę wolnego czasu i przyszło mi do głowy, żeby poszukać jakiegoś polskiego CrackMe, które jeszcze nie zostało rozwiązane. W końcu natrafiłam na takie: http://cc-team.org/index.php?name=projekty&show=43 . Na stronie teamu nie ma jak widać rozwiązania, szperając w Google też nic nie znalazłam. Cóż, wygląda na to, że czekało na nie ponad 7 lat😀
Zadanko nie jest trudne (szacuję jakiś 2-3 poziom według skali z Crackmes.de), ale bardzo przyjemne i myślę że świetnie się nadaje, żeby zilustrować pewne techniki początkującym.

Jest trochę nietypowe – to nie jest CrackMe z gatunku „znajdź flagę” lub „napisz keygena”, nie ma jedynego słusznego rozwiązania. Mam jednak nadzieję, że trafiłam w oczekiwania autora (jeśli mnie czyta to oczywiście zapraszam do skomentowania :)). Kod loadera wygląda tak: loder.c

CrackMe
Zacznijmy od pobrania crackme: cm8 (mirror: cm8)
Jest to 32-bitowy ELF.
Cel: „odpalic program tak by wyswietlil napis gratulacyjny i zwrocil wartosc 0.”
i szczegóły: „mile widziane rozwiazania bez edycji binarki aczkolwiek jak ktos zrobi z edycja (tylko logiczna) tez sie zaliczy😉.”

Po odpaleniu program wyświetla napis powitalny: „crackme6 by arigo” a potem kończy działanie. Łatwo zgadnąć. że rozwiązanie powinno być podane jako parametr wywołania… Po kilku eksperymentach z rónymi parametrami, zauważyłam że aplikacja wysypuje się po podaniu 3 losowych parametrów… OK, zajrzyjmy więc do środka:

Analiza

Oto, co wypluł mi objdump:

$ objdump -d ./cm8

./cm8:     file format elf32-i386

Disassembly of section .text:

080480a0 <_start>:
 80480a0:    89 e6                    mov    %esp,%esi
 80480a2:    31 c0                    xor    %eax,%eax
 80480a4:    b0 04                    mov    $0x4,%al
 80480a6:    31 db                    xor    %ebx,%ebx
 80480a8:    43                       inc    %ebx
 80480a9:    68 6f 0a 41 41           push   $0x41410a6f
 80480ae:    68 61 72 69 67           push   $0x67697261
 80480b3:    68 20 62 79 20           push   $0x20796220
 80480b8:    68 6b 6d 65 36           push   $0x36656d6b
 80480bd:    68 63 72 61 63           push   $0x63617263
 80480c2:    89 e1                    mov    %esp,%ecx
 80480c4:    31 d2                    xor    %edx,%edx
 80480c6:    b2 12                    mov    $0x12,%dl
 80480c8:    cd 80                    int    $0x80
 80480ca:    89 f4                    mov    %esi,%esp
 80480cc:    58                       pop    %eax
 80480cd:    5b                       pop    %ebx
 80480ce:    59                       pop    %ecx
 80480cf:    5a                       pop    %edx
 80480d0:    5e                       pop    %esi
 80480d1:    68 cb cd 80 90           push   $0x9080cdcb
 80480d6:    68 31 c0 40 fe           push   $0xfe40c031
 80480db:    68 ff e6 31 db           push   $0xdb31e6ff
 80480e0:    68 4b 75 e9 5e           push   $0x5ee9754b
 80480e5:    68 88 06 41 46           push   $0x46410688
 80480ea:    68 8a 11 28 d0           push   $0xd028118a
 80480ef:    68 75 04 89 f9           push   $0xf9890475
 80480f4:    68 fe ca fe c2           push   $0xc2fecafe
 80480f9:    68 8a 06 8a 11           push   $0x118a068a
 80480fe:    68 31 c0 31 d2           push   $0xd231c031
 8048103:    68 31 db b3 5c           push   $0x5cb3db31
 8048108:    68 52 56 8d 0f           push   $0xf8d5652
 804810d:    68 ba 12 89 e6           push   $0xe68912ba
 8048112:    68 24 68 83 ba           push   $0xba836824
 8048117:    68 68 48 fb b1           push   $0xb1fb4868
 804811c:    68 a3 16 84 90           push   $0x908416a3
 8048121:    68 48 de 3e 68           push   $0x683ede48
 8048126:    68 ac 1f 68 51           push   $0x51681fac
 804812b:    68 bc 68 90 0f           push   $0xf9068bc
 8048130:    68 68 02 a4 82           push   $0x82a40268
 8048135:    68 61 c6 da 70           push   $0x70dac661
 804813a:    68 94 73 a4 68           push   $0x68a47394
 804813f:    68 8e 6b 68 7a           push   $0x7a686b8e
 8048144:    68 cd 68 8b ae           push   $0xae8b68cd
 8048149:    68 68 e4 28 96           push   $0x9628e468
 804814e:    68 28 88 34 f1           push   $0xf1348828
 8048153:    68 3e c7 22 68           push   $0x6822c73e
 8048158:    68 13 94 68 96           push   $0x96689413
 804815d:    68 9f 68 7e db           push   $0xdb7e689f
 8048162:    68 68 4e ee 6b           push   $0x6bee4e68
 8048167:    68 02 51 32 b2           push   $0xb2325102
 804816c:    68 d9 4d 3a 68           push   $0x683a4dd9
 8048171:    68 fd d8 68 2f           push   $0x2f68d8fd
 8048176:    68 f8 68 e9 50           push   $0x50e968f8
 804817b:    68 68 80 c5 28           push   $0x28c58068
 8048180:    68 48 50 3a b0           push   $0xb03a5048
 8048185:    68 95 85 86 68           push   $0x68868595
 804818a:    68 65 23 68 2f           push   $0x2f682365
 804818f:    68 6d 68 9d 3b           push   $0x3b9d686d
 8048194:    68 68 ad 17 a7           push   $0xa717ad68
 8048199:    68 1f b0 02 b8           push   $0xb802b01f
 804819e:    68 00 00 5a 68           push   $0x685a0000
 80481a3:    68 0f 85 9c 00           push   $0x9c850f
 80481a8:    68 5f 5f 3c 04           push   $0x43c5f5f
 80481ad:    68 a6 00 00 00           push   $0xa6
 80481b2:    68 f8 4f 0f 84           push   $0x840f4ff8
 80481b7:    68 31 c0 5f 89           push   $0x895fc031
 80481bc:    89 e7                    mov    %esp,%edi
 80481be:    6a 00                    push   $0x0
 80481c0:    56                       push   %esi
 80481c1:    52                       push   %edx
 80481c2:    51                       push   %ecx
 80481c3:    53                       push   %ebx
 80481c4:    50                       push   %eax
 80481c5:    ff e7                    jmp    *%edi

Niezbyt czytelne prawda? No ale po kolei…
Pierwsza część (do 80480c8) woła int80 z eax = 4 (sys_write) i bl=0x12 – na standardowe wyjście drukowane jest 0x12 znaków, które wcześniej zostały wrzucone na stos w formie DWORDów. Jest to wiadomość powitalna.

Druga część [80480ca do 80481c5] wygląda bardziej tajemniczo i nie trudno się domyślić, ona właśnie jest kluczem do rozwiązania. Najpierw wrzucamy coś na stos, a potem ten stos wykonujemy.
Proponuję załadować crackme do gdb z TUI.

gdb -tui ./cm8
Będziemy używać dwóch layoutów: layout asm (podgląd dizasemblera), layout regs (podgląd rejestrów).
Najpierw włączymy layout asm. Ustawimy breakpoint na linii, w której przekazywane jest sterowania do zawartości stosu: jump *edi. Teraz uruchamiamy aplikację z trzema losowymi argumentami…

arigo_cm8_0
Wciskamy ENTER, a kiedy wykonanie zatrzyma się na breakpoincie piszemy:
ni
by przejść do kolejnej instrukcji. Widok asemblera zmienił się – widzimy teraz kod, który jest na stosie.
arigo_cm8_1
Kod, który wcześniej był ukryty, nie ma już przed nami tajemnic🙂
Dla wygody skopiowałam sobie odkrytą część kodu i dodałam do niego opis. (Miej na uwadze, że przy każdym wykonaniu adres stosu się zmieni)

>│0xbffff4c8  xor  eax,eax  ; EAX = 0  
 │0xbffff4ca  pop  edi    ; EDI <- argc  
 │0xbffff4cb  mov  eax,edi   
 │0xbffff4cd  dec  edi   ; if (argc - 1) == 0  
 │0xbffff4ce  je  0xbffff57a ; goto exit_with_error  
 │0xbffff4d4  pop  edi    
 │0xbffff4d5  pop  edi    
 │0xbffff4d6  cmp  al,0x4  ; if (argc != 4) -> podano NIE 3 argumenty wywołania
 │0xbffff4d8  jne  0xbffff57a ; goto exit_with_error  
 │0xbffff4de  pop  edx    

 │0xbffff4df  push  0xb802b01f   ; wrzuca na stos 23 DWORDy ; 23 * 4 = 92 BYTE
 │0xbffff4e4  push  0x6da717ad  
 │0xbffff4e9  push  0x23653b9d  
 │0xbffff4ee  push  0x8685952f  
 │0xbffff4f3  push  0xb03a5048  
 │0xbffff4f8  push  0xf828c580  
 │0xbffff4fd  push  0xd8fd50e9  
 │0xbffff502  push  0x3a4dd92f  
 │0xbffff507  push  0xb2325102  
 │0xbffff50c  push  0x9f6bee4e  
 │0xbffff511  push  0x9413db7e  
 │0xbffff516  push  0x22c73e96  
 │0xbffff51b  push  0xf1348828  
 │0xbffff520  push  0xcd9628e4  
 │0xbffff525  push  0x6b8eae8b    
 │0xbffff52a  push  0xa473947a  
 │0xbffff52f  push  0x70dac661  
 │0xbffff534  push  0xbc82a402  
 │0xbffff539  push  0x1fac0f90  
 │0xbffff53e  push  0x3ede4851  
 │0xbffff543  push  0x908416a3  
 │0xbffff548  push  0x24b1fb48  
 │0xbffff54d  push  0x12baba83  
 │0xbffff552  mov  esi,esp  <- wierzchołek stosu  
 │0xbffff554  push  edx    
 │0xbffff555  push  esi    
 │0xbffff556  lea  ecx,[edi]  ; ECX =  1-st arg  
 │0xbffff558  xor  ebx,ebx  <- EBX = 0  
 │0xbffff55a  mov  bl,0x5c  <- EBX = 0x5c = 92  
 │0xbffff55c  xor  eax,eax  <- eax = 0  
 │0xbffff55e  xor  edx,edx  

loop_top : do EBX = 92 times:
 │0xbffff560  mov  al,BYTE PTR [esi]  <- pobiera ze stosu do AL    
 │0xbffff562  mov  dl,BYTE PTR [ecx]  <- pobiera z 1-szego argumentu do DL 
 │0xbffff564  dec  dl  ;   
 │0xbffff566  inc  dl  ; test DL, DL   

  /0xbffff568  jne  0xbffff56e  ; if (DL != 0) goto decode
 │ 0xbffff56a  mov  ecx,edi  ; resetuje ECX (wraca na początek bufora 1-szego argumentu)  
 │ 0xbffff56c  mov  dl,BYTE PTR [ecx]  ; znak z bufora do DL
decode:
 │0xbffff56e  sub  al,dl  ; AL -= DL  
 │0xbffff570  mov  BYTE PTR [esi],al   ECX ++
 │0xbffff573  inc  esi  ; take next byte  -> ESI ++  
 │0xbffff574  dec  ebx  ; decrement the counter -> EBX--
^---0xbffff575  jne  0xbffff560  ; if (ebx != 0)  goto loop_top

 │0xbffff577  pop  esi  ; przywraca wskażnik do wierzchołka stosu
 │0xbffff578  jmp  esi  ; przekazuje sterowanie do KODU NA STOSIE!
 
exit_with_error:
 │0xbffff57a  xor  ebx,ebx  <- ebx = 0  
 │0xbffff57c  xor  eax,eax  <- eax = 0  
 │0xbffff57e  inc  eax  ; eax = 1  
 │0xbffff57f  dec  bl   ; bl = (-1) = 0xff 
 │0xbffff581  int  0x80  ; sys_exit (-1)


Podsumowanie:

Co robi kod:
1. sprawdza, czy podano 3 argumenty wywołania – jeśli nie: sys_exit (-1)
2. w przeciwnym razie: wczuca 92 bajtów na stos
3. obiera pierwszy argument wywołania za KLUCZ
4. dekoduje zawartośc stosu, odejmując od niej wartośc klucza (znak po znaku). Pseudokod:

for (i = 0, j = 0; i < 92; i++, j++) { 	
	if (j >= key_len) j = 0;
	pushed[i] = pushed[i] - key[j];
}

Wynika z tego że KLUCZ może być praktycznie dowolny, o ile spełnia założenia i daje właściwy rezultat.
Ale czym jest właściwy rezultat? To już nie jest tak precyzyjnie zdefiniowane. Wiemy tylko, że musi:
– mieścić się w 92 bajtach
– drukować komunikat gratulacyjny
– kończyć aplikację bez błędów

W poprzednich CrackMe tego samego autora komunikat gratulacyjny brzmiał:
got it😉
Postanowiłam się tego trzymać.
Jeśli dokładnie przyjrzymy się zadaniu, zauważymy, że zawiera ono części rozwiązania, które tylko trzeba poskładać razem i dostosować.
Drukowanie komunikatu:

B0 04                 mov     al, 4
31 DB                 xor     ebx, ebx
43                    inc     ebx             ; fd
68 2D 29 0A 41        push    410A292Dh       ; '-)/0aA'
68 69 74 20 3B        push    3B207469h       ; 'it ;'
68 67 6F 74 20        push    20746F67h       ; 'got '
89 E1                 mov     ecx, esp        ; addr
31 D2                 xor     edx, edx
B2 0B                 mov     dl, 0Bh         ; len
CD 80                 int     80h             ; LINUX - sys_write

I wyjście: sys_exit(0)

33 DB      xor  ebx,ebx  <- ebx = 0  
33 C0      xor  eax,eax  <- eax = 0  
40         inc  eax  <- eax = 1  
CD 80      int  0x80  ; sys_exit (0)

Po przepisaniu opkodów:

char solution[] = { 0xB0, 0x04,
		0x31, 0xDB,
		0x43,
		0x68, 0x2d,  0x29, 0x0A, 0x41,
		0x68, 0x69, 0x74, 0x20, 0x3b,
		0x68, 0x67,  0x6f, 0x74, 0x20,
		0x89, 0xE1,
		0x31, 0xD2,
		0xB2, 0x0B,
		0xCD, 0x80,
		0x33 , 0xdb,
		0x33, 0xc0,
		0x40,
		0xcd, 0x80
	};

Potrzebujemy jeszcze bajtów, które były wrzucane na stos:

	char pushed[] = {0x83, 0xba, 0xba, 0x12, 0x48, 0xfb, 0xb1, 0x24,
		0xa3, 0x16, 0x84, 0x90, 0x51, 0x48, 0xde, 0x3e, 0x90, 0x0f,
		0xac, 0x1f, 0x02, 0xa4, 0x82, 0xbc, 0x61, 0xc6, 0xda, 0x70,
		0x7a, 0x94, 0x73, 0xa4, 0x8b, 0xae, 0x8e, 0x6b, 0xe4, 0x28,
		0x96, 0xcd, 0x28, 0x88, 0x34, 0xf1, 0x96, 0x3e, 0xc7, 0x22,
		0x7e, 0xdb, 0x13, 0x94, 0x4e, 0xee, 0x6b, 0x9f, 0x02, 0x51,
		0x32, 0xb2, 0x2f, 0xd9, 0x4d, 0x3a, 0xe9, 0x50, 0xfd, 0xd8,
		0x80, 0xc5, 0x28, 0xf8, 0x48, 0x50, 0x3a, 0xb0, 0x2f, 0x95,
		0x85, 0x86, 0x9d, 0x3b, 0x65, 0x23, 0xad, 0x17, 0xa7, 0x6d,
 		0x1f, 0xb0, 0x02, 0xb8};

Teraz zostało tylko zdekodowanie:

	size_t size = sizeof(solution);
	for (int i = 0, j = 0; i < size ; i++, j++) {
		unsigned char res = pushed[i]- solution[j];
		printf("\\x%02x", res);
	}

A oto wynik:
\xd3\xb6\x89\x37\x05\x93\x84\xfb\x99\xd5\x1c\x27\xdd\x28\xa3\xd6\x29\xa0\x38\xff\x79\xc3\x51\xea\xaf\xbb\x0d\xf0\x47\xb9\x40\xe4\x4b\xe1\x0e
musimy go teraz podać jako pierwszy parametr do ./cm8
Można to zrobić np za pomocą takiego dedykowanego loadera:

#include <stdio.h>
#include <unistd.h>

char *args[4] = {"0", 
	"\xd3\xb6\x89\x37\x05\x93\x84\xfb\x99\xd5\x1c\x27\xdd\x28\xa3\xd6\x29\xa0\x38\xff\x79\xc3\x51\xea\xaf\xbb\x0d\xf0\x47\xb9\x40\xe4\x4b\xe1\x0e", 
	"2",
	"3" 
};

int main()
{
	execve("./cm8", args, NULL);
	return 0;
}

arigo_cm8_solved
Koniec! Mam nadzieję że się podobało🙂

Opublikowano CrackMe, RCE | Otagowano , , | 3 komentarzy

Rozwiązanie CONfidence 2012 Crackme

Wstęp

Cześć, CrackMe które opiszę w tym poście pochodzi z konkursu o wejściówkę na tegoroczne CONfidence. Samej konferencji nie muszę chyba nikomu przedstawiać… Po szczegóły możecie w razie potrzeby zajrzeć na oficjalną stronę.


CrackMe



Oryginalny link
(hasło do rar-a: ESET)
Dla tych, których nie interesuje odpakowywanie execa, tylko sam algorytm, przygotowałam już rozpakowaną wersję.

CrackMe zostało napisane w assemblerze i spakowane MPRESS-em.
Odpakowywanie było bardzo proste, nie wymagało używania żadnych skomplikowanych technik ani narzędzi. Wystarczyło prześledzić kod krokowo w OllyDbg i po znalezieniu OEP zdumpować obraz za pomocą pluginu OllyDump. Plugin ten poradził sobie też bez problemu z odbudową importów.

Użyte narzędzia

  • PEid (opcjonalnie, do przeglądu wstępnego przed analizą)
  • ImmunityDbg / OllyDbg + plugin OllyDump
  • Drobne narzędzia własne napisane w C++ które opiszę później

Co CrackMe robi

– pobiera nazwę użytkownika (wymagana długość: od 4 do 31 znaków)
-pobiera hasło (wymagana długość: dokładnie 24 znaki; ze zbioru [A-Za-o])
-tworzy 3 hashe MD5 w następujący sposób:
#1 -> md5(username)
#2 -> md5(#1)
#3 -> md5(#2)

input: hasherezade

hash#1 = D88EB947A504FCF6C3D9DCA5F84DE42A
hash#2 = 12EB2430F671103B94D11F34D375CC0D
hash#3 = B23D343E81CEE206B9D180B7B0189010

Hashe są potem używane do generacji kodu MMX, który pełni ważną rolę w procesie weryfikacji hasła.
Najtrudniejszym (jedynym?) wyzwaniem tego CrackMe było złamanie hasha generowanego przez procedurę MMX.
Procedura ta była łatwa do zauważenia, bo znajdowała się bezpośrednio przed decyzją, czy hasło jest poprawne. Wyjście tej procedury jest porównywane z hashem#1. Jeśli jest mu równe, to hasło jest uznane za poprawne. Funkcja jest wywoływana poprzez rejestr EAX:

Funkcja znajduje się w pamięci pod adresem 0x403090 (do 0×404355). 1536 instrukcji MMX… Długie i zamotane, no nie?

Pierwszym moim (i z tego co wiem nie tylko moim) wrażeniem, było zbrutowanie tego… Na szczęście, zazwyczaj pierwsze wrażenie to tylko WRAŻENIE🙂. Oczywiście, brutforce trwałby wieki… Musiał być więc jakiś inny sposób.
Kiedy przyjrzymy się bliżej, zauważymy w tym chaosie logikę🙂.
Lubię pracować rozdzielając problem na dane i szukane…

Dane i szukane

Wejście  -w rejestrach od MM0 do MM5

Wyjście – w rejestrach MM0 i MM1

Stan rejestrów na początku wykonania funkcji:
MM0,MM1 -> ? (podane hasło – w przetworzonej formie)
MM2,MM3 -> hash#2
MM4,MM5 -> hash#3
(zwróć uwagę na endiany!)

MM0 i MM1 są teraz wypełnione jakimś „tajemniczym” hashem. Prostymi eksperymentami możemy ustalić, że jest on zależny od podanego hasła. Ale na głębszą analizę przyjdzie jeszcze czas – na razie nie wnikajmy… W rejestrach [MM2, MM3]  jest hash#2, natomiast w [MM4, MM5] – hash#3.  Co istotne, zawartość tych rejestrów pozostaje ta sama od początku do końca wykonania procedury. ( Kiedy to zauważyłam, byłam już pewna że ta funkcja da się odwrócić  :))

Stan rejestrów na końcu funkcji (zmiany są podświetlone)
MM0,MM1 -> ?? (powinien być hash#1)
MM2,MM3 -> hash#2
MM4,MM5 -> hash#3

RejestryMM7, MM8 służą za rejestry pomocnicze w wykonywanych obliczeniach.
Oczekiwanym wyjściem jest hash#1 w rejestrach MM0, MM1…

Krótko mówiąc, powinniśmy mieć na początku w rejestrach MM0, MM1 wartości, które po wszystkich tych operacjach zwrócą nam hash#1 w MM0, MM1…

Nie ma innej opcji – ta długa funkcja musi zostać odwrócona…
Przyjrzyjmy się jej bliżej…
Jest tu 256 bloków, po 6 instrukcji każdy. Zbudowane są według jednego schematu. Poniżej przykład:

Dla każdego N-tego bloku:
1. Rezultat bloku N-1 (przechowywany w MM0 lub MM1) jest kopiowany do jednego z „rejestrów pomocniczych” – MM7 lub MM6
2. Połowa hasha#2 lub hasha#3 jest kopiowana do „rejestru pomocniczego” – MM6 lub MM7
3. JPewne 3 operacje są wykonywane na kopii połowy hasha
4. Wynik tych operacji jest dodawany/odejmowany/xorowany z rezultatem bloku N-2 (przechowywanym w MM1 lub MM0)

To już sporo informacji! Dla każdej operacji, możemy łatwo obliczyć wartość, o którą wartość MM0/MM1 jest modyfikowana. Znamy też ostatnią wartość MM0 i MM1 – bo ma to być hash#1.

Teraz już chyba widać, że jedyne, co mamy do zrobienia to:

– odwrócić ostatnią instrukcjęw każdym bloku (tj zamienić operację na przeciwną, np PADDB na PSUBB itd…)
– ustawić bloki w odwrotnej kolejności (pierwszy ma być ostatni itd…)
–  wtedy – po podaniu jako inputu w rejestrach MM0, MM1 hash#1 – dostaniemy na wyjściu (też w MM0, MM1)  szukaną wartość (zakodowane hasło :))

Jak to zrobić? Możliwości jest wiele, innym razem opiszę jakieś ciekawsze i bardziej skomplikowane… Ale zamiast się bawić w przerost formy nad treścią, skupmy się na rozwiązaniu zadania – a było nim znalezienie poprawnego klucza dla swojego imienia i nazwiska (a nie pisanie keygena).

Zrobiłam to więc prostym i brzydkim sposobem – na oryginalnym exe-ku. Skopiowałam część pamięci zawierającą wszystkie instrukcje, odwróciłam je swoim małym narzędziem parsującym, napisanym na tą okazję w C++ i wkleiłam znowu w to samo miejsce.

Po lewej – końcówka oryginalnej procedury
Po prawej – początek odwróconej

Kiedy podałam w MM0 i MM1 hash#1, procedura zwróciła mi następujące wartości:

MM0 = AFEC FFD1 F7D1 9AE0
MM1 = 8597 249E 0642 1F2D

Jest to zakodowane hasło – teraz tylko trzeba je odkodować.

Kodowanie/ odkodowywanie hasła

Kodowanie hasła jest połączone w jedno z weryfikacją. To bardzo prosta procedura. Każdy znak hasła jest przetwarzany w następujący sposób ( oznaczmy funkcję przetwarzającą jako f1: )

f1:
if pass[i] in [a-o] : value = pass[i] – 0×41 – 0×6
if pass[i] in [A-Z] : value = pass[i] – 0×41
w przeciwnym razie – nieprawidłowe hasło

Znaki są nie tylko przetwarzane, ale też dodawne do wielomianu. Hasło jest dzielone na części, po 3 znaki każda:

3 znaki hasła ->
( f1(pass[n]) * 0x29 + f1( pass[n+1] ) ) * 0x29 + f1(pass[n+2]) –>
4 bajty „zakodowanego hasła”

Jako ilustrację, w jaki sposób odwrócić procedurę, zamieściłam swój  Simple Chunk Decoder.

MM0 = AFEC FFD1 F7D1 9AE0
MM1 = 8597 249E 0642 1F2D

9AE0 F7D1 FFD1 AFEC  1F2D 0642 249E 8597

Hasło odkodowane przez Simple Chunk Decoder

Teraz wystarczy skopiować hasło w odpowiednie pole… I gotowe!

Koniec bajki!🙂

Post Scriptum

  • Jeśli ktoś czuje niedosyt i chce poczytać alternatywne rozwiązanie, to polecam Vnd’s homepage
  • Kolejny post będzie o pisaniu keygena do tego crackme – opiszę w nim dokładnie procedurę generującą kod MMX – więc wszystkich zainteresowanych serdecznie zapraszam!
Opublikowano ConFidence, CrackMe, RCE | 1 komentarz

Rozwiązanie GhostCrackMe

Rozwiązanie do mojego CrackMe, napisanego rok temu na konkurs Pimp My CrackMe

Poniższy opis był robiony na szybko, będzie zilustrowany i uzupełniony

Zlamanie aplikacji wymaga odtworzenia i zrozumienia oryginalnego algorytmu, za pomocą analizy kodu, a następnie zaimplementowanie go w postaci biblioteki DLL. Przykladowa implementacja zostala dolaczona do tego opisu [LilithDLL_src].

Aplikacja jest spakowana bardzo prostym protektorem mojego autorstwa.
Stub pakera umieszczony jest w ostatniej (dodanej) sekcji (.cdata).
Odpakowuje on sekcje kodu, Xorujac kolejno jej bajty z bajtami stuba (od poczatku stuba, az do najlizszego bajtu 0xCC). Jest to jednoczesnie zabezpieczenie antydebugingowe, nie pozwalajace ustawiac breakpontow na stubie.
OEP oraz poczatek i wielkosc sekcji szyfrowanej sa przechowywane w formie zaszyfrowanej. Do odszyfrowania wykorzystuje sie operacje xor z fragmentami kodu pakera. Dane te sa przechowywane w srodku kodu, aby utrudnic ich zlokalizowanie. Dodatkowe poprzedzenie ich instrukcja CALL (która nie jest interpretowana podczas wykonania) zasmieca i zaciemnia kod.

Importy sa przepisane do niestandardowego formatu i ladowane przez stub umieszczony w przedostaniej sekcji (.reloc). Nie sa jednak zaszyfrowane i podstawowa wiedza o budowie importów wystarcza do ich odtworzenia. Stub ladujacy importy sprawdza PEB.NtGlobalFlags w celu wykrycia debugowania.

W sumie, techniki utrudniajace analize to:

  1. SMC (w wyniku zastosowania kod wykorzystany do operacji XOR nie jest juz tym samym kodem, którym byl na wejsciu do programu)
  2. Zmieniona wartosc ImageSize
  3.   Sprawdzenie flag: PEB.isDebuggerPresent i PEB.NtGlobalFlags – w przypadku wykrycia debugera sterowanie przekierowywane jest na nieprawidlowa sciezke, co skutkuje zawieszeniem sie aplikacji
  4. Caly kod pakera jest uzywany do deszyfrowania oryginalnej sekcji –> ustawienie breakpointa na kodzie stuba podczas odpakowywania, wywola nieprawidlowy rezultat.

Dzialanie algorytmu:

I.Tworzenie Loginu:
Skladowe: nazwa zalogowanego uzytkownika, ProfileGuid
Z profile guid usuwane sa wszystkie znaki, które NIE sa literami. Tak przetworzony, sklejany jest z nazwa uzytkownika.

II. Tworzenie klucza:
A. Budowanie matrycy
Algorytm tworzenia klucza jest inspirowany algorytmem „TheGameOfLife”, ma jednak nieco zmienione reguly. Istnieje matryca liter( o wymiarach [13][13] ), taka jak przedstawiono ponizej:

a b c d e k l m n o v w x
----------------------------
|_|_|_|_|_|_|_|_|_|_|_|_|_|
f|_|_|_|_|_|_|_|_|_|_|_|_|_|
g|_|_|_|_|_|_|_|_|_|_|_|_|_|
h|_|_|_|_|_|_|_|_|_|_|_|_|_|
i|_|_|_|_|_|_|_|_|_|_|_|_|_|
j|_|_|_|_|_|_|_|_|_|_|_|_|_|
p|_|_|_|_|_|_|_|_|_|_|_|_|_|
r|_|_|_|_|_|_|_|_|_|_|_|_|_|
s|_|_|_|_|_|_|_|_|_|_|_|_|_|
t|_|_|_|_|_|_|_|_|_|_|_|_|_|
u|_|_|_|_|_|_|_|_|_|_|_|_|_|
y|_|_|_|_|_|_|_|_|_|_|_|_|_|
z|_|_|_|_|_|_|_|_|_|_|_|_|_|

Kazde pole moze przyjmowac wartosc ze zbioru [0,1,2].

Pola, przypisane kolejnym literom sa inicjalizowane w oparciu o Login. Przypisuje sie im wtedy wartosc 2.
1. Nie rozróznia sie liter malych i duzych
2. Wszystkie znaki, które nie sa literami sa zamieniane na ‘x’
3. Do inicjalizacji sluza tylko unikalne znaki

(Dla wygody opisu, komórki o wartosci róznej od zera beda zwane dalej organizmami.)

Po zainicjowaniu tablicy tworzona jest „populacja poczatkowa” – wedlug nastepujacych regul:
-Przestrzen pomiedzy kazdymi dwoma organizmami, lub miedzy organizmem i koncem linii jest zapelniana organizmami o wartosci 1
-Wypelnianie zachodzi najpierw w poziomie, potem w pionie

Przyklad: login = „acet”
Tablica po zainicjalizowaniu:

2 0 2 0 2 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
2 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0

Tworzenie populacji poczatkowej:

-w poziomie:

2 1 2 0 2 1 1 1 1 1 1 1 1
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
2 1 1 1 1 1 1 1 1 1 1 1 1
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0

– pionie:

2 1 2 0 2 1 1 1 1 1 1 1 1
1 0 1 0 1 0 0 0 0 0 0 0 0
1 0 1 0 1 0 0 0 0 0 0 0 0
1 0 1 0 1 0 0 0 0 0 0 0 0
1 0 1 0 1 0 0 0 0 0 0 0 0
1 0 1 0 1 0 0 0 0 0 0 0 0
1 0 1 0 1 0 0 0 0 0 0 0 0
1 0 1 0 1 0 0 0 0 0 0 0 0
1 0 1 0 1 0 0 0 0 0 0 0 0
2 1 1 1 1 1 1 1 1 1 1 1 1
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0

Po stworzeniu populacji, zaczyna sie tworzenie kolejnych generacji. W kazdym kroku, tablica jest przetwarzana wedlug tej samej reguly. Sasiedztwo jest zdefiniowane tak jak w GameOfLife (kazda komórka ma 8 sasiadów).
-jesli organizm ma 0 sasiadów – umiera „z samotnosci”
-jesli ma dokladnie 1 sasiada – rozmnaza sie (zapelnia wszystkie wolne komórki dookola siebie)
-jesli ma wiecej niz 3 sasiadów – „gloduje” (jesli mial wartosc 2 to zmienia na 1, a jesli mial 1 to umiera )

B. Przeksztalcanie matrycy
Klucz sklada sie z 13 czlonów, wypisywanych w reprezentacji hexadecymalnej. Przyklad:

14F935-0000D-974AA-066FC-BC362-019F3-BC362-01A0E-91C3C-142C39-ACFEC-AE99A-40135

Jest on niczym wiecej jak alternatywna reprezentacja matrycy, w której kazdy z 13 wierszy jest traktowany jako liczba w ukladzie trójkowym (a nastepnie wyswietlany hexadecymalnie).
Matrycowa reprezentacja powyzszego klucza:

2 1 2 0 2 1 1 1 1 1 0 0 1
0 0 0 0 0 0 0 0 0 0 1 1 1
1 0 1 1 1 1 1 0 0 1 1 1 1
0 0 0 1 1 0 0 0 1 1 1 1 0
1 1 1 0 0 1 1 1 1 1 1 0 1
0 0 0 0 1 0 0 0 1 0 0 0 1
1 1 1 0 0 1 1 1 1 1 1 0 1
0 0 0 0 1 0 0 0 1 1 0 0 1
1 0 1 0 1 0 0 0 0 0 0 0 1
2 1 1 1 0 1 1 1 1 1 1 1 1
1 1 0 0 0 0 0 0 0 0 0 0 0
1 1 0 0 1 0 0 0 0 0 1 1 1
0 1 1 1 1 0 0 0 0 0 1 1 1

III. Weryfikacja klucza:
1. Aplikacja LilithGhost tworzy matryce (M1) dla wygenerowanego loginu. Przeksztalca ja N razy (N generacji), gdzie N =login.length
2. Sprawdza obecnosc biblioteki „Lilith.dll”. Jesli biblioteka istnieje, to laduje ja i wywoluje funkcje _secretspell2

  • Funkcja ta powinna: stworzyc plik z kluczem (Lilith.key)– stanem matrycy dla tego samego loginu, przeksztalconej N-1 razy
  • Zwrócic sume wszystkich pól dla matrycy przeksztalconej N razy. Jezeli wartosc zwracana nie zgadza sie z suma pól M1, plik DLL jest uznawany za nie spelniajacy warunków.

3. LilithGhost tworzy matryce M2 w oparciu o Lilith.key. Wykonuje na niej 1 dodatkowe przeksztalcenie.
4. Sprawdza, czy M1==M2. Jesli tak, klucz jest uznany za prawidlowy.
Dodatkowe informacje

Po zmienieniu subsystemu na konsolowy, aplikacja wyswietla 2 podpowiedzi:

  • poczatkowa matryce dla biezacego loginu
  • matryce zbudowana w oparciu o plik z kluczem (jesli jakis zostal dostarczony)

Podpowiedz ta jest tez furtka do manipulacji na pliku, sluzacej analizie algorytmu.

Opublikowano RCE | Dodaj komentarz