Correction silencieuse d'une vulnérabilité dans le noyau Linux

Thu 08 January 2009 by gabriel

Le 26 décembre 2008, un patch concernant le protocole SCTP a été publié par les développeurs du noyau, sans mentionner l'impact sur la sécurité. Nous détaillerons dans ce billet les bases du protocole SCTP, avant d'analyser la vulnérabilité pour déterminer ses conséquences.

Description de SCTP

Le 26 décembre 2008, un patch concernant le protocole SCTP a été publié par les développeurs du noyau. Le message de commit, ne comportant aucune indication sur l'impact de la sécurité du noyau, est le suivant :

sctp: Avoid memory overflow while FWD-TSN chunk is received with bad stream ID

If FWD-TSN chunk is received with bad stream ID, the sctp will not do the
validity check, this may cause memory overflow when overwrite the TSN of
the stream ID.

This patch fix this problem by discard the chunk if stream ID is not
less than MIS.

SCTP (Stream Control Transmission Protocol), défini en partie dans la RFC 4960, est un protocole réseau de transport destiné originellement au transport de services de téléphonie au dessus d'IP. Il offre des services similaires aux protocoles TCP et UDP réunis, en assurant la fiabilité et l'ordre des séquences, ainsi que le contrôle de congestion. Une implémentation de ce protocole est présente dans la plupart des systèmes d'exploitation modernes. Ainsi SCTP est activé par défaut en tant que module dans la majorité des distributions Linux.

Une session SCTP est représentée par l'image suivante. Les informations associées à chaque paquets correspondent au type de chunk présent, les paquets du clients étant représentés en vert, et ceux du serveur en jaune. Le client et le serveur sont ici sur une même machine.

sctp.png

La structure d'un paquet SCTP est relativement simple, et possède la forme suivante :

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+===============================================================+ ========
|     Source Port Number        |     Destination Port Number   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  Common
|                      Verification Tag                         |  Header
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           Checksum                            |
+===============================================================+ ========
|  Chunk1 Type  | Chunk1 Flags  |        Chunk1 Length          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
\                                                               \
/                         Chunk1 Value                          /
\                                                               \
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
\                                                               /  Chunk
/                                                               \  Fields
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  ChunkN Type  | ChunkN Flags  |        ChunkN Length          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
\                                                               \
/                         ChunkN Value                          /
\                                                               \
+===============================================================+ ========

Le chunk FWD-TSN, décrit dans la RFC 3758, possède quant à lui la forme suivante :

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Type = 192  |  Flags = 0x00 |        Length = Variable      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      New Cumulative TSN                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Stream-1              |       Stream Sequence-1       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
\                                                               /
/                                                               \
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Stream-N              |       Stream Sequence-N       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Le nombre maximum de streams entrant et sortant est négocié par les paquets INIT et INIT_ACK pendant l'établissement de la connexion. Le message de commit indique que le bug est déclenché lorsqu'un paquet SCTP contient un chunk de type FWD-TNS dont l'identifiant du ou des streams est supérieur à la somme du nombre de streams entrant et sortant. Ce type de paquet peut être envoyé après l'établissement de la connexion, c'est-à-dire après l'échange des paquets INIT et COOKIE. Le bug est simplement corrigé en rejetant ces paquets silencieusement.

Analyse de la vulnérabilité

L'analyse du patch2 et des sources du noyau montre que le bug est déclenché en prenant le chemin d'exécution suivant :

net/sctp/ssnmap.c:
/* Storage size needed for map includes 2 headers and then the
 * specific needs of in or out streams.
 */
static inline size_t sctp_ssnmap_size(__u16 in, __u16 out)
{
    return sizeof(struct sctp_ssnmap) + (in + out) * sizeof(__u16);          [1]
}


/* Create a new sctp_ssnmap.
 * Allocate room to store at least 'len' contiguous TSNs.
 */
