Reparar un sistema después de renombrar ld.so

Posted on Monday, October 14, 2013

Reparar un sistema después de renombrar ld.so

Bien, una vez que hayamos provocado el desastre, nos daremos cuenta de la que hemos liado porque cada vez que intentemos ejecutar cualquier ejecutable obtendremos el siguiente mensaje:

No existe el fichero o el directorio

Esto pasa porque lo que no está encontrando es ld.so que es una biblioteca con la que están enlazados todos los ejecutables dinámicos, así que lo primero que tenemos que hacer es no cerrar ninguna sesión ni ningún programa que tengamos abierto, porque como no podemos ejecutar ningún ejecutable dinámico, cada programa que cerremos será un programa que podríamos necesitar y no podremos volver a abrir.

Si tenemos busybox instalado estaremos de suerte porque es un ejecutable estático y no necesita de ninguna biblioteca para ejecutarse. Así que ya lo tenemos resuelto:

busybox sh

Con eso lanzamos un shell en donde tendremos un montón de comandos estándar de UNIX implementados en un solo ejecutable enlazado estáticamente desde el que podremos restaurar la biblioteca ld.so a su ubicación anterior y a tener más cuidado la próxima vez.

Lista de comandos implementados en busybox
Lista de comandos implementados en busybox

Lamentablemente, este no era mi caso (nota mental: instalar busybox en todos los servidores), así que tuve que currármelo un poco más, pero una vez que te das cuenta de lo que tienes que hacer, es muy fácil.

Nótese que el caso del que estoy hablando es el de haber renombrado la biblioteca ld.so. Si lo que has hecho ha sido borrarla, estás perdido. No te quedará más remedio que recurrir a Live CD o similar para recuperar la biblioteca. Vale, sí, el Live CD normalmente es una opción mucho más rápida y fácil que cualquiera de las que estoy tratando aquí, pero no es la primera opción cuando se trata de un servidor que estás administrando remotamente.

Vale, de lo primero que tenemos que darnos cuenta es de que no podemos usar ningún comando externo al shell que estemos ejecutando, que voy a asumir que es bash, porque si usas otras cosas es que andas a complicarte la vida sin un porqué. Pero tendremos a nuestra disposición un montón de comandos internos de bash aunque lamentablemente ninguno de ellos nos servirá a priori para copiar o renombrar ficheros.
Aunque si usamos un poco la imaginación, nos daremos cuenta de que lo que podría ser equivalente a copiar un fichero sería coger un fichero, enviarlo a stdin, ejecutar un comando que tome stdin y lo envíe a stdout para poder después redirigirlo a un fichero.
Es decir, sabemos que con:

comando < fichero

hacemos que fichero sirva como entrada estándar de comando y con

comando > fichero

hacemos que la salida de comando se escriba en fichero.

Por lo tanto, si tenemos un comando que coja la entrada estándar y la envíe tal cual a la salida estándar, podemos copiar ficheros con:

comando < origen > destino

Y esto es básicamente lo que hace cat. Es decir,

cat < origen > destino

es esencialmente lo mismo que

cp origen destino

Pero cat es un comando externo, necesitamos implementar nuestro propio cat con comandos internos. Así que necesitamos una secuencia de comandos que tomen la entrada estándar y la muestren por la salida estándar.
Bien, el comando interno de bash que muestra lo que queramos por la salida estándar es el echo de toda la vida, ¿verdad? Y un comando que coja algo de la entrada estándar, a mí el primero que se me ocurre es read. Ahora lo que tenemos que conseguir es que lo que read toma de la entrada estándar echo sea capaz de mostrarlo por la salida estándar, y eso es muy fácil si sabemos cómo funciona read, que lo que hace es guardar en la variable que le digamos la línea que le llegue de la entrada estándar. Y como echo es capaz de mostrar por salida estándar esa variable, ya tenemos nuestro «cat interno»:

while read line
do
  echo $line
done < origen > destino

A lo mejor no sabes la ruta exacta de alguno de los ficheros y necesitamos el listado de ficheros de un directorio. Pero ls es un comando externo, tampoco tenemos ls. Sin embargo, podemos obtener el listado de ficheros simplemente con tab completion. Aunque si tenemos activado alguno de esos autocompletados inteligentes puede que no nos liste todos los ficheros o que incluso nos liste lo que le dé la gana y que no tenga nada que ver con los ficheros de un directorio. Podemos desactivar el autocompletado inteligente con:

complete -r

Así que si queremos saber el contenido de /etc escribimos:

comando /etc/

y pulsamos dos veces el tabulador. Si tenemos desactivado el autocompletado programable, da igual que comando usamos, como si es uno que no existe:

Usando el autocompletado de bash para listar el contenido de un directorio
Usando el autocompletado de bash para listar el contenido de un directorio

