AOS - 13 - KERNEL PROGRAMMING BASICS VI


Lecture Info

  • Data: [2019-10-21 lun]

  • Sito corso: link

  • Slides: AOS - 2 KERNEL PROGRAMMING BASICS

  • Progresso unità: 6/6

  • Argomenti:

    • Homework #2

    • Dispatcher code

    • Security on 4.17

    • Kernel compiling

    • System map

1 Homework #2: SCTD


1.1 Requirements

This homework deals with the implementation of a Linux kernel module that discovers at run-time the positioning of the system-call table. The simplifying assumption is that the module programmer can exploit the kernel-side name of some system-call (hence the corresponding actual address) in order to perform the discovery phase. The module should provide the current memory address of the system-call table via a kernel level message accessible via the 'dmesg' shell command.


1.2 Solution

La discovery della tabella viene fatta sfruttando la relazione tra la locazione di memoria del codice di una system call con il suo particolare indice. In particolare sappiamo che vale la seguente relazione

sys_call_table_addr[__NR_sys_call] = sys_call_address

L'idea è quindi di utilizzare il codice numero di una sys-call e l'indirizzo logico della stessa sys-call per fare uno scan della memoria. Dato un indirizzo addr della memoria, posso fare il seguente check per capire se, potenzialmente, addr punta alla sys_call_table

if (addr[__NR_sys_call] = sys_call_address) {
  // possible hit
 }

Notiamo che questo procedimento ci potrebbe dare anche dei falsi positivi, ovvero delle locazioni di memoria che rispettano il vincolo ma che non puntano alla vera syscall table. Andando ad aumentare il numero di check, ovvero controllando che la stessa relazione sussiste anche per altre syscalls, aumentiamo anche il grado di certezza che l'indirizzo trovato sia veramente l'indirizzo iniziale della sys_call_table.


Notiamo che per implementare correttamente questa idea dobbiamo partire da qualche indirizzo che si trova, nella memoria logica del kernel, prima della syscall table.

Per risolvere questa problematica è utile tenere a mente il fatto che il kernel è una tecnologia che viene tradizionalmente costruita tramite un metodo che prende il nome di incremental linking . Tale tecnica consiste nel costruire software complessi in modo incrementale, aggiungendo, di volta in volta, nuovi blocchi al software precedentemente scritto. Questi blocchi che vengono aggiunto hanno la proprietà che i loro indirizzi logici si trovano dopo gli indirizzi logici dei blocchi già presenti.

Per il nostro particolare problema, questo significa che se prendiamo l'indirizzo di una system call generica, il suo indirizzo è sempre precedente a quello della system call table.

Il pezzo di codice principale che fa la discovery è quindi il seguente

unsigned long *get_syscall_table(void)
{
  unsigned long *syscall_table;
  unsigned long int i;

  // Nei kernel <= 3.x il simbolo sys_close viene espotarto. 
  for (i = (unsigned long int)sys_close; i < ULONG_MAX; i += sizeof(void *)) {
    syscall_table = (unsigned long *)i;

    // Check di correttezza
    if (syscall_table[__NR_close] == (unsigned long)sys_close)
      return syscall_table;
  }
  
  return NULL;
}

Per terminare, notiamo che se non trovo la syscall table e continuo a scorrere la memoria fino ad arrivare alla fine della memoria andrò a generare un segmentation fault, in quanto sto tentando di accedere a una memoria che non esiste. In questo caso poi il modulo sarebbe lockato in memoria, in quanto è bloccato dal kernel per tutta l'esecuzione dell'istruzione init_module().

2 Syscall Dispatcher


2.1 Dispatcher for int 0x80 (kernel 2.4)

