Linux kernel hacking: contenitori di processi/2

Nell’articolo precedente abbiamo analizzato i concetti base dei Linux cgroup. Abbiamo visto un esempio pratico di come lo scheduler CFS possa attuare una distribuzione equa della risorsa CPU tra i vari cgroup. Infine, abbiamo realizzato un cgroup subsystem molto semplice (il noop-cgroup), sfruttando solamente la funzionalità di raggruppamento dei processi.

In questa seconda parte analizzeremo più in dettaglio l’aspetto di programmazione in kernel space, realizzando un cgroup subsystem un po’ più avanzato, interfacciandoci anche con un’altra parte del kernel: le system call.


Overview delle system call in Linux

Le system call costituiscono le interfacce a disposizione dei processi utente per comunicare con il kernel.

Tutte le operazioni che richiedono permessi “privilegiati” non disponibili a livello utente (es. IO su device, creazione di nuovi processi, comunicazioni con altri processi, etc.) richiedono l’invocazione di una system call.

Se la richiesta viene autorizzata, il kernel esegue il codice della system call in modalità privilegiata e ritorna il risultato al processo utente che riprende la normale esecuzione.

Questo concetto è pensato fondamentalmente come meccanismo di sicurezza generico per impedire ai processi di eseguire operazioni comuni di particolare criticità.

Alcune delle system call più note sono le seguenti: open(), read(), write(), close(), wait(), exec(), fork(), exit(), kill(),… Consultate la manpage syscalls(2) tramite man syscalls per avere una lista dettagliata delle system call disponibili in Linux.

L’implementazione del particolare meccanismo di gestione delle system call in Linux varia da architettura ad architettura. Ad esempio, nei vecchi processori x86 la migrazione tra user-space e kernel-space viene effettuata tramite un meccanismo di interrupt software (per la precisione viene invocato l’interrupt 0x80, passando l’ID della system call da invocare per mezzo del registro EAX). Nei più recenti processori IA-32 e x86_64, invece, vengono utilizzare le istruzioni sysenter e sysexit, messe a disposizione dell’architettura stessa.

Il fattore comune del meccanismo di system call in Linux per tutte le architetture è costituito dalla sys_call_table, un vero e proprio array dove ogni generico elemento “i” rappresenta un puntatore alla funzione da eseguire quando viene invocata la system call i-esima.

L’entry point delle system call è costituito dalla funzione system_call(), tipicamente implementata in assembly, per la necessità di dover manipolare direttamente i registri della CPU e lo stack, dipendentemente da ciascuna architettura target.

Definizione del syscall-cgroup

Ogni system call costituisce potenzialmente un punto critico per la sicurezza dell’intero sistema, perché di fatto ogni processo può accedere a tutto ciò che mette a disposizione il kernel-space.

In molti casi potremmo voler restringere il set di system call disponibili per determinati gruppi di processi, cioè realizzare qualcosa di simile ad un “firewall” di system call, che permetta la creazione di vere e proprie “sandbox” sicure in cui confinare i processi potenzialmente insicuri o di cui non ne conosciamo tutti i dettagli (es. closed software o plugin binary only).

Ad esempio, per quale motivo un browser web o un player multimediale devono poter accedere a system call quali sysctl(), mount(), init_module() o addirittura poter aprire socket in listen su un sistema desktop, esponendolo magari a potenziali attacchi esterni?

Da qua l’idea del syscall-cgroup: usiamo i cgroup per definire determinate classi di processi (come abbiamo visto nel precedente noop-cgroup) e aggiungiamo un hook opportuno nella funzione system_call() per controllare se il cgroup che ha invocato la system call è autorizzato o meno ad eseguirla.

Ad ogni cgroup verrà associato un array di N elementi, dove N rappresenta il numero di system call disponibili su Linux. Ogni elemento i-esimo potrà assumere valori “0” o “1” rispettivamente se la system call i-esima è abilitata o disabilitata all’interno del cgroup.

Come interfaccia di configurazione del syscall-cgroup useremo ancora il cgroup filesystem, mettendo a disposizione tre file:

  • syscall.disable: usato per disabilitare le system call;
  • syscall.enable: usato per abilitare le system call;
  • syscall.status: disponibile in sola lettura per ottenere la lista delle system call abilitate o meno.

Per disabilitare una system call nel generico cgroup “foo”, basterà scrivere l’ID della system call nel file foo/syscall.disable.

Ad esempio, per disabilitare la system call 62 (kill):

$ echo 62 | sudo tee /mnt/foo/syscall.disable

Analogamente per riabilitare la medesima system call (sempre nel cgroup “foo”):

$ echo 62 | sudo tee /mnt/foo/syscall.enable

NOTA: possiamo trovare la lista completa di tutti gli ID delle system call nel file /usr/include/asm/unistd_64.h (o /usr/include/asm/unistd_32.h su sistemi a 32-bit).

