Profundice en los espacios de nombres de Linux, parte 2

En la parte anterior, simplemente sumergimos nuestros dedos en las aguas del espacio de nombres y al mismo tiempo vimos lo fácil que era comenzar el proceso en un espacio de nombres UTS aislado. En esta publicación cubriremos el espacio de nombres de usuario.


Entre otros recursos relacionados con la seguridad, los espacios de nombres de usuario aíslan los identificadores de usuarios y grupos en el sistema. En esta publicación, nos centraremos exclusivamente en los recursos de identificación de usuarios y grupos (UID y GID, respectivamente), ya que desempeñan un papel fundamental en la realización de comprobaciones de permisos y otras actividades relacionadas con la seguridad en todo el sistema.


En Linux, estos ID son simplemente enteros que identifican a los usuarios y grupos en el sistema. Y algunos de ellos se asignan a cada proceso con el fin de establecer a qué operaciones / recursos puede acceder este proceso. La capacidad de un proceso de dañar depende de los permisos asociados con las ID asignadas.


Espacios de nombres de usuario


Ilustraremos las capacidades de los espacios de nombres de usuario utilizando solo ID de usuario. Exactamente las mismas acciones se aplican a las ID de grupo, que abordaremos más adelante en esta publicación.

El espacio de nombres de usuario tiene su propia copia de identificadores de usuario y grupo. Luego, el aislamiento le permite asociar el proceso con otro conjunto de ID, dependiendo del espacio de nombres de usuario al que pertenece actualmente. Por ejemplo, el proceso $pid puede ejecutarse desde la root (UID 0) en el espacio de nombres de usuario P y de repente continúa ejecutándose desde el proxy (UID 13) después de cambiar a otro espacio de nombres de usuario Q.


¡Los espacios de usuario se pueden anidar! Esto significa que una instancia de un espacio de nombres personalizado (principal) puede tener cero o más espacios de nombres secundarios, y cada espacio de nombres secundario puede, a su vez, tener sus propios espacios de nombres secundarios y así sucesivamente ... (hasta alcanzar el límite de 32 niveles de anidamiento). Cuando se crea un nuevo espacio de nombres C , Linux establece el espacio de nombres de usuario actual del proceso P que crea C como padre para C y esto no se puede cambiar más adelante. Como resultado, todos los espacios de nombres de usuario tienen exactamente un padre, formando una estructura de espacios de nombres en forma de árbol. Y, como en el caso de los árboles, una excepción a esta regla está en la parte superior, donde tenemos el espacio de nombres raíz (o inicial, predeterminado). Esto, si aún no está haciendo algún tipo de magia de contenedor, es muy probable que sea el espacio de nombres de usuario al que pertenecen todos sus procesos, ya que este es el único espacio de nombres de usuario desde que se inició el sistema.


En esta publicación, utilizaremos los mensajes de comando P $ y C $ para indicar el shell que se está ejecutando actualmente en el espacio de nombres de usuario P primario y C secundario respectivamente.

Asignaciones de ID de usuario


El espacio de nombres de usuario, de hecho, contiene un conjunto de identificadores y cierta información que conecta estas ID con un conjunto de ID de otro espacio de nombres de usuario: este dúo define una idea completa de las ID de los procesos disponibles en el sistema. Veamos cómo podría verse:


 P$ whoami iffy P$ id uid=1000(iffy) gid=1000(iffy) 

En otra ventana de terminal, comencemos el shell usando unshare (el indicador -U crea un proceso en el nuevo espacio de nombres de usuario):


 P$ whoami iffy P$ unshare -U bash #    ,     user namespace C$ whoami nobody C$ id uid=65534(nobody) gid=65534(nogroup) C$ ls -l my_file -rw-r--r-- 1 nobody nogroup 0 May 18 16:00 my_file 

Espera un minuto, quien? Ahora que estamos en un shell anidado en C , ¿el usuario actual se convierte en nadie? Podríamos haber adivinado que dado que C es un nuevo espacio de nombres de usuario, el proceso puede tener un tipo diferente de ID. Por lo tanto, probablemente no esperábamos que se mantuviera iffy , pero nobody no es gracioso. Por otro lado, es genial porque obtuvimos el aislamiento que queríamos. Nuestro proceso ahora tiene una sustitución de ID diferente (aunque rota) en el sistema, actualmente ve a todos como nobody y a cada grupo como nogroup .


