Kod som körs under Linux kan delas in i två halvor, kernelspace och userspace. I kernel space körs Linuxkärnan med sådant som drivrutiner och minneshantering. Resten ligger i userspace. Med kommandot strace kan alla hopp från user -> kernelspace visas. Testa att kompilera följande Hello World program och kör strace på det.
#include <stdio.h> int main(void) { puts("Hello, World!"); return 0; }
Kompilera med:
gcc -o hello hello.c
För att se alla hopp till Linuxkärnan som det lilla programmet hello gör skriv:
strace ./hello
execve("./hello", ["./hello"], [/* 35 vars */]) = 0
...
write(1, "Hello, World!\n", 14) = 14
exit_group(0)
Först anropas execve som startar en ny process med programmet ./hello. Sen har jag utelämnat massa rader som har med dynamisk laddning av glibc att göra. Själva strängen “Hello, World!\n” skrivs som synes ut med systemanropet write. Libc-funktionen puts() har alltså lagt till en newline på slutet av strängen och sedan bett Linuxkärnan skriva ut den via systemanropet write(). Första argumentet till write är en etta, vilket betyder stdout. Andra argumentet är själva strängen och sista argumentet (14) är längden på strängen.
För att göra programmet lite effektivare skulle alltså write() kunna anropas direkt:
int main(void) { write(1, "Hello, World!\n", 14); return 0; }
Tyvärr blir ovanstående kod endast marginellt snabbare än originalet eftersom även write() hoppar in i libc och gör en massa saker innan till sist systemanropet write() anropas. Se efter själv med objdump:
objdump -d ./hello ... 080482c8 <write@plt>: 80482c8: ff 25 7c 95 04 08 jmp *0x804957c 80482ce: 68 08 00 00 00 push $0x8 80482d3: e9 d0 ff ff ff jmp 80482a8 <_init+0x30> ... 08048374 <main>: 8048374: 8d 4c 24 04 lea 0x4(%esp),%ecx 8048378: 83 e4 f0 and $0xfffffff0,%esp 804837b: ff 71 fc pushl -0x4(%ecx) 804837e: 55 push %ebp 804837f: 89 e5 mov %esp,%ebp 8048381: 51 push %ecx 8048382: 83 ec 14 sub $0x14,%esp 8048385: c7 44 24 08 0e 00 00 movl $0xe,0x8(%esp) ; Argument 3 (längden) 804838c: 00 804838d: c7 44 24 04 70 84 04 movl $0x8048470,0x4(%esp) ; Argument 2 (adressen) 8048394: 08 8048395: c7 04 24 01 00 00 00 movl $0x1,(%esp) ; Argument 1 (fildeskriptorn) 804839c: e8 27 ff ff ff call 80482c8 ; Hoppa till write@plt WTF?? 80483a1: b8 00 00 00 00 mov $0x0,%eax 80483a6: 83 c4 14 add $0x14,%esp 80483a9: 59 pop %ecx 80483aa: 5d pop %ebp 80483ab: 8d 61 fc lea -0x4(%ecx),%esp 80483ae: c3 ret 80483af: 90 nop ...
Det börjar bra, argumenten läggs på stacken och sedan skulle man förvänta sig ett syscall, men nej. Istället anropas write@plt, som i sin tur anropar…osv (den intresserade kan läsa om PLT). Om man följer det ännu längre kommer man till sist till en instruktion int $0×80 som gör själva hoppet in i kernel space. Så var det förr i tiden i alla fall. Det går fortfarande att göra int $0×80 för att hoppa, men nyare processorer har ett snabbare sätt att göra systemanrop, sysenter.
För ett par år sedan kom Linus Torvalds och hans legion av hackers på att int $0×80 tog flera gånger längre tid att exekvera på nyare processorer än sysenter. Men på gamla processorer finns inte sysenter. Det löste han genom att låta Linuxkärnan detektera om sysenter finns och är snabbare än int $0×80 när kärnan bootar. Sen lägger kärnan in något mycket märkligt som kallas VDSO (Virtual Dynamic Shared Object). Från userspace ser det ut som ett vanligt delat bibliotek (shared object) men det är inte vanligt på något sätt, det ligger nämligen i Linuxkärnan. Pröva ldd:
ldd hello linux-gate.so.1 => (0xb7f3d000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7dd9000) /lib/ld-linux.so.2 (0xb7f3e000)
I linux-gate ligger funktionen som gör hoppet till kernelspace. För att krångla till det ännu mer slumpas adressen till linux-gate varje gång programmet startar (försvårar för elak kod att gissa adresser). För att titta på linux-gate kan man skriva ett litet program:
#include <stdio.h> #define ADDRESS 0xb7fe3000 int main(void) { unsigned char *p = (unsigned char *)ADDRESS; FILE *fp = fopen("linux-gate.vdso", "w"); if (!fp) { return 1; } /* Dump page to file */ fwrite(p, 4096, 1, fp); fclose(fp); return 0; }
I ovanstående program har jag hårdkodat adressen. Det kunde jag göra genom att tillfälligt stänga av randomiseringen av linux-gate, dumpa sidan och sedan slå på randomiseringen igen. Ett annat sätt är att lägga in kod som tittar i filen /proc/self/maps och tar adressen från början av raden som slutar med [vdso], men det är överkurs. Randomiseringen slås av med ‘echo 0 > /proc/sys/kernel/randomize_va_space‘. Kommandot måste köras som root. Titta på den dumpade filen med:
objdump -d linux-gate.vdso ffffe400 <__kernel_vsyscall>: ffffe400: 51 push %ecx ffffe401: 52 push %edx ffffe402: 55 push %ebp ffffe403: 89 e5 mov %esp,%ebp ffffe405: 0f 34 sysenter ffffe407: 90 nop ...
Där har vi funktionen som gör hoppet, sysenter i det här fallet. Hade jag bootat på en äldre processor skulle det stått int $0×80 istället för sysenter. I bägge fallen fungerar det att lägga argumenten i registren eax-edx och sedan hoppa till __kernel_vsyscall och det är just detta som glibc gör i sina innersta delar.
Nästa fråga är då själva adressen, den varierar ju varje gång man startar programmet så det går inte att hoppa till en fast adress (förbaskade randomisering!). Det finns två sätt att lösa detta. Enklast och minst säkert är att hoppa med ‘call *%gs:0×10‘. När programmet startar upp läggs nämligen adressen för TCB (Thread Control Block) i registret gs och adressen till __kernel_syscall återfinns oftast på offset 0×10. Structen heter tcbhead_t och är definierad internt i glibc. Det är offseten från starten på structen till fältet uintptr_t sysinfo man är ute efter. Den kan fås med offsetof() ifall man har tillgång till tls.h vid kompileringen, vilket man oftast inte har eftersom det är en intern fil för glibc.
En säkrare metod är att titta på ELF-headern och leta reda på AT_SYSINFO som innehåller samma adress.
#include <elf.h> unsigned int at_sysinfo; int main(int argc, char* argv[], char* envp[]) { Elf32_auxv_t *auxv; /* Find address of __kernel_vsyscall */ while(*envp++ != NULL); /* *envp = NULL marks end of envp */ /* auxv->a_type = AT_NULL marks the end of auxv */ for (auxv = (Elf32_auxv_t *)envp; auxv->a_type != AT_NULL; auxv++) { if( auxv->a_type == AT_SYSINFO) { printf("AT_SYSINFO is: 0x%x\n", auxv->a_un.a_val); at_sysinfo = auxv->a_un.a_val; } } ... }
Ovanstående kod letar reda på AT_SYSINFO och kopierar adressen till den globala variabeln atsysinfo. Det är just denna adress man ska hoppa till för att göra ett syscall. Visserligen är det lite krångligt, men efter att atsysinfo initierats kan man sedan använda den adressen för att göra blixtsnabba systemanrop, helt utan att blanda in libc. Nackdelen är att det blir ungefär lika portabelt som en flygel. Det fungerar endast med Linux 2.6 och uppåt, endast med 32-bitars x86 processorer och garanterat inte med något annat operativsystem.

Slutligen lite källkod med exempel på hur man kan göra egna icke-portabla systemanrop, utan libc. Observera att du behöver minst gcc-3.4 för att kompilera C-exemplet (__attribute__ ((fastcall)) stöds från och med 3.4). Assemblerexemplena fungerar även med äldre versioner.
För att titta på assemblerkoden i emacs kan följande rad passa bra i din .emacs fil, ‘(asm-comment-char ?#). Alternativt M-x set-variable [enter] asm-comment-char [enter] ?#
Information till denna bloggpost har hämtats från
Visst är det märkligt att FRA-lagen, datalagringsdirektivet och de andra lagarna som inkräktar på den personliga integriteten verkar införas en efter en på så kort tid.



