Como desofuscar scripts do PowerShell

Desofuscar um script do PowerShell é desfazer, camada por camada, as transformações que um atacante aplicou para esconder o que o script realmente faz — e fazê-lo sem jamais executar o script. Uma linha suspeita que chega como um muro de Base64, ou como um emaranhado de códigos de caracteres e concatenação de strings, geralmente é por baixo um comando perfeitamente comum do PowerShell: um download, uma carga decodificada, um comando que roda em memória e nunca toca o disco. A ofuscação não está ali para ser engenhosa. Está ali para passar pelos scanners de assinatura, para tornar a linha de comando ilegível à primeira vista e para fazer o analista humano desistir antes da parte interessante. Este artigo explica por que cada camada existe, como desfazê-la como texto inerte e o que as peças encontradas realmente significam. Você pode percorrer todo o fluxo no desofuscador de PowerShell, que decodifica camada por camada inteiramente no seu navegador — nada é executado e nada sai da sua máquina.

Por que os atacantes ofuscam o PowerShell — e por que a forma codificada é a primeira coisa a desfazer

O PowerShell vem embutido em todas as máquinas Windows, pode baixar e executar código da internet sem escrever um arquivo no disco e roda com os privilégios de quem o lançou. Essa combinação é a razão de ser uma das ferramentas mais abusadas do framework MITRE ATT&CK, catalogada como técnica T1059.001, e de a primeira coisa que um atacante faz com um comando malicioso do PowerShell ser escondê-lo. Um comando em texto simples como DownloadString seguido de Invoke-Expression é trivialmente detectável: qualquer scanner, qualquer revisão de logs e qualquer analista que olhe a linha de comando do processo vai detectá-lo. Codificado, comprimido e picado em pedaços, o mesmo comando vira uma cadeia de caracteres que não corresponde a nenhuma assinatura conhecida e não significa nada para um leitor humano.

Essa também é a razão de a forma codificada ser a primeira coisa a desfazer, antes de qualquer outra. Até decodificá-la, você não sabe se está diante de um instalador, de uma brincadeira ou de um ladrão de credenciais — e qualquer outro julgamento é um palpite. A boa notícia é que a ofuscação quase sempre é reversível por desenrolamento estático: a carga precisa poder ser decodificada pela máquina que a executa, então cada camada tem um inverso determinístico. Base64 decodifica, gzip infla, códigos de caracteres voltam a ser letras e strings concatenadas se juntam. O trabalho é mecânico, não misterioso, e é o passo de maior valor na triagem de um script suspeito: uma única decodificação transforma um "blob ilegível" em um comando legível sobre o qual dá para raciocinar de verdade.

-EncodedCommand e UTF-16LE: por que o Base64 que parece um amontoado geralmente é texto legível

A forma mais comum de ofuscação do PowerShell que você vai encontrar é um único parâmetro: powershell.exe -EncodedCommand seguido de uma longa string Base64. Parece caracteres aleatórios, mas quase sempre é apenas texto comum de script, codificado de um jeito que surpreende na primeira vez que se vê. O -EncodedCommand do PowerShell espera que o Base64 decodifique para UTF-16LE — dois bytes por caractere, com um byte zero depois de cada letra ASCII — porque essa é a codificação das strings .NET no Windows. Assim, o Base64 do comando inofensivo Write-Host "hello" começa com SQB3AHIAaQB0AGUALQBIAG8AcwB0ACA… e o padrão revelador são os caracteres "A" repetidos que vêm de todos aqueles bytes zero.

Saber disso muda a forma de ler um blob codificado. Uma string Base64 que decodifica para um amontoado em UTF-8 provavelmente não é UTF-8: tente primeiro UTF-16LE e o "amontoado" se resolve em PowerShell legível. O mesmo vale para um blob Base64 nu, sem nenhum parâmetro -EncodedCommand à vista — tokens longos de Base64 válido que decodificam para texto imprimível são o mesmo truque sem a bandeira. A regra prática é simples: quando vir um token Base64 longo numa linha de comando do PowerShell, suponha que é um script até a decodificação provar o contrário, e decodifique-o de forma inerte — como texto, numa ferramenta que nunca o execute — em vez de colá-lo numa janela de chat ou num terminal para ver o que acontece.

O bolo de camadas: compressão, códigos de caracteres, concatenação, -f, acentos graves

Cargas reais raramente param numa camada. Depois que o -EncodedCommand é decodificado, o script por baixo geralmente está codificado de novo: um blob comprimido com gzip envolto em outra string Base64, ou uma sequência de códigos de caracteres como [char]68+[char]105 que soletra um comando número por número, ou uma string picada em fragmentos e colada de volta com sinais de mais, ou uma string de formato com o operador -f preenchendo as peças em tempo de execução, ou acentos graves espalhados pelas palavras-chave para que Do`wnloadString não corresponda mais a uma busca ingênua por DownloadString. Cada camada existe por um motivo diferente. A compressão encolhe uma carga longa numa linha curta e derrota por completo a busca por texto. Códigos de caracteres e concatenação derrotam a correspondência de assinaturas garantindo que as palavras suspeitas nunca apareçam literalmente no arquivo. O operador -f e os acentos graves derrotam buscas simples por strings mantendo o script sintaticamente válido.

A defesa contra todas elas é a mesma: desenrolar uma camada de cada vez, em ordem, e parar quando nada mais se aplicar. Decodifique o Base64, depois verifique se o resultado está comprimido e infle-o, depois resolva os códigos de caracteres, junte os fragmentos, substitua os argumentos de formato e remova os acentos graves — cada passo produz texto simples com o qual o próximo passo pode trabalhar. Uma ferramenta que faz isso como uma linha do tempo visível, um cartão por camada com o antes e o depois, vale mais do que uma ferramenta que emite silenciosamente uma resposta final, porque a linha do tempo é em si uma evidência: mostra quais camadas estavam presentes e, quando o desenrolamento para antes da hora — "não foi possível desenrolar mais" — isso é um achado, não uma falha. Isso diz que a carga contém algo dinâmico, criptografado ou construído em tempo de execução que a análise estática sozinha não alcança.

Registro de blocos de script 4104: como scripts longos são divididos e remontados

Há uma fonte de PowerShell desofuscado que não exige nenhuma decodificação: a própria máquina. Quando o registro de blocos de script está ativado, o Windows registra o conteúdo de cada bloco de script que o PowerShell processa como evento 4104 no log Microsoft-Windows-PowerShell/Operational — e registra o código como o PowerShell o viu, depois que o mecanismo já expandiu o -EncodedCommand e as camadas de ofuscação. Uma linha suspeita que chegou como Base64 aparece no evento 4104 como o script legível que realmente executou. Por isso esse recurso é a configuração de registro do PowerShell mais valiosa que um defensor pode ativar, e por isso vem desativado por padrão: é verboso, e os logs podem conter credenciais e outros dados sensíveis, então a Microsoft recomenda o registro de eventos protegido junto com ele para qualquer uso além de diagnósticos.

O problema é que o Windows divide blocos de script longos em vários eventos 4104. Cada evento carrega o mesmo ScriptBlockId, um MessageNumber e um MessageTotal — a parte 2 de 5, por exemplo — e o script completo só existe quando as partes são agrupadas por esse ID, ordenadas por número de mensagem e concatenadas. Um único evento isolado pode terminar no meio de uma palavra, e uma busca que leia só o primeiro fragmento vai perder o que a carga faz no final. A remontagem é, portanto, um passo mecânico com uma única coisa a verificar: se falta alguma parte. Se o log diz 2 de 3 e a parte 2 nunca chegou — porque o log rolou ou foi limpo — o texto remontado está incompleto e qualquer conclusão extraída dele é provisória. A mesma lógica de agrupamento é a que o visualizador de EVTX usa ao sinalizar eventos 4104 em arquivos .evtx exportados; veja também o artigo complementar Ler arquivos .evtx sem uma máquina Windows.

Lendo os indicadores: berços de download, bypass de AMSI, bypass de política de execução

Depois que o script fica legível, o próximo passo não é declará-lo malicioso — é ler os indicadores que ele contém como pistas a verificar. Um berço de download — DownloadString, DownloadFile, Net.WebClient, Invoke-WebRequest, bitsadmin, certutil puxando um arquivo de uma URL — diz que o script busca código de algum lugar, e a próxima pergunta é de onde, e se esse destino é esperado na sua rede. Um bypass de AMSI — amsiInitFailed, AmsiUtils ou código que remenda a interface de varredura antimalware — diz que o autor queria que o script rodasse sem ser varrido, o que é um sinal forte, mas não um veredito: pentesters e algumas ferramentas legítimas fazem o mesmo. Um bypass de política de execução (-ExecutionPolicy Bypass, -ep bypass) diz que o autor não queria que as proteções do próprio PowerShell parassem o script, o que é comum tanto em malware quanto na automação comum que roda scripts não assinados.

A disciplina é tratar cada indicador como uma pergunta, não como uma resposta. Um berço de download mais uma janela oculta (-WindowStyle Hidden) mais um comando codificado é um padrão que merece ser escalado; qualquer um deles sozinho é um padrão que merece ser verificado. O que transforma uma pista em veredito é o contexto: quem lançou o processo, qual foi o processo pai, se a URL contatada é conhecida, se o mesmo script aparece em outras máquinas. Um desofuscador que lista os indicadores com o trecho correspondente e uma nota de "verificar no contexto" está fazendo o trabalho honesto; um que carimba um script como "malicioso" ou "limpo" a partir de uma lista de padrões está exagerando o que a análise estática pode saber. E o inverso também importa: um script sem indicadores encontrados não está provado como seguro — ele pode estar simplesmente usando uma técnica que a lista não cobre, ou construindo sua carga dinamicamente em tempo de execução.

O fluxo de trabalho seguro: nunca cole no chat, nunca execute, decodifique de forma inerte

A regra mais importante ao lidar com um script suspeito do PowerShell é que você nunca o executa para descobrir o que ele faz, e nunca o cola numa janela de chat para perguntar a uma IA. Executá-lo executa a carga — esse é o resultado que você está tentando evitar. Colá-lo num chat envia um script potencialmente malicioso para um serviço de terceiros, deixa-o num histórico de conversa que você não controla e muitas vezes ainda devolve uma recusa, porque sistemas de IA rotineiramente se recusam a desofuscar scripts que podem ser maliciosos. O fluxo de trabalho seguro é propositalmente tedioso: copie o blob para um desofuscador local do navegador, ou para uma máquina de análise em sandbox, e trate a saída decodificada como texto inerte para ler, nunca como código para executar.

A partir daí, o fluxo é uma lista curta. Primeiro, decodifique camada por camada até o texto parar de mudar, e anote onde o desenrolamento parou. Segundo, leia os indicadores e anote o que cada um significaria se fosse verdade, depois verifique cada um contra o contexto — o processo pai, a URL de destino, a máquina de onde veio. Terceiro, entregue o resultado às pessoas ou sistemas feitos para o próximo passo: seu console de EDR, uma detonação em sandbox, sua equipe de resposta a incidentes. O trabalho de um desofuscador termina no texto legível e em indicadores honestos; o veredito pertence ao sandbox e ao analista, não à ferramenta de decodificação. Se o script veio de uma máquina que você gerencia, verifique se o registro de blocos de script estava ligado — se estava, os eventos 4104 são a verdade de base do que executou, e vale a pena coletá-los antes que o log role.

Fontes

Perguntas frequentes

É seguro desofuscar um script suspeito do PowerShell?

Sim, se a decodificação for puramente estática — tratar o blob como texto e nunca executá-lo. Um desofuscador local do navegador que decodifica Base64, infla gzip, resolve códigos de caracteres e junta strings está apenas rearranjando texto; nada é executado. O que não é seguro é colar o script num terminal, clicar duas vezes num arquivo .ps1 ou colá-lo num serviço de chat, o que ou o executa ou o envia a terceiros. Mantenha a decodificação inerte e a saída decodificada como texto para ler.

Decodifiquei o -EncodedCommand e o resultado ainda parece codificado. E agora?

Isso é normal: cargas reais empilham camadas. O texto decodificado pode ser, por sua vez, um blob gzip envolto em Base64, ou uma string construída a partir de códigos [char], concatenação ou o operador de formato -f. Desenrole uma camada de cada vez — decodifique, depois verifique se há compressão, depois resolva os códigos de caracteres e as junções — até o texto parar de mudar. Se parar de mudar enquanto continua ilegível, a carga provavelmente está criptografada ou é construída dinamicamente em tempo de execução, e o desenrolamento estático chegou ao limite: isso é um achado para entregar a um sandbox, não uma falha da ferramenta.

Um resultado limpo significa que o script é seguro?

Não. Um resultado limpo significa que nenhum padrão de indicador conhecido correspondeu e que cada camada que podia ser desenrolada foi desenrolada — não prova que o script seja inofensivo. Uma carga pode estar criptografada, pode construir suas strings em tempo de execução a partir de valores que só a máquina em execução conhece, ou pode simplesmente usar uma técnica que a lista de indicadores não cobre. Trate um resultado limpo como "a análise estática não encontrou nada" e verifique num sandbox ou com seu EDR antes de decidir qualquer coisa. O mesmo vale ao contrário: um único indicador é uma pista a verificar, não um veredito.