Cambiando el ID del Usuario y del Grupo

Introducción

Es común ver que un servicio de red comienza ejecutándose con privilegios de superusuario para - tan pronto como sea posible - posteriormente renunciar a esos privilegios con vistas a disminuir los problemas de seguridad. La necesidad de arrancar el proceso en modo superusuario puede deberse a la necesidad del servicio/aplicación de tener acceso a recursos privilegiados (sockets, ficheros) en momentos determinados de la ejecución.

En los sistemas Unix cada usuario debe ser miembro de al menos un grupo que es el grupo primario. Un usuario puede ser miembro de otros grupos que son referidos como grupos suplementarios y que se listan en el fichero /etc/group.

Los procesos Unix tiene un grupo efectivo (EGID) y un grupo real (PGID). Normalmente son iguales.

$ id
uid=1007(someuser) gid=1007(someuser),1012(education)

Las Variables UID y GID Real y Efectivo

Perl proporciona cuatro variables especiales ($<, $(, $>, $)) que controlan el UID y el GID del proceso actual. La siguiente sesión (del usuario root) con el depurador muestra su significado:

mymachine:~# perl -wde 0
main::(-e:1):   0
  DB<1> p "real user ID: $<. real group ID: $(. effective user ID: $>. effective user group: $)\n"
real user ID: 0. real group ID: 0 0. effective user ID: 0. effective user group: 0 0

  DB<2> p $uid = getpwnam('someuser')
9999
  DB<3> p $gid = getgrnam('someuser')
9999
  DB<4> x ($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell,$expire) = getpwuid(9999)
0  'someuser'                             # nombre
1  'XXXXXchumchumXXXXXX444444444445555'   # pass
2  9999                                   # uid
3  9999                                   # gid
4  ''                                     # quota
5  ''                                     # comment
6  'Usuario de prueba fluflu 2,,,'        # gcos
7  '/home/someuser'                       # dir
8  '/bin/bash'                            # shell
9  undef                                  # expire
  DB<5> $> = 9999
  DB<6> !!ls
ls: .: Permiso denegado
(Command exited 2)
  DB<7> $> = $<
  DB<8> !!ls
2utf8                 etc                                                      makedoct           remove
........................................................................................................
  DB<8>

Cambio de Personalidad

El cambio de los UID en $< (real) y en $> (efectivo) hace que el cambio sea permanente. Es imposible recuperar el status de root si el programa renuncia a sus privilegios de root cambiando estas dos variables. Esta es la opción recomendada ya que previene la posibilidad de que los intrusos exploten fallos en el programa para ganar privilegios de root.

Grupos Suplementarios

En la mayor parte de los sistemas UNIX se soporta la idea de los grupos suplementarios: grupos en los que el usuario tiene privilegios pero que no son el grupo primario del usuario. El comando newgrp puede ser usado para cambiar el grupo actual durante una sesión. Si se provee la opción - (un guión) se reiniciarán las variables de entorno como si se hubiera entrado en el sistema:

$ ls -ltr ~/alu/0708/alu3607/InserciondeunaSubrutina
ls: /home/lhp/alu/0708/alu3607/InserciondeunaSubrutina: Permiso denegado
$ newgrp www-data
$ ls -ltr ~/alu/0708/alu3607/InserciondeunaSubrutina
total 8
-rwxrwx--- 1 www-data www-data 1228 2008-04-28 12:21 insercionSubrutina.pl
-rwxrwx--- 1 www-data www-data  181 2008-04-28 12:21 info.txt

El usuario root puede cambiar su grupo efectivo a cualquier grupo. A partir de ese momento sus privilegios serán los del grupo efectivo.

Un usuario normal no puede -en general - cambiar el grupo efectivo. El ejemplo anterior - ejecutado por un usuario ordinario - muestra que hay excepciones a esta regla.

En tales sistemas cuando accedemos al valor de $( o $) obtenemos una cadena de GIDs separados por espacios. El primero de ellos es el GID real y los siguientes son suplementarios.

