SuperSnabbaSyscalls

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 $0x80 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 $0x80 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 $0x80 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 $0x80 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 $0x80 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:0x10‘. 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 0x10. 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

Annonser

Om albertveli

Grävande programmerare.
Det här inlägget postades i Programmering och har märkts med etiketterna . Bokmärk permalänken.

2 kommentarer till SuperSnabbaSyscalls

  1. Intressant.
    Känns som att slumpningen är onödig om man kan gräva fram den ändå…

    Och när får vi se exempel på lite malware? 🙂

  2. Old AF fart skriver:

    De flesta malware brukar bara göra <i<int 0x80 eftersom det är kortare och funkar med alla Linuxkärnor. Fast jag tror det går att stänga av int 0x80 på de senaste kärnorna. En hel del malware skulle sluta fungera om man gör så. Dessutom kan man slänga på flaggan -fstack-protector till gcc för att skydda mot buffer overflows. Randomisering av allt möjligt i kärnan hjälper också till.

Kommentera

Fyll i dina uppgifter nedan eller klicka på en ikon för att logga in:

WordPress.com Logo

Du kommenterar med ditt WordPress.com-konto. Logga ut / Ändra )

Twitter-bild

Du kommenterar med ditt Twitter-konto. Logga ut / Ändra )

Facebook-foto

Du kommenterar med ditt Facebook-konto. Logga ut / Ändra )

Google+ photo

Du kommenterar med ditt Google+-konto. Logga ut / Ändra )

Ansluter till %s