Rozdział XXIX - Funkcje uruchomieniowe Część 2


Na tej lekcji kontynuujemy temat funkcji uruchomieniowych biblioteki Win32. Nauczymy się sposobów modyfikowania testowanego procesu.

Załaduj {ten przykład}

 

Teoria

Z poprzedniej lekcji wiemy, jak załadować testowany proces i jak obsługiwać występujące w nim zdarzenie uruchomieniowe. Aby być użytecznym, nasz program musi umieć modyfikować testowany proces. W bibliotece Win32 istnieje kilka funkcji specjalnie przeznaczonych do tego celu.

ReadProcessMemory PROTO hProcess:            DWORD,
                        lpBaseAddress:       DWORD,
                        lpBuffer:            DWORD,
                        nSize:               DWORD,
                        lpNumberOfBytesRead: DWORD

Następne dwie funkcje API wymagają nieco wyjaśnień. W wielozadaniowym systemie operacyjnym, takim jak system Windows, może istnieć kilka programów pracujących jednocześnie. System Windows daje każdemu wątkowi pewien przedział czasu. Gdy przedział ten kończy się, system Windows zamraża bieżący wątek i przełącza wykonanie na następny, który posiada najwyższy priorytet. Tuż przed przełączeniem zachowywany jest stan rejestrów bieżącego wątku, aby gdy nadejdzie jego czas, mógł być z powrotem reaktywowany. System Windows potrafi odtworzyć ostatnie "środowisko" wątku. Zapamiętane wartości rejestrów są wspólnie nazywane kontekstem.

Wróćmy do tematu. Gdy pojawia się zdarzenie uruchomieniowe, system Windows zawiesza testowany proces. Kontekst procesu zostaje zapamiętany. Ponieważ testowany proces jest zawieszony, możemy mieć pewność, iż wartości kontekstu pozostaną niezmienione. Wartości te możemy pobrać przy pomocy funkcji GetThreadContext, a zmienić za pomocą SetThreadContext.

Te dwie funkcje API są bardzo potężne. Przy ich pomocy masz pod palcami moc w stylu VxD nad testowanym procesem: możesz zmienić zapamiętane wartości rejestrów, a tuż przed wznowieniem wątku wartości w kontekście zostaną z powrotem umieszczone w rejestrach. Każda zmiana w kontekście odzwierciedli się w testowanym procesie. Pomyśl o tym: możesz nawet zmienić zawartość rejestru licznika rozkazów eip i przekierować wykonanie procesu do dowolnego adresu! W normalnych warunkach nie mógłbyś tego dokonać.

GetThreadContext PROTO hThread:DWORD, lpContext:DWORD

Funkcja SetThreadContext posiada identyczne parametry. Zobaczmy, jak wygląda struktura CONTEXT:

MAXIMUM_SUPPORTED_EXTENSION EQU 512
SIZE_OF_80387_REGISTERS     EQU 80

FLOATING_SAVE_AREA STRUCT
    ControlWord   DWORD ?
    StatusWord    DWORD ?
    TagWord       DWORD ?
    ErrorOffset   DWORD ?
    ErrorSelector DWORD ?
    DataOffset    DWORD ?
    DataSelector  DWORD ?
    RegisterArea  BYTE SIZE_OF_80387_REGISTERS DUP(?)
    Cr0NpxState   DWORD ?
FLOATING_SAVE_AREA ENDS

CONTEXT STRUCT 

    ContextFlags DWORD ? 

; Ta sekcja jest zwracana, gdy ContextFlags
; zawiera wartość CONTEXT_DEBUG_REGISTERS 

    iDr0 DWORD ? 
    iDr1 DWORD ? 
    iDr2 DWORD ? 
    iDr3 DWORD ? 
    iDr6 DWORD ? 
    iDr7 DWORD ?

; Ta sekcja jest zwracana, gdy ContextFlags
; zawiera wartość CONTEXT_FLOATING_POINT 

    FloatSave FLOATING_SAVE_AREA <>

; Ta sekcja jest zwracana, gdy ContextFlags
; zawiera wartość CONTEXT_SEGMENTS 

    regGs DWORD ? 
    regFs DWORD ? 
    regEs DWORD ? 
    regDs DWORD ?