A seguire il codice del dispatcher che gestisce la trap 0x80 all'interno del kernel 2.4. Ricordiamo che prima dell'esecuzione di questo codice il firmware ha già cambiato lo stack utilizzando il TSS e salvato nel nuovo stack varie informazioni sullo stato del processore.

      ENTRY(system_call) 
              push %eax            // Save orig_eax = sys_call_number
              SAVE_ALL             // Macro that saves all CPU registers on stack 
              GET_CURRENT(%ebx)

              testb $0x02, tsk_ptrace(%ebx)
              jne tracesys

              // vulnerable to speculative-execution attacks
              cmpl $(NR_syscalls), %eax
              jae badsys
              call *SYMBOL_NAME(sys_call_table)(, %eax, 4)
        
              movl %eax, EAX(%esp)

              ....

Notiamo in particolare la parte del codice vulnerabile ad attacchi di tipo speculativo: Il codice controlla il registro EAX con la taglia della system call. Se il numero è più grande della taglia, allora chiamata la routine badsys , altrimenti chiama la particolare routine indicizzando l'indirizzo base della syscall table tramite il registro EAX.


2.2 Dispatcher for syscall (kernel 2.4)

Utilizzando la fast system call path tramite l'istruzione syscall il firmware non fa più nulla, ed è solo il software che imposta la stack da far vedere alla system call. Nel fast system call path utilizziamo i model specific registers per passare il controllo a questo blocco di codice.

      ENTRY(system_call)

              swapgs

              // Parte del lavoro che prima veniva fatto dal firmware adesso
              // viene fatto dal software.
              movq %rsp, PDAREF(pda_oldrp)
              movq PDAREF(pda_kernelstack), %rsp
              sti
              SAVE_ARGS 8, 1
              movq %rax, ORIG_RAX-ARGOFFSET(%rsp)
              movq %rcx, RIP-ARGOFFSET(%rsp)
              GET_CURRENT(%rcx)
        
              testl $PT_TRACESYS, tsk_ptrace(%rcx)
              jne tracesys

              // vulnerable to speculative-execution attacks. 
              cmpq $__NR_syscall_max, %rax
              ja badsys

              movq %r10, %rcx
              call *sys_call_table(, %rax, 8)
        
              movq  %rax, RAX-ARGOFFSET(%rsp)
              .globl ret_from_sys_call

              ...

2.2.1 swapgs instruction

L'istruzione swapgs è una istruzione privilegiata che permette di cambiare il valore base del selettore GS con il valore contenuto in un model specific register ( IA32_KERNEL_GS_BASE ).

Quando si utilizza l'istruzione syscall infatti non c'è nessuna kernel stack nell'entry point lato kernel, e non c'è nemmeno un modo semplice per ottenere un puntatore a dalle strutture dalle quali si può leggere il kernel stack pointer. Viene quindi utilizzata questa istruzione per creare della per-cpu memory e permettere al kernel di leggere in modo veloce le sue strutture dati.

Questo comporta che quando facciamo il set-up della memoria dobbiamo fare il setup del MSR utilizzato dall'istruzione swapgs.



2.3 Dispatcher (kernel 4.17)

Nelle vecchie versioni del kernel per eseguire una system call si andava a comparare il valore contenuto nel registro RAX con il numero massimo delle system call, e a seconda del risultato si andare a chiamare una particolare routine.

Come abbiamo già trattato nel primo modulo del corso, codice del genere è molto vulnerabile alla speculatività dell'hardware. È stato dunque necessario rendere il codice del dispatcher più sicuro.

Nel kernel 4.xx questo problema è stato sistemato. In particolare è stato introdotto un ulteriore dispatcher che non fa altro che controllare il valore nel registro RAX e impone che il suo valore sia all'interno dello schema di indicizzazione prima di chiamare le funzioni.

// Taken from
// https://github.com/torvalds/linux/blob/master/arch/x86/entry/common.c
__visible noinstr void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
        nr = syscall_enter_from_user_mode(regs, nr);

        // ....
        
        instrumentation_begin();
        if (likely(nr < NR_syscalls)) {
                nr = array_index_nospec(nr, NR_syscalls);
                regs->ax = sys_call_table[nr](regs);
        }

        // ....         
}               

