2015-10-15 18 views
17

Passare a EXIT_SUCCESS dopo aver impostato la modalità strict seccomp. È la pratica corretta, per chiamare syscall(SYS_exit, EXIT_SUCCESS); alla fine del principale?seccomp --- come EXIT_SUCCESS?

#include <stdlib.h> 
#include <unistd.h> 
#include <sys/prctl.h>  
#include <linux/seccomp.h> 
#include <sys/syscall.h> 

int main(int argc, char **argv) { 
    prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT); 

    //return EXIT_SUCCESS; // does not work 
    //_exit(EXIT_SUCCESS); // does not work 
    // syscall(__NR_exit, EXIT_SUCCESS); // (EDIT) This works! Is this the ultimate answer and the right way to exit success from seccomp-ed programs? 
    syscall(SYS_exit, EXIT_SUCCESS); // (EDIT) works; SYS_exit equals __NR_exit 
} 

// gcc seccomp.c -o seccomp && ./seccomp; echo "${?}" # I want 0 
+0

Non puoi semplicemente restituire EXIT_SUCCESS? (Woops: non importa - non ha guardato il tuo codice abbastanza da vicino.) – Steven

+0

Ho avuto lo stesso problema, il mio processo viene ucciso. – Lev

+0

È molto strano che '_exit (EXIT_SUCCESS)' non funzioni, poiché la manpage indica chiaramente che, in modalità seccomp strict, "Le uniche chiamate di sistema che il thread chiamante è autorizzato a fare sono read (2), write (2)), _exit (2) (ma non exit_group (2)) e sigreturn (2). " (dove i numeri tra parentesi sono ovviamente sezioni manuali). –

risposta

10

Come spiegato in eigenstate.org e in SECCOMP (2):

L'unico sistema richiama che il thread chiamante è consentito fare sono leggere (2), scrittura (2), _exit (2) (ma non exit_group (2)), e sigreturn (2). Altre chiamate di sistema portano alla consegna di un segnale SIGKILL.

Di conseguenza, ci si aspetterebbe _exit() a lavorare, ma è una funzione wrapper che invoca exit_group(2) che non è consentita in modalità rigorosa ([1], [2]), in tal modo il processo viene ucciso.

è ancora presentate nel exit(2) - Linux man page:

In glibc fino alla versione 2.3, la funzione wrapper _exit() invocato la chiamata di sistema del kernel con lo stesso nome. Poiché glibc 2.3, , la funzione wrapper richiama exit_group (2), per terminare tutti i thread in un processo.

stesso accade con la return dichiarazione, che dovrebbe finire per uccidere il processo, in modo molto simile con _exit().

stracing il processo fornirà un'ulteriore conferma (per consentire a questa di presentarsi, si deve non set PR_SET_SECCOMP basta commentare prctl()) e ho ottenuto un risultato simile per entrambi i casi non lavorativi:

