Voglio conoscere l'esatta differenza tra le funzionalità di Linux e seccomp.
Spiegherò l'esatto differenze di seguito, ma ecco la spiegazione generale:Le capacità implicano vari controlli nelle funzioni del kernel raggiungibili dalle chiamate di sistema. Se il controllo fallisce (ovvero il processo non dispone delle capacità necessarie), la chiamata di sistema viene generalmente eseguita per restituire un errore. Il controllo può essere eseguito all'inizio di una chiamata di sistema specifica o più in profondità nel kernel in aree che potrebbero essere raggiungibili tramite più chiamate di sistema diverse (come la scrittura su un file privilegiato specifico).
Seccomp è un filtro syscall che viene applicato a tutte le chiamate di sistema prima che vengano eseguite. Un processo può impostare un filtro che consente loro di revocare il diritto di eseguire determinate chiamate di sistema o argomenti specifici per determinate chiamate di sistema. Il filtro è solitamente sotto forma di bytecode eBPF, che il kernel utilizza per verificare se una chiamata di sistema è consentita o meno per quel processo. Una volta applicato, un filtro non può essere affatto allentato, ma solo reso più rigoroso (supponendo che le chiamate di sistema responsabili del caricamento di una politica seccomp siano ancora consentite).
Si noti che alcune chiamate di sistema non possono essere limitate né da seccomp né dalle capacità perché non sono chiamate di sistema reali. Questo è il caso delle chiamate vDSO, che sono implementazioni in spazio utente di diverse chiamate di sistema che non necessitano strettamente del kernel. Tentativo di bloccare getcpu()
o gettimeofday()
è inutile per questo motivo, poiché un processo utilizzerà comunque il vDSO invece della chiamata di sistema nativa. Per fortuna, queste chiamate di sistema (e le loro implementazioni virtuali associate) sono in gran parte innocue.
Inoltre, le funzionalità di Linux usano seccomp internamente o è il contrario o sono entrambe completamente diverse.
Sono implementati in modo completamente diverso internamente. Ho scritto un'altra risposta altrove sull'attuale implementazione di varie tecnologie di sandbox spiegando in cosa differiscono e a cosa servono.
Capacità
Molte chiamate di sistema che eseguono operazioni privilegiate possono includere un controllo interno per garantire che il processo chiamante abbia capacità sufficienti. Il kernel memorizza l'elenco delle capacità di un processo e, una volta che un processo elimina le capacità, non può recuperarle. Ad esempio, provando a scrivere su /dev/cpu/*/msr
fallirà a meno che il processo non chiami open()
syscall ha CAP_SYS_RAWIO
. Questo può essere visto nel codice sorgente del kernel responsabile della modifica degli MSR (funzionalità della CPU di basso livello):
static int msr_open(struct inode *inode, struct file *file)
{
unsigned int cpu = iminor(file_inode(file));
struct cpuinfo_x86 *c;
if (!capable(CAP_SYS_RAWIO))
return -EPERM;
if (cpu >= nr_cpu_ids || !cpu_online(cpu))
return -ENXIO; /* No such CPU */
c = &cpu_data(cpu);
if (!cpu_has(c, X86_FEATURE_MSR))
return -EIO; /* MSR not supported */
return 0;
}
Alcune chiamate di sistema non funzioneranno per niente se la capacità corretta non è presente, come vhangup()
:
SYSCALL_DEFINE0(vhangup)
{
if (capable(CAP_SYS_TTY_CONFIG)) {
tty_vhangup_self();
return 0;
}
return -EPERM;
}
Le capacità possono essere considerate come ampie classi di funzionalità privilegiate che possono essere rimosse in modo selettivo da un processo o da un utente. Le funzioni specifiche che hanno controlli di capacità variano da versione del kernel a versione del kernel, e spesso ci sono litigi tra gli sviluppatori del kernel sul fatto che una data funzione debba o meno richiedere capacità per essere eseguita. Generalmente , la riduzione delle capacità di un processo migliora la sicurezza riducendo il numero di azioni privilegiate che può eseguire. Tieni presente che alcune funzionalità sono considerate equivalenti a root , il che significa che, anche se disabiliti tutte le altre funzionalità, in alcune condizioni possono essere utilizzate per riottenere le autorizzazioni complete. Molti esempi sono forniti dal creatore di grsecurity, Brad Spengler. Un esempio ovvio sarebbe CAP_SYS_MODULE
che consente di caricare moduli del kernel arbitrari. Un altro sarebbe CAP_SYS_ADMIN
che è una capacità catch-all quasi equivalente a root.
Modalità 1 seccomp
Ci sono due tipi di seccomp:mode 1 (strict) e mode 2 (filter). La modalità 1 è estremamente restrittiva e, una volta abilitata, consente solo quattro chiamate di sistema. Queste chiamate di sistema sono read()
, write()
, exit()
e rt_sigreturn()
. A un processo viene immediatamente inviato il fatale SIGKILL
segnale dal kernel se tenta di utilizzare una chiamata di sistema che non è nella whitelist. Questa modalità è la modalità seccomp originale e non richiede la generazione e l'invio di bytecode eBPF al kernel. Viene effettuata una chiamata di sistema speciale, dopo la quale la modalità 1 sarà attiva per tutta la durata del processo:seccomp(SECCOMP_SET_MODE_STRICT)
o prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT)
. Una volta attivo, non può essere disattivato.
Di seguito è riportato un programma di esempio che esegue in modo sicuro il bytecode che restituisce 42:
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <linux/seccomp.h>
/* "mov al,42; ret" aka "return 42" */
static const unsigned char code[] = "\xb0\x2a\xc3";
void main(void)
{
int fd[2], ret;
/* spawn child process, connected by a pipe */
pipe(fd);
if (fork() == 0) {
close(fd[0]);
/* enter mode 1 seccomp and execute untrusted bytecode */
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);
ret = (*(uint8_t(*)())code)();
/* send result over pipe, and exit */
write(fd[1], &ret, sizeof(ret));
syscall(SYS_exit, 0);
} else {
close(fd[1]);
/* read the result from the pipe, and print it */
read(fd[0], &ret, sizeof(ret));
printf("untrusted bytecode returned %d\n", ret);
}
}
La modalità 1 è la modalità originale ed è stata aggiunta allo scopo di rendere possibile l'esecuzione di bytecode non attendibili per calcoli grezzi. Un processo broker eseguirà il fork di un figlio (e probabilmente imposterà la comunicazione tramite pipe) e il figlio abiliterà seccomp, impedendogli di fare qualsiasi cosa tranne leggere e scrivere da descrittori di file che sono già aperti ed uscire. Questo processo figlio potrebbe quindi eseguire in modo sicuro bytecode non attendibile. Non molte persone hanno usato questa modalità, ma prima che Linus potesse lamentarsi abbastanza forte da ucciderlo, il team di Google Chrome ha espresso il desiderio di usarlo per il proprio browser. Ciò creò un rinnovato interesse per seccomp e lo salvò da una morte prematura.
Modalità 2 seccomp
La seconda modalità, filter, chiamata anche seccomp-bpf, consente al processo di inviare una policy di filtro a grana fine al kernel, consentendo o negando intere chiamate di sistema o argomenti specifici di chiamate di sistema o intervalli di argomenti. La politica specifica anche cosa succede in caso di violazione (ad esempio, il processo dovrebbe essere interrotto o la chiamata di sistema dovrebbe essere semplicemente negata?) e se la violazione deve essere registrata o meno. Poiché le chiamate di sistema di Linux sono conservate nei registri e quindi possono essere solo numeri interi, è impossibile filtrare il contenuto della memoria a cui potrebbe puntare un argomento di chiamata di sistema. Ad esempio, sebbene tu possa impedire open()
dall'essere chiamato con il O_RDWR
in grado di scrivere o O_WRONLY
flag, non è possibile inserire nella whitelist il singolo percorso aperto. La ragione di ciò è che, per seccomp, il percorso non è altro che un puntatore alla memoria contenente il percorso del filesystem con terminazione null. Non c'è modo di garantire che la memoria che contiene il percorso non sia stata modificata da un thread di pari livello tra il passaggio del controllo seccomp e il puntatore che viene dereferenziato, a meno di metterlo nella memoria di sola lettura e negare l'accesso alle chiamate di sistema relative alla memoria. Spesso è necessario utilizzare LSM come AppArmor.
Questo è un programma di esempio che utilizza la modalità 2 seccomp per garantire che possa stampare solo il suo PID corrente. Questo programma utilizza la libreria libseccomp che semplifica la creazione di filtri seccomp eBPF, sebbene sia anche possibile farlo nel modo più duro senza alcuna libreria di astrazione.
#include <seccomp.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
void main(void)
{
/* initialize the libseccomp context */
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
/* allow exiting */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
/* allow getting the current pid */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(getpid), 0);
/* allow changing data segment size, as required by glibc */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);
/* allow writing up to 512 bytes to fd 1 */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 2,
SCMP_A0(SCMP_CMP_EQ, 1),
SCMP_A2(SCMP_CMP_LE, 512));
/* if writing to any other fd, return -EBADF */
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EBADF), SCMP_SYS(write), 1,
SCMP_A0(SCMP_CMP_NE, 1));
/* load and enforce the filters */
seccomp_load(ctx);
seccomp_release(ctx);
printf("this process is %d\n", getpid());
}
La modalità 2 seccomp è stata creata perché la modalità 1 aveva ovviamente i suoi limiti. Non tutte le attività possono essere separate in un processo di puro bytecode che potrebbe essere eseguito in un processo figlio e comunicare tramite pipe o memoria condivisa. Questa modalità ha molte più funzioni e la sua funzionalità continua ad essere lentamente ampliata. Tuttavia, ha ancora i suoi lati negativi. L'uso sicuro della modalità 2 seccomp richiede una profonda conoscenza delle chiamate di sistema (vuoi bloccare kill()
dall'uccisione di altri processi? Peccato, puoi terminare i processi con fcntl()
anche!). È anche fragile, poiché le modifiche alla libc sottostante possono causare rotture. La glibc open()
la funzione, ad esempio, non usa più sempre la chiamata di sistema con quel nome e invece può usare openat()
, infrangendo i criteri che consentivano solo il primo.