; Ta sekcja jest zwracana, gdy ContextFlags
; zawiera wartość CONTEXT_INTEGER

    regEdi DWORD ? 
    regEsi DWORD ? 
    regEbx DWORD ? 
    regEdx DWORD ? 
    regEcx DWORD ? 
    regEax DWORD ? 

; Ta sekcja jest zwracana, gdy ContextFlags
; zawiera wartość CONTEXT_CONTROL 

    regEbp  DWORD ? 
    regEip  DWORD ? 
    regCs   DWORD ? 
    regFlag DWORD ? 
    regEsp  DWORD ? 
    regSs   DWORD ? 

; Ta sekcja jest zwracana, gdy ContextFlags
; zawiera wartość CONTEXT_EXTENDED_REGISTERS

    ExtendedRegisters DWORD MAXIMUM_SUPPORTED_EXTENSION DUP(?)

CONTEXT ENDS

Jak możesz zaobserwować, pola tych struktur są kopiami prawdziwych rejestrów mikroprocesora. Zanim będziesz mógł skorzystać z tej struktury, musisz określić w polu ContextFlags, którą grupę rejestrów chcesz odczytać lub zapisać. Na przykład, jeśli chcesz odczytać / zapisać wszystkie rejestry, to musisz określić w ContextFlags wartość CONTEXT_FULL. Jeśli jedynie chcesz zapisać / odczytać pola regEbp, regEip, regCs, regFlag, regEsp i regSs, to musisz określić w ContextFlags wartość CONTEXT_CONTROL.

Używając strukturę CONTEXT powinieneś pamiętać o jednej rzeczy: musi ona być wyrównana do granicy podwójnego słowa, w przeciwnym wypadku otrzymasz dziwne wyniki w systemach NT/2000/XP. Powyżej deklaracji tej struktury należy zatem umieścić dyrektywę asemblera ALIGN DWORD (wyrównanie do granicy podwójnego słowa), np. w sposób następujący:

ALIGN DWORD

MyContext CONTEXT <>

 

Przykład

Pierwszy przykład demonstruje zastosowanie funkcji DebugActiveProcess. Najpierw potrzebujesz uruchomić program docelowy o nazwie win.exe, który wchodzi w pętlę nieskończoną tuz przed pokazaniem swojego okna na ekranie. Następnie uruchamiasz ten przykład, który przyłączy się do win.exe i zmodyfikuje jego kod tak, aby program ten wyszedł z pętli nieskończonej i pokazał swoje własne okno.

Plik DEBUG2.ASM

 

.386

.MODEL FLAT, STDCALL

OPTION CASEMAP:NONE

INCLUDE    \masm32\include\windows.inc
INCLUDE    \masm32\include\kernel32.inc
INCLUDE    \masm32\include\comdlg32.inc
INCLUDE    \masm32\include\user32.inc
INCLUDELIB \masm32\lib\kernel32.lib
INCLUDELIB \masm32\lib\comdlg32.lib
INCLUDELIB \masm32\lib\user32.lib

.DATA

AppName       DB "Przykład uruchamiania nr 2", 0
ClassName     DB "SimpleWinClass", 0
SearchFail    DB "Nie można odnaleźć docelowego procesu", 0
TargetPatched DB "Program naprawiony!", 0
buffer        DW 9090h

.DATA?

DBEvent   DEBUG_EVENT <>
ProcessId DD ?
ThreadId  DD ?

ALIGN DWORD

context CONTEXT <>

.CODE

