Post

Partie 26 - Seccomp - fonctionnement (1/2)

Seccomp : fonctionnement (1/2)

Avoir la possibilitĂ© d’exĂ©cuter un shellcode dans un programme exploitĂ© permet d’avoir accĂšs Ă  un trĂšs grand nombre d’actions possibles :

  • ouvrir un terminal ;
  • gĂ©rer des connexions rĂ©seau ;
  • ouvrir, lire et Ă©crire des fichiers ;
  • et bien plus encore.

Une idĂ©e qui peut rapidement venir Ă  l’esprit est la suivante : mon programme “Hello world” ne fait qu’afficher une chaĂźne de caractĂšres. Pourquoi ne pas restreindre les appels systĂšme autorisĂ©s Ă  read et write uniquement ?

Eh bien c’est justement ce que permet de faire seccomp !

Qu’est-ce que seccomp ?

seccomp signifie SECCure COMPuting (informatique sĂ©curisĂ©e đŸ„–). Il s’agit d’un mĂ©canisme disponible sous Linux qui permet de filtrer les appels systĂšme accessibles depuis une application.

Il y a principalement trois maniĂšres de le mettre en place :

  • bloquer certains appels systĂšme (liste noire) ;
  • autoriser certains appels systĂšme (liste blanche) ;
  • filtrer certains appels systĂšme en fonction de leurs arguments.

seccomp est une fonctionnalité assez ancienne mais qui reste utilisée dans des applications qui nécessitent une sécurité accrue :

Contrairement Ă  ce que l’on pourrait penser, seccomp n’est pas un mĂ©canisme de cloisonnement (sandboxing) Ă  part entiĂšre.

Il se limite Ă  restreindre les appels systĂšme qu’un programme est autorisĂ© Ă  invoquer, en appliquant un filtrage au niveau du noyau, sans fournir Ă  lui seul d’isolation complĂšte de l’environnement d’exĂ©cution.

Comment cela fonctionne ?

🔒 Le mode strict (SECCOMP_SET_MODE_STRICT)

Initialement seccomp limitait les appels systÚme que peut exécuter un programme aux quatre suivants :

  • read ;
  • write;
  • exit ;
  • sigreturn.

Si le programme exécute un autre appel systÚme que ceux-là, le signal SIGKILL sera envoyé au programme.

Ah ouais c’est la hess 😆 !

Eh oui 
 Le programme ouvre un fichier ? ⛔. Le programme utilise des sockets rĂ©seau ? ⛔. Ainsi, Ă  part un programme bateau type “Hello World”, on ne peut pas faire grand chose 😕. SeccompliquĂ© cette histoire 


📝 Le mode filtrage (SECCOMP_SET_MODE_FILTER)

Bon je vous rassure, le prĂ©cĂ©dent mode n’est (quasiment ?) plus utilisĂ©. A la place on utilise plutĂŽt le mode filtrage qui permet bien plus de flexibilitĂ©.

Ce mode permet de dĂ©finir des rĂšgles prĂ©cises indiquant quels appels systĂšme sont autorisĂ©s, refusĂ©s ou bloquĂ©s, ce qui rend seccomp plus adaptĂ© aux besoins d’une application.

Pour écrire une liste de rÚgles nous utilisons des BPF ou Filtre de Paquets de Berkeley.

Et 
 le rapport avec Berkeley ?

Le nom Berkeley Packet Filter (BPF) tire son nom de l’UniversitĂ© de Californie Ă  Berkeley, oĂč il a Ă©tĂ© dĂ©veloppĂ© Ă  la fin des annĂ©es 1980. À l’origine, il a Ă©tĂ© conçu par des chercheurs de Berkeley pour filtrer efficacement les paquets rĂ©seau sans avoir Ă  les copier inutilement entre le noyau et l’espace utilisateur. Le terme Berkeley fait donc rĂ©fĂ©rence au lieu de conception du projet, et non Ă  une technologie rĂ©seau en particulier đŸ€–.

De toute façon, la maniĂšre dont ces filtres sont mis en place ne nous intĂ©resse pas particuliĂšrement, puisque nous ne sommes pas dans un cours de dĂ©veloppement sĂ©curisĂ©. En revanche, ce qui nous importe rĂ©ellement est de savoir comment dĂ©terminer, lors de l’analyse d’un programme, si l’exĂ©cution de tel ou tel appel systĂšme est autorisĂ©e ou non.

📝 Les rùgles de filtrage

Pour ce faire, nous allons utiliser l’outil seccomp-tools qui permet notamment d’extraire les rĂšgles de filtrage BPF mises en place dans un programme en l’exĂ©cutant via l’option dump.