linux12:/home/users/grad1459>gcc seccomp.c -o seccomp 
linux12:/home/users/grad1459>strace ./seccomp 
execve("./seccomp", ["./seccomp"], [/* 24 vars */]) = 0 
brk(0)         = 0x8784000 
access("/etc/ld.so.nohwcap", F_OK)  = -1 ENOENT (No such file or directory) 
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb775f000 
access("/etc/ld.so.preload", R_OK)  = -1 ENOENT (No such file or directory) 
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 
fstat64(3, {st_mode=S_IFREG|0644, st_size=97472, ...}) = 0 
mmap2(NULL, 97472, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7747000 
close(3)        = 0 
access("/etc/ld.so.nohwcap", F_OK)  = -1 ENOENT (No such file or directory) 
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\220\226\1\0004\0\0\0"..., 512) = 512 
fstat64(3, {st_mode=S_IFREG|0755, st_size=1730024, ...}) = 0 
mmap2(NULL, 1739484, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xdd0000 
mmap2(0xf73000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a3) = 0xf73000 
mmap2(0xf76000, 10972, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xf76000 
close(3)        = 0 
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7746000 
set_thread_area({entry_number:-1 -> 6, base_addr:0xb7746900, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0 
mprotect(0xf73000, 8192, PROT_READ)  = 0 
mprotect(0x8049000, 4096, PROT_READ) = 0 
mprotect(0x16e000, 4096, PROT_READ)  = 0 
munmap(0xb7747000, 97472)    = 0 
exit_group(0)       = ? 
linux12:/home/users/grad1459> 

Come potete vedere, si chiama exit_group() spiegando tutto!


Ora come correttamente affermato, "SYS_exit equals __NR_exit"; ad esempio è definita mit.syscall.h:

#define SYS_exit __NR_exit 

così gli ultimi due chiamate sono equivalenti, cioè è possibile utilizzare quello che ti piace, e l'uscita dovrebbe essere questo:

linux12:/home/users/grad1459>gcc seccomp.c -o seccomp && ./seccomp ; echo "${?}" 
0 

PS

Si può anche definire uno filter e utilizzare:

prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, filter); 

come spiegato nel collegamento eigenstate, per consentire _exit() (o, in senso stretto, exit_group(2)), ma farlo solo se è davvero necessario e sapere cosa si sta facendo.

+0

Inoltre, anche il motivo per cui "return EXIT_SUCCESS;" fallisce è lo stesso: anche la libreria GNU C fa un 'exit_group()'. Ho qualche C freestanding per SYSV ABI x86-64 che dimostra che il syscall 'exit' funziona bene dopo la chiamata' prctl (PR_SET_SECCOMP, SECCOMP_MODE_STRICT) ', se sei interessato. –

+1

L'esecuzione del file binario in 'strace' (ad esempio' strace./Example') è sufficiente per dimostrare che la libreria C usa il syscall 'exit_group' invece di' exit', comunque. –

+0

Ehi @NominalAnimal! :) Sì, buona idea, è stato anche menzionato in uno dei link, ma non è stato visualizzato, aggiornato! Ti piace la risposta adesso? – gsamaras

6

Il problema si verifica perché la libreria GNU C utilizza il exit_group syscall, se è disponibile, in Linux anziché exit, per la funzione _exit() (vedi sysdeps/unix/sysv/linux/_exit.c per verifica), e come documentato nel man 2 prctl, il exit_group syscall non è consentito dal severo filtro seccomp.

Poiché la chiamata alla funzione _exit() si verifica all'interno della libreria C, non è possibile inserirla con la nostra versione (che farebbe semplicemente il syscall exit). (Il normale pulizia processo è fatto altrove;. In Linux, la funzione _exit() fa solo la chiamata di sistema finale che termina il processo)

Potremmo chiedere agli sviluppatori della libreria GNU C di utilizzare la exit_group syscall in Linux solo quando ci sono più di un thread nel processo corrente, ma sfortunatamente, non sarebbe facile, e anche se aggiunto in questo momento, ci vorrebbe un po 'di tempo prima che la funzione sia disponibile sulla maggior parte delle distribuzioni Linux.

Fortunatamente, possiamo eliminare il filtro rigoroso predefinito e invece definire il nostro. C'è una piccola differenza di comportamento: il segnale apparente che uccide il processo cambierà da SIGKILL a SIGSYS. (Il segnale non viene effettivamente consegnato, poiché il kernel uccide il processo, solo il numero di segnale apparente che ha causato la morte del processo cambia.)

Inoltre, non è nemmeno così difficile. Ho sprecato un po 'di tempo a esaminare alcuni macro trucchi della GCC che avrebbero reso banale la gestione dell'elenco delle syscalls consentite, ma ho deciso che non sarebbe stato un buon approccio: l'elenco delle syscalls permesse dovrebbe essere attentamente considerato - noi solo aggiungi exit_group() rispetto al filtro rigoroso, qui! - Quindi renderlo un bit difficile è okay.

Il codice seguente, dire example.c, è stato verificato funzionare su un kernel 4.4 (dovrebbe funzionare su kernel 3.5 o successiva) su x86-64 (sia x86 e x86-64, cioè 32 bit e 64 binari bit rate). Dovrebbe funzionare su tutte le architetture Linux, tuttavia, e richiede non o utilizzare la libreria libseccomp.

#define _GNU_SOURCE 
#include <stdlib.h> 
#include <stddef.h> 
#include <sys/prctl.h> 
#include <sys/syscall.h> 
#include <linux/seccomp.h> 
#include <linux/filter.h> 
#include <stdio.h> 

static const struct sock_filter strict_filter[] = { 
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof (struct seccomp_data, nr))), 

    BPF_JUMP(BPF_JMP | BPF_JEQ, SYS_rt_sigreturn, 5, 0), 
    BPF_JUMP(BPF_JMP | BPF_JEQ, SYS_read,   4, 0), 
    BPF_JUMP(BPF_JMP | BPF_JEQ, SYS_write,  3, 0), 
    BPF_JUMP(BPF_JMP | BPF_JEQ, SYS_exit,   2, 0), 
    BPF_JUMP(BPF_JMP | BPF_JEQ, SYS_exit_group, 1, 0), 

    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL), 
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW) 
}; 