start:

    INVOKE FindWindow, ADDR ClassName, NULL
    .IF eax!=NULL
        INVOKE GetWindowThreadProcessId, eax, ADDR ProcessId
        mov       ThreadId, eax
        INVOKE DebugActiveProcess, ProcessId
        .WHILE TRUE
            INVOKE WaitForDebugEvent, ADDR DBEvent, INFINITE
            .BREAK .IF DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
            .IF DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
                mov             context.ContextFlags, CONTEXT_CONTROL
                INVOKE GetThreadContext, DBEvent.u.CreateProcessInfo.hThread, ADDR context
                INVOKE WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess,
                                           context.regEip, ADDR buffer, 2, NULL
                INVOKE MessageBox, 0, ADDR TargetPatched, ADDR AppName,
                                   MB_OK + MB_ICONINFORMATION
            .ELSEIF DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
                .IF DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
                    INVOKE ContinueDebugEvent, DBEvent.dwProcessId,
                                               DBEvent.dwThreadId,
                                               DBG_CONTINUE   
                    .CONTINUE
                .ENDIF
            .ENDIF
            INVOKE ContinueDebugEvent, DBEvent.dwProcessId,
                                       DBEvent.dwThreadId,
                                       DBG_EXCEPTION_NOT_HANDLED
        .ENDW
    .ELSE
        INVOKE MessageBox, 0, ADDR SearchFail, ADDR AppName, MB_OK + MB_ICONERROR
    .ENDIF
    INVOKE ExitProcess, 0

END start

 

To będzie nasz testowany proces. W rzeczywistości jest to lekko zmodyfikowany program z lekcji o prostym oknie, w którym została wstawiona pętla nieskończona tuż przed wejściem do pętli wiadomości.

Plik WIN.ASM

 

.386

.MODEL FLAT, STDCALL

OPTION CASEMAP:NONE

INCLUDE    \masm32\include\windows.inc
INCLUDE    \masm32\include\user32.inc
INCLUDE    \masm32\include\kernel32.inc
INCLUDELIB \masm32\lib\user32.lib
INCLUDELIB \masm32\lib\kernel32.lib

WinMain PROTO :DWORD, :DWORD, :DWORD, :DWORD

.DATA

ClassName DB "SimpleWinClass", 0
AppName   DB "Okno z pętlą nieskończoną", 0

.DATA?

hInstance   HINSTANCE ?
CommandLine LPSTR     ?

.CODE

start:

    INVOKE GetModuleHandle, NULL
    mov    hInstance, eax
    INVOKE GetCommandLine
    mov    CommandLine, eax
    INVOKE WinMain, hInstance, NULL, CommandLine, SW_SHOWDEFAULT
    INVOKE ExitProcess, eax

WinMain PROC hInst:     HINSTANCE,
             hPrevInst: HINSTANCE,
             CmdLine:   LPSTR,
             CmdShow:   DWORD

LOCAL wc   : WNDCLASSEX
LOCAL msg  : MSG
LOCAL hwnd : HWND

    mov    wc.cbSize, SIZEOF WNDCLASSEX
    mov    wc.style, CS_HREDRAW OR CS_VREDRAW
    mov    wc.lpfnWndProc, OFFSET WndProc
    mov    wc.cbClsExtra, NULL
    mov    wc.cbWndExtra, NULL
    push   hInstance
    pop    wc.hInstance
    mov    wc.hbrBackground, COLOR_WINDOW+1
    mov    wc.lpszMenuName, NULL
    mov    wc.lpszClassName, OFFSET ClassName
    INVOKE LoadIcon, NULL, IDI_APPLICATION
    mov    wc.hIcon, eax
    mov    wc.hIconSm, eax
    INVOKE LoadCursor, NULL, IDC_ARROW
    mov    wc.hCursor, eax
    INVOKE RegisterClassEx, ADDR wc
    INVOKE CreateWindowEx, NULL, ADDR ClassName, ADDR AppName,
           WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
           CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL,
           hInst, NULL
    mov    hwnd, eax

; Ten rozkaz powoduje powstanie pętli nieskończonej

    jmp $

; Tutaj rozpoczyna się dalsza część programu

    INVOKE ShowWindow, hwnd, SW_SHOWNORMAL
    INVOKE UpdateWindow, hwnd

    .WHILE TRUE
        INVOKE GetMessage, ADDR msg, NULL, 0, 0
        .BREAK .IF (!eax)
        INVOKE TranslateMessage, ADDR msg
        INVOKE DispatchMessage, ADDR msg
    .ENDW

    mov eax, msg.wParam
    ret

WinMain ENDP

WndProc PROC hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

    .IF uMsg==WM_DESTROY
        INVOKE PostQuitMessage, NULL
    .ELSE
        INVOKE DefWindowProc, hWnd, uMsg, wParam, lParam       
        ret
    .ENDIF

    xor eax, eax
    ret