La información que vincula un UID de un espacio de nombres de usuario a otro se denomina mapeo de ID de usuario . Es una tabla de búsqueda para identificar ID en el espacio de nombre de usuario actual para ID en otro espacio de nombre y cada espacio de nombre de usuario está asociado con exactamente una asignación de UID (además de otra asignación de GID para ID de grupo).


Este mapeo es lo que está roto en nuestro shell no unshare . Resulta que los nuevos espacios de nombres de usuario comienzan con un mapeo vacío, y como resultado, Linux usa el horrible usuario nobody por defecto. Necesitamos arreglar esto antes de que podamos hacer cualquier trabajo útil en nuestro nuevo espacio de nombres. Por ejemplo, actualmente, las llamadas al sistema (como setuid ) que intentan trabajar con el UID fallarán. Pero no tengas miedo! Fiel a la tradición de todo es archivo , Linux presenta este mapeo utilizando el sistema de archivos /proc en /proc/$pid/uid_map (en /proc/$pid/gid_map para el GID), donde $pid es el ID del proceso. Llamaremos a estos dos archivos archivos de mapas.


Archivos de mapas


Los archivos de mapas son archivos especiales en el sistema. ¿Qué son especiales? Bueno, al devolver diferentes contenidos cada vez que los lees, dependiendo de lo que esté leyendo tu proceso. Por ejemplo, el archivo de mapa /proc/$pid/uid_maps devuelve la asignación de los UID del espacio de nombres de usuario que posee el proceso $pid a los UID en el espacio de nombres de usuario del proceso de lectura. Y, como resultado, el contenido devuelto al proceso X puede diferir de lo que regresó al proceso Y , incluso si leen el mismo archivo de mapa al mismo tiempo.


En particular, el proceso X , que lee el archivo de mapa UID /proc/$pid/uid_map , recibe un conjunto de cadenas. Cada línea asigna un rango continuo de UID al espacio de nombres de usuario C del proceso $pid , correspondiente a un rango de UID en otro espacio de nombres.


Cada línea tiene el formato $fromID $toID $length , donde:


  • $fromID es el UID inicial del rango para el espacio de nombres de usuario del proceso $pid
  • $lenght es la longitud del rango.
  • La traducción de $toID depende del proceso de lectura X. Si X pertenece a otro espacio de nombres de usuario U , entonces $toID es el UID inicial del rango en U que se asigna desde $fromID . De lo contrario, $toID es el UID de inicio del rango en P , el espacio de nombres del usuario primario del proceso C.

Por ejemplo, si un proceso lee el archivo /proc/1409/uid_map y entre las líneas recibidas puede ver 15 22 5 , los UID del 15 al 19 en el espacio de nombres de usuario del proceso 1409 asignan a los UID 22-26 de un espacio de nombres de usuario separado del proceso de lectura.


Por otro lado, si un proceso lee del archivo /proc/$$/uid_map (o un archivo de mapa de cualquier proceso que pertenezca al mismo espacio de nombres de usuario que el proceso de lectura) y recibe 15 22 5 , entonces UID de 15 a 19 en el espacio de nombres de usuario C se asigna a los UID del 22 al 26 del padre para el espacio de nombres de usuario C.


Probémoslo:


 P$ echo $$ 1442 #   user namespace... C$ echo $$ 1409 # C      ,     C$ cat /proc/1409/uid_map #  #   namespace P      # UIDs    UID    P$ cat /proc/1442/uid_map 0 0 4294967295 # UIDs  0  4294967294  P  #  4294967295 -  ID no user -  C. C$ cat /proc/1409/uid_map 0 4294967295 4294967295 

Bueno, eso no fue muy emocionante, ya que estos fueron dos casos extremos, pero eso dice algunas cosas allí:


  1. El espacio de nombres de usuario recién creado tendrá archivos de mapas vacíos.
  2. El UID 4294967295 no es mapeable y no es apto para su uso incluso en el espacio de nombres de usuario root . Linux usa este UID específicamente para indicar la ausencia de una ID de usuario .

Escribir archivos de mapa UID


Para arreglar nuestro espacio de nombres de usuario C recién creado, solo necesitamos proporcionar nuestras asignaciones necesarias escribiendo sus contenidos en los archivos de mapas para cualquier proceso que pertenezca a C (no podemos actualizar este archivo después de escribir en él). Escribir en este archivo le dice a Linux dos cosas:


  1. Qué UID están disponibles para los procesos relacionados con el espacio de nombres de usuario de destino C.
  2. Qué UID en el espacio de nombres de usuario actual corresponden a los UID en C.

Por ejemplo, si escribimos lo siguiente desde el espacio de nombres de usuario primario P en el archivo de mapa para el espacio de nombres secundario C :


 0 1000 1 3 0 1 

esencialmente le decimos a Linux que:


  1. Para los procesos en C , los únicos UID que existen en el sistema son los UID 0 y 3 . Por ejemplo, la llamada al sistema setuid(9) siempre terminará con algo así como una identificación de usuario no válida .
  2. Los UID 1000 y 0 en P corresponden a los UID 0 y 3 en C. Por ejemplo, si un proceso que se ejecuta con UID 1000 en P cambia a C , encontrará que después de cambiar, su UID se ha convertido en root 0 .

Propietario de espacio de nombres y privilegios


En una publicación anterior, mencionamos que al crear nuevos espacios de nombres, se requiere acceso con nivel de superusuario. Los espacios de nombres de usuario no imponen este requisito. De hecho, otra característica es que pueden poseer otros espacios de nombres.


Cada vez que se crea un espacio de nombres que no es usuario N , Linux asigna el espacio de nombres de usuario actual P del proceso que crea N para ser el propietario del espacio de nombres N. Si se crea P junto con otros espacios de nombres en la misma llamada al sistema de clone , Linux asegura que P se creará primero y se convertirá en el propietario de otros espacios de nombres.


El propietario de los espacios de nombres es importante porque un proceso que solicite realizar una acción privilegiada en un recurso que no sea un espacio de nombres de usuario tendrá sus privilegios UID contrastados con el propietario de este espacio de nombres de usuario y no con el espacio de nombres de usuario raíz. Por ejemplo, supongamos que P es el espacio de nombres de usuario primario del elemento secundario C , y P y C poseen su propio espacio de nombres de red M y N, respectivamente. Un proceso puede no tener privilegios para crear los dispositivos de red incluidos en M , pero puede hacerlo para N.


La consecuencia de tener un propietario de espacio de nombres para nosotros es que podemos eliminar el requisito de sudo al ejecutar comandos usando unshare o isolate si también solicitamos la creación de un espacio de nombres de usuario. Por ejemplo, unshare -u bash requerirá sudo , pero unshare -Uu bash ya no será:


 # UID 1000 --      user namespace P. P$ id uid=1000(iffy) gid=1000(iffy) #           # network namespace. P$ ip link add type veth RTNETLINK answers: Operation not permitted #     ,     #  user  network namespace P$ unshare -nU bash # :  sudo C$ ip link add type veth RTNETLINK answers: Operation not permitted # ,  . ,  # UID 0 (root)    ,  #     nobody.   . C$ echo $$ 13294 #   P,   UID 1000  P  UID 0  C P$ echo "0 1000 1" > /proc/13294/uid_map #   ? C$ id uid=0(root) gid=65534(nogroup) C$ ip link add type veth # ! 

Desafortunadamente, volveremos a aplicar el requisito de superusuario en la próxima publicación, ya que isolate necesita privilegios de root en el espacio de nombres de usuario root para configurar correctamente el espacio de nombres Mount y Network. Pero seguramente eliminaremos los privilegios del proceso del equipo para asegurarnos de que el equipo no tenga permisos innecesarios.

Cómo se resuelven los ID


Acabamos de ver un proceso que se ejecuta cuando un usuario normal 1000 repente cambió a root . No se preocupe, no hubo escalada de privilegios. Recuerde que esto es solo una identificación de mapeo : mientras nuestro proceso piensa que es el root del sistema, Linux sabe que root , en su caso, significa el UID 1000 habitual (gracias a nuestro mapeo). Entonces, en un momento en que los espacios de nombres que pertenecen a su nuevo espacio de nombres de usuario (como el espacio de nombres de red en C ) reconocen sus derechos como root , otros (como el espacio de nombres de red en P ) no lo hacen. Por lo tanto, el proceso no puede hacer nada que el usuario 1000 no pueda hacer.


