Zmiana warunku w binarce (objdump, assembler)


Poniższe pliki będą wyjaśnione:
 warunek.c

warunek
warunek_poprawiony

Jest sobie prosty program, przykład zwykłego warunku if() :) int main ( void ) { int a = 0x61; if( a == 0x62 ) printf("hacked :)\n"); return 0; } Widzimy tu, że nigdy nie wykona się printf("hacked :)\n"); gdyż warunek nie zostanie spełniony. Na sztywno ustawiana jest zmienna "a" (0x61 to nic innego jak szesnastkowy zapis liczby 97 czyli ascii a) :) Wrzucona jest więc pod zmienną "a" wartość 0x61. Następnie porównujemy ją z inną wartością - 0x62 (98 = ascii b). a != 0x62 (a nie jest równe b, więc mamy tutaj fałsz) Gdyby jednak była to prawda to wykonałaby się funkcja printf() a tak.. hmm nie wykona się :P Może to głupi przykład, ale prosto wyjaśnia, z jakim problemem mamy do czynienia. W programach zabezpieczonych hasłem często wystarczy zmienić jeden warunek, aby program wykonywał się dalej, bez konieczności podania prawdziwego hasła. Wyobraźmy sobie, że ta zmienna "a" to nasze hasło, które pobierane jest od użytkownika. Tutaj oczywiście jest już ono "pobrane" i siedzi w zmiennej "a" :) Jak to obejść nie mając kodu źródłowego, by pomimo wadliwego hasła program wykonywał się dalej? :) W tym przypadku wykonanie programu polega na wypisaniu zdania "hacked :)" na ekran. Można obejść to oczywiście różnie, na 31337 sposobów :) np. poprzez LD_PRELOAD itp. :) Ale czasami i to jest zabezpieczone i nie ma innego wyjścia i trzeba dorwać się do binarek i pozmieniać tam troszkę :) Oczywiście ten przykład będzie prosty, bo wiadomo co szukać :) Przyda się tutaj objdump :) Bierzemy naszą binarkę:
warunek i piszemy tak: objdump -D warunek|less Szukamy sekcji main, gdyż warunek był w main() właśnie. Dla ułatwienia wkleję to :) 080483f0 < main >: 80483f0: 55 push %ebp 80483f1: 89 e5 mov %esp,%ebp 80483f3: 83 ec 18 sub $0x18,%esp 80483f6: c7 45 fc 61 00 00 00 movl $0x61,0xfffffffc(%ebp) 80483fd: 83 7d fc 62 cmpl $0x62,0xfffffffc(%ebp) 8048401: 75 10 jne 8048413 < main+0x23> 8048403: 83 c4 f4 add $0xfffffff4,%esp 8048406: 68 70 84 04 08 push $0x8048470 804840b: e8 f0 fe ff ff call 8048300 <_init+0x68> 8048410: 83 c4 10 add $0x10,%esp 8048413: 31 c0 xor %eax,%eax 8048415: eb 00 jmp 8048417 < main+0x27> 8048417: c9 leave 8048418: c3 ret 8048419: 90 nop 804841a: 90 nop 804841b: 90 nop 804841c: 90 nop 804841d: 90 nop 804841e: 90 nop 804841f: 90 nop Zajmijmy się trzema najważniejszymi dla nas linijkami :) 80483f6: c7 45 fc 61 00 00 00 movl $0x61,0xfffffffc(%ebp) 80483fd: 83 7d fc 62 cmpl $0x62,0xfffffffc(%ebp) 8048401: 75 10 jne 8048413 < main+0x23> Nie wnikając za bardzo w assembler można zauważyć tu coś takiego: movl $0x61.... - wrzuć do pamięci wartość 0x61, ooo.. czyżby to było nasze "a"? :)) Nie ważne to jest w sumie, gdyż w większych programach nie będziecie mieć tak łatwo :) Tu mogą być całe ciągi znaków czy coś i jeszcze nie muszą być ładowane kolejno do pamięci :P Kolejna linijka: cmpl $0x62,0xfffffffc(%ebp) hmm.. program tu porównuje właśnie wartości 0x62 z tym, co już siedzi sobie w pamięci z 0x61 Pamiętacie? if( a == 0x62 ) To jest właśnie to porównanie :) Idziemy jednak dalej... jne 8048413 < main+0x23> HA! I chodzi tu o tą właśnie linijkę! :) Co tu się dzieje? Jeśli warunek powyżej byłby prawdziwy (jeśli a byłoby równe 0x62; a == b a nie jest) to program wykonywałby się dalej. jne - jeśli nie jest to prawda Tutaj następuje skok pod inny adres programu 8048413, gdyż warunek nie został spełniony :/ Zobaczmy, co tam siedzi pod tym adresem 8048413: 31 c0 xor %eax,%eax XOR rejestrów eax z eax, czyli w wyniku będzie "0" bo zawsze będą takie same. Dla przypomnienia tabela dla XOR: xor 0 0 = 0 xor 0 1 = 1 xor 1 0 = 1 xor 1 1 = 0 czyli jedynka gdy różne :) (ale ojej nie wnikajmy w asm =)) Głównie chodzi o to, że tu szykuje się nam wyskok z programu, czyli return 0 :) Ten xor %eax,%eax przygotował sobie "0" do return'a :) Jak widać dalej.. 8048415: eb 00 jmp 8048417 < main+0x27> skok pod adres 8048417 pod którym z kolei widać już: 8048417: c9 leave 8048418: c3 ret Wyjście :\ A co się stało z kawałkiem programu, który miał nam napisać na ekran printf("hacked :)\n"); !??!?! Uhh.. No niestety, warunek był tak napisany, że instrukcje wywołujące nasz printf() zostały ominięte.. gdyż nastąpił skok przy nie spełnionym warunku (jne 8048413). No to zabieramy się do poprawiania tego niedopatrzenia =) Co by tu popsuć.. Najprościej jest nadpisać instrukcję skoku czymś innym :) Ale czym?? Jest taka instrukcja NOP :) Ona nie robi nic (tzn. robi coś, bo przestawia licznik programu o 1, ale z punktu widzenia działania programu ona jest pusta) Mogliście ją zaobserwować dalej w sekcji main: 8048419: 90 nop Ma ona wartość 0x90 w hex. Czemu 90? Bo każdej instrukcji asm przyporządkowana jest wartość szesnastkowa rozkazu procesora a nop = 90 :) No to do dzieła.. Trzeba teraz nadpisać ten niefortunny skok do końca programu NOP'ami :))))) W ten sposób, nawet jeśli porównanie będzie złe to program "oleje" to, zamiast skakać na koniec programu to wykona instrukcję pustą NOP :) Ale które instrukcje nadpisać i jak???? :\ Hmm :) Przypomnijmy sobie: 80483f6: c7 45 fc 61 00 00 00 movl $0x61,0xfffffffc(%ebp) 80483fd: 83 7d fc 62 cmpl $0x62,0xfffffffc(%ebp) 8048401: 75 10 jne 8048413 < main+0x23> Ładowało to 0x61 do pamięci, porównywało z 0x62, jeśli było fałszywe to skok.. No to mamy to miejsce :) O tutaj: 8048401: 75 10 jne 8048413 < main+0x23> Rzucamy okiem na liczby po środku, które dla instrukcji NOP były równe 90. Tutaj mamy 75 10. Instrukcje w procesorach typu x86 mają różne długości i dlatego tutaj mamy więcej liczb :) Nie przejmujcie się tym. Można oby dwie te liczby nadpisać NOP'ami :) 90 90 No dobrze.. ale jak?? :) Bierzemy binarke i przeglądamy ją w edytorze. Może to być np. mc :) Oczywiście wyświetlamy ją sobie w hex. Szukamy sekwencji liczb szesnastkowych, które odpowiadają za wyżej wymienione instrukcje w sekcji main :) Poniżej podane są te hex'y już beż ich znaczenia w asm. c7 45 fc 61 00 00 00 83 7d fc 62 75 10 a jest to nasze: movl, cmpl, jne Szukamy.. Szukamy... O, znalezione? :) Teraz wystarczy dobrać się do tych ostatnich: 75 10 W mc możemy włączyć tryb zapisu podczas przeglądania pliku w hex. Można wówczas zmieniać pojedyncze wartości hex :) Zmieniamy 75 10 na 90 90 Zapisujemy zmiany. Powinniście uzyskać plik podobny do tego:
warunek_dup Jest to już poprawiony plik :) Uruchomcie go i zobaczcie co się stanie :) HA! Można też dla lepszego poznania jeszcze raz zobaczyć objdump ale tym razem tego poprawionego pliku :) objdump -D warunek_dup|less I co tu się zmieniło w sekcji main? :)))) ....... 80483f6: c7 45 fc 61 00 00 00 movl $0x61,0xfffffffc(%ebp) 80483fd: 83 7d fc 62 cmpl $0x62,0xfffffffc(%ebp) 8048401: 90 nop 8048402: 90 nop 8048403: 83 c4 f4 add $0xfffffff4,%esp 8048406: 68 70 84 04 08 push $0x8048470 ........ Jest nop nop :) Czyli działa :D Tam był oczywiście przykład programu w stylu: if( prawda ) { /* program */ } return 0; Ale jeśli warunek byłby w stylu: jeśli hasło_nieprawidłowe wyjdz z programu, a potem reszta kodu? Przykład: if ( fałsz ) exit(0); else { /* dalsza czesc programu */ } wtedy lepiej zmienić jne na je i je na jne, odwracając działanie warunku, niezależnie od tego, jak zbudowany jest program :) [ Lcamtuf ]. Ale to już Wasza praca domowa :) Albo jak mi się zachce to opiszę dokładniej jak to się robi :) Pozdrawiam wszystkie psuje :) Neutrinka neutrina at kwant.info http://www.kwant.info