WndProc ENDP

END start

Analiza

    INVOKE FindWindow, ADDR ClassName, NULL

 

Nasz program wymaga dołączenia go do testowanego procesu przy pomocy funkcji DebugActiveProcess, która potrzebuje numeru ID procesu. Numer identyfikacyjny ID procesu możemy otrzymać wywołując funkcję GetWindowThreadProcessId, która z kolei potrzebuje uchwytu okna jako parametru. Zatem najpierw należy uzyskać uchwyt okna.

Przy pomocy FindWindow możemy określić nazwę klasy okna, które jest nam potrzebne. Zwrócony zostaje uchwyt do okna utworzonego przez tę klasę. Jeśli otrzymamy wartość NULL, to żadne okno nie powstało na podstawie tej klasy.

 

    .IF eax!=NULL
        INVOKE GetWindowThreadProcessId, eax, ADDR ProcessId
        mov    ThreadId, eax
        INVOKE DebugActiveProcess, ProcessId

 

Po otrzymaniu numeru identyfikacyjnego ID procesu możemy wywołać funkcję DebugActiveProcess. Następnie wchodzimy do pętli oczekującej na zdarzenia uruchomieniowe.

 

    .IF DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
        mov    context.ContextFlags, CONTEXT_CONTROL
        INVOKE GetThreadContext, DBEvent.u.CreateProcessInfo.hThread,
                                 ADDR context

 

Gdy otrzymamy powiadomienie CREATE_PROCESS_DEBUG_INFO, to oznacza ono zawieszenie działania procesu testowanego, co umożliwi nam przeprowadzenie w nim operacji chirurgicznej. W tym przykładzie zastąpimy instrukcję skoku (jmp $ - kod 0EBh 0FEh) tworzącego pętlę nieskończoną dwiema instrukcjami NOP (kod 90h).

Najpierw musimy otrzymać adres tej instrukcji w obrębie procesu. Ponieważ testowany proces do czasu przyłączenia naszego programu już znajduje się w pętli nieskończonej, rejestr eip będzie zawsze wskazywał na tę instrukcję. Zatem wystarczy pobrać zawartość rejestru eip. Do osiągnięcia tego celu wykorzystujemy funkcję GetThreadContext. Ustawiamy pole ContextFlags na wartość CONTEXT_CONTROL, aby przekazać funkcji GetThreadContext, iż chcemy wypełnić strukturę CONTEXT rejestrami sterującymi.

 

    INVOKE WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess,
                               context.regEip, ADDR buffer, 2, NULL

 

Teraz gdy posiadamy zawartość rejestru eip, możemy wywołać WriteProcessMemory w celu zastąpienia instrukcji "jmp $" dwiema instrukcjami NOP, co pomoże naszemu procesowi testowanemu wyjść trwale z pętli nieskończonej. Po tej operacji wyświetlamy użytkownikowi krótką wiadomość, a następnie wywołujemy ContinueDebugEvent w celu wznowienia działania procesu testowanego. Ponieważ instrukcja "jmp $" została zastąpiona instrukcjami NOP, proces ten zacznie wykonywać dalsze instrukcje, co spowoduje wyświetlenie okna i wejście do pętli wiadomości. Jako dowód na ekranie pojawi się jego okno.

Drugi przykład wykorzystuje nieco inne podejście do przerwania pętli nieskończonej w procesie testowanym.

Plik DEBUG3.ASM

 

.386

.MODEL FLAT, STDCALL

OPTION CASEMAP:NONE

INCLUDE    \masm32\include\windows.inc
INCLUDE    \masm32\include\kernel32.inc
INCLUDE    \masm32\include\comdlg32.inc
INCLUDE    \masm32\include\user32.inc
INCLUDELIB \masm32\lib\kernel32.lib
INCLUDELIB \masm32\lib\comdlg32.lib
INCLUDELIB \masm32\lib\user32.lib

.DATA

AppName      DB "Przykład uruchamiania nr 3", 0
ClassName    DB "SimpleWinClass", 0
SearchFail   DB "Nie można odnaleźć docelowego procesu", 0
LoopSkipped  DB "Przeskoczone dwa bajty!", 0

