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
|