$ perl -wde 0
  DB<1> p "real group = $(\neffective group = $)\n"
real group = 1040 1040 1012 33
effective group = 1040 1040 1012 33
  DB<3> x $gid = getgrgid(33)
0  'web-user'
  DB<4> x $gid = getgrgid(1012)
0  'education'
  DB<5> x $gid = getgrgid(1040)
0  'pp2'

Cambiando de Grupo

Para cambiar el identificador de grupo real GID asigne un único número (no una lista) a la variable $(. Para cambiar el GID efectivo asigne un sólo número a $). Si se quiere cambiar la lista de grupos suplementarios asigne una lista de números separados por espacios. El primer número será el real (efectivo) y el resto los suplementarios. Se puede forzar que la lista de grupos suplementarios sea vacía repitiendo el GID dos veces:

$) = "33 33";

Cambiando de Usuario en el Servidor: Rutinas de Soporte en Daemon1

La subrutina init_server toma ahora tres argumentos: el nombre del fichero de PID, el nombre del usuario y el nombre del grupo. La novedad está en que llamamos a change_privileges.

  27  sub init_server {
  28    my ($user,$group);
  29    ($pidfile,$user,$group) = @_;
  30    $pidfile ||= getpidfilename();
  31    my $fh = open_pid_file($pidfile);
  32    become_daemon();
  33    print $fh $$;
  34    close $fh;
  35    init_log();
  36    change_privileges($user,$group) if defined $user && defined $group;
  37    return $pid = $$;
  38  }

La subrutina change_privileges obtiene el uid y el gid asociados con el usuario usando getpwnam y getgrnam . Si falla detiene el programa con el correspondiente mensaje de error.

  55  sub change_privileges {
  56    my ($user,$group) = @_;
  57    my $uid = getpwnam($user)  or die "Can't get uid for $user\n";
  58    my $gid = getgrnam($group) or die "Can't get gid for $group\n";
  59    $) = "$gid $gid";
  60    $( = $gid;
  61    $> = $uid;   # change the effective UID (but not the real UID)
  62  }

Obsérve usamos directamente die: como hemos ya instalado los manejadores de __DIE__ y __WARN__ en la subrutina init_log estos mensajes aparecerán en el sistema de logs:

 113  sub init_log {
 114    setlogsock('unix');
 115    my $basename = basename($0);
 116    openlog($basename,'pid',FACILITY);
 117    $SIG{__WARN__} = \&log_warn;
 118    $SIG{__DIE__}  = \&log_die;
 119  }