Si vous manipulez les programmes de ce chapitre via le conteneur Docker, l’outil seccomp-tools y est dĂ©jĂ  installĂ©.

Si vous ne souhaitez pas exĂ©cuter le programme dont les rĂšgles de filtrage sont Ă  extraire, il est possible d’utiliser l’option disasm. NĂ©anmoins, il faudra extraire en amont le filtre BPF brut du programme.

Exemple de test

Prenons un exemple concret pour comprendre comment lire ces filtres. Voici le programme que nous utiliserons dans cet exemple :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <stddef.h>

/* Macros utilitaires simples */
#define SC_ALLOW(syscall) \
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, syscall, 0, 1), \
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)

#define SC_KILL \
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS)


int install_seccomp(void)
{
    struct sock_filter filter[] = {
        /* Chargement du numero de l’appel systeme */
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
                 offsetof(struct seccomp_data, nr)),

        /* Appels systeme autorises */
        SC_ALLOW(__NR_read),
        SC_ALLOW(__NR_write),
        SC_ALLOW(__NR_open),

        /* Gestion de la memoire (maximum 3) */
        SC_ALLOW(__NR_brk),
        SC_ALLOW(__NR_mmap),
        SC_ALLOW(__NR_munmap),

        /* Tout le reste est interdit */
        SC_KILL,
    };

    struct sock_fprog prog = {
        .len = sizeof(filter) / sizeof(filter[0]),
        .filter = filter,
    };

    /* Obligatoire pour installer un filtre seccomp */
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
        perror("prctl(NO_NEW_PRIVS)");
        return -1;
    }

    if (syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog)) {
        perror("seccomp");
        return -1;
    }

    return 0;
}

int main(void) {
    printf("[+] Installation du filtre seccomp\n");

    if (install_seccomp() < 0) {
        fprintf(stderr, "[-] Echec de l’installation de seccomp\n");
        exit(1);
    }

    printf("[+] Seccomp installe\n");
    printf("[+] Tentative d’execution de execve(\"/bin/sh\")...\n");

    char *argv[] = {"/bin/sh", NULL};
    char *envp[] = {NULL};

    execve("/bin/sh", argv, envp);

    /* Cette ligne ne sera JAMAIS atteinte */
    perror("execve");
    return 0;
}

Il n’est pas nĂ©cessaire de s’intĂ©resser Ă  la fonction install_seccomp. Ce programme implĂ©mente un filtre BPF grĂące Ă  seccomp via syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog).

Normalement les commentaires ont dĂ» vous aider Ă  comprendre ce que fait le programme :

  • autoriser seulement 6 appels systĂšme ;
  • tous les autres appels systĂšme sont interdits.

AccĂšs au conteneur Docker :

1
2
3
docker build -t pwn-seccomp-exemple-1 .

docker run -it --rm -p 1234:1234 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined pwn-seccomp-exemple-1

Compilons le programme avec gcc main.c -o exe et exécutons-le :

1
2
3
4
5
6
$ ./exe

[+] Installation du filtre seccomp
[+] Seccomp installe
[+] Tentative d’execution de execve("/bin/sh")...
[1]    52597 invalid system call (core dumped)  ./exe

On pouvait s’y attendre : execve n’est pas exĂ©cutĂ©. Le code de retour est le suivant :

1
2
echo $?
159

Or 159 == 128 + 31 oĂč 31 est le signum de SIGSYS: Bad system call.

Bah là c’est trivial de comprendre le filtre pourquoi a-t-on besoin d’utiliser un outil tel que seccomp-tools alors ?

Ici nous avons accĂšs au code source. Avez-vous jetĂ© un Ɠil Ă  ce que donne la dĂ©compilation de install_seccomp đŸ€Ż ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
__int64 __fastcall install_seccomp()
{
  __int64 v7; // [rsp+0h] [rbp-90h] BYREF
  // (...)
  unsigned __int64 v64; // [rsp+88h] [rbp-8h]

  v64 = __readfsqword(0x28u);
  
  v8 = 32;
  v9 = 0;
  v10 = 0;
  v11 = 0;
  v12 = 21;
  v13 = 0;
  v14 = 1;
  v15 = 0;
  v16 = 6;
  v17 = 0;
  v18 = 0;
  v19 = 0x7FFF0000;
  v20 = 21;
  v21 = 0;
  v22 = 1;
  v23 = 1;
  v24 = 6;
  v25 = 0;
  v26 = 0;
  v27 = 0x7FFF0000;
  v28 = 21;
  v29 = 0;
  v30 = 1;
  v31 = 2;
  v32 = 6;
  v33 = 0;
  v34 = 0;
  v35 = 0x7FFF0000;
  v36 = 21;
  v37 = 0;
  v38 = 1;
  v39 = 12;
  v40 = 6;
  v41 = 0;
  v42 = 0;
  v43 = 0x7FFF0000;
  v44 = 21;
  v45 = 0;
  v46 = 1;
  v47 = 9;
  v48 = 6;
  v49 = 0;
  v50 = 0;
  v51 = 0x7FFF0000;
  v52 = 21;
  v53 = 0;
  v54 = 1;
  v55 = 11;
  v56 = 6;
  v57 = 0;
  v58 = 0;
  v59 = 0x7FFF0000;
  v60 = 6;
  v61 = 0;
  v62 = 0;
  v63 = 0x80000000;
  LOWORD(v7) = 14;
  if ( prctl(38, 1, 0, 0, 0, a6, v7, &v8) )
  {
    perror("prctl(NO_NEW_PRIVS)");
    return 0xFFFFFFFFLL;
  }
  else if ( syscall(317, 1, 0, &v7) )
  {
    perror("seccomp");
    return 0xFFFFFFFFLL;
  }
  else
  {
    return 0;
  }
}

Je pense que l’on sera d’accord sur le fait qu’un outil permettant de les extraire automatiquement ne serait pas de refus 😉.

prctl(PR_SET_NO_NEW_PRIVS, (...)) doit toujours ĂȘtre appelĂ© avant d’appliquer le filtre seccomp.

L’une des autres consĂ©quence importante de PR_SET_NO_NEW_PRIVS est que le processus courant (et ses potentiels processus fils) ne pourront pas gagner plus de privilĂšges qui en avaient au moment de l’appel de prctl(PR_SET_NO_NEW_PRIVS, (...)).

Cela signifie notamment que les bits SUID et SGID seront ignorĂ©s. RIP 🙃.

Utilisons la commande seccomp-tools dump ./exe pour extraire le filtrage mis en place :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ seccomp-tools dump ./exe
[+] Installation du filtre seccomp
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000000  A = sys_number
 0001: 0x15 0x00 0x01 0x00000000  if (A != read) goto 0003
 0002: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0003: 0x15 0x00 0x01 0x00000001  if (A != write) goto 0005
 0004: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0005: 0x15 0x00 0x01 0x00000002  if (A != open) goto 0007
 0006: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0007: 0x15 0x00 0x01 0x0000000c  if (A != brk) goto 0009
 0008: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0009: 0x15 0x00 0x01 0x00000009  if (A != mmap) goto 0011
 0010: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0011: 0x15 0x00 0x01 0x0000000b  if (A != munmap) goto 0013
 0012: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0013: 0x06 0x00 0x00 0x80000000  return KILL_PROCESS

Nous remarquons tout d’abord la prĂ©sence de la chaĂźne de caractĂšres [+] Installation du filtre seccomp mais pas celle des autres. On en dĂ©duit que :

  • le programme est rĂ©ellement exĂ©cutĂ© et les rĂšgles de filtrage ne sont pas extraites statiquement ;
  • le programme n’est pas exĂ©cutĂ© dans son entiĂšretĂ©. Son exĂ©cution est arrĂȘtĂ©e au moment du syscall qui fait appel Ă  seccomp.

Ensuite, pour comprendre le filtre mis en place il suffit de lire linĂ©airement la partie Ă  droite de haut en bas oĂč A est le potentiel appel systĂšme Ă  exĂ©cuter.

Lorsque l’appel systĂšme a le droit d’ĂȘtre exĂ©cutĂ©, ALLOW est retournĂ©. Lorsqu’il n’a pas le droit d’ĂȘtre exĂ©cutĂ©, KILL_PROCESS est retournĂ©.

Oui mais là c’est facile, c’est un exemple bateau 😮 


Trùs bien. Analysons donc le cas d’un programme un peu plus connu.

Exemple réel : chromium

Nous avons vu un peu plus haut que chromium utilise seccomp afin de renforcer sa sĂ©curitĂ©. Nous devrions donc pouvoir en extraire le filtre seccomp, nan ? Tout d’abord installons-le. Pour les distributions basĂ©es sur Ubuntu/Debian cela peut se faire avec sudo apt install chromium-browser.

Ensuite, lançons le en arriÚre-plan sans utiliser la partie graphique :

1
2
chromium --headless --disable-gpu &
[1] 52665

Utilisons seccomp-tools avec le PID de chromium :

1
seccomp-tools dump --pid 52665

Vous devriez normalement tomber sur une erreur de ce type :

1
2
3
4
[ERROR] Operation not permitted - ptrace attach failed
        PTRACE_SECCOMP_GET_FILTER requires CAP_SYS_ADMIN
        Try:
            sudo env "PATH=$PATH" seccomp-tools dump --pid 52665

Bon. Essayons avec la commande via sudo :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ sudo env "PATH=$PATH" seccomp-tools dump --pid 52665

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x12 0xc000003e  if (A != ARCH_X86_64) goto 0020
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x1d 0xffffffff  if (A != 0xffffffff) goto 0034
 0005: 0x15 0x00 0x03 0x00000125  if (A != pipe2) goto 0009
 0006: 0x20 0x00 0x00 0x0000001c  A = flags >> 32 # pipe2(fildes, flags)
 0007: 0x54 0x00 0x00 0x00000000  A &= 0x0
 0008: 0x15 0x0e 0x17 0x00000000  if (A == 0) goto 0023 else goto 0032
 0009: 0x15 0x00 0x16 0x00000010  if (A != ioctl) goto 0032
 0010: 0x20 0x00 0x00 0x0000001c  A = cmd >> 32 # ioctl(fd, cmd, arg)
 0011: 0x15 0x00 0x03 0x00000000  if (A != 0x0) goto 0015
 0012: 0x20 0x00 0x00 0x00000018  A = cmd # ioctl(fd, cmd, arg)
 0013: 0x15 0x13 0x00 0x0000541c  if (A == 0x541c) goto 0033
 0014: 0x15 0x12 0x00 0x00005412  if (A == 0x5412) goto 0033
 0015: 0x20 0x00 0x00 0x0000001c  A = cmd >> 32 # ioctl(fd, cmd, arg)
 0016: 0x54 0x00 0x00 0x00000000  A &= 0x0
 0017: 0x15 0x00 0x0e 0x00000000  if (A != 0) goto 0032
 0018: 0x20 0x00 0x00 0x00000018  A = cmd # ioctl(fd, cmd, arg)
 0019: 0x15 0x0d 0x0b 0x0000541c  if (A == 0x541c) goto 0033 else goto 0031
 0020: 0x15 0x00 0x0d 0x40000003  if (A != ARCH_I386) goto 0034
 0021: 0x20 0x00 0x00 0x00000000  A = sys_number
 0022: 0x15 0x00 0x03 0x0000014b  if (A != i386.pipe2) goto 0026
 0023: 0x20 0x00 0x00 0x00000018  A = args[1]
 0024: 0x54 0x00 0x00 0x00000080  A &= 0x80
 0025: 0x15 0x07 0x06 0x00000080  if (A == 128) goto 0033 else goto 0032
 0026: 0x15 0x00 0x05 0x00000036  if (A != i386.ioctl) goto 0032
 0027: 0x20 0x00 0x00 0x00000018  A = level # setsockopt(fd, level, optname, optval, optlen)
 0028: 0x15 0x04 0x00 0x0000541c  if (A == 0x541c) goto 0033
 0029: 0x15 0x03 0x00 0x0000541c  if (A == 0x541c) goto 0033
 0030: 0x15 0x02 0x00 0x00005412  if (A == 0x5412) goto 0033
 0031: 0x15 0x01 0x00 0x00005412  if (A == 0x5412) goto 0033
 0032: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0033: 0x06 0x00 0x00 0x0005000d  return ERRNO(13)
 0034: 0x06 0x00 0x00 0x00000000  return KILL

Et voilĂ  😎 ! Nous n’allons pas dĂ©cortiquer le filtre utilisĂ© par chromium nĂ©anmoins nous y reviendrons dans le prochain chapitre afin d’analyser les erreurs d’implĂ©mentation qui peuvent ĂȘtre prĂ©sentes dans un filtre seccomp.

📋 Synthùse

Ce qui est pas mal quand on s’intĂ©resse Ă  seccomp du point de vue d’un attaquant, c’est que l’on a pas nĂ©cessairement besoin de comprendre comment l’implĂ©menter en dĂ©tails.

En revanche, ce qui nous intĂ©resse est de savoir comment comprendre et lire le filtre seccomp et ce que cela implique en termes de protection dans un programme. Ça tombe bien ! Voyons dĂ©sormais comment tirer profit des vulnĂ©rabilitĂ©s prĂ©sentes dans un filtre seccomp.

This post is licensed under CC BY-NC 4.0 by the author.