Back
Featured image of post [PT-BR] Metamorfismo explicado na prática

[PT-BR] Metamorfismo explicado na prática

Metamorfismo

Metamorfismo

Introdução

Desta vez, o objeto de estudo é o Metamorfismo. Acho que este é o próximo passo após o polimorfismo, um passo que alcançará o desenvolvimento ofensivo em outro nível: o pico mais alto da automutação, o maior passo em direção à furtividade perfeita.

Básico

O metamorfismo é um conceito muito interessante que basicamente significa o seguinte:
Significa que o código se modifica a cada vez que executado.
A diferença entre metamorfismo e o polimorfismo é:

  • Polimorfismo: significa criptografar o próprio código e descriptografar em run-time para se executado
  • Metamorfismo: significa modificar o próprio código, mudar o proprio assembly em run-time.

Claro, como se poderia esperar, é quase impossível criar um código totalmente metamórfico. Não vou inserir os detalhes aqui, mas se você não acredita em mim, experimente. Grande parte deste artigo terá como objetivo mostrar onde o metamorfismo deve ser utilizado para ser realmente útil.

Isso é, quando um software de antivírus é escrito e consegue detectar com sucesso o uso de um conceito, método e teoria, ele será capaz de detectar todos os vírus futuros baseados nos mesmos conceitos.
Mas depois de modificar todos os três aspectos, o antivírus deve ser totalmente redesenhado. Isso pode levar a uma redução da eficiência na abordagem em relação aos conceitos antigos também, porque qualquer método novo tem a capacidade de fazer os métodos antigos funcionarem pior do que antes. Não vou continuar essa dissertação aqui, mas pense nisso …

Para analisar um malware desse tipo. O que é necessário? Precisamos conhecer alguns valores importantes e isso é tudo o que é necessário para entender o código:

  1. O local do código do malware
  2. Os locais das chaves de descriptografia
  3. O algoritmo de descriptografia
  4. O local do código original (se movido)
  5. O ponto de entrada original

Não nos importamos com o algoritmo de descriptografia aqui, portanto, verificaremos apenas o resto das coisas.

Basicamente, todos os valores acima são armazenados dentro do seu código num determinado endereço. Coisas como essas são comuns:

OldEip  dd      0
...
        mov     dword ptr [ebp+OldEip], eax

O humano irá olhar em seu código e quando ele finalmente localizar e entender as linhas acima, ele irá programar seu script para olhar para o endereço de OldEip e obter o valor de lá. Não há necessidade de interferência humana ao escanear algo tão simples. Agora o software localizou o eip original do programa infectado e pode remover o hook com segurança apenas restaurando-o. Esta é uma forma muito simples de mitigação.

Como podemos prevenir tal coisa, ou como podemos pelo menos tornar isso mais difícil? Isso é explicado por alguns métodos de metamorfismo.

Métodos

Método de “multiple locations”

OldEip1 dd      0
...
OldEip2 dd      0
...
OldEip3 dd      0

        mov     dword ptr [ebp+OldEip1], eax
metamorph1 = $-4

Agora, nosso mecanismo metamórfico tem que fazer o seguinte: decidir aleatoriamente qual endereço usar, preencher com o valor correto, preencher o outro com valores aleatórios e ir para o endereço ebp + metamorph1 e preencher o endereço com o valor necessário.

Para onde isso leva? Cada vez que o vírus se propaga o local onde o antigo entrypoint está armazenado será diferente… E também, a instrução que o acessa será diferente de geração em geração. Não sei se você percebe a força dessa coisa. É claro que é facilmente superável localizando a própria instrução de acesso e obtendo o endereço de lá. Mas, pense nisso:

Oldeip1         dd      0
...
Oldeip2         dd      0
...
...
codeaddress1    dd      0
...
codeaddress2    dd      0
...
...

        mov     dword ptr [ebp+OldEip1], eax
        ...
        mov     dword ptr [ebp+codeaddress1], eax

Agora, as duas instruções têm a seguinte aparência quando debugadas:

mov [ebp+XXXXXXX], eax

Começando a entender meu ponto? Imagine que você tem 10 valores para fazer metaformose em torno do código, cada um tendo 10 lugares possíveis e cada um sendo acessado cerca de 3 vezes, necessariamente e outras 7 virão lixo… Você sabe quantas gerações uma pessoa deve gerar para entender o que é o significado real do código e quão difícil seria localizar os valores necessários para analisar?

A modificação de instrução

