seccomp-¿Cómo EXIT_SUCCESS?
linux sandbox (2)
Vaya a EXIT_SUCCESS después de establecer el modo estricto seccomp. ¿Es la práctica correcta, llamar a syscall(SYS_exit, EXIT_SUCCESS);
al final de main?
#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
Como se explica en eigenstate.org y en SECCOMP (2) :
Las únicas llamadas al sistema que el subproceso que llama está autorizado a realizar son read (2), write (2), _exit (2) ( pero no exit_group (2)), y sigreturn (2). Otras llamadas al sistema dan como resultado la entrega de una señal SIGKILL.
Como resultado, uno esperaría que _exit()
funcione, pero es una función de envoltura que invoca exit_group(2)
que no está permitida en modo estricto ( [1] , [2] ), por lo que el proceso se detiene.
Incluso se informa en exit (2) - Página de manual de Linux :
En glibc hasta la versión 2.3, la función de envoltura _exit () invocó la llamada del sistema del kernel del mismo nombre. Desde glibc 2.3, la función wrapper invoca exit_group (2) , para terminar todos los hilos en un proceso.
Lo mismo sucede con la declaración de return
, que debería terminar matando tu proceso, de la misma manera que con _exit()
.
El proceso de extracción del proceso proporcionará una confirmación adicional (para que esto se muestre, no debe configurar PR_SET_SECCOMP; simplemente comentar prctl()
) y obtuve una salida similar para ambos casos que no funcionan:
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>
Como puedes ver, se llama a exit_group()
, ¡explicando todo!
Ahora, como ha indicado correctamente, " SYS_exit equals __NR_exit
"; por ejemplo, está definido en mit.syscall.h :
#define SYS_exit __NR_exit
por lo tanto, las dos últimas llamadas son equivalentes, es decir, puedes usar la que más te guste, y la salida debería ser esta:
linux12:/home/users/grad1459>gcc seccomp.c -o seccomp && ./seccomp ; echo "${?}"
0
PD
Por supuesto, usted podría definir un filter
y usar:
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, filter);
como se explica en el enlace eigenstate, para permitir _exit()
(o, estrictamente hablando, exit_group(2)
), pero hágalo solo si realmente lo necesita y sabe lo que está haciendo.
El problema ocurre, porque la biblioteca GNU C usa el exit_group
syscall, si está disponible, en Linux en lugar de exit
, para la función _exit()
(vea sysdeps/unix/sysv/linux/_exit.c
para verificación), y como documentado en man 2 prctl
, el exit_group
exit_group no está permitido por el filtro seccomp estricto.
Debido a que la llamada a la función _exit()
ocurre dentro de la biblioteca de C, no podemos interponerla con nuestra propia versión (que solo haría el syscall de exit
). (La limpieza del proceso normal se realiza en otra parte; en Linux, la función _exit()
solo realiza el último syscall que termina el proceso).
Podríamos pedir a los desarrolladores de la biblioteca GNU C que utilicen el exit_group
syscall en Linux solo cuando haya más de un hilo en el proceso actual, pero desafortunadamente, no sería fácil, e incluso si se añadiera ahora mismo, llevaría bastante tiempo. La característica estará disponible en la mayoría de las distribuciones de Linux.
Afortunadamente, podemos deshacernos del filtro estricto predeterminado y, en su lugar, definir el nuestro. Hay una pequeña diferencia en el comportamiento: la señal aparente que mata el proceso cambiará de SIGKILL
a SIGSYS
. (La señal no se entrega realmente, ya que el núcleo destruye el proceso; solo cambia el número de señal aparente que causó la muerte del proceso).
Además, esto ni siquiera es tan difícil. Perdí un poco de tiempo buscando algunos trucos de macro de GCC que harían trivial administrar la lista de llamadas permitidas, pero decidí que no sería un buen enfoque: la lista de llamadas permitidas debería ser considerada cuidadosamente. agregue exit_group()
comparado al filtro estricto, aquí! - Así que hacerlo un poco difícil está bien.
Se ha verificado que el siguiente código, por ejemplo example.c
, funciona en un kernel 4.4 (debería funcionar en los kernels 3.5 o posteriores) en x86-64 (tanto para x86 como para x86-64, es decir, binarios de 32 y 64 bits) . Sin embargo, debería funcionar en todas las arquitecturas de Linux y no requiere ni utiliza la biblioteca 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;
}
Compilar utilizando por ejemplo
gcc -Wall -O2 example.c -o example
y correr usando
./example
o bajo strace
para ver las llamadas a syscalls y bibliotecas realizadas;
strace ./example
El programa strict_filter
BPF es realmente trivial. El primer código de operación carga el número de llamada al acumulador. Los siguientes cinco códigos de operación lo comparan con un número de llamada aceptable, y si se encuentran, saltan al código de operación final que permite la llamada al sistema. De lo contrario, el segundo código de operación último mata el proceso.
Tenga en cuenta que aunque la documentación se refiere a que sigreturn
es el syscall permitido, el nombre real del syscall en Linux es rt_sigreturn
. ( sigreturn
fue desaprobado a favor de rt_sigreturn
hace años).
Además, cuando se instala el filtro, los kernel/seccomp.c
de kernel/seccomp.c
se copian en la memoria del kernel (consulte kernel/seccomp.c
en las fuentes del kernel de Linux), por lo que no afecta al filtro de ninguna manera si los datos se modifican más adelante. Tener las estructuras static const
tiene cero impacto en la seguridad, en otras palabras.
Utilicé static
ya que no es necesario que los símbolos estén visibles fuera de esta unidad de compilación (o en un binario extraído), y const
que los datos estén en la sección de datos de solo lectura del binario ELF.
La forma de un BPF_JUMP(BPF_JMP | BPF_JEQ, nr, equals, differs)
es simple: el acumulador (el número de syscall) se compara con nr
. Si son iguales, entonces se omiten los siguientes opcodes equals
. De lo contrario, se omiten los siguientes opcodes diferentes.
Dado que los casos de igual a igual saltan al código de operación final, puede agregar nuevos códigos de operación en la parte superior (es decir, justo después del código de operación inicial), incrementando la cuenta de saltos de igual para cada uno.
Tenga en cuenta que printf()
no funcionará después de que se instale el filtro seccomp, porque internamente, la biblioteca de C quiere hacer un fstat
syscall (en salida estándar) y un brk
syscall para asignar algo de memoria para un búfer.