Podstawy teoretyczne

  • Procesor wykonując program działa sekwencyjnie wg następującego schematu:
    1. Pobierz rozkaz wskazywany przez IP (Instruction Pointer)
    2. Zdekoduj rozkaz
    3. Wykonaj rozkaz i zaktualizuj flagi stanu
    4. Zwiększ IP, wróć do punktu 1
  • Przykład, kod w asemblerze:
    start:
        mov     eax, 10
        mov     ecx, 2
        sub     edx, edx
        idiv    ecx
    Co stworzył kompilator: (od lewej - adres, bajty rozkazu, instrukcje)
    start:
       0:   b8 0a 00 00 00          mov    eax,10
       5:   b9 02 00 00 00          mov    ecx,2
       a:   29 d2                   sub    edx,edx
       c:   f7 f9                   idiv   ecx
  • Prześledźmy to po kolei:
    1. IP = 0, wskazuje na instrukcję mov eax, 10
    2. Procesor odczytuje bajt b8
    3. Procesor dekoduje, że jest to instrukcja mov eax, LICZBA, pobiera jeszcze 4 bajty stanowiące liczbę
    4. W tym momencie IP = 5, wskazuje na bajt b9 czyli instrukcję mov ecx, LICZBA, itd.
  • Pisząc w asemblerze mamy możliwość odwzorowania 1-do-1 tego co zaprogramujemy i tego co zostanie wykonane. Natomiast kompilator C (i innych języków) zamienia program na określone sekwencje instrukcji asemblera - czasem lepiej, czasem gorzej niż człowiek.
  • Wyżej wspomniane zostały eax i ecx, co to jest? Procesor ma wbudowany w siebie szereg rejestrów. Każdy rejestr to jednostka pamiętająca pewną liczbę bitów. Można powiedzieć w uproszczeniu, że eax i ecx przypominają zmienne z języka C.
  • Dostępne rejestry:
32-bitEAXECXEDXEBXEDIESI
16-bit AX CX DX BX DI SI
8-bit  AHAL  CHCL  DHDL  BHBL        
  • Przykład: (AX = 1234, AH = 4, AL = 210)
AXb15b14b13b12b11b10b09b08b07b06b05b04b03b02b01b00
AHb15b14b13b12b11b10b09b08        
AL        b07b06b05b04b03b02b01b00
1234100000010011010010
  • Podstawowe instrukcje:
    • mov CEL, ŹRÓDŁO - przekopiuj wartość ze ŹRÓDŁA do CEL u np. mov eax, ecx skopiuje zawartość ecx do eax
    • add CEL, WARTOŚĆ - dodaj do CELu WARTOŚĆ np. add eax, 1 doda 1 do eax
    • sub CEL, WARTOŚĆ - jw. ale odejmowanie
    • div REJESTR - dzieli zawartość eax przez REJESTR. Wynik zapisany będzie w eax, reszta z dzielenia w edx ale UWAGA!, rejestr edx musi być wcześniej wyzerowany!
    • mul REJESTR - mnoży eax przez REJESTR
  • Procesor posiada jeszcze specjalny rejestr stanu, do którego zazwyczaj nie sięga się bezpośrednio. Jak napisano powyżej, rejestr stanu zmienia się w ogólności po każdym wykonaniu instrukcji i odzwierciedla on status systemu.
  • Aby wykonać kod warunkowo (tzn. odpowiednik if() czy pętli z języka C), należy ze sobą połączyć dwie instrukcje:
    • cmp CEL, WARTOŚĆ - porównanie CELu z WARTOŚCIĄ np. cmp eax, ecx porówna rejestr eax z ecx; po takim porównaniu ustawione zostaną flagi
    • j?? ETYKIETA - jest to skok warunkowy do ETYKIETY; znaki zapytania to odpowiednie litery, których należy użyć do określonej funkcjonalności:
      • je - jump-if-equal, skocz jeśli porównane wartości są równe
      • jne - jump-if-not-equal, skocz jeśli nie są równe
      • jl oraz jle - jump-if-less oraz jump-if-less-or-equal, skocz jeśli CEL jest mniejszy od WARTOŚCI (jle = jeśli mniejszy lub równy)
      • jg oraz jge - jump-if-greater oraz jump-if-greater-or-equal, skocz jeśli CEL jest większy od WARTOŚCI (jle = jeśli większy lub równy)

Instrukcja warunkowa if()

C:

if (n > 4) {
    ...
}

Asembler:

; odpowiednik if
    cmp     eax, 4
    jle     koniec_if
    ...
koniec_if:

Warto zauważyć, że w C programista pisze if() myśląc przy jakim warunku wykonać kod, natomiast w asemblerze programista myśli przy jakim warunku przeskoczyć kod :)

Pętla while()

C:

while (n < 5) {
    ...
    n = n + 1;
}

Asembler:

poczatek_while:
    cmp     eax, 5
    jge     koniec_while
    ...
    add     eax, 1
    jmp     poczatek_while
koniec_while:

Instrukcja jmp to bezwarunkowy skok - zawsze skacze do podanej etykiety, niezależnie od rejestru stanów.

Szablon do własnej pracy

Używany kompilator na zajęciach: http://flatassembler.net/download.php

Przykładowy kod: http://www.cs.put.poznan.pl/tzok/example.asm

format pe console
entry start

include 'win32ax.inc'

section '.import' import data readable
        library msvcrt, 'msvcrt.dll'

        import  msvcrt,\
                printf, 'printf',\
                scanf, 'scanf',\
                system, 'system'