Isso é um pouco complicado e você precisa aprender um pouco sobre a extensão de instruções. Não é muito difícil, mas você terá que criá-lo testando-o várias vezes em um debbuger. Lembre-se de que aqui você não está gerando um descriptografador polimórfico (onde você tem um buffer vazio e pode preenchê-lo), mas está trabalhando em um código compilado que tem um tamanho definitivo e links por toda parte. A ideia é modificar uma determinada instrução para que não seja facilmente localizada.

Primeira etapa: realocação de instrução

Para isso, você precisará economizar espaço em diferentes partes do seu código e elas devem se parecer de alguma forma com uma sub-rotina:

place1  proc
        space1  db 20 dup(0x90) ; isso vai repetir 20x "0x90"(a instrução nop)
        ret
place1  endp

Você pode ter, digamos, cerca de 10 lugares para cada parte do código metamórfico. Sempre que esta instrução for chamada, você deverá providenciar a chamada para ela. Imagine para o acima:

        call    place1
        ...
place1  proc
        mov     [ebp+OldEip1], eax
        ret
place1  endp

Agora, se o seu gerador aleatório decidir que o código deve ser “metamorfizado” em outro place(digamos, place2), tudo o que ele precisará fazer é mover a instrução para lá e modificar também a chamada para ler call place2

Esta é a primeira ideia: sua instrução pode vagar pelo código. Pense que você pode ter, digamos, 15 lugares como esse e 10 ou mais instruções para usar metamorfose. Seu gerador de números aleatórios escolherá um lugar para cada um e você ainda terá algum para preencher com lixo.

Segunda etapa: mutação real do código

Aqui você precisa cuidar da duração da instrução. Como você notou, eu escolhi aleatoriamente o tamanho de um local para 20 bytes (aliás: você pode ter tamanhos diferentes). Isso significa que você não pode colocar uma instrução ou grupo de instruções com mais de 20 bytes, caso contrário, eles sobrescreverão o código a seguir.

Vamos voltar às nossas instruções aqui:

i1)	mov	[ebp+Oldeip], eax
	ret

Deixe-me ser criativo e criar outros grupos de instrução que façam a mesma coisa:

i2)	push	[ebp+Oldeip]
	pop	eax
	ret

i3)	push	edx
	lea	edx, Oldeip
	add	edx, ebp
	mov	[edx], eax
	pop	edx
	ret

i3)	push	eax
	lea	eax, [ebp+Oldeip-1]
	inc	eax
	pop	[eax]
	ret

Agora, seu gerador de números aleatórios escolherá uma das instruções acima e simplesmente preencherá seu lugar. O que isso traz? Isso torna ainda mais difícil para o scanner automático (desde que ele possa pesquisar todos os lugares) saber a qual endereço você está endereçando (oldEip1, 2, etc…).

Resumo

Por enquanto, vamos fazer uma pausa e ver o que tudo isso pode gerar:

                              ┌─────────────┐
                              │ call placeX │
                              └──────┬──────┘
     ┌───────┬───────┬───────┬───────┼───────┬───────┬───────┬───────┐
     │       │       │       │       │       │       │       │       │
 ┌──────┐┌──────┐┌──────┐┌──────┐┌──────┐┌──────┐┌──────┐┌──────┐┌──────┐
 │place1││place2││place3││place4││place5││place6││place7││place8││place9│
 └───┬──┘└───┬──┘└───┬──┘└───┬──┘└───┬──┘└───┬──┘└───┬──┘└───┬──┘└───┬──┘
     └───────┴───────┴───────┴───────┼───────┴───────┴───────┴───────┘
          ┌────────────────┬─────────┴───────┬─────────────────┐
   ┌──────┴────────┐┌──────┴────────┐┌───────┴───────┐┌────────┴──────┐
   │  i1           ││  i2           ││  i3           ││  i4           │
   └──────┬────────┘└──────┬────────┘└───────┬───────┘└────────┬──────┘
          └────────────────┴─────────┬───────┴─────────────────┘
            ┌───────────┬────────────┼───────────┬───────────┐
       ┌────┴─────┐┌────┴─────┐┌─────┴────┐┌─────┴────┐┌─────┴────┐
       │ Address1 ││ Address2 ││ Address3 ││ Address4 ││ Address5 │
       └──────────┘└──────────┘└──────────┘└──────────┘└──────────┘

Basicamente, qualquer rota descendente pode ser gerada pelo processo metamórfico (por exemplo, chamada para place5, com conjunto de instruções i1 que acessa o endereço Address5). Quase todos os lugares e endereços devem ser usados, cada um para uma instrução diferente. O conjunto de instruções deve ser mais amplo porque, para instruções diferentes, devemos fazer metamorfose no código específico. Mas os locais e os endereços podem ser comuns a todas as instruções.