2.3.1 SYSCALL_DEFINE macros

Dal kernel 4.17 poi le entry presenti nella system call table non puntato più alla routine effettiva, ma puntano a dei wrapper che mascherano i valori dello stato della CPU che non vengono utilizzati dalle system calls. Questo crea un ulteriore livello di protezione e sicurezza del sistema.

Per programmare a livello kernel nuove system call a partire dal kernel 4.17 sono quindi state introdotte delle nuove macro chiamate: SYSCALL_DEFINE0, SYSCALL_DEFINE1, e via dicendo. L'utilizzo di una di queste macro permette di generare due funzioni: una chiamata sys_name e l'altra chiamata __se_sys_name . La funzione intermedia, sys_name , si occupa di nascondere i parametri non utilizzati alla funzione effettiva, __se_sys_name .

Per fare un esempio, se vogliamo utilizzare la macro SYSCALL_DEFINE2, dobbiamo scrivere il seguente codice lato kernel

SYSCALL_DEFINE2(name, param1type, param1name, param2type, param2name) {
  // Body implementing the kernel side system call
}


2.3.2 PTI

Infine, nei dispatcher moderni in cui è presente la PTI (Page Table Isolation), c'è una istruzione che cambia la visione della memoria a quella del kernel. La macro utilizzata per fare questo è chiamata SWITCH_TO_KERNEL_CR3.


3 Virtual Dynamic Shared Object (VDSO)

L'idea dietro al VDSO è che il kernel crea una o più pagine in memoria e le monta nello spazio di indirizzamento di tutti i processi quando sono caricati in memoria. Questa pagina contiene del codice. Originariamente conteneva il codice neccessario per entrare e uscire in modo kernel utilizzando il fast system call path. In particolare il VDSO utilizzata l'indirizzamento lineare per bypassare la segmentazione e le relative operazioni di memoria. Questo comporta un numero minore di accessi alla memoria e quindi permette un passaggio lato kernel più performante. La posizione del VDSO all'interno dell'address space è randomizzata in modo da aumentare la sicurezza.

Il VDSO è stato utilizzato per sfruttare in maniera trasparente il fast system call path per alcune system call critiche dal punto di vista della temporizzazione, tra cui gettimeofday() .

Per le macchine i386 il vdso è definito nel seguente file

  /usr/src/linux/arch/i386/kernel/vsyscall-sysenter.S

3.1 Addr of VDSO

Tramite la system call getauxval() possiamo ottenere l'indirizzo in memoria in cui è situato il VDSO. Possiamo leggere questo oggetto tramite objdump.

#include <sys/auxv.h>

void *vdso = (uintptr_t) getauxval(AT_SYSINFO_EHDR);

Le applicazioni non devono preoccuparsi di utilizzare il vdso, in quanto queste facility offerte dal kernel vengono utilizzate dalla standard-C library.


3.2 SYS-CALL/vdso.c

In questo programma leggiamo 4 pagine di memoria che contengono codice messo a disposizione dal kernel nel vDSO. Possiamo leggere il contenuto di questa memoriza utilizzando objdump.

gcc vdso.c ./a.out > vdso.bin objdump -D vdso.bin

4 SYS-CALL/fast-vs-slow-syscall.c

In questo esempio vediamo la differenza in termine di performance tra l'utilizzo della fast system call path e della slow system call path.

Per cercare di misurare il tempo speso per accedere al lato kernel dobbiamo minimizzare il tempo richiesto per accedere al driver di I/O. L'idea è quindi quella di utilizzare il driver /dev/null, che è un driver lightweight che ritorna il controllo appena lo prende.

5 DUAL-SYSCALL-TABLE-HACKING/sys_call_table_hacker.c

In questo esempio andiamo a montare due light system call all'interno delle due system call table: quella utilizzata lavorando in modalità 32 bit, e quella utilizzata lavorando in modalità a 64bit.

