Build Your Own Bash Obfuscator

Ofuscador con Bash

Nota: Los pasos descritos en este tutorial se aplicaron en sistemas operativos basados en UNIX.

Introducción:

En esta guía, repasaremos el proceso para crear tu propio ofuscador en Bash. La ofuscación es la práctica de hacer deliberadamente que el código sea más difícil de entender, a menudo para proteger la propiedad intelectual o para disuadir la ingeniería inversa. Exploraremos varias técnicas para lograr este objetivo usando scripts Bash.

Descarga el código fuente antes de continuar:

Motivación

Hace algún tiempo, me encontré con la necesidad de crear un ofuscador que me permitiera ejecutar aplicaciones Bash en un servidor sin que el código fuente fuera evidente o fácil de entender para los usuarios finales. No fue una innovación criptográfica, pero cumplió con mi propósito.

Existen algunas buenas alternativas de código abierto que se pueden usar para ofuscar scripts, pero personalmente no me sirvieron. Ya sea porque eran compiladores en C que requieren compilación en la máquina de destino, o los ofuscadores Bash que encontré no respetaban saltos de línea, espacios, etc.

Ofuscadores

Un ofuscador es un programa que toma el contenido de un archivo (usualmente código fuente) y lo reescribe de manera que sea difícil de entender para el ojo humano, pero que siga siendo ejecutable o interpretable por la máquina. Debo aclarar que esto degrada el rendimiento de tu aplicación, ya que ciertas porciones del código necesitan ser descifradas y luego interpretadas. Además, tiende a ser inseguro porque requiere invocar la función o método “eval”, y en la mayoría de los lenguajes de programación existe la premisa: “eval es peligroso”, ya que te expones a fallas de seguridad graves si no sabes lo que estás haciendo.

Ejemplo de ofuscación en JavaScript usando: https://www.obfuscator.io/

// Paste your JavaScript code here
function hi() {
console.log("Hello World!");
}
hi();

Resultado

function _0x3ec8(_0x296d7c,_0x2693b2){var _0x5613e7=_0x5613();return _0x3ec8=function(_0x3ec8e4,_0x401d60){_0x3ec8e4=_0x3ec8e4-0x6d;var _0x19ddc2=_0x5613e7[_0x3ec8e4];return _0x19ddc2;},_0x3ec8(_0x296d7c,_0x2693b2);}(function(_0x41232c,_0xebe4dc){var _0x3342e0=_0x3ec8,_0x40c3c7=_0x41232c();while(!![]){try{var _0x32d38b=-parseInt(_0x3342e0(0x6e))/0x1+parseInt(_0x3342e0(0x74))/0x2*(-parseInt(_0x3342e0(0x77))/0x3)+-parseInt(_0x3342e0(0x70))/0x4+-parseInt(_0x3342e0(0x76))/0x5*(parseInt(_0x3342e0(0x73))/0x6)+-parseInt(_0x3342e0(0x75))/0x7*(-parseInt(_0x3342e0(0x72))/0x8)+parseInt(_0x3342e0(0x6f))/0x9+parseInt(_0x3342e0(0x71))/0xa;if(_0x32d38b===_0xebe4dc)break;else _0x40c3c7['push'](_0x40c3c7['shift']());}catch(_0xf65114){_0x40c3c7['push'](_0x40c3c7['shift']());}}}(_0x5613,0x59530));function hi(){var _0xb4568e=_0x3ec8;console['log'](_0xb4568e(0x6d));}function _0x5613(){var _0x3f2f47=['1774328LjslCb','12zuPtpB','86006GRGAVr','14XwSEoS','1291735TXNvTp','15KiTYuV','Hello\x20World!','655804PBgVaU','2602566hXhSme','882660GBsfGs','12412940HmFyOD'];_0x5613=function(){return _0x3f2f47;};return _0x5613();}hi();

Creando tu propio ofuscador en Bash

Nota: Nunca uses eval, es muy peligroso. No utilices este script en producción, ya que es extremadamente inseguro. Procede solo si sabes lo que estás haciendo o entiendes los riesgos.

El ofuscador leerá el script de destino, dividirá el contenido en bloques, generará variables, realizará una especie de compresión y añadirá redundancia. Cuando nuestro script ofuscado se ejecute, generará la versión clara en el directorio /tmp, luego cargará esa versión en la memoria interpretando las secciones generadas, y finalmente eliminará el archivo generado. Para finalizar, añadiremos un bonito banner para darle estilo a nuestro ofuscador.

Qué hace que un ofuscador sea un ofuscador:

Genera nombres de variables largos y sin sentido, como se suele hacer con binarios, hexadecimales, etc., como identificadores. Hay mucha redundancia; el código es decodificado o descifrado usando una larga pila de llamadas a funciones. Abusa de eval: un ofuscador no sería un ofuscador si no abusáramos de eval. Transformaciones simétricas, es decir, nuestro objetivo es dificultar la lectura y comprensión, pero siempre habrá una manera de revertir el código fuente.

Herramientas necesarias para ejecutar el ofuscador:

  • figlet
  • bash
  • base64
  • gunzip
  • perl
  • dbus-uuidgen
  • basename
  • cat
  • awk

Herramientas necesarias para ejecutar el código ofuscado:

  • awk
  • gunzip
  • base64
  • bash
  • tail
  • eval

Definiendo la estructura del archivo ofuscado

El archivo ofuscado que vamos a generar se inspira en la técnica utilizada por instaladores autoejecutables en Bash en el mundo de Linux.

El ejecutable ofuscado consta de dos partes:

||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|||||| INITIAL BASH CODE
|||||| MARKER
|||||| COMPRESSED CODE
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
  • El marcador es un delimitador explícito para ayudar al intérprete a localizar las partes que necesitan ser procesadas. Todo el contenido después del marcador se considerará datos comprimidos.
  • El código inicial tiene la función de cargar y descomprimir el bloque ofuscado.
  • El código comprimido son datos representados en base64 que se comprimen con gzip.

Implementación

Antes de continuar, presento el código final como referencia, de lo contrario, no tendrías una guía de lo que voy a explicar.

obs.sh
 #!/bin/bash
output=$3
filex=tmpmainfile.sh
echo $"$(cat $2)" > $filex
banner=$(figlet "$1" | gzip | base64)
banner="base64 -d <<<\"$banner\" | gunzip"
key="# ===BANNER==="
perl -i -pe  "s@$key@$banner@gms" ${filex}
currentDir=$(dirname "$0")
cp $currentDir/template.sh $output
funcaname="xf"$(dbus-uuidgen)
barridoName="b"$(dbus-uuidgen)
markerName="m"$(dbus-uuidgen)
markerNameVar="m"$(dbus-uuidgen)
echo -n $markerNameVar"=\$(awk '/^___"$markerName"___/ {print NR+1; exit 0; }' \"\${0}\");" >> $output
echo -n $barridoName"=\$(tail -n+\${"$markerNameVar"} \"\${0}\");" >> $output
echo "" >> $output
echo 'function '$funcaname'(){' >> $output
echo 'echo $(base64 -d <<< "$1"| gunzip);' >> $output
echo '}' >> $output
REGX=()
IN=$(cat $filex)
while IFS=$'\n' read -ra ADDR; do
for i in "${ADDR[@]}"; do
varxxa="X02D"$(dbus-uuidgen)
cz=$(echo $"$i" | tr -d '\0'| gzip| base64 )
echo -n $varxxa"=\$($funcaname \"$cz\");" >> $output
REGX+=($varxxa)
done
done <<< "$IN"
echo -n $"pbv921=\$(eval \"echo \$\\\"\$"$barridoName"\\\"\");" >> $output
echo -n "b17320=\"\$(cat \${0})\";" >> $output
echo -n "echo \$\"\$pbv921\" > \${0};" >> $output
echo -n "echo \$\"\$(\${0} \${@})\" && echo \$\"\$b17320\" > \${0};" >> $output
echo "exit 0;"  >> $output
echo "___"$markerName"___" >> $output
for i in "${REGX[@]}"; do
echo -n "$"$i"\$z" >> $output
done
echo -n "\"\"" >> $output
rm $filex &2>/dev/null
Usage
sudo chmod u+x ./obs.sh
./obs.sh  "BANNER" "code_to_obfuscate.sh" "output_file.sh"

Template.sh

Crea este archivo junto a obs.sh con el nombre template.sh

#!/bin/bash
z="
";

Generando Código Comprimido

El ofuscador necesita leer el script original y dividirlo en piezas para posteriormente transformar y manipular cada línea. Para hacer esto, elegiremos un carácter que servirá como separador; en mi caso, elegí el salto de línea (Carácter de Salto de Línea \n). Cada porción del archivo se asignará a una variable que se añadirá a un archivo de salida.

En resumen:

  • Leer el archivo original.
  • Dividir el contenido usando el símbolo de salto de línea (\n).
  • Cada fragmento se almacenará en una nueva variable.
  • Las nuevas variables se guardarán en un nuevo archivo (el archivo ofuscado).
  • Para aumentar la confusión, nuestro archivo final será minimizado.
IN=$(cat "input.sh")
output="obfuscated.sh"
while IFS=$'\n' read -ra ADDR; do
for i in "${ADDR[@]}"; do
varxxa="var"$(dbus-uuidgen)
echo -n $varxxa"=$i" >> $output
done
done <<< "$IN"

Explicación:

  • “IN” lee el archivo que queremos ofuscar.
  • “output” es el nombre del archivo final.
  • “while IFS.. done «< $IN” nos permite leer un archivo usando \n como separador de bloques. Cada bloque de texto se almacenará en la variable ADDR. Un bloque de texto puede contener una o más líneas.
  • “for i in…” nos permite iterar sobre las líneas del bloque extraído.
  • “varxxa” genera el nombre de las variables que nuestro ofuscador usará para crear confusión mental. Utilizamos uuid para generar un id único.
  • “echo -n $varxxa…” asigna a la variable generada el contenido del bloque recortado y lo almacena en el archivo de destino.

El código anterior tiene una falla: cada línea del bloque está en texto plano. Para solucionar esto, utilizaremos compresión gzip. Ahora bien, la salida de gzip no es fácil de manejar, ya que contiene caracteres especiales ocultos, por lo que el siguiente paso será usar base64 para almacenar el resultado en el archivo de destino.

funcaname="xf"$(dbus-uuidgen)
echo "" >> $output
echo 'function '$funcaname'(){' >> $output
echo 'echo $(base64 -d <<< "$1"| gunzip);' >> $output
echo '}' >> $outputt

Notarás que hemos añadido echo -n $varxxa”=$($funcaname "$cz");. Esta línea invoca dinámicamente la función que descomprimirá el texto en base64. Para que esto funcione, debemos declarar la función de antemano (el nombre de la función se genera aleatoriamente) y guardarla en el archivo de destino.

Marcador

El archivo ofuscado, una vez ejecutado, comenzará leyendo su propio contenido con el propósito claro de buscar el marcador. Todo el contenido después del marcador se considerará código comprimido. Para lograr esto, usamos awk para recorrer el archivo ofuscado línea por línea desde el marcador hasta el final del archivo (EOF).

Este bloque genera dinámicamente el marcador y la lógica para detectar la parte comprimida, se generan variables, y todo esto se guarda en el archivo de destino ofuscado:

barridoName="b"$(dbus-uuidgen)
markerName="m"$(dbus-uuidgen)
markerNameVar="m"$(dbus-uuidgen)
echo -n $markerNameVar"=\$(awk '/^___"$markerName"___/ {print NR+1; exit 0; }' \"\${0}\");" >> $output
echo -n $barridoName"=\$(tail -n+\${"$markerNameVar"} \"\${0}\");" >> $output
# barridoName contains the name of the variable that will store the compressed content once the obfuscator detects the marker.
# markerNameVar is the name of the variable that will store the line of code where the marker is located.
# markerName is the delimiter or marker dynamically generated by the obfuscator.

La siguiente sección hará que el programa termine antes de llegar a la línea del marcador. Luego imprimirá el delimitador y todos los bloques comprimidos al final del archivo de destino.

echo "exit 0;"  >> $output
echo "___"$markerName"___" >> $output
for i in "${REGX[@]}"; do
echo -n "$"$i"\$z" >> $output
done
echo -n "\"\"" >> $output
rm $filex &2>/dev/nullt
# rm $filex &2>/dev/null: This line will cause the obfuscated file to delete temporary data created in the tmp folder.

Código Bash Inicial

El código inicial tiene la función de cargar y descomprimir el bloque ofuscado. Todo el contenido que se encuentre después del marcador se considerará como datos comprimidos. Notarás que el marcador es un delimitador colocado explícitamente para ayudar al intérprete a localizar las partes que necesitan ser procesadas.

echo -n $"pbv921=\$(eval \"echo \$\\\"\$"$barridoName"\\\"\");" >> $outpu
echo -n "b17320=\"\$(cat \${0})\";" >> $output
echo -n "echo \$\"\$pbv921\" > \${0};" >> $output
echo -n "echo \$\"\$(\${0} \${@})\" && echo \$\"\$b17320\" > \${0};" >> $output
# We use eval to evaluate the decompressed code, with its content stored in the variable named in barridoName.

Añadiendo un Banner a Nuestro Archivo Ofuscado

figlet nos ayuda en esta tarea; utilizamos esta herramienta pasando el texto del banner definido en el argumento $1 de nuestro ofuscador. ¡Oh, mira, más compresión y contenido ambiguo en nuestro archivo ofuscado!

filex=tmpmainfile.s
echo $"$(cat $2)" > $filex
banner=$(figlet "$1" | gzip | base64)
banner="base64 -d <<<\"$banner\" | gunzip"
key="# ===BANNER==="
perl -i -pe  "s@$key@$banner@gms" ${filex}h
# We practically replace the placeholder marker "===BANNER===" with the content from figlet, this after decompressing it.