AOS - 13 - KERNEL PROGRAMMING BASICS VI
Lecture Info
Data:
Sito corso: link
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.