Volviendo a change_privileges, cambiamos los grupos efectivo y real a los del usuario y el UID efectivo al del usuario. La lista de grupos suplementarios es vaciada. El orden importa pues un proceso puede cambiar de grupo solamente cuando se esta ejecutando con privilegios de root.

  59    $) = "$gid $gid";
  60    $( = $gid;
  61    $> = $uid;   # change the effective UID (but not the real UID)
Obsérvese que no se cambia el UID real lo que permite que el proceso pueda ganar la identidad de root si fuera necesario.

El bloque END{} es responsable de la eliminación del fichero de PID. Como dicho fichero fué creado cuando el servidor se ejecutaba como root es necesario recuperar los privilegios:

 154  END {
 155    $> = $<;  # regain privileges
 156    unlink $pidfile if defined $pid and $$ == $pid
 157  }

Cambiando de Usuario en el Servidor: El Programa

Ahora el programa define nuevas constantes USER, GROUP. El puerto usado es además un puerto privilegiado.

 8  use constant PORT      => 1002;
 9  use constant PIDFILE   => '/var/run/eliza_hup.pid';
10  use constant USER      => 'nobody';
11  use constant GROUP     => 'nogroup';

Después de abrir el socket para la escucha llamamos a la función init_server pasándole los tras argumentos que ahora requiere:

18  my $port = $ARGV[0] || PORT;
19  my $listen_socket = IO::Socket::INET->new(LocalPort => $port,
20                                            Listen    => 20,
21                                            Proto     => 'tcp',
22                                            Reuse     => 1);
23  die "Can't create a listening socket: $@" unless $listen_socket;
24  my $pid = init_server(PIDFILE,USER,GROUP,$port);

Los hijos creados para manejar cada nueva conexión cambian su UID real en la subrutina prepare_child:

80  sub prepare_child {
81    my $home = shift;
82    if ($home) {
83      local($>,$<) = ($<,$>);   # become root again (briefly)
84      chdir  $home || croak "chdir(): $!";
85      chroot $home || croak "chroot(): $!";
86    }
87    $< = $>;  # set real UID to effective UID
88  }

Utilizando chroot

La subrutina muestra otra estrategia común para proteger al sistema de potenciales problemas con bugs que pudiera tener el servidor: La llamada a chroot hace que el camino usado como argumento se convierta en la raíz de la jerarquía de directorios para el proceso.

Los efectos de chroot no pueden ser modificados.

chroot no cambia el directorio actual por lo que es necesario llamar a chdir antes. Para llamar a chroot hay que tener privilegios de root.

Sin embargo, no queremos que todos los procesos ejecuten chroot. De otro modo no estaríamos en condiciones de suprimir el fichero PID.

12  use constant ELIZA_HOME => '/var/www/';
13
..  ......................................
27
28  while (my $connection = $listen_socket->accept) {
29    my $host = $connection->peerhost;
30    my $child = launch_child(undef,ELIZA_HOME);
31    if ($child == 0) {
32      $listen_socket->close;
33      log_notice("Accepting a connection from $host\n");
34      interact($connection);
35      log_notice("Connection from $host finished\n");
36      exit 0;
37    }
38    $connection->close;
39  }

Los hijos ejecutan chroot pero no asi el proceso principal. Esto se hace por medio del método launch_child el cual llama a prepare_child

  64  sub launch_child {
  65    my $callback = shift;
  66    my $home     = shift;
  67    my $signals = POSIX::SigSet->new(SIGINT,SIGCHLD,SIGTERM,SIGHUP);
  68    sigprocmask(SIG_BLOCK,$signals);  # block inconvenient signals
  69    log_die("Can't fork: $!") unless defined (my $child = fork());
  70    if ($child) {
  71      $CHILDREN{$child} = $callback || 1;
  72    } else {
  73      $SIG{HUP} = $SIG{INT} = $SIG{CHLD} = $SIG{TERM} = 'DEFAULT';
  74      prepare_child($home);
  75    }
  76    sigprocmask(SIG_UNBLOCK,$signals);  # unblock signals
  77    return $child;
  78  }

En este ejemplo utilizamos /var/www como directorio para chroot. Este es el directorio de la web estática para apache y no es probable que contenga información confidencial.

 1  #!/usr/bin/perl -w -T
 .   ..........
 7
 8  use constant PORT      => 1002;
 9  use constant PIDFILE   => '/var/run/eliza_hup.pid';
10  use constant USER      => 'nobody';
11  use constant GROUP     => 'nogroup';
12  use constant ELIZA_HOME => '/var/www/';

Un asunto importante cuando se hace chroot es que si el programa necesita ficheros posteriormente al cambio chroot o requiere librerías que son cargadas dinámicamente (cosa bastante habitual en Perl) estas deberían estar accesibles en el nuevo sistema de archivos. Además hay que tener en cuenta que la modificación implica el cambio de todos los nombres de camino. Asi un fichero que estuviera en /var/www/tutu/titi pasará a accederse mediante el camino /tutu/titi

Debe tenerse especial cuidado si se usa AutoLoader. Autoloader retrasa la compilación de los módulos hasta su uso. Por ello los ficheros .al que genera deberán estar accesibles en el sistema de archivos del chroot.



Subsecciones
Casiano Rodríguez León
Licencia de Creative Commons
Programación Distribuida y Mejora del Rendimiento
por Casiano Rodríguez León is licensed under a Creative Commons Reconocimiento 3.0 Unported License.

Permissions beyond the scope of this license may be available at http://campusvirtual.ull.es/ocw/course/view.php?id=44.
2012-06-19