Claro, não preciso dizer que o endereço dos locais e dos endereços deve ser o mais mutilado possível dentro do código real.

Avançado

Agora, vamos avançar para uma coisa mais profunda. Imagine que existe uma pessoa realmente masoquista(eu teria medo desse ser humano) que percebeu a forma como seu código se comporta e quer encontrar todos os endereços onde seu código armazena o EIP.
Ele poderia gerar, por exemplo, 500 amostras do seu código e ter 10 pessoas para analisá-los.
Não seria muito difícil, bastaria uma tabela a ser preenchida com os deslocamentos dos lugares, endereços e onde buscar o endereço dentro da instrução. Você acha que todas as situações seriam encontradas em tantas gerações? Claro, se você não usar um metamorfismo lento e inteligente.
Esse tipo de metamorfismo lento significaria o seguinte: cada uma das três variáveis ​​(local, endereço e conjunto de instruções) deveria ser alterada em momentos diferentes, uma vez que um contador ultrapassasse o valor 20. Então, a cada 20 gerações o lugar mudava. A cada 20 gerações o endereço muda, etc.
Isso nos garante que em pelo menos 20 gerações algo não mudaria. Isso significa que para obter todas as 10 possibilidades para o local, pelo menos 200 gerações devem ser criadas e toda vez que o número aleatório deve gerar um número diferente… o que é quase impossível. 200+ 200+ 200, isso significa 600 gerações e com a suposição de que o randomizador gere exatamente o que você deseja.
Acho que em 6.000 gerações, as condições dificilmente serão atendidas. Analisar 6.000 gerações é… bem, pelo menos suicida…

Para adicionar ainda mais complexidade a isso, pode-se usar uma invenção que conheci com o nome de
"Madness Jump Table".

Vamos supor que você fez seu código metamorfosear(sim, essa palavra existe, olha aqui) a instrução:

mov [ebp+OldEip], eax

em um call local, com todos os links apresentados acima. E vamos imaginar que essa instrução aparecerá 5 vezes em seu código (talvez algumas vezes apenas como isca). Não seria muito bom escreve-la todas as vezes por uma chamada para local. O uso de uma “Madness Jump Table” resolveria isso.

  instruction1: call treeEntry1
  instruction2: call treeEntry2
  instruction3: call treeEntry3
  instruction4: call treeEntry4
  instruction5: call treeEntry5

  treeEntry1: jmp subEntry11
  treeEntry2: jmp subEntry12
  treeEntry3: jmp subEntry13
  treeEntry4: jmp subEntry14 - these are equal
  treeEntry5: jmp subEntry14 /

  subEntry11: jmp subEntry21
  subEntry12: jmp subEntry22
  subEntry13: jmp subEntry23 - these are equal
  subEntry14: jmp subEntry23 /

  subEntry21: jmp subEntry31
  subEntry22: jmp subEntry32 - these are equal
  subEntry23: jmp subEntry32 /

  subEntry31: jmp subEntry41
  subEntry32: jmp subEntry41

  subEntry41: jmp place

Ok, vamos sequir a instrução 3 como exemplo:

pretreeEntry3-> subEntry13-> subEntry23-> subEntry32-> subEntry41-> place

Não importa com qual instrução você comece, você acaba no mesmo endereço: place (observe que o call place foi substituído por um jmp place, porque a chamada já foi feita desde o início e não queremos dois endereços na stack)

Agora, por favor, olhe atentamente para a tabela acima. Imagine que em cada bloco de árvore você destrói o lado esquerdo (os jumps) entre eles de forma completamente aleatória. Acontece alguma coisa? Não, porque de qualquer maneira, o traço ainda levará ao mesmo lugar. Mas você terá 5 instruções que irão saltar cada uma através de 6 cada vez que diferentes lugares de salto, cada vez que cheguem a um lugar diferente, onde um conjunto diferente de instruções é aplicado para usar um valor que está armazenado em um lugar diferente, o que é absolutamente necessário para a execução do programa … Você compilou o que eu acabei de dizer?

Isso diminuirá a velocidade do seu código? Nem um pouco … Vai aumentar o tamanho. Claro, um pouco, mas não tanto. 20 saltos e chamadas no total significam 100 bytes, mais 20 bytes por conjunto de instruções (desde que tenhamos 10 conjuntos de instruções), dá outros 200. Portanto, um total de 300 bytes adicionados ao seu código como lado funcional. Além disso, o lugar adicional foi ocupado pelo armazenamento de endereços e armazenamento de conjuntos de instruções.

