Bazele hackingului. Alocare dinamica a memoriei și indicatorilor
Cum funcționează alocarea și repartizarea? Cum funcționează indicatorii obișnuiți și inteligenți în C ++? Cum să recunoaștem operatorii de memorie folosind un dezasamblator care nu înțelege natura lor adevărată? Pentru a înțelege toate acestea, trebuie să dezasamblăm mecanismele de alocare a memoriei dinamice a aplicației (cu alte cuvinte, heap-ul) celor doi populari compilatori pe octeți și să identificăm diferențele în activitatea lor. Prin urmare, în articol așteptăm o mulțime de liste de dezasamblare și cod C ++.
IDENTIFICAREA ACESTUI INDICE
Acest indicator este o adevărată cheie de aur sau, dacă preferați, o linie de salvare pentru a evita înecarea în oceanul turbulent al OOP. Datorită acestui fapt puteți determina apartenența funcției apelate la una sau alta clasă. Deoarece toate funcțiile non-virtuale ale unui obiect sunt numite direct - la adresa reală, obiectul este, așa cum ar fi, împărțit în funcțiile sale constitutive în etapa de compilare. Fără aceste indicații, ar fi fundamental imposibil să se restabilească ierarhia funcției!
Astfel, identificarea corectă a acestui lucru este foarte importantă. Singura problemă este cum să o distingem de indicatori la matrice și structuri? La urma urmei, o instanță a unei clase este identificată de pointerul this (dacă acesta indică memoria alocată, aceasta este o instanță a clasei), dar acesta în sine, prin definiție, este un pointer care se referă la o instanță a clasei. Un cerc vicios! Din fericire, există o lacună ... Codul care manipulează acest pointer este destul de specific, ceea ce face posibilă distincția dintre toate celelalte indicatoare.
De fapt, fiecare compilator are propriul scriere de mână, ceea ce este foarte recomandat să înveți prin dezasamblarea propriilor programe C ++, dar există recomandări universale care se aplică majorității implementărilor. Deoarece acesta este un argument implicit al fiecărei funcții care face parte dintr-o clasă, este logic să amânați vorbirea despre identificarea acesteia până la secțiunea „Identificarea argumentelor funcției”. Aici vom discuta despre modul în care cei mai populari compilatori implementează trecerea acestui indicator.
Aici vorbim desigur despre arhitectura x64. Pe o platformă pe 32 de biți, parametrii aliniați la dimensiunea de 32 de biți sunt trecuți prin stivă. Pe de altă parte, lucrurile sunt mai interesante pe o platformă pe 64 de biți: primele patru argumente întregi sunt transmise în registrele RCX, RDX, R8, R9. Dacă există mai multe argumente întregi, restul sunt plasate pe stivă. Argumentele cu valori în virgulă mobilă sunt transmise în registrele XMM0, XMM1, XMM2, XMM3. În acest caz, argumentele pe 16 biți sunt transmise prin referință. Rețineți că aici este vorba despre convenția de apelare în sistemele de operare Microsoft (Microsoft ABI); lucrurile stau diferit în sistemele de tip Unix. Dar să nu ne stropim atenția asupra lor.
Ambele compilatoare pe care le-am testat, Visual C ++ 2019 și C ++ Builder 10.3, indiferent de convenția de apelare a funcției (__cdecl, __clrcall, __stdcall, __fastcall, __thiscall) trec acest pointer în registrul RCX, care se potrivește cu natura sa: acesta este un argument întreg.
IDENTIFICAREA OPERATORILOR NOI ȘI ȘTERGIȚI
Operatorii noi și ștergere sunt traduși de compilator în apeluri către funcții de bibliotecă, care pot fi recunoscute în același mod ca funcțiile obișnuite de bibliotecă. În special, IDA Pro este capabil să recunoască automat funcțiile bibliotecii, eliminând această preocupare de pe umerii cercetătorului. Cu toate acestea, nu toată lumea are IDA Pro și este departe de a fi întotdeauna la momentul potrivit și, în plus, nu cunoaște toate funcțiile bibliotecii și, din cele pe care le știe, nu le recunoaște întotdeauna noi și șterge ... Într-un cuvânt, există motive pentru a le identifica. manual este suficient.
Implementarea noilor și ștergerilor poate fi orice, dar majoritatea compilatoarelor Windows implementează rareori funcții de heap pe cont propriu. De ce asta? Este mult mai ușor să accesați serviciile sistemului de operare. Cu toate acestea, ar fi naiv să ne așteptăm să apară HeapAlloc în loc de nou și HeapFree în loc de ștergere. Nu, compilatorul nu este atât de simplu! Își poate refuza plăcerea de a „sculpta păpușile cuiburi”? Noul operator este tradus în noua funcție, care apelează malloc pentru a aloca memorie, în timp ce malloc, la rândul său, se referă la HeapAlloc (sau similaritatea acestuia, în funcție de implementarea bibliotecii de memorie) - un fel de „wrapper” al procedurii API Win32 cu același nume. Imaginea cu eliberarea memoriei este similară.
A intra în jungla apelurilor imbricate este prea obositor. Ar putea fi identificate noi și șterse în alt mod, cu mai puțin efort și durere de cap? Sigur ca poti! Să ne amintim tot ce știm despre nou:
new ia un singur argument - numărul de octeți de memorie alocată, iar acest argument în majoritatea covârșitoare a cazurilor este calculat în etapa de compilare, adică este o constantă;
dacă obiectul nu conține nici date, nici funcții virtuale, dimensiunea sa este egală cu una (blocul minim de memorie alocat doar pentru a avea ceva care să indice spre acest pointer); vor exista o mulțime de apeluri de aici, cum ar fi
mov ecx, 1; mărimea
sunați la XXX
unde XXX este adresa nouă! În general, obiectele au de obicei o dimensiune mai mică de o sută de octeți ... caută o funcție frecvent numită cu un argument constant mai mic de o sută de octeți;
noua funcție este una dintre cele mai populare funcții de bibliotecă, căutați o funcție cu o mulțime de referințe încrucișate;
cea mai caracteristică: new returnează indicatorul this, iar acest lucru este foarte ușor de identificat chiar și cu o scurtă privire asupra codului (de obicei este returnat în registrul RCX);
rezultatul returnat de nou este întotdeauna verificat pentru zero (de către operatori precum testul RCX, RCX) și dacă este într-adevăr zero, constructorul (dacă există) nu este apelat.
Nou are „semnele de naștere” mai mult decât suficiente pentru o identificare rapidă și fiabilă; nu este absolut necesar să pierdeți timpul analizând codul acestei funcții! Singurul lucru de reținut: nou este folosit nu numai pentru a crea noi instanțe de obiecte, ci și pentru a aloca memorie pentru tablouri (structuri) și, ocazional, pentru variabile unice (cum ar fi int * x = new int, care este, în general, o nebunie, dar unii fac acest lucru ). Din fericire, este foarte ușor să faci diferența dintre cele două - nici matricele, nici structurile, nici variabilele individuale nu au acest indicator!
Șterge mai greu de identificat. Această funcție nu are caracteristici specifice. Da, este nevoie de un singur argument - un indicator către regiunea de memorie pentru a fi eliberat și, în majoritatea cazurilor, acesta este indicatorul. Dar pe lângă asta, zeci, dacă nu sute de alte funcții acceptă acest lucru! Mai devreme, în era pietrelor pe 32 de biți, cercetătorul avea un indiciu convenabil că în majoritatea cazurilor ștergerea accepta acest indicator prin stivă, iar restul funcțiilor prin registru. În prezent, așa cum am văzut de mai multe ori, orice funcții acceptă parametri prin registre:
mov rcx, [rsp + 58h + bloc]; bloc
apel de ștergere operator (nul *, nesemnat __int64)
În acest caz, IDA a recunoscut ștergerea fără ezitare.
În plus, ștergerea nu returnează nimic, dar câte funcții fac același lucru? Singurul indiciu este că apelul de ștergere urmează apelul către destructor (dacă există), dar din moment ce constructorul este identificat ca funcția care precede ștergerea, se formează un cerc vicios!
Nu rămâne altceva decât să analizăm conținutul funcției: ștergeți mai devreme sau mai târziu apelurile HeapFree (deși există și opțiuni aici: de exemplu, Borland / Embarcadero conține biblioteci care funcționează cu heap la un nivel scăzut și memorie liberă apelând VirtualFree). Din fericire, IDA Pro recunoaște ștergerea în majoritatea cazurilor și nu trebuie să vă supărați.
Ce se întâmplă dacă IDA nu recunoaște ștergerea? Codul va arăta cam așa:
mov rcx, [rsp + 58h + bloc]; bloc
sunați la XXX
cmp [rsp + 58h + bloc], 0
jnz scurt loc_1400010B0
O analiză superficială arată: în prima linie din registrul RCX, evident pentru a fi trecut ca parametru, este plasat un bloc de memorie. Se pare că este un pointer de entitate. Și după ce ați apelat XXX, acest bloc de memorie este comparat cu zero, iar dacă blocul nu este zero, adresa este sărită. În acest mod simplu, putem identifica cu ușurință ștergerea chiar dacă IDA nu o definește.


Niciun comentariu: