A köteg megváltoztatása
Miért van ez szükség
A verem cseréje akkor értelmes, ha:
- Olyan függvényt hívsz, amelyhez nagy stack szükséges, és nem akarod, hogy az aktuálisat használja.
- Olyan függvényt hívsz, amelyhez nagy stack szükséges, amikor a saját verem majdnem teljes (például ugyanabban a verem túlcsorduláskezelőjében).
32 bites Intel-kompatibilis processzorok felhasználói módú alkalmazások esetén az oldal mérete 4096 bájt.
Rövid elemzés után megtudtuk, hogy a választott módszer (megközelítés) helyes, de a végrehajtás téves volt. Néhány napot töltöttem a hibakereső mögött, és kiderítettem valamit arról, hogy a rendszer maga szervezi a veremt. Az eredmények a figyelem középpontjába kerülnek.
Egy kis elmélet
A PUSH parancs
POP Team
A RETN parancs
Ugyanúgy működik, mint a pop, de az operandus az implicit módon az eip-kiterjesztett utasításmutatóregiszter. Ezzel a paranccsal meg lehet változtatni a parancsmutató tartalmát, bár nincsenek kifejezett utasítások annak megváltoztatására.
Stack méret
Mint minden jó dolog ebben a világban, a veremnek van egy határa vagy mérete. Vagyis végtelenül nem adhat be adatokat. Amint elérte a határértéket, a rendszer egy EXCEPTION_STACK_OVERFLOW kivételt dob. Később később megvizsgáljuk a művelet részleteit. Alapértelmezés szerint a veremméret egy megabájt. Ez az érték megváltoztatható a "/ STACK" linker opcióval. Minden rendszernél a rendszer szervezi a veremét.
verem szervezet
A WindowsXP-ben a STACK_SIZE_PARAM_IS_A_RESERVATION zászló használatával megadhatja a fenntartott verem méretét.
Az eredetileg átvitt oldalak utolsó részére a PAGE_GUARD zászló van beállítva. Ahogy a hívásfa növekszik, a rendszer továbbküldi a fizikai memóriakészlet több oldalát. A "normál" verem utolsó oldalát soha nem továbbítják, és mindig fenntartva maradnak.
A kötegelés
A verem legutóbb átküldött oldala mindig PAGE_GUARD zászló van beállítva.
Teljesen feltöltött verem esetén ez nem így van. Az átvett utolsó oldalnak ugyanaz az attribútuma, mint az átvitt összes többi oldal, nevezetesen a PAGE_READWRITE.
Amikor ez az oldal elér, a rendszer EXCEPTION_GUARD_PAGE kivételt dob.
Ha a kötegelt oldalak elérésekor jelentkezik, akkor a rendszer önmagában feldolgozza azt. Minden más esetben a kivételt az alkalmazásnak adják át. Nem tudom, hogy a rendszer milyen szinten határozza meg, hogy ez kivétel a veremhez, de tudom, hogy milyen adatokat csinál. Erről később.
Az EXCEPTION_GUARD_PAGE kivételkezelő eltávolítja a PAGE_GUARD attribútumot azon oldalról, amelyen a hiba történt, és megpróbálja elküldeni a következő oldalt. Ha a következő oldal az utolsó, akkor nem adódik át, és a kezelő EXCEPTION_STACK_OVERFLOW értéket generál. Ha nem az utolsó, az oldal a PAGE_GUARD és a PAGE_READWRITE zászlókkal kerül továbbításra, és a stream verem következő órás oldalává válik.
Így a rendszernek tudnia kell legalább három dolgot a veremről, mint struktúráról:
Valójában a rendszer a következő struktúrát használja a köteg kezelésére:
Nem tudom biztosan, de feltételezhetem, hogy az első két mező elavult és figyelmen kívül hagyható. Legalább a [2] és a kernel32.dll-ben az áramlás létrehozásakor visszaállnak.
Áramlási információblokk
A TIB olyan szerkezet, amely már egy másik struktúra kezdetén - a TEB. TEB - Menet környezet blokk. A TEB teljes leírása, nem fogjuk figyelembe venni, de a TIB struktúráját megadhatjuk. Ez dokumentált az NTDDK. A winnt.h fejlécfájlban is megtalálható.
Egy nagyon egyszerű és fontos funkció, amely a jelenlegi téma TIB hivatkozását adja vissza.
Most elméllel felfegyverkezve próbáld meg végrehajtani a veremedet, vagy inkább próbáld meg helyettesíteni az aktuális szálköteget.
Stack helyettesítés
Így a verem meglehetősen bonyolult struktúrájú, és nem korlátozhatjuk magunkat az esp regiszter egyetlen frissítésére. Mit kell cserélnem a kötegre?
A verem visszaállításához a következőket kell tennie:
- Állítsa vissza az NT_TIB struktúra StackBase és StackLimit mezőinek régi értékeit.
- Visszaszerezni az esp regisztert.
- Engedje el a memóriát a köteg alatt.
A köteg cseréjére és helyreállítására vonatkozó minden munkának két funkciója van:
A kezdeti kötegméret hét oldalra van állítva, az utóbbi pedig egy watchdog.
A köteghatár az őrséget megelőző oldalra van állítva.
Az előző állapot mentése és a TIB mezők módosítása
Itt van egy kivonat a SetNewStack funkcióból. Mindezt tovább fogják vizsgálni.
Az első két sor tárolja a StackLimit és StackBase mezők korábbi értékeit. Ezeket új értékek határozzák meg. A TIB-struktúra valójában ellentmondásos állapotban van, hiszen maga az esp regiszter még nem frissült. Ha bármilyen helyzet fordul elő, ahol a rendszernek szüksége van a veremre vonatkozó információra, bármi megtörténhet. Ne feledje, hogy a kontextusváltás nem ilyen művelet.
Mivel a SetNewStack funkció két paramétert tartalmaz, a köteg így néz ki:
Az esp regiszter megváltoztatása és kilépés a SetNewStack funkcióból
Ha már el is felejtetted, akkor a TIB-ben a 0x4-es eltolásnál a verem alapja (már új). Ezt az értéket az ebp és az esp regiszterbe helyezzük. A következő két parancs célja az eljárásból való visszatérés.
A SetNewStack funkció teljes szövege
Mivel a bemeneti paraméterek elérése az ebp regiszteren keresztül történik, ennek megfelelően módosítani kell. Az első parancs az ebx regiszterben az első paraméter. Ezután az ebp regiszter értékét tároljuk, majd az esp rekordból egy értéket állítunk be, mínusz négy. Miért? Az a tény, hogy a fordító egy standard prologot és egy epilogót hoz létre egy olyan függvényhez, amely így néz ki:
A prologum után a verem így néz ki:
Az első paraméter eléréséhez az ebp regisztert kell használnunk, amelyet 8-mal növelünk. De az ebp-et nem helyeztük el a veremre a funkciónkban, ezért az [ebp + 8] hozzáfér a második paraméterhez. Ennek a helyzetnek a kijavítása érdekében javítjuk az ebp-t.