Bien, con estas dos ideas ya tenemos una especie de implementación interna de cp y ls.

Esta sería una implementación mejorable ya que habría que tener en cuenta algunos casos especiales, pero da igual, no la vamos a mejorar porque no nos vale. No nos vale porque lo que queremos copiar es una biblioteca, así que tiene que tener permisos de ejecución y, hasta donde yo sé, ningún fichero creado con comandos internos de bash tendrá permisos de ejecución. Y tampoco podemos cambiarle los permisos porque chmod es un comando externo.

Así que habrá que pensar otra cosa. Y la otra cosa es todavía más sencilla, hasta el punto de que te sientes un poco tonto cuando te das cuenta. Porque claro, tu sigues teniendo tu biblioteca ld.so, lo que pasa es que la tienes con otro nombre.
Como decíamos, ld.so es el cargador dinámico, es la biblioteca que carga todas las demás y cuando nuestro ejecutable solicita una biblioteca, normalmente solicita simplemente el nombre de la biblioteca y ld.so se encarga de buscar la ubicación de esa biblioteca. Pero la ruta de ld.so sí que está con su ubicación exacta en el ejecutable, porque hasta que no tenemos ld.so cargado no hay «nadie» que se encargue de buscar las biblioteca por su nombre en vez de por su ruta completa.

Esto es así con los ejecutables enlazados dinámicamente, que al tener parte del código «externalizado» en bibliotecas (.so en UNIX y .dll en Windows) nos permite compartir ese código con otras aplicaciones que también lo usan en vez de repetir el mismo código en todas las aplicacines que lo necesitan para ahorrar espacio en disco y memoria. El problema es que si eliminamos alguna biblioteca el programa dejará de funcionar, como es el caso. Además, un cambio de versión en la biblioteca que tenemos instalada puede hacer que los programas que la usan dejen de funcionar.

Por otra parte están los ejecutables enlazados estáticamente, en los que ningún código se externaliza en bibliotecas sino que un simple ejecutable ya contiene todo el código necesario para funcionar. Cuentan con la ventaja de ser más tolerantes a errores y desastres del sistema y a incompatibilidades, con la contrapartida de que ocupan más:

Los ejecutables estáticos ocupan más pero no dependen de bibliotecas externas.
Los ejecutables estáticos ocupan más pero no dependen de bibliotecas externas.

La biblioteca ld.so, como no podía ser de otra manera ya que es la que carga todas las demás, está enlazada estáticamente. Pero además, en Linux es autoejecutable:

Esto nos permite hacer el proceso de carga de una forma un poco distinta. En vez de lanzar nuestro ejecutable que provocará que se cargue ld-linux.so para después pasar a cargar el resto de bibliotecas dinámicas, cargaremos primero ld-linux.so para después cargar el ejecutable (que ya no cargará ld.so porque ya está cargado) y después el ld.so que tenemos cargado ya podrá cargar el resto de bibliotecas dinámicas.
La forma de decirle a ld.so el ejecutable que queremos cargar es pasándoselo como parámetro en la línea de comandos:

/lib/my_renamed_ld-linux.so /bin/bash

Así que ahora ya podemos ejecutar cp para copiar ficheros (nótese que tenemos que incluir la ruta completa del fichero que queremos ejecutar):

/lib/my_renamed_ld-linux.so /bin/cp origen destino

Además, una vez que el loader ha cargado, podemos indiciarle en la variable de entorno LD_PRELOAD una lista de bibliotecas que queremos cargar antes de iniciar la ejecución del programa separadas por puntos. Así, si resulta que hay alguna otra biblioteca necesaria que también hemos renombrado, podemos indicarle explícitamente la nueva ruta de esta biblioteca:

LD_PRELOAD="/lib/my_renamed_libc.so:/lib/my_renamed_librt.so" /lib/my_renamed_ld-linux.so /bin/cp origen destino

Lecciones aprendidas:

  • No renombrar ld.so a la ligera.
  • Con un poco de imaginación, podemos ingeniárnoslas para copiar ficheros u obtener el listado de un directorio usando solo comandos internos de bash.
  • Los ejecutables enlazados estáticamente ocupan mucho más, pero algunos pueden salvarnos la vida, especialmente busybox.
  • El loader de Linux se comporta como un ejecutable más que puede ser llamado desde la línea de comandos pasándole como parámetro el ejecutable que queremos ejecutar. Esto permite incluso que para determinados ejecutables se pueda usar un loader personalizado en vez de usar el del sistema.

Por cierto, a lo largo de este artículo unas veces me referí al loader de Linux como ld.so y otras como ld-linux.so. ld.so es el nombre genérico que usan los loaders de UNIX, incluido el de Linux para el antiguo formato de ejecutable a.out. El loader de Linux para los actuales ejecutables ELF es ld-linux.so.

Y sí, «loader» en español sería «cargador», pero suena tan raro...