section '.data' data readable writeable
        format_in    db '%d', 0
        format_out   db '%d', 13, 10, 0
        n            dd ?

section '.code' code readable writeable executable
start:
        cinvoke scanf, format_in, n
        mov     eax, [n]
        add     eax, 10
        sub     edx, edx
        mov     ecx, 5
        idiv    ecx
        cinvoke printf, format_out, eax
        cinvoke system, 'pause'
        ret

Komentarz:

  • Do wywołania funkcji z języka C używamy składni cinvoke NAZWA, ARGUMENT1, ARGUMENT2, ...
  • Napisy to zmienne typu db, koniecznie zakończone zerem. Znak nowej linii to 13, 10 (zobacz przykład powyżej, zmienna format_out)
  • Liczby całkowite to zmienne typu dd
  • Zapis mov eax, [n] oznacza "zawartość zmiennej n wpisz do rejestru eax"

Debugowanie

Używany debuger na zajęciach: http://www.ollydbg.de/odbg110.zip

Debugowanie to proces kontrolowanego uruchomienia aplikacji. Oznacza to tyle, że można wykonywać program instrukcja po instrukcji obserwując dokładnie jak zmieniają się wartości w rejestrach i flagi stanu.

Po włączeniu OllyDbg wczytujemy nasz skompilowany program przez opcję File->Open

Legenda:

  • zielony - adresy załadowanego do pamięci programu
  • fioletowy - instrukcje w czytelnej dla maszyny postaci
  • żółty - instrukcje w czytelnej dla człowieka postaci :)
  • czerwony - wartości rejestrów (pionowa kolumna poniżej to wszystkie flagi tworzące razem rejestr stanu)

Aby przejść jedną instrukcję do przodu, naciskamy F8. Po każdym kroku zmienione rejestry oraz flagi zapalają się na czerwono.

Ćwiczenia na laboratorium

  • Proszę wykorzystać poniższy, minimalny kod do tworzenia rozwiązań zadań na ćwiczeniach:
    format pe console
    entry start

    include 'win32ax.inc'

    section '.code' code readable writeable executable
    start:
            mov     bl, 1
            mov     cl, 2
            mov     dl, 3
            ; kontynuuj tutaj!
            ret

Tematy zaawansowane

  • Jak widać na powyższych przykładach, kod to po prostu bajty, pewne liczby... Można je zmieniać w samym programie, jest to idea SMC (self-modifying code) używana np. przez wirusy i z tego też powodu często zabroniona przez systemy operacyjne. Przykład:
    start:
            mov     eax, 10
            sub     eax, 1
            mov     dword [start + 1], eax
            jnz     start
    Animacja:
  • Dlaczego to działa?
    • Do eax trafia wartość 10
    • Następnie w eax znajduje się wartość o 1 mniejsza
    • Zapisujemy to pod adres [start + 1], czyli co to oznacza? Spójrz na sam początek tej strony gdzie znajduje się przykład jak wygląda instrukcja mov eax, LICZBA; są to bajty b8 ?? ?? ?? ?? gdzie znaki zapytania oznaczają liczbę. A zatem zapis pod adres [start + 1] nadpisuje te cztery bajty!
    • Instrukcja sub ustawia flagę ZF jeśli po jej wykonaniu wynikiem będzie zero. Natomiast instrukcja mov nie zmienia żadnych flag. Dlatego sekwencja: sub, mov, jnz start to prosta pętla, która będzie wracać do punktu startu dopóki w eax nie znajdzie się zero.
  • Procesory Intel/AMD mają bardzo bogaty zbiór instrukcji. Niektóre są bardzo specyficzne, niektóre są nawet nieudokumentowane ;). Tę samą rzecz można często wykonać na kilka sposobów. Przykład, jak dodać 1 od rejestru eax:
    ; dodaj 1
    83 c0 01                add    eax,0x1

    ; inkrementuj
    40                      inc    eax

    ; odejmij -1
    83 e8 ff                sub    eax,0xffffffff

    ; wpisz 1 do rejestru, odejmij od niego
    29 d2                   sub    edx,edx
    42                      inc    edx
    29 d0                   sub    eax,edx
    Zupełnie różne bajty, a wynik ten sam.
  • Idąc dalej tym tropem, okazuje się, że nawet taka sama instrukcja może mieć kilka wersji maszynowych (różne bajty kodują to samo), przykład:
    6a 00                   push   0x0
    68 00 00 00 00          push   0x0
    W tym przypadku bajt 6a oznacza umieść na stosie wartość o wielkości 1-bajta, natomiast 68 to umieść na stosie wartość o wielkości 4-bajtów
  • Z innych ciekawych instrukcji:
    • jecxz ETYKIETA - skocz do etykiety jeśli ecx jest równe 0 (nie potrzeba wykonywać insturkcji cmp)
    • cmov?? CEL, WARTOŚĆ - działa jak instrukcja mov, ale działa warunkowo; pod znaki zapytania należy wstawić końcówki jak w insturkcjach skoku np. cmovz albo cmovle
  • Jak w języku C zamienić dwie wartości miejscami? Trzeba się posłużyć zmienną pomocniczą:
    int m = 2, n = 5;
    int tmp = m;
    m = n;
    n = tmp;
    A w asemblerze wystarczy jedna instrukcja (i oszczędza się pamięć, bo nie potrzeba dodatkowej zmiennej):
    xchg    eax, ecx