Linux: kod samomodyfikujący się

Autor: Wojciech Muła
Dodany:28.07.2002
Aktualizacja:13.07.2008

Asembler

Kernel Linuksa x86 działa w trybie chronionym, do ochrony pamięci procesów używane są nie tylko atrybuty segmentów, ale również atrybuty stron. Proces otrzymuje od systemu dwa selektory z tablicy GDT: 4 — dla kodu, 5 — dla danych, nie ma odrębnego deskryptora stosu. Oba mają bazę 0x00000000 i limit 4GB (choć formalnie procesy są umieszczane w pierwszych 3GB pamięci) — tak więc oba segmenty obejmują ten sam obszar pamięci. Ochrona danych jest więc realizowana przede wszystkim poprzez atrybuty stron, i tak:

Można to łatwo sprawdzić za pomocą prostego programu:

segment .text

global _start
_start:
        xor eax, eax
        mov  ax, ds
        lsl ebx, eax    ; ebx == 0xffffffff

        verw ax         ; sprawdzamy czy _segment_ może być zapisywany
        jnz  .error     ; oczywiście może

        mov [ds:0], eax ; i tu dostajemy błąd ochrony pamięci
 .error:
        mov eax, 1
        mov ebx, 0
        int 0x80

Wracając do meritum: aby móc modyfikować kod należy zmienić atrybut stron pamięci. Umożliwia to funkcja systemowa sys_mprotect (numer 125), jej parametry są następujące:

rejestr typ danych opis
ebx void* adres początku obszaru (wewnętrznie jest wyrównywany do granicy 4kB strony)
ecx unsgined long długość tego obszaru (również odpowiednio wyrównywana)
edx int

prawa dostępu zdefiniowane w asm/mman.h:

  • PROT_READ (strona może być czytana)
  • PROT_WRITE (strona może być zapisywana)
  • PROT_EXEC (kod umieszczony na stronie może być wykonywany)

Funkcja zwraca następujące wartości błędów: EINVAL, EFAULT, EACCESS. Oczywiście można zmieniać atrybuty stron które należą do procesu, pozostałe nie są dostępne.

Oto przykładowy program, który modyfikuje argument rozkazu mov eax, imm32.

segment .text

global _start
_start:
        mov eax, 125
        mov ebx, smc_address
        mov ecx, 4096
        mov edx, PROT_READ | PROT_WRITE | PROT_EXEC
        int 0x80

        or  eax, eax
        jnz .error      ; gdyby coś poszło nie tak, choć nie powinno

        mov [smc_address], 0xaabbccdd

        mov eax, 0x0000000000
smc_address equ $-4

        ; w tym miejscu eax==0xaabbccdd

        ; należy pamiętać o ponownym zablokowaniu zapisu,
        ; tak na wszelki wypadek

 .error:
        mov eax, 1
        mov ebx, 0
        int 0x80

Język C i funkcje biblioteczne [13.07.2008] new

Większość opisanych tutaj rzeczy można wykonać bezpośrednio z poziomu języka C. W pliku sys/mman.h znajduje się funkcja mprotect, która przyjmuje adres (wyrównany do granicy strony), rozmiar obszaru oraz prawa dostępu. Oczywiście poza tym, należy wykonać działania niskopoziomowe.

Prosty program x86linux_smc.c wykonuje dokładnie to samo działanie, co przedstawiony wyżej program asemblerowy:

uint32_t function() {
        uint32_t result;

        __asm__ volatile (
        "smc_address:                   \n"     // global label
        "       mov $0xbadcaffe, %%eax  \n"     // instruction we will patch
        : "=a" (result)
        );

        return result;
}


int main(int argc, char* argv[]) {
        uint32_t patch_val;

        if (argc > 1)
                patch_val = strtol(argv[1], NULL, 0);
        else
                patch_val = 0x11223344;

        printf("Before patch function() returned 0x%08x\n", function());

        // obtain global label address
        uint32_t address;
        __asm__ volatile ("mov $smc_address, %%eax" : "=a" (address));


        // change page rights
        // address must be aligned at page boundary
        if (mprotect((void*)(address & 0xfffff000), 4096, PROT_EXEC | PROT_WRITE | PROT_READ)) {
                printf("mprotect: %s\n", strerror(errno));
                return 1;
        }

        printf("Argument of mov instruction: 0x%08x\n", patch_val);

        // change argument of mov instruction (opcode: b8 xx yy zz ww)
        *(uint32_t*)(address+1) = patch_val;


        printf("After patch function() returned 0x%08x\n", function());
        return 0;
}

Konwencja wywołań funkcji systemowych w asemblerze

W Linuksie funkcje systemowe są dostępne przez przerwanie 0x80, argumenty są ładowane w następującej kolejności: eax, ebx, ecx, edx, edi, esi, ebp.

Numer funkcji systemowej jest przekazywany w eax, natomiast pozostałe argumenty zależą już od rodzaju funkcji, i oczywiście nie wszystkie muszą być wykorzystane. Status operacji zwracany jest w rejestrze eax, gdy operacja wykona się bezbłędnie jego wartość jest równa 0, w przeciwnym razie jest to ujemna liczba — zanegowana wartość z pliku asm/errno.h. Pozostałe rejestry nie są zmieniane.

Po pełen opis funkcji systemowych odsyłam do stron, których linki są umieszczone na http://www.linuxassembly.org

Poniżej użyteczne makro (NASM):

; %1 - eax
; %2 - ebx
; %3 - ecx
; %4 - edx
; %5 - esi
; %6 - edi
; %7 - ebp
%macro syscall 1-7

__syscall_move eax, %1
%if %0 >= 2
        __syscall_move ebx, %2
%endif
%if %0 >= 3
        __syscall_move ecx, %3
%endif
%if %0 >= 4
        __syscall_move edx, %4
%endif
%if %0 >= 5
        __syscall_move esi, %5
%endif
%if %0 >= 6
        __syscall_move edi, %6
%endif
%if %0 >= 7
        __syscall_move ebp, %7
%endif

int 0x80
%endmacro

%macro __syscall_move 2
%ifnidni %1,%2
        mov %1, %2
%endif
%endmacro

Przykład użycia (hello world):

segment .data

hello db "hello world!", 0xa

hellolen equ $-hello
stdout   equ 1

segment .text

global _start
_start:
        syscall 4, stdout, hello, hellolen      ; wypisz na stdout napis
        syscall 1, 0                            ; zakończenie programu z kodem błędu 0

Makro zapobiega generowaniu zbędnych przesłań w przypadku, gdy któreś rejestry mają żądane wartości. Wystarczy w miejsce znanego parametru podać nazwę rejestru odpowiadającemu temu parametrowi, np.:

syscall 4, ebx, ecx, 5

wygeneruje kod:

mov eax, 4
mov edx, 5
int 0x80