Cómo desofuscar scripts de PowerShell

Desofuscar un script de PowerShell es deshacer, capa por capa, las transformaciones que un atacante aplicó para ocultar lo que el script hace realmente — y hacerlo sin ejecutar jamás el script. Una línea sospechosa que llega como un muro de Base64, o como una maraña de códigos de caracteres y concatenación de cadenas, suele ser debajo un comando de PowerShell perfectamente ordinario: una descarga, una carga útil decodificada, un comando que se ejecuta en memoria y nunca toca el disco. La ofuscación no está ahí para ser ingeniosa. Está ahí para pasar los escáneres de firmas, para que la línea de comandos sea ilegible de un vistazo y para que el analista humano se rinda antes de llegar a la parte interesante. Este artículo explica por qué existe cada capa, cómo deshacerla como texto inerte y qué significan realmente las piezas que encuentres. Puedes recorrer todo el flujo en el desofuscador de PowerShell, que decodifica capa por capa por completo en tu navegador: nada se ejecuta y nada sale de tu máquina.

Por qué los atacantes ofuscan PowerShell — y por qué la forma codificada es lo primero que hay que deshacer

PowerShell viene integrado en todas las máquinas Windows, puede descargar y ejecutar código de internet sin escribir un archivo en el disco y se ejecuta con los privilegios de quien lo lanzó. Esa combinación es la razón por la que es una de las herramientas más abusadas del marco MITRE ATT&CK, catalogada como técnica T1059.001, y por la que lo primero que hace un atacante con un comando malicioso de PowerShell es ocultarlo. Un comando en texto plano como DownloadString seguido de Invoke-Expression es trivialmente detectable: cualquier escáner, cualquier revisión de registros y cualquier analista que mire la línea de comandos del proceso lo detectará. Codificado, comprimido y troceado, el mismo comando se convierte en una cadena de caracteres que no coincide con ninguna firma conocida y que no significa nada para un lector humano.

Esa es también la razón por la que la forma codificada es lo primero que hay que deshacer, antes que nada. Hasta que la decodifiques, no sabes si estás ante un instalador, una broma o un ladrón de credenciales, y cualquier otro juicio que hagas es una conjetura. La buena noticia es que la ofuscación casi siempre es reversible por desenredo estático: la carga útil tiene que poder ser decodificada por la máquina que la ejecuta, así que cada capa tiene una inversa determinista. Base64 se decodifica, gzip se infla, los códigos de caracteres vuelven a ser letras y las cadenas concatenadas se unen. El trabajo es mecánico, no misterioso, y es el paso de mayor valor al clasificar un script sospechoso: una sola decodificación convierte un «blob ilegible» en un comando legible sobre el que realmente se puede razonar.

-EncodedCommand y UTF-16LE: por qué el Base64 que parece galimatías suele ser texto legible

La forma más común de ofuscación de PowerShell que encontrarás es un solo interruptor: powershell.exe -EncodedCommand seguido de una larga cadena Base64. Parece caracteres aleatorios, pero casi siempre es solo texto de script ordinario, codificado de una forma que sorprende la primera vez que se ve. El -EncodedCommand de PowerShell espera que el Base64 se decodifique a UTF-16LE — dos bytes por carácter, con un byte cero después de cada letra ASCII — porque esa es la codificación de las cadenas .NET en Windows. Así, el Base64 del comando inofensivo Write-Host "hello" empieza por SQB3AHIAaQB0AGUALQBIAG8AcwB0ACA… y el patrón revelador son las «A» repetidas que provienen de todos esos bytes cero.

Saber esto cambia la forma de leer un blob codificado. Una cadena Base64 que se decodifica a galimatías UTF-8 probablemente no sea UTF-8 en absoluto: prueba primero UTF-16LE y el «galimatías» se resuelve en PowerShell legible. Lo mismo vale para un blob Base64 desnudo sin interruptor -EncodedCommand a la vista: los tokens largos de Base64 válido que se decodifican a texto imprimible son el mismo truco sin la bandera. La regla práctica es simple: cuando veas un token Base64 largo en una línea de comandos de PowerShell, supón que es un script hasta que la decodificación demuestre lo contrario, y decodifícalo de forma inerte — como texto, en una herramienta que nunca lo ejecute — en lugar de pegarlo en una ventana de chat o en una terminal para ver qué pasa.

La tarta de capas: compresión, códigos de caracteres, concatenación, -f, acentos graves

Las cargas útiles reales rara vez se detienen en una capa. Una vez decodificado el -EncodedCommand, el script que hay debajo suele estar codificado otra vez: un blob comprimido con gzip envuelto en otra cadena Base64, o una secuencia de códigos de caracteres como [char]68+[char]105 que deletrea un comando número a número, o una cadena troceada en fragmentos y vuelta a pegar con signos más, o una cadena de formato con el operador -f rellenando las piezas en tiempo de ejecución, o acentos graves espolvoreados por las palabras clave para que Do`wnloadString ya no coincida con una búsqueda ingenua de DownloadString. Cada capa existe por una razón distinta. La compresión reduce una carga larga a una línea corta y derrota por completo la búsqueda de texto. Los códigos de caracteres y la concatenación derrotan la coincidencia de firmas al garantizar que las palabras sospechosas nunca aparezcan literalmente en el archivo. El operador -f y los acentos graves derrotan las búsquedas simples de cadenas manteniendo el script sintácticamente válido.

La defensa contra todas ellas es la misma: desenredar una capa cada vez, en orden, y parar cuando ya no se aplique ninguna. Decodifica el Base64, luego comprueba si el resultado está comprimido e ínflalo, luego resuelve los códigos de caracteres, une los fragmentos, sustituye los argumentos de formato y elimina los acentos graves; cada paso produce texto plano con el que puede trabajar el siguiente. Una herramienta que haga esto como una línea de tiempo visible, una tarjeta por capa con el antes y el después, vale más que una herramienta que emite en silencio una respuesta final, porque la línea de tiempo es en sí misma evidencia: muestra qué capas estaban presentes y, cuando el desenredo se detiene antes de tiempo —«no se pudo desenredar más»—, eso es un hallazgo, no un fallo. Te dice que la carga contiene algo dinámico, cifrado o construido en tiempo de ejecución que el análisis estático por sí solo no puede alcanzar.

Registro de bloques de script 4104: cómo se dividen los scripts largos y se vuelven a ensamblar

Hay una fuente de PowerShell desofuscado que no requiere ninguna decodificación: la propia máquina. Cuando el registro de bloques de script está activado, Windows registra el contenido de cada bloque de script que procesa PowerShell como evento 4104 en el registro Microsoft-Windows-PowerShell/Operational, y registra el código tal como lo vio PowerShell, después de que el motor ya haya expandido el -EncodedCommand y las capas de ofuscación. Una línea sospechosa que llegó como Base64 aparece en el evento 4104 como el script legible que realmente se ejecutó. Por eso esta función es el ajuste de registro de PowerShell más valioso que puede activar un defensor, y por eso viene desactivada por defecto: es verbosa y los registros pueden contener credenciales y otros datos sensibles, así que Microsoft recomienda el registro de eventos protegido junto a ella para cualquier uso más allá del diagnóstico.

El inconveniente es que Windows divide los bloques de script largos en varios eventos 4104. Cada evento lleva el mismo ScriptBlockId, un MessageNumber y un MessageTotal —la parte 2 de 5, por ejemplo— y el script completo solo existe cuando las partes se agrupan por ese ID, se ordenan por número de mensaje y se concatenan. Un solo evento aislado puede terminar a mitad de palabra, y una búsqueda que lea solo el primer fragmento pasará por alto lo que la carga hace al final. El reensamblaje es, por tanto, un paso mecánico con una sola cosa que comprobar: si falta alguna parte. Si el registro dice 2 de 3 y la parte 2 nunca llegó —porque el registro rotó o se borró—, el texto reensamblado está incompleto y cualquier conclusión extraída de él es provisional. La misma lógica de agrupación es la que usa el visor de EVTX cuando marca eventos 4104 en archivos .evtx exportados; consulta también el artículo complementario Leer archivos .evtx sin una máquina Windows.

Lectura de los indicadores: cunas de descarga, evasión de AMSI, evasión de la política de ejecución

Una vez que el script es legible, el siguiente paso no es declararlo malicioso: es leer los indicadores que contiene como pistas por verificar. Una cuna de descarga —DownloadString, DownloadFile, Net.WebClient, Invoke-WebRequest, bitsadmin, certutil extrayendo un archivo de una URL— te dice que el script obtiene código de algún lugar, y la siguiente pregunta es de dónde y si ese destino es esperable en tu red. Una evasión de AMSI —amsiInitFailed, AmsiUtils o código que parchea la interfaz de análisis antimalware— te dice que el autor quería que el script se ejecutara sin ser escaneado, lo cual es una señal fuerte pero no un veredicto: los pentesters y algunas herramientas legítimas hacen lo mismo. Una evasión de la política de ejecución (-ExecutionPolicy Bypass, -ep bypass) te dice que el autor no quería que las protecciones propias de PowerShell detuvieran el script, algo común tanto en el malware como en la automatización ordinaria que ejecuta scripts sin firmar.

La disciplina consiste en tratar cada indicador como una pregunta, no como una respuesta. Una cuna de descarga más una ventana oculta (-WindowStyle Hidden) más un comando codificado es un patrón que merece escalarse; cualquiera de ellos por sí solo es un patrón que merece comprobarse. Lo que convierte una pista en veredicto es el contexto: quién lanzó el proceso, cuál fue su proceso padre, si la URL que contacta es conocida, si el mismo script aparece en otras máquinas. Un desofuscador que enumera los indicadores con el fragmento coincidente y una nota de «verificar en contexto» está haciendo el trabajo honesto; uno que estampa un script como «malicioso» o «limpio» a partir de una lista de patrones está exagerando lo que el análisis estático puede saber. Y lo contrario también importa: un script sin indicadores encontrados no está probado como seguro; puede estar usando simplemente una técnica que la lista no cubre, o construyendo su carga dinámicamente en tiempo de ejecución.

El flujo de trabajo seguro: nunca lo pegues en el chat, nunca lo ejecutes, decodifica de forma inerte

La regla más importante al manejar un script sospechoso de PowerShell es que nunca lo ejecutes para averiguar qué hace, y nunca lo pegues en una ventana de chat para preguntarle a una IA. Ejecutarlo ejecuta la carga útil: ese es el resultado que intentas evitar. Pegarlo en un chat envía un script potencialmente malicioso a un servicio de terceros, lo deja en un historial de conversación que no controlas y a menudo te devuelve una negativa de todos modos, porque los sistemas de IA suelen negarse a desofuscar scripts que pueden ser maliciosos. El flujo de trabajo seguro es aburrido a propósito: copia el blob en un desofuscador local del navegador, o en una máquina de análisis en sandbox, y trata la salida decodificada como texto inerte para leer, nunca como código para ejecutar.

A partir de ahí, el flujo es una lista corta. Primero, decodifica capa por capa hasta que el texto deje de cambiar, y anota dónde se detuvo el desenredo. Segundo, lee los indicadores y anota lo que significaría cada uno si fuera cierto, y luego verifica cada uno contra el contexto: el proceso padre, la URL de destino, la máquina de la que vino. Tercero, entrega el resultado a las personas o sistemas creados para el siguiente paso: tu consola de EDR, una detonación en sandbox, tu equipo de respuesta a incidentes. El trabajo de un desofuscador termina en el texto legible y en indicadores honestos; el veredicto pertenece al sandbox y al analista, no a la herramienta de decodificación. Si el script vino de una máquina que gestionas, comprueba si el registro de bloques de script estaba activado: si lo estaba, los eventos 4104 son la verdad de base de lo que se ejecutó, y merece la pena recogerlos antes de que el registro rote.

Fuentes

Preguntas frecuentes

¿Es seguro desofuscar un script sospechoso de PowerShell?

Sí, si la decodificación es puramente estática: tratar el blob como texto y no ejecutarlo nunca. Un desofuscador local del navegador que decodifica Base64, infla gzip, resuelve códigos de caracteres y une cadenas solo está reordenando texto; nada se ejecuta. Lo que no es seguro es pegar el script en una terminal, hacer doble clic en un archivo .ps1 o pegarlo en un servicio de chat, que o lo ejecuta o lo envía a un tercero. Mantén la decodificación inerte y la salida decodificada como texto para leer.

Decodifiqué el -EncodedCommand y el resultado sigue pareciendo codificado. ¿Y ahora?

Eso es normal: las cargas reales apilan capas. El texto decodificado puede ser a su vez un blob gzip envuelto en Base64, o una cadena construida a partir de códigos [char], concatenación o el operador de formato -f. Desenreda una capa cada vez —decodifica, luego comprueba si hay compresión, luego resuelve los códigos de caracteres y las uniones— hasta que el texto deje de cambiar. Si deja de cambiar mientras sigue siendo ilegible, la carga probablemente esté cifrada o se construya dinámicamente en tiempo de ejecución, y el desenredo estático ha llegado a su límite: eso es un hallazgo para entregar a un sandbox, no un fallo de la herramienta.

¿Un resultado limpio significa que el script es seguro?

No. Un resultado limpio significa que ningún patrón de indicador conocido coincidió y que se desenredó cada capa que podía desenredarse; no prueba que el script sea inofensivo. Una carga puede estar cifrada, puede construir sus cadenas en tiempo de ejecución a partir de valores que solo conoce la máquina en ejecución, o puede usar simplemente una técnica que la lista de indicadores no cubre. Trata un resultado limpio como «el análisis estático no encontró nada» y verifícalo en un sandbox o con tu EDR antes de decidir nada. Lo mismo vale al revés: un solo indicador es una pista por verificar, no un veredicto.