Possiamo utilizzare questo modulo per fare dei test prestazionali tra int 0x80 e syscall più corretti, in quanto la sys call che andiamo a chiamare è ancora più light-weight di quella che scrive in /dev/null.

6 Kernel Software Organization

Il software livello kernel è suddiviso nelle seguenti directories

  • kernel: process and user management.

  • mm: basic memory management.

  • ipc: interprocess communication management.

  • fs: virtual file system management.

  • net: network management.


6.1 Kernel Compilation

La compilazione del kernel fa utilizzo della tecnologia dei Makefiles.


6.1.1 Configuring the Kernel

Scegliere la corretta configurazione per compilare il core del kernel è una operazione molto difficile e time-consuming. Abbiamo varie strade per generare una configurazione, tra cui:

  • allyesconfig: utilizzando questa opzione molto spesso non riusciamo a compilare il core per via di conflitti tra moduli.

  • allnoconfig: utilizzando questa opzione molto spesso riusciamo a compilare il core, ma il core compilato non supporta molti moduli. Potrebbe persino non supportare un file system.

  • Rispondere a tutte le domande: time-consuming e necessita una buona conoscenza sul kernel e sulla macchina su cui stiamo lavorando.

  • utilizzare un vecchio file di configurazione: Il vecchio file di configurazione è stato copiato in /boot durante l'installazione del precedente kernel. Possiamo quindi semplicemente utilizzare quello.

  • ottenere un file di configurazione dal web.



6.1.2 Compilation steps

Una volta configurato il kernel possiamo compilare ed installare il kernel tramite i seguenti comandi.

# Crea la configurazione del kernel da utilizzare per la fase
# successiva di compilazione. 
make config (or menuconfig)

# Costruisci il core del kernel
make

# Costruisci i moduli di linux da utilizzare in modo dinamico
make modules

# Sposta i moduli del kernel costruiti prima in una directory
# particolare. 
#
# Side-effects: writes into /lib/modules
make modules_install (ROOT)

# Installa il kernel compilato 
#
# Side-effects: writes into /boot the following things:
# 1) the kernel image
# 2) the system map
# 3) the config file
make install (ROOT)

# Crea un RAM-disk, in cui inseriamo determinate cose che vogliamo
# inserire dal kernel ma che non sono incluse nel core del kernel. 
mkinitrd (or mkinitramfs) -o initrd.img-<vers><vers>

# Aggiorna grub per poter accedere al nuovo kernel. Abbiamo due modi
# per farlo: 
update-grub
# oppure, 
grub-mkconfig -o /boot/grub/bruug.cfg


6.1.3 Role of initrd

Initrd è un ram disk e viene utilizzato come un temporaneo file system per far girare i programmi durante la fase di start-up del sistema. Una volta che la fase di start-up è finita un diverso file system può essere montato.



6.2 System Map

La system map contiene l'anatomia dell'immagine del kernel ottenuta dopo la compilazione. Nella system map sono presenti tutte le informazioni dei simboli presenti nella compile-time image del kernel.

Per ogni simbolo presente nella system è presente un indirizzo e un tag, che determina la sua "storage class" determinata dal processo di compilazione. Abbiamo varie storage classes, tra cui:

  • 'T', denota una funzione globale (non-statica, ma non necessariamente exported);

  • 't', denota una funzione locale (i.e. statica) relativa all'unità di compilazione.

  • 'D', denota un dato globale.

  • 'd', denota un dato locale.

  • 'R', denota un dato globale che è read-only.

  • 'r', denota un dato locale che è read-only.

Ricordiamo che dal kernel 4.8 di default è stato introdotta la kASLR. Se kASLR è attivato, le informazioni della system map sono inutili. In ogni caso è possibile disattivare kASLR andando ad inserire il comando nokaslr nella command line del kernel, accessibile tramite il bootloader (tipo grub).

In ogni caso, la system map è anche riportata parzialmente nello (pseudo) file /proc/kallsysm , che viene utilizzato per descrivere quali sono i simboli che i moduli nuovi possono utilizzare.