struct sctp_ssnmap *sctp_ssnmap_new(__u16 in, __u16 out,
                    gfp_t gfp)
{
    struct sctp_ssnmap *retval;
    int size;

    size = sctp_ssnmap_size(in, out);
    if (size <= MAX_KMALLOC_SIZE)
        retval = kmalloc(size, gfp);                                         [2]
    else
        retval = (struct sctp_ssnmap *)
              __get_free_pages(gfp, get_order(size));
       if (!retval)
               goto fail;

       if (!sctp_ssnmap_init(retval, in, out))
               goto fail_map;



/* Initialize a block of memory as a ssnmap.  */
static struct sctp_ssnmap *sctp_ssnmap_init(struct sctp_ssnmap *map, __u16 in,
                                           __u16 out)
{
       memset(map, 0x00, sctp_ssnmap_size(in, out));

       /* Start 'in' stream just after the map header. */
       map->in.ssn = (__u16 *)&map[1];



net/sctp/sm_sideeffect.c:
/* This is the side-effect interpreter.  */
static int sctp_cmd_interpreter(sctp_event_t event_type,
                                sctp_subtype_t subtype,
                                sctp_state_t state,
                                struct sctp_endpoint *ep,
                                struct sctp_association *asoc,
                                void *event_arg,
                                sctp_disposition_t status,
                                sctp_cmd_seq_t *commands,
                                gfp_t gfp)
{
[...]
        while (NULL != (cmd = sctp_next_cmd(commands))) {
                switch (cmd->verb) {
[...]
                case SCTP_CMD_PROCESS_FWDTSN:                                [3]
                        sctp_cmd_process_fwdtsn(&asoc->ulpq,
                                                cmd->obj.ptr);



/* Process variable FWDTSN chunk information. */
static void sctp_cmd_process_fwdtsn(struct sctp_ulpq *ulpq,
                                    struct sctp_chunk *chunk)
{
        struct sctp_fwdtsn_skip *skip;
        /* Walk through all the skipped SSNs */
        sctp_walk_fwdtsn(skip, chunk) {                                      [4]
                sctp_ulpq_skip(ulpq, ntohs(skip->stream),
                                     ntohs(skip->ssn));



net/sctp/ulpqueue.c:
/* Skip over an SSN. This is used during the processing of
 * Forwared TSN chunk to skip over the abandoned ordered data
 */
void sctp_ulpq_skip(struct sctp_ulpq *ulpq, __u16 sid, __u16 ssn)
{
        struct sctp_stream *in;

        /* Note: The stream ID must be verified before this routine.  */
        in  = &ulpq->asoc->ssnmap->in;                                       [5]

        /* Is this an old SSN?  If so ignore. */
        if (SSN_lt(ssn, sctp_ssn_peek(in, sid)))                             [6]
                return;

        /* Mark that we are no longer expecting this SSN or lower. */
        sctp_ssn_skip(in, sid, ssn);



include/net/sctp/structs.h:
/* Skip over this ssn and all below. */
static inline void sctp_ssn_skip(struct sctp_stream *stream, __u16 id,
                                 __u16 ssn)
{
        stream->ssn[id] = ssn+1;                                             [7]

Lors de l'établissement de la connexion, la taille de la table des numéros de séquence des streams est négociée, et calculée en [1]. Cette table est ensuite allouée en [2] par un appel à kmalloc(), ou __get_free_pages(). Lors de la réception en [3] d'un paquet possédant un chunk de type FWD-TSN, la fonction sctp_cmd_process_fwdtsn() est appelée. Pour chaque association de numéro de stream et de numéro de séquence, la fonction sctp_ulpq_skip() est appelée [4]. La table des numéros de séquence est associée à la variable in en [5]. En [6], si le numéro de séquence associé au stream est inférieur à la dernière valeur enregistrée, l'association est rejetée. Sinon, le numéro de séquence plus un écrit à l'indice du numéro de stream dans la table des numéros de séquences [7].

L'attaquant contrôle donc plusieurs paramètres :

  • la taille du buffer alloué par kmalloc() en [2], dépendant uniquement du nombre maximum de streams entrant et sortant négocié pendant l'établissement connexion ;
  • l'indice du tableau (sur 2 octets), et la valeur écrite dans celui-ci, en [7].

Il peut donc écrire des valeurs arbitraires, relativement à un buffer alloué dans le tas dont il contrôle la taille. L'envoi de paquet possédant un chunk FWD-TSN arbitraire nécessite cependant l'utilisation de raw sockets.

Pour résumer :

  • Le déclenchement de la vulnérabilité nécessite qu'un service SCTP soit en écoute sur la machine cible, car le noyau acceptera les paquets FWD-TSN uniquement si la connexion SCTP a été correctement établie. Une machine sans service SCTP actif n'est pas exploitable.
  • Une machine avec un service SCTP actif est vulnérable à un déni de service, et potentiellement à une exécution de code arbitraire à distance.
  • Une machine avec un service SCTP contrôlé par l'attaquant est vulnérable à une exécution de code arbitraire. Une exploitation locale réussie de cette vulnérabilité donne donc les privilèges du noyau à l'attaquant.

Exploitation

Le déni de service à distance est déclenchable en établissant la connexion SCTP, puis en envoyant un paquet possédant un chunk de type FWD-TSN dont les numéros de stream sont invalides. Avec des numéros de stream et des numéros de séquence aléatoires, des valeurs aléatoires seront écrites en mémoire. En répétant ces étapes de multiples fois, le tas de la machine cible sera corrompu, provoquant le crash du noyau, et donc le déni de service de la machine.

Si l'attaquant possède un compte local sur la machine, il peut créer puis lancer un serveur SCTP de telle façon que la réception d'un paquet FWD-TSN écrive en mémoire des valeurs choisies judicieusement, l'idée étant d'exécuter un code arbitraire en mode noyau. Les méthodes d'exploitation classiques de kernel heap overflow, initialement décrites par qobaiashi, s'appliquent ici. Sur la machine cible, l'attaquant allouera deux slabs adjacents. Le premier sera libéré, et le second écrasé lors de la réception du paquet contenant un chunk de type FWD-TSN. Suivant le type de slab écrasé, il sera par exemple possible de modifier le pointeur d'une fonction, et d'appeler un shellcode mmapé dans l'espace d'adressage userland.

Conclusion

Comme l'a écrit Cédric Blancher dans un billet de son blog il y a déjà quelques mois, Linus Torvalds a exprimé publiquement sa décision de ne plus indiquer les bugs posant des problèmes de sécurité, les vulnérabilités étant pour lui des bugs comme les autres. Dans le cas présent, cette vulnérabilité a été remarquée par Eugene Teo, bien que non indiquée dans le patch. Mais qu'en est-il des centaines d'autres patchs ?

L'impact de cette vulnérabilité est critique. Avec un accès local à la machine et la possibilité d'envoyer des paquets SCTP depuis l'extérieur, il est réalisable d'exécuter un code arbitraire avec les privilèges du noyau. Il sera donc fortement recommandé de mettre à jour sa distribution lorsqu'une mise à jour sera disponible. En attendant, il est toujours possible de désactiver le module SCTP si celui-ci n'est pas indispensable. Vu le lourd historique en terme de sécurité du module SCTP, il est d'ailleurs conseillé de toujours le désactiver. Enfin, l'application des patchs de grsecurity reste toujours une mesure préventive importante pour sécuriser un noyau, en compliquant sérieusement l'exploitation de vulnérabilités connues et inconnues.