static const struct sock_fprog strict = { 
    .len = (unsigned short)(sizeof strict_filter/sizeof strict_filter[0]), 
    .filter = (struct sock_filter *)strict_filter 
}; 

int main(void) 
{ 
    /* To be able to set a custom filter, we need to set the "no new privs" flag. 
     The Documentation/prctl/no_new_privs.txt file in the Linux kernel 
     recommends this exact form: */ 
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { 
     fprintf(stderr, "Cannot set no_new_privs: %m.\n"); 
     return EXIT_FAILURE; 
    } 
    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &strict)) { 
     fprintf(stderr, "Cannot install seccomp filter: %m.\n"); 
     return EXIT_FAILURE; 
    } 

    /* The seccomp filter is now active. 
     It differs from SECCOMP_SET_MODE_STRICT in two ways: 
     1. exit_group syscall is allowed; it just terminates the 
      process 
     2. Parent/reaper sees SIGSYS as the killing signal instead of 
      SIGKILL, if the process tries to do a syscall not in the 
      explicitly allowed list 
    */ 

    return EXIT_SUCCESS; 
} 

Compilare utilizzando ad es.

gcc -Wall -O2 example.c -o example 

e di esecuzione utilizzando

./example 

o sotto strace per vedere le chiamate di sistema e le chiamate di libreria fatto;

strace ./example 

Il programma strict_filter BPF è davvero banale. Il primo codice operativo carica il numero di syscall nell'accumulatore. I successivi cinque opcode lo confrontano con un numero di syscall accettabile e, se trovato, passa all'opcode finale che consente a syscall. Altrimenti il ​​penultimo codice operativo uccide il processo.

Si noti che sebbene la documentazione faccia riferimento a sigreturn come syscall consentito, il nome effettivo di syscall in Linux è rt_sigreturn. (sigreturn è stato ritirato a favore di rt_sigreturn anni fa.)

Inoltre, quando il filtro è installato, gli opcode vengono copiati nella memoria del kernel (vedere kernel/seccomp.c nelle origini del kernel di Linux), quindi non influisce in alcun modo sul filtro se i dati vengono modificati in seguito. Avere le strutture static const ha zero impatto sulla sicurezza, in altre parole.

Ho utilizzato static poiché non è necessario che i simboli siano visibili all'esterno dell'unità di compilazione (o in un file binario non sottoposto a stripping) e const per inserire i dati nella sezione di dati di sola lettura del file binario ELF.

Il modulo di BPF_JUMP(BPF_JMP | BPF_JEQ, nr, equals, differs) è semplice: l'accumulatore (il numero di syscall) viene confrontato con nr. Se sono uguali, i successivi codici opzionali equals vengono saltati. In caso contrario, i successivi codici operativi differs vengono saltati.

Dal momento che i casi di uguali si spostano sull'opcode finale, è possibile aggiungere nuovi codici opzionali nella parte superiore (ovvero, subito dopo l'opcode iniziale), incrementando il numero di salti uguali per ciascuno.

noti che printf() non funziona dopo l'installazione del filtro seccomp, dato che internamente, la libreria C vuole fare un fstat syscall (standard output), e una chiamata di sistema brk per allocare della memoria per un buffer.

+1

Questo è uno di quei momenti in cui vorrei dividere la taglia. –

+2

@Rhymoid la taglia è come un atomo! :) Non ti preoccupare però, Nomimal è un ragazzo fantastico! – gsamaras