.DATA?

DBEvent   DEBUG_EVENT <>
ProcessId DD ?
ThreadId  DD ?

ALIGN DWORD

context CONTEXT <>

.CODE

start:

    INVOKE FindWindow, ADDR ClassName, NULL
    .IF eax!=NULL
        INVOKE GetWindowThreadProcessId, eax, ADDR ProcessId
        mov    ThreadId, eax
        INVOKE DebugActiveProcess, ProcessId

        .WHILE TRUE
            INVOKE WaitForDebugEvent, ADDR DBEvent, INFINITE
            .BREAK .IF DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
            .IF DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT                 
                mov    context.ContextFlags, CONTEXT_CONTROL                 
                INVOKE GetThreadContext, DBEvent.u.CreateProcessInfo.hThread, ADDR context 
                add    context.regEip, 2 
                INVOKE SetThreadContext, DBEvent.u.CreateProcessInfo.hThread, ADDR context 
                INVOKE MessageBox, 0, ADDR LoopSkipped, ADDR AppName,
                                   MB_OK + MB_ICONINFORMATION 
            .ELSEIF DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
                .IF DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
                    INVOKE ContinueDebugEvent, DBEvent.dwProcessId,
                                               DBEvent.dwThreadId,
                                               DBG_CONTINUE   
                    .CONTINUE
                .ENDIF
            .ENDIF
            INVOKE ContinueDebugEvent, DBEvent.dwProcessId,
                                       DBEvent.dwThreadId,
                                       DBG_EXCEPTION_NOT_HANDLED
        .ENDW
    .ELSE
        INVOKE MessageBox, 0, ADDR SearchFail, ADDR AppName, MB_OK + MB_ICONERROR
    .ENDIF
    INVOKE ExitProcess, 0

END start

 

    .IF DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT 
        mov    context.ContextFlags, CONTEXT_CONTROL 
        INVOKE GetThreadContext, DBEvent.u.CreateProcessInfo.hThread,
                                 ADDR context 
        add    context.regEip, 2 
        INVOKE SetThreadContext, DBEvent.u.CreateProcessInfo.hThread,
                                 ADDR context 

 

Program ten również wywołuje funkcję GetThreadContext do pobrania bieżącej zawartości rejestru eip, lecz zamiast zastępowania instrukcji jmp $, zwiększa on wartość pola regEip o 2, aby "przeskoczyć ponad" tą instrukcją. W wyniku tej modyfikacji po wznowieniu procesu testowanego rozpocznie on wykonywanie poleceń za instrukcją skoku.

Teraz możesz zobaczyć potęgę funkcji Get/SetThreadContext. Można również modyfikować inne rejestry w strukturze, co będzie od razu odzwierciedlone w testowanym procesie po jego wznowieniu. Wolno ci nawet wstawiać instrukcje int 3h, aby wywoływać zatrzymania kontrolne procesu testowanego.

 

Autorem kursu jest Iczelion. Kurs programowania Windows znalazł się na serwerze I LO w Tarnowie za pisemną zgodą autora.
Tłumaczenie z języka angielskiego, opracowanie HTML i konwersję przykładów programów wykonał mgr Jerzy Wałaszek.



List do administratora Serwisu Edukacyjnego Nauczycieli I LO

Twój email: (jeśli chcesz otrzymać odpowiedź)
Temat:
Uwaga: ← tutaj wpisz wyraz  ilo , inaczej list zostanie zignorowany

Poniżej wpisz swoje uwagi lub pytania dotyczące tego rozdziału (max. 2048 znaków).

Liczba znaków do wykorzystania: 2048

 

W związku z dużą liczbą listów do naszego serwisu edukacyjnego nie będziemy udzielać odpowiedzi na prośby rozwiązywania zadań, pisania programów zaliczeniowych, przesyłania materiałów czy też tłumaczenia zagadnień szeroko opisywanych w podręcznikach.



   I Liceum Ogólnokształcące   
im. Kazimierza Brodzińskiego
w Tarnowie

©2017 mgr Jerzy Wałaszek

Dokument ten rozpowszechniany jest zgodnie z zasadami licencji
GNU Free Documentation License.