Cada vez que un proceso en un espacio de nombres de usuario anidado realiza una operación que requiere la verificación de permisos, por ejemplo, crear un archivo, su UID en este espacio de nombres de usuario se compara con la ID de usuario equivalente en el espacio de nombres de usuario raíz al atravesar las asignaciones en el árbol de espacio de nombres a la raíz. Hay un movimiento en la dirección opuesta, por ejemplo, cuando lee ID de usuario, como hacemos con ls -l my_file . El UID del propietario my_file asigna desde el espacio de nombres de usuario raíz al actual y el ID final correspondiente (o nadie si la asignación estuvo ausente en algún lugar a lo largo de todo el árbol) se asigna al proceso de lectura.


ID de grupo


Incluso si fuéramos root en C , todavía estamos asociados con el terrible nogroup como nuestra ID de grupo. Solo necesitamos hacer lo mismo para el /proc/$pid/gid_map . Antes de que podamos hacer esto, necesitamos deshabilitar la llamada al sistema setgroups (esto no es necesario si nuestro usuario ya tiene una capacidad CAP_SETGID en P , pero no asumiremos esto, ya que esto generalmente viene con privilegios de superusuario) escribiendo "denegar "al archivo proc/$pid/setgroups :


 #  13294 -- pid  unshared  C$ id uid=0(root) gid=65534(nogroup) P$ echo deny > /proc/13294/setgroups P$ echo "0 1000 1" > /proc/13294/gid_map #  group ID   C$ id uid=0(root) gid=0(root) 

Implementación


El código fuente de esta publicación se puede encontrar aquí .

Como puede ver, existen muchas dificultades asociadas con la administración de espacios de nombres de usuario, pero la implementación es bastante simple. Todo lo que necesitamos hacer es escribir un montón de líneas en un archivo; era triste averiguar qué y dónde escribir. Sin más preámbulos, estos son nuestros objetivos:


  1. Clonar un proceso de equipo en su propio espacio de nombres de usuario.
  2. Escriba en los archivos de mapas UID y GID del proceso del equipo.
  3. Restablezca todos los privilegios de superusuario antes de ejecutar el comando.

1 logra simplemente agregando el indicador CLONE_NEWUSER a nuestra llamada al sistema de clone .


 int clone_flags = SIGCHLD | CLONE_NEWUTS | CLONE_NEWUSER; 

Para 2 agregamos la función prepare_user_ns , que representa cuidadosamente un usuario regular 1000 como root .


 static void prepare_userns(int pid) { char path[100]; char line[100]; int uid = 1000; sprintf(path, "/proc/%d/uid_map", pid); sprintf(line, "0 %d 1\n", uid); write_file(path, line); sprintf(path, "/proc/%d/setgroups", pid); sprintf(line, "deny"); write_file(path, line); sprintf(path, "/proc/%d/gid_map", pid); sprintf(line, "0 %d 1\n", uid); write_file(path, line); } 

Y lo llamaremos desde el proceso principal en el espacio de nombres del usuario principal justo antes de señalar el proceso de comando.


  ... //      . int pipe = params.fd[1]; //      namespace ... prepare_userns(cmd_pid); //   ,     . ... 

Para el paso 3 actualizamos la función cmd_exec para asegurarnos de que el comando se ejecute desde el usuario habitual sin privilegios 1000 que proporcionamos en la asignación (recuerde que el usuario raíz 0 en el espacio de nombres de usuario del proceso del equipo es el usuario 1000 ):


  ... //   ' '   . await_setup(params->fd[0]); if (setgid(0) == -1) die("Failed to setgid: %m\n"); if (setuid(0) == -1) die("Failed to setuid: %m\n"); ... 

¡Y eso es todo! isolate ahora inicia el proceso en un espacio de nombres de usuario aislado.


 $ ./isolate sh ===========sh============ $ id uid=0(root) gid=0(root) 

Hubo bastantes detalles en esta publicación sobre cómo funcionan los espacios de nombres de usuario, pero al final, configurar la instancia fue relativamente sencillo. En la próxima publicación, veremos la posibilidad de ejecutar un comando en nuestro propio espacio de nombres Mount usando isolate (revelando el secreto detrás de la declaración FROM del Dockerfile ). Allí tendremos que ayudar a Linux un poco más para configurar correctamente la instancia.

Source: https://habr.com/ru/post/459574/


All Articles