Claro, como eu disse, o metamorfismo só deve ser usado em lugares onde você realmente precisa tornar os dados difíceis de serem compreendidos e alcançados, porque muito metamorfismo pode levar a executáveis ​​enormes e nenhuma substância real, para não mencionar o seu trabalho inútil adicional.

Aplicando isso

Onde aplicar?

Deixe-me dar algumas dicas sobre onde eu acho que o metamorfismo deve ser aplicado. Em primeiro lugar, suponho que você trabalhe em um código de realocação automática; nesse tipo, uma parte do código original é movida para algum lugar no final do arquivo criptografado, assim como o resto do código original. O vírus se insere no local liberado e ao terminar o trabalho descriptografa o código e o coloca de volta. Isso é necessário porque, de outra forma, alguns antivírus inteligentes poderiam encontrar o seguinte: carregue a amostra infectada como um processo de depuração, localize seu handler (se houver) e encontre uma maneira de forçá-lo a retornar ao host. Em seguida, monitore o endereço do retorno dentro da seção de código e, assim, o ponto de entrada original é divulgado. Ao se posicionar sobre a mesma área onde o ponto de entrada original estava, o antivírus não pode assumir que o eip irá de alguma forma “escapar” daquela área e pular para muito longe significa que esse é o ponto de entrada original. Para obter o ponto de entrada original, ele deve rastrear toda a execução, o que é perigoso e quase impossível para ele, ou fazer a varredura do código em busca de valores. E aí vem nossas instruções metamórficas.

Então, vamos ver onde o paradigma metamórfico se aplica:

  1. entrypoint original
  2. endereço do bloco de código original
  3. chave de criptografia do código original
  4. comprimento do pedaço de código original

Se você é inteligente o suficiente para criar um mecanismo metamórfico para esconder as coisas acima e as instruções que os acessam, é isso!!
Você não precisa metamorfosear coisas como instruções matemáticas comuns e assim por diante. Você tem que se concentrar nas instruções importantes!

Algumas dicas

Dicas adicionais de furtividade

Como eu disse, você pode querer criar um conjunto dedicado de endereços para cada um dos conceitos metamorfizados (por exemplo, oldEip, endereço do bloco de código, etc.). No entanto, à luz das técnicas acima, apenas um dos vários endereços será realmente usado, enquanto o resto deve ser apenas para chamariz. Para torná-lo perfeito, você não deve deixar em nenhuma circunstância esses valores como 0. Isso seria um erro fatal. Se o antivírus localizar todos os endereços, todo o resto em nosso algoritmo é inútil, porque ele irá desconsiderar todo aquele que seja igual a 0.

Além disso, você não deve colocar valores aleatórios. Por quê? Um software de antivírus inteligente poderia localizar o eip real de um conjunto de muitos valores apenas verificando qual é maior que o RVA(Relative Virtual Address) da seção de código e menor que o RVA + o tamanho bruto. Para resolver isso, simplesmente faça seu gerador de números aleatórios gerar pequenos números positivos, negue-os se quiser (outra suposição aleatória) e adicione esses randoms ao eip original. Desta forma, todos os endereços terão valores muito semelhantes ao redor do eip rva original.

Para otimizar um pouco as coisas: não armazene os conjuntos de instruções em algum lugar e apenas mova-os para o local na hora da metamorfose. Basta criar os locais com as instruções já presentes e quando desejar modificar basta trocar dois deles entre eles. Ou toda vez que você quiser mudar apenas troque todos eles entre eles aleatoriamente.

Como colocar todas essas informações e não se perder em seu próprio código? Isso significa que você sabe exatamente de onde começa e coloca tudo no papel. Então, a Madness Jump Table oferece um lugar muito bom para ocultar dados. Projete a tabela e coloque os endereços entre os saltos. Você pode até inserir algum chamariz lá (como prefixos 0FFh antes dos saltos para fazer o código compilador parecer um fusca velho 👍).

Criptografe muito bem o core do engine metamórfico. Para isso, sugiro um algoritmo não linear com várias passagens (como um loop infinito). Dentro do engine metamórfico, use um engodo de endereço. Não vou entrar em detalhes com esta técnica, vou apenas apresentá-la brevemente:

Ao invés de escrever:

mov     [ebp+offset metamorph1], ebx

escreva:

mov     edx, offset metamorph1
...
mov     eax, 12
...
sub     ebp, 24
......
mov     [ebp+eax*2+edx], ebx

Dessa forma, ao disassemblar seu código, seria muito mais difícil para o analisador entender o que você pensou ali. A última instrução pode aparecer muitas vezes dentro do código.

Como codar?

Os componentes de um engine metamórfico

  1. O gerador de endereço
  2. O preenchimento de instruções
  3. O place filler
  4. O handler da jump table

1. O gerador de endereço

Esta é a parte que move os dados de um endereço para outro. Requer uma tabela como esta:

AddressTable:
    Ahunk1:
      size1 = x
      _addr11 dd offset address11
      _addr12 dd offset address12
      ...
      _addr1x dd offset address1x
    Ahunk2:
      size2 = y
      _addr21 dd offset address21
      _addr22 dd offset address22
      ...
      _addr2y dd offset address2y
    ...
    AhunkN
      ...

onde cada pedaço é usado para um valor específico (como oldEip ou um endereço de código), e cada addressAB representa locais possíveis dentro da área de dados onde o valor real pode ser armazenado.

A engine irá analisar cada pedaço, dado seu tamanho, ir em cada endereço (alinhado com o identificador delta, é claro) e preenchê-lo com um valor aleatório ou o valor real, conforme decidir. Exatamente quando o endereço do valor real é decidido, o preenchedor de instrução deve ser chamado diretamente para evitar futuras passagens pelas tabelas. O preenchimento da instrução diz à instrução para endereçar no endereço específico onde os dados reais são colocados.

2. O preenchimento de instruções

Este também precisa de uma tabela, como esta:

InstructionTable:
    Ihunk1:
      __size = a
      _instr11  dd offset instruction11
      _byteoffset11 = 3
      ...
    ...

onde cada pedaço é correspondente aos pedaços acima. Cada instruçãoAB representa o endereço da instrução que deseja usar um valor (um mov [ebp+oldEip], eax, por exemplo), e byteoffset representa em qual deslocamento os endereços dos dados devem ser colocados. Por exemplo, neste caso:

instruction11:
    push edx
    mov edx, [ebp+oldEip]
    mov [edx], eax
    pop edx

a primeira instrução tem 1 byte de comprimento e a segunda 6 bytes, e o endereço de oldEip é armazenado no quarto byte a partir do endereço da instruction11. Você pode simplesmente calcular esses valores inserindo TurboDebugger, digitando as instruções e, em vez de oldEip, coloque 8888888h e veja em qual byte ele inicia.

Esta parte da engine recebe o endereço dos dados do gerador de endereço. Em seguida, ele irá para o deslocamento de cada instrução e preencherá no deslocamento de byte adequado o endereço que recebeu. Em seguida, ele escolherá uma das instruções e passará seu número para o preenchedor do lugar.

3. O place filler

Esta parte não precisa de outra tabela. Ele simplesmente fragmentará os conjuntos de instruções entre eles, conforme mantido na tabela InstructionTable, e para a instrução a ser executada (conforme recebida do preenchedor de instruções), ele passará esse valor para o handler da jump table.

4. O handler da jump table

O handler da jump table simplesmente divide entre eles os saltos em cada bloco de salto e então substitui a instrução jmp place pelo salto apropriado para o endereço que recebeu do preenchedor de local (a instrução a ser executada). Em seguida, para cada call, ele escolherá uma entrada aleatória na árvore da tabela de salto e a preencherá usando esta tabela:

FinalTable:
    Fhunk1:
      ____size = 5
      _call11 db offset _caller11
      ...

Tudo isso configurado, seu código terá em algum lugar dentro dele esta instrução:

_caller11: call StoreEipTree

A árvore da tabela de salto da loja eip guiará a chamada através da árvore aleatória. Ele finalmente alcançará um proc que conterá um dos muitos conjuntos de instruções que você preparou para colocar um valor em [ebp + oldEip], onde o endereço oldEip será um dos muitos lugares em que você terá que armazenar esse valor.

Como você pode ver, é muito fácil entender como funciona, à medida que constrói o código metamórfico, mas é muito difícil entender como se você só tiver o disassembler e um monte de tabelas(criptografadas). Observe também que, usando a maneira acima, todos os dados ainda podem passar pelo processo de metamorfose repetidas vezes.

Palavras finais

Obrigado por ter lido até o final, espero que tenha gostado do conteudo e estamos abertos para recomendações e criticas obrigado novamente e até a proxima…


Autor do post: R3tr0

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy