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)
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>
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.
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'
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";
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 }
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 }
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
.