Autor: | Wojciech Muła |
---|---|
Dodany: | 28.07.2002 |
Aktualizacja: | 13.07.2008 |
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:
|
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
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; }
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