Cu ceva in urma am intrebat pe forum ce inseamna shellcode, ce face el samd. Intrucat nu am fost suficient lamurit, m-am gandit sa ma documentez mai mult si asta a rezultat in urmatorul tutorial: 1. Introduction 1.1 Ce este shellcode-ul? In domeniul securitatii calculatoarelor, luat ad-literam, shellcode-ul inseamna scrierea de cod care returneaza un remote shell cand este executat. Intelesul cuvantaului, insa, a evoluat, in zilele noastre reprezentand orice cod care urmeaza a fi inserat intr-o vulnerabilitate cu scopul ca aplicatia exploatata sa indeplineasca anumite sarcini, altele decat si le-a dorit programatorul. 1.2 Prerechizite Ne vom concentra pe scriere de cod pt sistem linux cu arhitectura x86. Aceleasi metode si principii pot fi aplicate si pe diferite variante de UNIX sau Win32, dar trebuie avute in vedere diferentele intre conventiile pentru apelul functiilor de sistem. Pentru a scrie cod in limbaj de asamblare vom folosi nasm. Vom avea nevoie si de un debugger sau de un tool pentru citirea object code-ului cum ar fi objdump. 1.3 Care sunt diferentele intre shellcode-ul de pe Windows si cel de pe Linux? Spre deosebire de Windows, Linux-ul ofera o interfata directa cu kernel-ul prin intermediul intreruperii (sau instructiunii, depinde de interpretare) int 0x80. De cealalta parte, Windows-ul nu dispune de o interfata directa cu kernel-ul. Sistemul lucreaza prin incarcarea adresei functiei care trebuie executata dintr-un DLL (Diynamic Link Library). Principala diferenta dintre cele doua sisteme este faptul ca adresele functiilor din Windows variaza de la o versiune la alta a sistemului de operare, in timp ce int 0x80 ramane aceeasi. 1.4 Cum gasim adresele functiilor din DLL-uri? Exista o multime de cai de a gasi aceste adrese. Cele mai raspandite doua metode sunt: gasirea adresei functiei la runtime sau folosirea memoriei hard-codata. Singurul DLL care se mapeaza in mod sigur pe spatiul de adrese al shellcode-ului este kernel32.dll. Acest DLL contine LoadLibrary si GetProcAddress, functii necesare pentru obrinerea oricaror adrese de functii. 1.5 NULL bytes. Problema NULL bytes consta in faptul ca shellcode-ul nu trebuie sa contina bytes nuli si asta deoarece codul inserat (exploit-ul) este un string. Dupa cum se stie, stringurile se termina cu NULL byte (in limbaje precum C sau C++). Daca avem NULL bytes in shellcode, acesta nu va functiona. 2. Informatii de background ? EAX, EBX, ECX si EDX sunt registri generali pe 32 de biti pe arhitecturi 80x86. ? eax mai poarta numele de acumulator, fiind folosit pentru a stoca rezultate aritmetice, ebx – registru baza, folosit pentru adresarea bazata si indexata (baza si offset), ecx – registru numarator utilizat pentru cicluri, edx – registru data. ? ah, bh, ch, dh – registre pe 16 biti (registrele superioare) ? al, bl, cl, dl – registre pe 16 biti (registrele inferioare) ? esi, edi – registre pe 32 de biti folosite de obicei ca indecsi si pentru apelul functiilor de sistem pe Linux 3. Tool-uri necesare ? gcc - compilator de C ? ld – linker ? nasm – asamblor ? objdump – dezasamblor 4. Linux Shellcoding Cand se testeaza shellcode-ul, este frumos sa fie bagat intr-un program si lasat sa mearga. Programul C de mai jos este (si va fi) folosit pentru a testa shellcode-ul char shellcode[] = "bytecode will go here!"; int main(int argc, char **argv) { int (*shell)(); shell = code; shell(); } Exemplu 1 – Exit Program Cea mai facila cale de a incepe este folosirea functiei de sistem exit, pentru simplitatea ei. Codul asm: ;exit.asm [SECTION .text] global _start _start: xor eax, eax ;curata registrul eax (eax = 0x000000 mov al, 1 ;codul pentru functia de sistem exit este 1 xor ebx,ebx ;curata ebx int 0x80 ;si…activeaza! Compilarea si extragerea byte code-ului se face in trei pasi simpli: ~$ nasm -felf exit.asm ~$ ld -o exit exit.o ~$ objdump -d exit Si obtinem: exiter: file format elf32-i386 Disassembly of section .text: 08048080 <_start>: 8048080: b0 01 mov $0x1,%al 8048082: 31 db xor %ebx,%ebx 8048084: cd 80 int $0x80 Octetii care ne trebuie sunt: b0 01 31 db cd 80. Avemc codul in C: const char shellcode[] = “\xb0\x01\x31\xdb\xcd\x80“; int main(int argc, char **argv) { int (*shell)(); shell = code; shell(); } Exemplu 2 - Hello Cand sunt accesate valori statice (in special stringuri) din shellcode, trebuie ca acesta sa fie scris astfel incat sa acceseze valorile statice fara a folosi in vreun fel maparea adresei acestora, si asta pentru ca programatorul nu stie in ce zona de memorie este incarcat codul. Acelasi lucru se intampla si cand se incearca executarea instructiunilor care au ca operand memoria (jmp, call, etc). Din fericire, se poate folosi adresarea relativa cu ajutorul familiei de instructiuni jmp/call. Pentru inceput, cosul in asmblare, neoptimizat: SEGMENT .text mov eax, 4 ; pune in eax valoare 4 (codul functiei de sistem write) mov ebx, 1 ; pune 1 in ebx (codul iesirii stdout) mov ecx, message ; pune pointerul la string in eax mov edx, 29 ; pune 11 in edx (lungimea stringului) int 80h ; si…activeaza! mov eax, 1 ; pune 1 in eax (codul functiei de sistem exit) mov ebx, 0 ; pune 0 in ebx (int status) int 80h ; si…activeaza! message db ‘hello world', 7, 10 Se observa ca exista in cod NULL bytes si de asemenea nu este rezolvata problema independentei pozitiei. Pentru asta se recurge la urmatorul truc: SEGMENT .text xor ebx, ebx ; curata registrii mul ebx ; mov al, 4 ; pune 4 in eax codul (functiei de sistem write) inc ebx ; incrementeaza ebx => ebx = 1 (stdout, int fd) jmp short get_string ; Daca nu specificam sa faca un salt de 8 biti, nasm va face ; jmp near, ceea ce inseamna 16bit offset la eip, pe cand noi continue: ; avem nevoie de 1 octet pop ecx ; extrage pointerul de deasupra stivei la stringul ;nostru, in eax mov dl, 29 ; pune 11 edx (size_t len) int 80h ; si…activeaza! mov al, 1 ; pune 1 in eax (syscall number of exit) dec ebx ; decrementeaza ebx => ebx = 0 (int status) int 80h ; intra in modul kernel get_string: call continue ; salt la eticheta continue si incarca stringul ca ret db ‘hello world', 7, 10 ~$ nasm -felf hello.asm ~$ ld -o hello hello.o ~$ objdump -d hello write: file format elf32-i386 Disassembly of section .text: 08048100 <continue-0x9>: 8048100: 31 db xor %ebx,%ebx 8048102: f7 e3 mul %ebx,%eax 8048104: b0 04 mov $0x4,%al 8048106: 43 inc %ebx 8048107: eb 0a jmp 8048113 <string> 08048109 <continue>: 8048109: 59 pop %ecx 804810a: b2 1d mov $0x1d,%dl 804810c: cd 80 int $0x80 804810e: b0 01 mov $0x1,%al 8048110: 4b dec %ebx 8048111: cd 80 int $0x80 08048113 <get_string>: 8048113: e8 f1 ff ff ff call 8048109 <continue> 8048118: 6b 65 72 6e imul $0x6e,0x72(%ebp),%esp 804811c: 65 gs 804811d: 6c insb (%dx),%es:(%edi) 804811e: 65 64 3a 20 cmp %fs:%gs:(%eax),%ah 8048122: 61 popa 8048123: 72 65 jb 804818a <_etext+0x55> 8048125: 20 79 6f and %bh,0x6f(%ecx) 8048128: 75 20 jne 804814a <_etext+0x15> Shellcode-ul nostru este: "\x31\xdb\xf7\xe3\xb0\x04\x43\xeb\x0a\x59\xb2\x1d\xcd\x80\xb0\x01" "\x4b\xcd\x80\xe8\xf1\xff\xff\xffhello world\a\n"; Tutorialul l-am tradus si adaptat pe cat am putut de bine din engleza. Sursele originale le gasiti aici: http://infosecwriters.com/hhworld/shellcode.txt Writing shellcode Shellcoding for Linux and Windows Tutorial