Infine, leggendo il file syscall.status sarà possibile fare il dump dell’array di configurazione:

$ cat /mnt/foo/syscall.status
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
...
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Dove, ricordiamo, “0” indica che la system call è abilitata, “1” che è disabilitata.

Alcuni potrebbero obiettare del minimalismo eccessivo utilizzato per l’interfaccia di configurazione. Ma ricordiamoci che nella programmazione in kernel-space il minimalismo è un requisito fondamentale. Realizzare funzionalità come il “pretty printing” o la risoluzione di numeri in nomi (come nel caso delle system call) non farebbe altro che aumentare inutilmente la dimensione del kernel, degradando potenzialmente le performance dell’intero sistema. E soprattutto aumenterebbe la probabilità di inserire dei bug nel codice!

Il kernel deve quindi esportare allo user-space delle ABI semplici e minimali; niente ci vieta poi di implementare un’interfaccia di configurazione in user-space più complessa, ad esempio che usi i nomi delle system call al posto degli ID, magari anche una GUI che stampi in verde le system call abilitate e in rosso quelle disabilitate. Lascio questo come esercizio per il lettore. ;-)

Implementazione del syscall-cgroup

Definiti i requisiti del nostro syscall-cgroup vediamo una possibile implementazione commentando passo passo la patch da applicare all’ultimo kernel vanilla disponibile (alla fine dell’articolo è riportata la patch per intero).

Per prima cosa definiamo il nuovo cgroup subsystem nel file cgroup_subsys.h, come avevamo fatto per il noop-cgroup:

—- a/include/linux/cgroup_subsys.h
+++ b/include/linux/cgroup_subsys.h
@@ -59,4 +59,8 @@ SUBSYS(freezer)
 SUBSYS(net_cls)
 #endif

+#ifdef CONFIG_CGROUP_SYSCALL
+SUBSYS(syscall)
+#endif
+
 /* */

E aggiungiamo un parametro alla configurazione del kernel per abilitare o meno tale funzionalità in fase di compilazione:

—- a/init/Kconfig
+++ b/init/Kconfig
@@ -528,6 +528,12 @@ config CGROUP_FREEZER
         Provides a way to freeze and unfreeze all tasks in a
         cgroup.

+config CGROUP_SYSCALL
+       bool "Syscall cgroup subsystem"
+       depends on CGROUPS && FTRACE_SYSCALLS
+       help
+         Provides a way to filter certain system calls in a cgroup.
+
 config CGROUP_DEVICE
       bool "Device controller for cgroups"
       depends on CGROUPS && EXPERIMENTAL

La dipendenza con FTRACE_SYSCALLS è dovuta al fatto che per implementare il meccanismo di filtraggio delle system call ci appoggeremo al meccanismo di tracciamento di queste, già disponibile nei recenti kernel vanilla (inutile reinventare la ruota, ovviamente…).

Passiamo adesso alla parte più corposa, l’implementazione vera e propria del syscall-cgroup:

—- /dev/null
+++ b/kernel/cgroup_syscall.c
@@ -0,0 +1,197 @@
+/*
+ * cgroup_syscall.c
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 021110-1307, USA.
+ *
+ * Copyright (C) 2008 Andrea Righi 
+ */
+
+#include <linux/kernel.h>
+#include <linux/cgroup.h>
+#include <linux/ftrace.h>
+#include <linux/seq_file.h>
+#include <linux/slab.h>
+#include <trace/syscall.h>

Passiamo a definire la dimensione del nostro array per identificare le system call abilitate o meno. Come numero di elementi prendiamo tutte le system call che ftrace riesce a tracciare:

+
+#define CGROUP_SYSCALL_MAX     FTRACE_SYSCALL_MAX
+#define SYSCALL_SIZE           (CGROUP_SYSCALL_MAX * sizeof(int))
+

Definiamo poi la lista di possibili stati delle system call, abilitate o disabilitate, usando una struttura di enum:

+enum {
+       CGROUP_SYSCALL_ENABLE = 0,
+       CGROUP_SYSCALL_DISABLE = 1,
+};
+

E definiamo l’unità base di ogni cgroup che utilizza il syscall-cgroup subsystem. Essa avrà come attributi la struttura di metadati dei cgroup cgroup_subsys_state (come nel noop-cgroup) e in più l’array delle system call, allocato dinamicamente (int *syscall):

+struct syscall_cgroup {
+       struct cgroup_subsys_state css;
+       int *syscall;
+};
+

Definiamo poi due funzioni per ottenere la struttura syscall_cgroup da un task o da un cgroup stesso (la definizione di queste funzioni è abbastanza standard, si vedano anche implementazioni di altri cgroup subsystem ad esempio):

+static struct syscall_cgroup *sys_cgroup_from_task(struct task_struct *task)
+{
+       return container_of(task_subsys_state(task, syscall_subsys_id),
+                               struct syscall_cgroup, css);
+}
+
+static struct syscall_cgroup *sys_cgroup_from_cgroup(struct cgroup *cgrp)
+{
+       return container_of(cgroup_subsys_state(cgrp, syscall_subsys_id),
+                       struct syscall_cgroup, css);
+}
+

Passiamo quindi a definire le funzioni di creazione e rimozione dei vari cgroup relativi al nostro syscall-cgroup subsystem. In fase di creazione di un cgroup (quando facciamo un mkdir nel cgroupfs) verrà istanziata la struct syscall_cgroup e il relativo array di system call, inizializzato tutto a CGROUP_SYSCALL_ENABLE (cioè di default abilitiamo tutte le system call):

+static struct cgroup_subsys_state *
+sys_cgroup_create(struct cgroup_subsys *ss, struct cgroup *cgrp)
+{
+       struct syscall_cgroup *syscg;
+       int i;
+
+       syscg = kzalloc(sizeof(*syscg), GFP_KERNEL);
+       if (unlikely(!syscg))
+               return ERR_PTR(-ENOMEM);
+       syscg->syscall = kmalloc(SYSCALL_SIZE, GFP_KERNEL);
+       if (!syscg->syscall) {
+               kfree(syscg);
+               return ERR_PTR(-ENOMEM);
+       }
+       for (i = 0; i < CGROUP_SYSCALL_MAX; i++)
+               syscg->syscall[i] = CGROUP_SYSCALL_ENABLE;
+       return &syscg->css;
+}
+

In fase di rimozione di un cgroup (lanciando un rmdir sulla directory nel cgroupfs) molto banalmente verranno liberati l’array delle system call e la struttura del syscall_cgroup stesso, allocate precedentemente in fase di creazione:

+static void sys_cgroup_destroy(struct cgroup_subsys *ss, struct cgroup *cgrp)
+{
+       struct syscall_cgroup *syscg = sys_cgroup_from_cgroup(cgrp);
+
+       kfree(syscg->syscall);
+       kfree(syscg);
+}
+

Passiamo adesso all’interfaccia verso lo user-space, definenedo i file syscall.* che compariranno nel cgroupfs e le relative operazioni di lettura e scrittura.

La sys_cgroup_read() verrà invocata quando leggiamo il file syscall.status, che si occuperà del dump dell’array corrispondente al relativo cgroup.

La sys_cgroup_write() si occuperà di modificare la configurazione dell’array delle system call, dipendentemente dal numero che verrà scritto e dal file syscall.enable o syscall.disable, identificati rispettivamente dall’attributo .private:

+static struct cftype sys_cgroup_files[] =  {
+       {
+               .name = "enable",
+               .write_u64 = sys_cgroup_write,
+               .private = CGROUP_SYSCALL_ENABLE,
+       },
+       {
+               .name = "disable",
+               .write_u64 = sys_cgroup_write,
+               .private = CGROUP_SYSCALL_DISABLE,
+       },
+       {
+               .name = "status",
+               .read_seq_string = sys_cgroup_read,
+       },
+};
+

Di seguito viene riportata l’implementazione delle due funzioni di lettura e scrittura:

+static int
+sys_cgroup_read(struct cgroup *cgrp, struct cftype *cft, struct seq_file *m)
+{
+       struct syscall_cgroup *syscg;
+       int i;
+
+       syscg = sys_cgroup_from_cgroup(cgrp);

Per il dump dell’array useremo le funzioni seq_printf() e seq_putc() che corrispondono alle analoghe fprintf() e fputc() in user-space, accettando uno stream seq_file come primo argomento:

+       for (i = 0; i < CGROUP_SYSCALL_MAX; i++)
+               seq_printf(m, "%d ", syscg->syscall[i]);
+       seq_putc(m, '\n');
+       return 0;
+}
+
+static int sys_cgroup_write(struct cgroup *cgrp, struct cftype *cft, u64 nr)
+{
+       struct syscall_cgroup *syscg = sys_cgroup_from_cgroup(cgrp);
+

Per la scrittura provvederemo a fare dei controlli preliminari sulla validità del system call ID inserito (cioè che rientri nel range delle system call valide) e impediremo il blocco delle system call per il root cgroup, onde evitare che processi di sistema critici quali “init”, “udev” o “dbus” vengano bloccati, compromettendo le funzionalità dell’intero sistema.

+       if (nr >= CGROUP_SYSCALL_MAX)
+               return -EINVAL;
+       /* It is safer to avoid syscall filtering for the root cgroup... */
+       if (cgrp->parent == NULL)
+               return -EPERM;

Infine, provvederemo ad “accendere” o “spegnere” la system call a seconda che la scrittura sia avvenuta nel file syscall.enable o syscall.disable (identificato dall’attributo cft->private).

+       if (!cgroup_lock_live_group(cgrp))
+               return -ENODEV;
+       switch (cft->private) {
+       case CGROUP_SYSCALL_ENABLE:
+               syscg->syscall[nr] = CGROUP_SYSCALL_ENABLE;
+               break;
+       case CGROUP_SYSCALL_DISABLE:
+               syscg->syscall[nr] = CGROUP_SYSCALL_DISABLE;
+               break;
+       default:
+               WARN_ON(1);
+               break;
+       }
+       cgroup_unlock();
+       return 0;
+}
+

Per tutti i task che vengono spostati in un cgroup diverso dal root cgroup dovrà poi essere abilitato obbligatoriamente il tracciamento di tutte le system call per poter attuare la nostra politica di filtering.

Per fare ciò definiamo le funzioni sys_cgroup_attach_task() e sys_cgroup_fork_task(), invocate tutte le volte che viene collegato o forkato un nuovo processo dentro un qualsiasi cgroup.

Le funzioni provvederanno semplicemente a settare il bit TIF_SYSCALL_TRACE all’interno della task_struct del nuovo thread, affinché per ogni system call venga invocata la routine di tracing, dove inseriremo il nostro hook.

+static void
+sys_cgroup_attach_task(struct cgroup_subsys *ss, struct cgroup *cgrp,
+                       struct cgroup *old_cgrp, struct task_struct *task)
+{
+       if (cgrp->parent)
+               set_tsk_thread_flag(task, TIF_SYSCALL_TRACE);
+}
+
+static void
+sys_cgroup_fork_task(struct cgroup_subsys *ss, struct task_struct *task)
+{
+       struct cgroup *cgrp;
+
+       rcu_read_lock();
+       cgrp = sys_cgroup_from_task(task)->css.cgroup;
+       if (cgrp->parent)
+               set_tsk_thread_flag(task, TIF_SYSCALL_TRACE);
+       rcu_read_unlock();
+}
+

Infine l’ultima funzione che ci serve per il nostro cgroup è la sys_cgroup_populate(), per popolare il cgroupfs con i nostri file di configurazione syscall.*:

+static int sys_cgroup_populate(struct cgroup_subsys *ss, struct cgroup *cgrp)
+{
+       return cgroup_add_files(cgrp, ss, sys_cgroup_files,
+                               ARRAY_SIZE(sys_cgroup_files));
+}
+

Abbiamo così concluso l’implementazione del cgroup subsystem vero e proprio e possiamo dichiarare la struttura con i rispettivi attributi e i metodi definiti sopra:

+struct cgroup_subsys syscall_subsys = {
+       .name = "syscall",
+       .subsys_id = syscall_subsys_id,
+       .early_init = 0,
+       .create = sys_cgroup_create,
+       .destroy = sys_cgroup_destroy,
+       .attach = sys_cgroup_attach_task,
+       .fork = sys_cgroup_fork_task,
+       .populate = sys_cgroup_populate,
+};
+

Quello che resta da fare è definire un hook da mettere nella funzione di tracciamento delle system call per attuare la policy di filtraggio.

A tal proposito definiamo la cgroup_syscall_check() che accetta come argomento il numero della system call invocata, controlla se essa è autorizzata o meno, consultando l’elemento corrispondente nell’array delle system call del cgroup a cui afferisce il task corrente (current), e ritorna rispettivamente “0” in caso di successo, o un codice di errore se la system call deve essere bloccata:

+int cgroup_syscall_check(int nr)
+{
+       struct syscall_cgroup *syscg;
+       int ret = 0;
+
+       if ((nr >= CGROUP_SYSCALL_MAX) || (nr < 0))
+               return -ENOSYS;
+
+       rcu_read_lock();
+       syscg = sys_cgroup_from_task(current);
+       if (unlikely(syscg->syscall[nr] == CGROUP_SYSCALL_DISABLE)) {

Oltre a ritornare il codice di errore vogliamo anche riportare nei log del kernel la storia di tutte le system call bloccate con le informazioni del processo e del cgroup che le hanno generate:

+               struct cgroup *cgrp = syscg->css.cgroup;
+               char path[128];
+
+               cgroup_path(cgrp, path, sizeof(path));
+               rcu_read_unlock();
+               printk(KERN_WARNING
+                       "blocked syscall %d for task %d (%s) in cgroup %s\n",
+                       nr, current->pid, current->comm, path);
+               ret = -EPERM;
+       } else
+               rcu_read_unlock();
+       return ret;
+}

Definiamo poi l’header file che conterrà il nostro hook, da utilizzare all’esterno dell’implementazione del cgroup subsystem, più precisamente nella funzione di tracciamento delle system call.

NOTA: se il cgroup-syscall non viene abilitato in fase di compilazione (parametro CONFIG_CGROUP_SYSCALL) il nostro hook ritornerà sempre “allow” per qualsiasi system call.

—- /dev/null
+++ b/include/linux/cgroup_syscall.h
@@ -0,0 +1,12 @@
+#ifndef CGROUP_SYSCALL_H
+#define CGROUP_SYSCALL_H
+
+#ifdef CONFIG_CGROUP_SYSCALL
+int cgroup_syscall_check(int nr);
+#else /* CONFIG_CGROUP_SYSCALL */
+static inline int cgroup_syscall_check(int nr)
+{
+       return 0;
+}
+#endif /* CONFIG_CGROUP_SYSCALL */
+#endif /* CGROUP_SYSCALL_H */

Fatto. Resta solo da piazzare l’hook nel posto giusto. In questo caso il “posto giusto” è costituito dalla funzione syscall_trace_enter(), invocata prima dell’esecuzione del codice di ogni system call vera e propria.

Se vi ricordate all’inizio dell’articolo abbiamo detto che ogni architettura utilizza una propria implementazione per la gestione delle system call, di conseguenza non ci sarà un unico “posto giusto” in cui piazzare l’hook, ma ce ne sarà uno per ciascuna implementazione della syscall_trace_enter().

Per semplicità ci limiteremo ad analizzare il caso delle architetture x86 (e x86_64):

—- a/arch/x86/kernel/ptrace.c
+++ b/arch/x86/kernel/ptrace.c
@@ -20,6 +20,7 @@
 #include <linux/security.h>
 #include <linux/audit.h>
 #include <linux/seccomp.h>
+#include <linux/cgroup_syscall.h>
 #include <linux/signal.h>

 #include <asm/uaccess.h>
@@ -1417,6 +1418,10 @@ asmregparm long syscall_trace_enter(struct pt_regs *regs)
           tracehook_report_syscall_entry(regs))
               ret = -1L;

+       /* Apply cgroup syscall filtering */
+       if (!ret)
+               ret = cgroup_syscall_check(regs->orig_ax);
+
       if (unlikely(test_thread_flag(TIF_SYSCALL_FTRACE)))
               ftrace_syscall_enter(regs);

Sempre all’inizio dell’articolo abbiamo detto che il system call ID nelle architetture x86* viene passato tramite il registro EAX (identificato dall’atributo orig_ax della struttura regs).

Il syscall-cgroup è pronto per essere testato. Resta da ricompilare il kernel abilitando l’opzione CONFIG_CGROUP_SYSCALL, come abbiamo visto nell’articolo precedente.

Disclaimer: utilizzare un proprio kernel ricompilato è sempre un’operazione piuttosto rischiosa, se si fanno degli errori si possono generare fastidiosi freeze di sistema o comprometterne addirittura l’utilizzo. Per questo motivo si consiglia di usare una tecnologia di virtualizzazione, ad esempio Qemu o KVM (anche l’autore ne fa ampio uso) prima di passare ad ambienti di “produzione” veri e propri.

Il syscall-cgroup in azione

Per validare l’efficacia del syscall-cgroup facciamo un semplice esempio creando una sandbox “client_sandbox” in cui viene filtrata la system call listen(). In questa sandbox non sarà possibile tirare su servizi di rete.

Montiamo il cgroupfs abilitando il syscall-cgroup:

$ sudo mount -cgroup -osyscall none /mnt

Creiamo il cgroup “client_sandbox” dove impediremo l’utilizzo della system call listen() per evitare che gli utenti tirino su dei servizi di rete:

$ sudo mkdir /mnt/client_sandbox

Supponiamo di trovarci in una macchina a 64-bit, e reperiamo dal file /usr/include/asm/unistd_64.h l’ID della system call listen():

$ grep NR_listen /usr/include/asm/unistd_64.h
 #define __NR_listen                             50
 __SYSCALL(__NR_listen, sys_listen)

Disabilitiamo la system call in questione:

$ echo 50 | sudo tee /mnt/client_sandbox/syscall.disable

Controlliamo che la configurazione sia avvenuta corretamente (ricordiamoci che le system call partano da “0”, mentre per il comando “cut” gli elementi partano da “1”):

$ cut -d' ' -f51 /mnt/client_sandbox/syscall.status
1

Spostiamo la shell corrente dentro il cgroup “client_sandbox”:

$ echo $$ | sudo tee /mnt/client_sandbox/tasks

Possiamo a questo punto verificare l’efficacia del nostro syscall-cgroup, ad esempio utilizzando il comando nc (o netcat in alcune distribuzioni), che serve a creare socket, sia lato client che lato server (come riportato dalla manpage netcat è il coltellino svizzero del TCP/IP).

Verificare le funzionalità dei socket lato client, collegandoci al demone sshd in locale:

$ nc localhost 22
SSH-2.0-OpenSSH_5.1p1 Debian-5ubuntu1
^C

La connessione è avvenuta correttamente dato che lato client la system call listen() non viene utilizzata (possiamo controllare ciò dall’output del comando “strace -f nc localhost 22″):

...
socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
rt_sigaction(SIGALRM, {0x402020, [ALRM], SA_RESTORER|SA_RESTART, 0x7ffcaaea7040}, {SIG_DFL}, 8) = 0
alarm(1)                                = 0
rt_sigprocmask(SIG_BLOCK, NULL, [], 8)  = 0
connect(3, {sa_family=AF_INET, sin_port=htons(22), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
rt_sigaction(SIGALRM, {SIG_IGN}, {0x402020, [ALRM], SA_RESTORER|SA_RESTART, 0x7ffcaaea7040}, 8) = 0
alarm(0)                                = 1
select(4, [0 3], NULL, NULL, {1, 0})    = 1 (in [3], left {0, 996010})
read(3, "SSH-2.0-OpenSSH_5.1p1 Debian-5ubu"..., 8192) = 39
write(1, "SSH-2.0-OpenSSH_5.1p1 Debian-5ubu"..., 39SSH-2.0-OpenSSH_5.1p1 Debian-5ubuntu1) = 39
...

Osserviamo invece cosa succede se proviamo a tirare su un servizio di rete, esempio aprendo un socket in listen sulla porta 8080:

$ nc -l localhost 8080
local listen fuxored : Function not implemented

Ed ecco il dettaglio di ciò che succede (notare il -ENOSYS restituito dalla listen):

...
socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 1)                      = -1 ENOSYS (Function not implemented)
write(2, "local listen fuxored"..., 20) = 20
write(2, " : Function not implemented\n"..., 28) = 28
close(4294967295)                 = -1 EBADF (Bad file descriptor)
exit_group(1)                     = ?

Se invece spostiamo il task in un cgroup in cui la listen() è autorizzata (es. il root cgroup), esso può tranquillamente avviare il servizio sulla porta 8080:

$ echo $$ | sudo tee /mnt/tasks
$ strace -f nc -l localhost 8080
...
socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 1)                            = 0
rt_sigaction(SIGALRM, {SIG_IGN}, {SIG_DFL}, 8) = 0
alarm(0)                                = 0
rt_sigprocmask(SIG_BLOCK, NULL, [], 8)  = 0
accept(3, ...

Il nostro firewall di system call sembra funzionare correttamente!

Conclusioni

In questa seconda parte dell’articolo sui Linux cgroup abbiamo analizzato l’implementazione di un cgroup subsystem completo.

Sfruttando le funzionalità di raggruppamento dei processi fornite dal framework dei cgroup e mettendo un hook opportuno nel kernel abbiamo visto come sia possibile implementare un vero e proprio firewall di system call che permetta la creazione di “sandbox” protette in cui far girare processi potenzialmente insicuri.

Un possibile sviluppo futuro potrebbe essere quello di analizzare anche gli argomenti delle system call, oltre all’ID, in modo da poter definire regole di filtering più complesse (es. autorizzare la open() solo su un certo filesystem o la listen() solo su un certo range di porte).

Ancora una volta abbiamo visto come l’infrastruttura dei cgroup consenta di creare funzionalità molto evolute con poche righe di codice… e tanta fantasia! :)

cgroup-syscall.patch

 arch/x86/kernel/ptrace.c       |    5 +
 include/linux/cgroup_subsys.h  |    4 +
 include/linux/cgroup_syscall.h |   12 +++
 init/Kconfig                   |    6 +
 kernel/Makefile                |    1 +
 kernel/cgroup_syscall.c        |  197 ++++++++++++++++++++++++++++++++++++++++
 6 files changed, 225 insertions(+), 0 deletions(-)

diff —git a/arch/x86/kernel/ptrace.c b/arch/x86/kernel/ptrace.c
index 23b7c8f..cb099d9 100644
—- a/arch/x86/kernel/ptrace.c
+++ b/arch/x86/kernel/ptrace.c
@@ -20,6 +20,7 @@
 #include <linux/security.h>
 #include <linux/audit.h>
 #include <linux/seccomp.h>
+#include <linux/cgroup_syscall.h>
 #include <linux/signal.h>

 #include <asm/uaccess.h>
@@ -1417,6 +1418,10 @@ asmregparm long syscall_trace_enter(struct pt_regs *regs)
           tracehook_report_syscall_entry(regs))
               ret = -1L;

+       /* Apply cgroup syscall filtering */
+       if (!ret)
+               ret = cgroup_syscall_check(regs->orig_ax);
+
       if (unlikely(test_thread_flag(TIF_SYSCALL_FTRACE)))
               ftrace_syscall_enter(regs);

diff —git a/include/linux/cgroup_subsys.h b/include/linux/cgroup_subsys.h
index 9c8d31b..529531c 100644
—- a/include/linux/cgroup_subsys.h
+++ b/include/linux/cgroup_subsys.h
@@ -59,4 +59,8 @@ SUBSYS(freezer)
 SUBSYS(net_cls)
 #endif

+#ifdef CONFIG_CGROUP_SYSCALL
+SUBSYS(syscall)
+#endif
+
 /* */
diff —git a/include/linux/cgroup_syscall.h b/include/linux/cgroup_syscall.h
new file mode 100644
index 0000000..66ae1d6
—- /dev/null
+++ b/include/linux/cgroup_syscall.h
@@ -0,0 +1,12 @@
+#ifndef CGROUP_SYSCALL_H
+#define CGROUP_SYSCALL_H
+
+#ifdef CONFIG_CGROUP_SYSCALL
+int cgroup_syscall_check(int nr);
+#else /* CONFIG_CGROUP_SYSCALL */
+static inline int cgroup_syscall_check(int nr)
+{
+       return 0;
+}
+#endif /* CONFIG_CGROUP_SYSCALL */
+#endif /* CGROUP_SYSCALL_H */
diff —git a/init/Kconfig b/init/Kconfig
index 7be4d38..4df1e96 100644
—- a/init/Kconfig
+++ b/init/Kconfig
@@ -528,6 +528,12 @@ config CGROUP_FREEZER
         Provides a way to freeze and unfreeze all tasks in a
         cgroup.

+config CGROUP_SYSCALL
+       bool "Syscall cgroup subsystem"
+       depends on CGROUPS && FTRACE_SYSCALLS
+       help
+         Provides a way to filter certain system calls in a cgroup.
+
 config CGROUP_DEVICE
       bool "Device controller for cgroups"
       depends on CGROUPS && EXPERIMENTAL
diff —git a/kernel/Makefile b/kernel/Makefile
index 4242366..86ab09a 100644
—- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -61,6 +61,7 @@ obj-$(CONFIG_CGROUP_DEBUG) += cgroup_debug.o
 obj-$(CONFIG_CGROUP_FREEZER) += cgroup_freezer.o
 obj-$(CONFIG_CPUSETS) += cpuset.o
 obj-$(CONFIG_CGROUP_NS) += ns_cgroup.o
+obj-$(CONFIG_CGROUP_SYSCALL) += cgroup_syscall.o
 obj-$(CONFIG_UTS_NS) += utsname.o
 obj-$(CONFIG_USER_NS) += user_namespace.o
 obj-$(CONFIG_PID_NS) += pid_namespace.o
diff —git a/kernel/cgroup_syscall.c b/kernel/cgroup_syscall.c
new file mode 100644
index 0000000..fd795c0
—- /dev/null
+++ b/kernel/cgroup_syscall.c
@@ -0,0 +1,197 @@
+/*
+ * cgroup_syscall.c
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 021110-1307, USA.
+ *
+ * Copyright (C) 2008 Andrea Righi 
+ */
+
+#include <linux/kernel.h>
+#include <linux/cgroup.h>
+#include <linux/ftrace.h>
+#include <linux/seq_file.h>
+#include <linux/slab.h>
+#include <trace/syscall.h>
+
+#define CGROUP_SYSCALL_MAX     FTRACE_SYSCALL_MAX
+#define SYSCALL_SIZE           (CGROUP_SYSCALL_MAX * sizeof(int))
+
+enum {
+       CGROUP_SYSCALL_ENABLE = 0,
+       CGROUP_SYSCALL_DISABLE = 1,
+};
+
+struct syscall_cgroup {
+       struct cgroup_subsys_state css;
+       int *syscall;
+};
+
+static struct syscall_cgroup *sys_cgroup_from_task(struct task_struct *task)
+{
+       return container_of(task_subsys_state(task, syscall_subsys_id),
+                               struct syscall_cgroup, css);
+}
+
+static struct syscall_cgroup *sys_cgroup_from_cgroup(struct cgroup *cgrp)
+{
+       return container_of(cgroup_subsys_state(cgrp, syscall_subsys_id),
+                       struct syscall_cgroup, css);
+}
+
+static struct cgroup_subsys_state *
+sys_cgroup_create(struct cgroup_subsys *ss, struct cgroup *cgrp)
+{
+       struct syscall_cgroup *syscg;
+       int i;
+
+       syscg = kzalloc(sizeof(*syscg), GFP_KERNEL);
+       if (unlikely(!syscg))
+               return ERR_PTR(-ENOMEM);
+       syscg->syscall = kmalloc(SYSCALL_SIZE, GFP_KERNEL);
+       if (!syscg->syscall) {
+               kfree(syscg);
+               return ERR_PTR(-ENOMEM);
+       }
+       for (i = 0; i < CGROUP_SYSCALL_MAX; i++)
+               syscg->syscall[i] = CGROUP_SYSCALL_ENABLE;
+       return &syscg->css;
+}
+
+static void sys_cgroup_destroy(struct cgroup_subsys *ss, struct cgroup *cgrp)
+{
+       struct syscall_cgroup *syscg = sys_cgroup_from_cgroup(cgrp);
+
+       kfree(syscg->syscall);
+       kfree(syscg);
+}
+
+static int
+sys_cgroup_read(struct cgroup *cgrp, struct cftype *cft, struct seq_file *m)
+{
+       struct syscall_cgroup *syscg;
+       int i;
+
+       syscg = sys_cgroup_from_cgroup(cgrp);
+       for (i = 0; i < CGROUP_SYSCALL_MAX; i++)
+               seq_printf(m, "%d ", syscg->syscall[i]);
+       seq_putc(m, '\n');
+       return 0;
+}
+
+static int sys_cgroup_write(struct cgroup *cgrp, struct cftype *cft, u64 nr)
+{
+       struct syscall_cgroup *syscg = sys_cgroup_from_cgroup(cgrp);
+
+       if (nr >= CGROUP_SYSCALL_MAX)
+               return -EINVAL;
+       /* It is safer to avoid syscall filtering for the root cgroup... */
+       if (cgrp->parent == NULL)
+               return -EPERM;
+       if (!cgroup_lock_live_group(cgrp))
+               return -ENODEV;
+       switch (cft->private) {
+       case CGROUP_SYSCALL_ENABLE:
+               syscg->syscall[nr] = CGROUP_SYSCALL_ENABLE;
+               break;
+       case CGROUP_SYSCALL_DISABLE:
+               syscg->syscall[nr] = CGROUP_SYSCALL_DISABLE;
+               break;
+       default:
+               WARN_ON(1);
+               break;
+       }
+       cgroup_unlock();
+       return 0;
+}
+
+static void
+sys_cgroup_attach_task(struct cgroup_subsys *ss, struct cgroup *cgrp,
+                       struct cgroup *old_cgrp, struct task_struct *task)
+{
+       if (cgrp->parent)
+               set_tsk_thread_flag(task, TIF_SYSCALL_TRACE);
+}
+
+static void
+sys_cgroup_fork_task(struct cgroup_subsys *ss, struct task_struct *task)
+{
+       struct cgroup *cgrp;
+
+       rcu_read_lock();
+       cgrp = sys_cgroup_from_task(task)->css.cgroup;
+       if (cgrp->parent)
+               set_tsk_thread_flag(task, TIF_SYSCALL_TRACE);
+       rcu_read_unlock();
+}
+
+static struct cftype sys_cgroup_files[] =  {
+       {
+               .name = "enable",
+               .write_u64 = sys_cgroup_write,
+               .private = CGROUP_SYSCALL_ENABLE,
+       },
+       {
+               .name = "disable",
+               .write_u64 = sys_cgroup_write,
+               .private = CGROUP_SYSCALL_DISABLE,
+       },
+       {
+               .name = "status",
+               .read_seq_string = sys_cgroup_read,
+       },
+};
+
+static int sys_cgroup_populate(struct cgroup_subsys *ss, struct cgroup *cgrp)
+{
+       return cgroup_add_files(cgrp, ss, sys_cgroup_files,
+                               ARRAY_SIZE(sys_cgroup_files));
+}
+
+struct cgroup_subsys syscall_subsys = {
+       .name = "syscall",
+       .subsys_id = syscall_subsys_id,
+       .early_init = 0,
+       .create = sys_cgroup_create,
+       .destroy = sys_cgroup_destroy,
+       .attach = sys_cgroup_attach_task,
+       .fork = sys_cgroup_fork_task,
+       .populate = sys_cgroup_populate,
+};
+
+int cgroup_syscall_check(int nr)
+{
+       struct syscall_cgroup *syscg;
+       int ret = 0;
+
+       if ((nr >= CGROUP_SYSCALL_MAX) || (nr < 0))
+               return -ENOSYS;
+
+       rcu_read_lock();
+       syscg = sys_cgroup_from_task(current);
+       if (unlikely(syscg->syscall[nr] == CGROUP_SYSCALL_DISABLE)) {
+               struct cgroup *cgrp = syscg->css.cgroup;
+               char path[128];
+
+               cgroup_path(cgrp, path, sizeof(path));
+               rcu_read_unlock();
+               printk(KERN_WARNING
+                       "blocked syscall %d for task %d (%s) in cgroup %s\n",
+                       nr, current->pid, current->comm, path);
+               ret = -EPERM;
+       } else
+               rcu_read_unlock();
+       return ret;
+}

Comments

  1. Ottima continuazione del primo articolo, Andrea. Davvero molto interessante e utile.

Policy per i commenti: Apprezzo moltissimo i vostri commenti, critiche incluse. Per evitare spam e troll, e far rimanere il discorso civile, i commenti sono moderati e prontamente approvati poco dopo il loro invio.