Evasão por de-otimização: quando o código se disfarça de si mesmo

As técnicas tradicionais de evasão dependem de código que se modifica em memória. Ferramentas modernas já sabem disso. A de-otimização ataca o problema numa camada diferente.

O problema com a solução que todo mundo usa

A maioria das ferramentas de evasão de antivírus resolve o problema da mesma forma: esconde o payload dentro de uma casca, e essa casca o descriptografa em memória na hora da execução. Packers, shellcode encoders, obfuscators — todos trabalham com alguma variação desse modelo. O resultado é um binário que parece inofensivo no disco mas que, em algum momento, escreve o código real numa região de memória e pula para ela.

Esse padrão tem um nome no mundo de detecção: self-modifying code em regiões RWE. E ele ficou velho.

Ferramentas como Moneta e PE-sieve são construídas exatamente para caçar esse comportamento. Elas varrem a memória do processo em tempo de execução, procuram regiões com permissões de leitura, escrita e execução simultaneamente, e analisam o que está dentro delas. Se o código que está sendo executado não corresponde ao que foi carregado do disco, o jogo acabou. A detecção não depende de assinatura, não depende de heurística de comportamento — ela depende de uma contradição matemática que o próprio processo cria ao rodar.

O problema não é que a técnica seja fraca. É que ela é estruturalmente detectável. Não importa quão sofisticado seja o packer, o momento em que ele escreve o payload descriptografado numa região executável, ele cria uma evidência que não pode ser apagada sem desfazer o próprio trabalho.


A pergunta que muda o enquadramento

Se o problema é o código que se modifica em tempo de execução, a solução óbvia é: e se o código não precisar se modificar?

É aí que a de-otimização de código de máquina entra como abordagem. A ideia não é esconder o payload dentro de outra coisa. É transformar o próprio payload de forma que ele permaneça semanticamente idêntico, mas estruturalmente irreconhecível — e fazer isso de forma estática, antes da execução, sem criar nenhuma região RWE, sem nenhuma escrita em memória em runtime.

O binário que vai para o disco já é o binário final. Não há descriptografia, não há automodificação. Há apenas instruções de máquina que fazem exatamente o que sempre fizeram, escritas de uma forma que nenhuma ferramenta de análise estática vai reconhecer como familiar.


Como a transformação funciona

O código de máquina não é uma linguagem com uma única forma de expressar cada operação. A arquitetura x86, em particular, tem redundância suficiente para representar o mesmo cálculo de dezenas de formas diferentes, todas igualmente válidas para o processador. A de-otimização explora isso de forma sistemática, usando transformações matemáticas que preservam o resultado mas destroem qualquer padrão reconhecível.

A primeira técnica é o particionamento aritmético. Uma instrução simples como ADD RAX, 5 pode ser substituída por uma sequência que calcula o mesmo valor através de operações intermediárias: ADD RAX, 3, seguido de ADD RAX, 2. O resultado é idêntico. Os bytes no binário são completamente diferentes. E a sequência pode ser particionada de formas diferentes a cada execução da transformação, garantindo que o mesmo payload nunca produza o mesmo padrão de bytes duas vezes.

A segunda é a transformação polinomial. Constantes numéricas, em vez de aparecerem diretamente como valores imediatos nas instruções, são representadas como resultados de expressões polinomiais que o processador calcula em tempo de execução. O valor 0x1337, por exemplo, pode ser derivado de (x² + 3x - 7) para algum x escolhido em tempo de transformação. O processador chega no mesmo lugar, mas o padrão de bytes que chegou lá não tem nenhuma relação visual com o destino.

A terceira é o inverso lógico, fundamentado nas leis de De Morgan. Operações como AND e OR podem ser reescritas usando suas equivalências lógicas invertidas: NOT (NOT A OR NOT B) é matematicamente idêntico a A AND B. Em código de máquina, isso se traduz em substituir instruções lógicas por sequências de instruções que o processador executa de forma diferente mas que produzem o mesmo estado nos registradores ao final.

A quarta é o particionamento lógico, que divide operações de máscara e manipulação de bits em sequências menores que preservam o efeito combinado. Uma máscara aplicada em uma instrução pode ser decomposta em duas ou três operações intermediárias, nenhuma das quais se parece com a original.

Combinadas, essas transformações são capazes de reescrever até 95% das instruções de um binário sem alterar seu comportamento. O binário resultante passa por análise estática como se fosse código gerado por um compilador diferente — porque, de certa forma, é.


Por que isso é diferente do que já existe

Obfuscação de código de máquina não é nova. O que diferencia essa abordagem é a ausência de padrões reconhecíveis na própria transformação.

Técnicas tradicionais de obfuscação inserem NOPs, jumps inúteis ou chamadas de função que não fazem nada. Isso cria um padrão diferente, mas ainda detectável: ferramentas de análise aprenderam a reconhecer sequências de instrução que não fazem sentido para um compilador normal. O código parece estranho de uma forma específica, e essa estranheza específica virou assinatura.

A de-otimização por transformações matemáticas não cria código estranho. Cria código que parece simplesmente diferente — como se tivesse sido compilado com flags de otimização diferentes, ou gerado por um compilador menos comum. As sequências resultantes são semanticamente corretas, sintaticamente válidas e estruturalmente plausíveis. Não há nada para um analisador estático apontar como anômalo, porque nada é anômalo. É apenas código fazendo o que código faz.


Onde a técnica tem limite

Honestidade técnica exige dizer que isso resolve um problema específico dentro de um problema maior.

A de-otimização ataca a camada estática da detecção: assinaturas de bytes, análise de padrões no binário, fingerprinting de compiladores. Ela não faz nada pela camada dinâmica, que é onde EDRs modernos como CrowdStrike, SentinelOne e Microsoft Defender for Endpoint colocam a maior parte do seu peso.

Um payload que sobrevive à análise estática ainda vai ser observado em tempo de execução. Se ele faz VirtualAlloc seguido de escrita de shellcode seguido de criação de thread, o EDR vai detectar o comportamento independente de como o código que gerou esses syscalls estava escrito no disco. A telemetria via ETW e os hooks em user-mode capturam o que o processo faz, não como ele parece.

Isso não invalida a técnica. Significa que ela pertence a uma cadeia de evasão, não é a cadeia inteira. Em alvos sem EDR maduro — que ainda representam a maioria dos ambientes em engagements reais — a de-otimização sozinha já representa uma superfície de detecção estática praticamente nula. Em alvos com EDR de última geração, ela precisa ser combinada com evasão de comportamento: syscalls diretas para evitar hooks em user-mode, sleep masking para esconder o payload em memória entre operações, e técnicas como module stomping para eliminar a dicotomia entre o que está no disco e o que está em memória.

A de-otimização resolve o problema para o qual ela foi projetada. O trabalho de um operador ofensivo competente é entender exatamente qual problema está sendo resolvido em cada camada — e não confundir uma peça do puzzle com o puzzle inteiro.


Ferramentas de detecção evoluem ao encontrar padrões. A de-otimização por transformações matemáticas ataca justamente a capacidade de formar padrões: cada transformação é parametrizada diferente, cada binário produzido é único, e a diversidade do output não é acidental — é o produto de escolhas matemáticas deliberadas. Isso não garante invisibilidade eterna. Garante que o custo de criar assinaturas para esse tipo de evasão sobe de forma desproporcional ao custo de aplicar as transformações. Em segurança, esse tipo de assimetria importa.