Önmódosító kód

A Wikipédiából, a szabad enciklopédiából

Az informatika területén az önmódosító kód (self-modifying code) olyan programkód, ami programfutás közben megváltoztatja saját utasításait – általában az utasítás-úthosszúság csökkentése és a teljesítmény növelése céljából, vagy egyszerűen a különben ismétlődő jellegű kód rövidítése, így a karbantartás egyszerűsítése érdekében. Az önmódosítás a „jelzőbit-beállítást” követő feltételes elágazás alternatívája, általában abból a célból, hogy a feltételek tesztelésének számát csökkentsék. Az „önmódosító kód” kifejezést csak olyan programkódra használják, ami szándékoltan módosítja önmagát, a programozó szándékával ellentétesen (pl. puffertúlcsordulás miatt) módosuló kódra nem.

A módszert gyakran használják teszt/debug célú kódrészletek az I/O ciklusokra eső többletszámítások (overhead) nélküli, feltételes előhívására.

A módosítások történhetnek:

  • kizárólag inicializálás közben – a bemeneti paraméterek alapján (ilyenkor a folyamat leírható úgy is, mint a szoftver konfigurálása és hardveres terminológiában nagyjából megfeleltethető a nyomtatott áramköri kártyákon a jumperek beállításának). A program belépési pontjaira mutató pointerek megváltoztatása az önmódosítással egyenértékű módszer, de szükségessé teszi a programkódban egy vagy több alternatív utasítás-útvonal létezését, növelve a bináris fájl méretét.
  • futásidő alatt ('on-the-fly') – a futás során bekövetkező programállapotoktól függően

A két eset bármelyikében a módosítások maguknak a gépi kódú utasításoknak a felülírásával végezhetők, például egy összehasonlítás és feltételes elágazás lecserélésével feltétel nélküli vezérlésátadásra vagy NOP utasításra. Az IBM/360 és a Z/Architecture utasításkészletében egy EXECUTE (EX) utasítás üzemszerűen felülírja a célutasítás második bájtját az 1. általános célú regiszter alsó 8 bitjével, a platformon ez az (időleges) utasításmódosítás standard és legitim módja.

Alkalmazása alacsony és magas szintű nyelvekben[szerkesztés | forrásszöveg szerkesztése]

Az önmódosítás megvalósítása az egyes programozási nyelvekben különböző lehet, attól függően, hogy interpretált nyelv-e, támogatja-e a pointereket, hozzáférést a különböző dinamikus fordító vagy értelmező „motorokhoz” stb.

  • létező utasítások felülírása (vagy utasításrészek, mint opkód, regiszter, jelzőbit vagy memóriacím),
  • teljes utasítások közvetlen létrehozása a memóriában – vagy akár teljes utasításszekvenciáké
  • forráskód-sorok létrehozása vagy módosítása, amit egy „mini-fordítási” vagy dinamikus interpretációs lépés követ (lásd az eval kifejezést)
  • teljes program dinamikus létrehozása, majd lefuttatása

Assembly nyelv[szerkesztés | forrásszöveg szerkesztése]

Assembly nyelv használatát feltételezve az önmódosító kód létrehozása nagyon egyszerű. Ugyanazokat az utasításokat létre lehet hozni a memóriában (vagy felülírni vele nem védett programtárban létező kódot), mint amit egy szabványos fordítóprogram is generálna tárgykódként (/bináris fájlként). Modern processzorok esetében a CPU-gyorsítótár működését is figyelembe kell venni, különben váratlan mellékhatások léphetnek fel. Ezt a módszert gyakran alkalmazták a futásidő elején történő feltételvizsgálatra („first time” conditions), ahogy az az alábbi, magyarázattal bőségesen ellátott IBM/360 assembler példában is olvasható. Az utasítás-felülírás módszerével (N×1)−1-edrészére csökkenti az utasítás-útvonalhosszúságot, ahol N a fájlban lévő rekordok száma (a −1 pedig a felülírás miatt fellépő többletszámítási költség).

SUBRTN CLI    SUBRTN,X'95'    ELŐSZÖR JÁRUNK ITT?  (ez az utasítás az első futás után azonnal felülírásra kerül)
       BNE    OPENED               (ÁTUGORJUK, HA A CLI OPCODE, = x'95' MÉG NEM LETT MEGVÁLTOZTATVA)
       MVC    SUBRTN(4),JUMP  IGEN, ÍRJUK FELÜL A TESZTELÉST EGY FELTÉTEL NÉLKÜLI UGRÓUTASÍTÁSSAL (ugyanolyan hosszú gépi kód)
       OPEN   INPUT                   és NYISSUK MEG A BEMENETI FÁJLT, hiszen az első alkalommal járunk itt
JUMP   B      OPENED          EZ A 4 BÁJTOS FELTÉTEL NÉLKÜLI UGRÓUTASÍTÁS ÍRJA FELÜL A 4 BÁJTOS UTASÍTÁST A ‘TEST’ CÍMKÉNÉL
OPENED GET    INPUT           ITT FOLYTATÓDIK A NORMÁL FELDOLGOZÁS
       ...

(A cserekód, azaz a feltétel nélküli ugrás némileg gyorsabb is az összehasonlító utasításnál, ráadásul csökkenti az utasítás-útvonalhosszat; ezek a különbségek N-szeresen jutnak érvényre. A ‘jump’ utasítás megtartja a helyhez kötöttséget, mivel közel helyezkedik el a felülírt utasításhoz, ami megfelelő kárpótlás az OPEN utáni szükségtelen utasítás beiktatásáért). Modernebb operációs rendszereken, ahol a programok védett memóriaterületeken helyezkednek el, a fenti technika nem alkalmazható, ehelyett a szubrutinra mutató pointert kell megváltoztatni. A pointer dinamikus tárterületen foglal helyet, így tetszőlegesen módosítható az első menet után, hogy átugorja az OPEN szakaszt (az, hogy a közvetlen ugró utasítás és a szubrutinra való hivatkozás helyett először egy pointert be kell tölteni, N utasítással megnöveli az útvonalhosszt, viszont a feltétel nélküli ugrás szükségtelenné válása miatt N utasítással csökkenni is fog).

Magas szintű nyelvek[szerkesztés | forrásszöveg szerkesztése]

Egyes lefordított nyelvek expliciten engedélyezik az önmódosító kódot. Például a COBOL-ban ismert ALTER parancsszó lehetővé teszi a programok számára, hogy módosítsák magukat. A batchprogramozásban is gyakran alkalmazott technika az önmódosítás.[1]A Clipper és a SPITBOL (egy SNOBOL-fordító) szintén segítséget nyújt önmódosító kód írásában. A B6700 rendszer Algol-fordítója az operációs rendszer felé olyan interfészt nyújtott, melyen keresztül a futó kód képes volt az Algol-fordítónak átadni egy karakterláncot vagy egy fájlnevet, majd elindítani a procedúra új verzióját.

Interpretált nyelvek esetében a „gépi kód” megegyezik a forrásszöveggel, így egyszerűbben megoldható a futásidejű szerkesztése: SNOBOL-ban a futó program forrásállításai egy szöveges tömb elemeit alkotják. Más nyelvek, mint a Perl, a Python és a JavaScript lehetővé teszik futásidőben új kód létrehozását és lefuttatását az eval függvénnyel, de nem engedik meg a már létező kód módosítását. A módosítás illúzióját (bár ténylegesen semmilyen kód nem íródik felül) a függvénypointerek módosítása adja, ahogy az alábbi JavaScript-példa mutatja:

    var f = function (x) {return x + 1};
 
    // új definíciót adunk f-nek:
    f = new Function('x', 'return x + 2');

A Lisp makrói szintén lehetővé teszik a futásidejű kódgenerálást anélkül, hogy programkódot tartalmazó karakterláncokkal kellene műveletet végezni.

A Push programozási nyelv genetikus programozási rendszer, amit kimondottan önmódosító programok írására alkottak meg. Nem magas szintű nyelv, de az assembly fölött helyezkedik el.[2]

Összetett módosítás[szerkesztés | forrásszöveg szerkesztése]

Az ablakozó rendszerek térhódítása előtt a parancssori felületeken elterjedt az olyan menürendszerek használata, ami egy futó parancssori szkript módosítását is magában foglalta. Tegyük fel, hogy egy DOS szkript (avagy batch file), nevezzük Menu.bat-nak, a következőket tartalmazza:

   :StartAfresh                ← a kettősponttal kezdődő sor címkét jelöl
   ShowMenu.exe

A Menu.bat parancssori indítása után a ShowMenu megjelentet egy menüt a képernyőn, valószínűleg használati információkkal, útmutatókkal stb. Miután a felhasználó kiválaszt valamit a menüből, ami a „valami” parancs végrehajtását foglalja magában, a ShowMenu kilép, de előtte újraírja a Menu.bat-ot a következőképpen:

   :StartAfresh
   ShowMenu.exe
   CALL C:\Commands\valami.bat
   GOTO StartAfresh

Mivel a DOS parancsértelmező nem fordítja le a batch file-t futtatáskor, továbbá nem olvassa be az egészet a memóriába a futtatás előtt vagy puffereli bármilyen módon, ezért a ShowMenu kilépése után a parancsértelmező talál egy új, végrehajtandó parancsot (ami a valami nevű fájl végrehajtásáról szól, a ShowMenu által alapul vett könyvtárban és az általa ismert protokollt követve), majd a parancs végrehajtása után visszatér a batch file elejére, újraindítja a ShowMenu programot, ami vár a következő választásra. Ha a felhasználó a kilépést választja, a batch file visszaíródna eredeti állapotába. Bár a kiindulási állapotban nem lenne szükség a címkére, az önmódosítás miatt fontos, hogy ott legyen a címke (vagy vele azonos hosszúságú szöveg), mivel a DOS parancsértelmező a következő parancs bájtpozícióját jegyzi meg, ezért az újraírt parancsfájlban a következő parancsnak ugyanott kell kezdődnie, mint az újraírás előtt.

A menürendszer kényelmessége (és egyéb hasznos funkciói) mellett, ennek a megoldásnak az is jellegzetessége, hogy a ShowMenu.exe-nek nem kell a memóriában lennie a kiválasztott parancs aktiválásakor, ami korlátozott memória esetén komoly előny lehet.

Vezérlő táblák[szerkesztés | forrásszöveg szerkesztése]

A vezérlőtábla-alapú (control table) értelmezők tekinthetők bizonyos értelemben önmódosítónak, mivel a táblabejegyzésekből kinyert értékeket használják (és nem közvetlenül a kódban található feltételes utasítások szerepelnek, "IF inputx = 'yyy'" formában).

Története[szerkesztés | forrásszöveg szerkesztése]

Az 1948 januárjában bemutatott elektromechanikus számítógép, az IBM SSEC képes volt az utasításainak módosítására, illetve adatként való kezelésére. Ezt azonban ritkán alkalmazták a gyakorlatban.[3] A számítástechnika korai napjaiban gyakran alkalmaztak önmódosító kódokat vagy az igen korlátozott méretű memória jobb kihasználására, vagy a teljesítmény javítására, gyakran mindkét célból. Szubrutinok hívására és az azokból való visszatérésre is használták olyan architektúrákon, melyek utasításkészlete csak egyszerű elágazó vagy kihagyó utasításokkal tudta a vezérlést módosítani. Ez a használata még mindig elterjedt egyes ultra-RISC architektúrákon, legalábbis elméletben; mint például a one instruction set computer esetén. Donald Knuth MIX architektúrája is önmódosító kóddal valósította meg a szubrutinhívásokat.

Felhasználása[szerkesztés | forrásszöveg szerkesztése]

Önmódosító kódot különböző célokra alkalmaznak, köztük:

  • Állapotfüggő ciklus félautomata optimalizálására.
  • Futásidejű kódgenerálásra, illetve algoritmus futásidejű vagy betöltési időben történő egyénítésére (utóbbi főleg a valós idejű grafika területén népszerű)
  • Objektumok beágyazott (inline) állapotának megváltoztatása, vagy a magas szintű closure konstrukció szimulációja
  • Szubrutinok híváscímére mutató pointer patchelése, általában a dinamikus programkönyvtár betöltési/inicializálási idejében, vagy minden meghívásakor, a szubrutin belső hivatkozásait úgy módosítva a paramétereknek megfelelően, hogy azok tényleges címét használja (indirekt önmódosítás).
  • Evolúciós számítástechnikai rendszerek, mint pl. genetikus programozás
  • Kódrejtés vagy -összezavarás a reverse engineering (kódvisszafejtés disassembler vagy debugger használatával) megakadályozására, víruskeresés elkerülésére vagy hasonló okból.
  • A memória 100%-ának (egyes architektúrákon) ismétlődő opkódokkal való feltöltése minden program és adat törlése céljából, vagy a hardver beégetése érdekében.
  • A kód tömörítése és későbbi futásidejű kicsomagolása (EXE-tömörítés), ha a memória vagy a lemezterület korlátozott.
  • Egyes nagyon korlátozott utasításkészletekben az önmódosító kód az egyetlen lehetőség bizonyos feladatok elvégzésére. Például a one instruction set computer (OISC) gép, aminek az egyetlen utasítása a kivonás-és-elágazás-ha-negatív (subtract-and-branch-if-negative) nem képes indirekt másolás (kb. a C nyelvű „*a = **b”) elvégzésére önmódosító kód nélkül.
  • Utasítások módosítása hibatűrés céljából.[4]

Állapotfüggő ciklus optimalizálása[szerkesztés | forrásszöveg szerkesztése]

Pszeudokódú példa:

repeat N times {
   if STATE is 1
      increase A by one
   else
      decrease A by one
   do something with A
}

Önmódosító kóddal a ciklust így lehetne megvalósítani:

 repeat N times {
    increase A by one
    do something with A
 }
 when STATE has to switch {
    replace the opcode "increase" above with the opcode to decrease, or vice versa
 }

Vegyük észre, hogy az opkód kétállapotú lecserélését egyszerűen így is írhatnánk: 'xor var at address with the value "opcodeOf(Inc) xor opcodeOf(dec)"'.

Az „N” értékétől és az állapotváltozás gyakoriságától függ, hogy melyik megoldást érdemes választani.

Álcázásként való használata[szerkesztés | forrásszöveg szerkesztése]

Önmódosító kódokkal próbálták elrejteni az 1980-as évekbeli, lemezről induló programok másolásvédelmi mechanizmusait pl. az IBM PC és Apple II platformokon. Például IBM PC (vagy -kompatibilis) gépeken a flopimeghajtót elérő 'int 0x13' utasítás nem található meg a futtatható állományban, de a program futásakor megjelenik a memóriában található példányban.

Önmódosító kódot használnak olyan programok is, amik álcázni próbálják valódi természetüket, ilyenek pl. a számítógépes vírusok és egyes shellcode-ok. Az önmódosító vírusok és shellcode-ok általában ráadásul polimorf kódolásúak is. A futó kód módosítását egyes támadásokban, pl. a puffertúlcsordulásos sebezhetőség kihasználásában is alkalmazzák.

Önhivatkozó gépi tanulási rendszerek[szerkesztés | forrásszöveg szerkesztése]

A hagyományos gépi tanulási rendszerekben fix, előre leprogramozott tanulási algoritmusok módosították a rendszer paramétereit. Az 1980-as években azonban Jürgen Schmidhuber számos önmódosító rendszert mutatott be, melyek képesek voltak saját tanulási algoritmusaik módosítására. A katasztrofális önmódosítást úgy kerülik el, hogy egy módosulat csak akkor futhat tovább, ha hasznosnak bizonyul valamilyen felhasználó által definiált fitnesz-, hiba- vagy jutalomfüggvény szerint.

Operációs rendszerek[szerkesztés | forrásszöveg szerkesztése]

Mivel az önmódosító kódok gyakran biztonsági rést jelentenek, valamennyi nagyobb operációs rendszer igyekszik ezeket a sebezhetőségeket ismertté válásuk után eltávolítani. Általában nem az a probléma, ha egy program szándékosan önmódosító módon van megírva, hanem hogy egy rosszindulatú támadó kiaknázhatja azokat.

A biztonsági rések okozta problémák miatt fejlesztették ki a W^X (azaz „write xor execute”) funkciót, ami megakadályozza, hogy a programok egy memórialapot egyszerre írhatóvá és végrehajthatóvá tegyenek. Egyes rendszerek megakadályozzák, hogy egy írható lap valaha futtatható legyen, még ha az írási jogot előtte el is távolítják. Más rendszerek meghagynak egyfajta hátsó bejáratot, és megengedik, hogy egy memórialapnak különböző leképezései legyenek jelen a rendszerben, különböző jogosultságokkal.

Viszonylag hordozható megoldás a W^X megkerülésére egy minden jogosultsággal rendelkező fájl létrehozása, majd memóriába (kétszer) történő leképezése. Linux alatt egy dokumentálatlan SysV osztottmemória-jelzőbittel lehet futtatható osztott memóriához jutni, és még fájlt sem kell létrehozni hozzá.

Mindent egybevéve, metaszinten a programok mindenképpen képesek lehetnek a viselkedésük módosítására azzal is, hogy máshol tárolt adatokat módosítanak (lásd metaprogramozás) vagy polimorfizmus segítségével.

A gyorsítótár és az önmódosító kód közötti kapcsolat[szerkesztés | forrásszöveg szerkesztése]

Olyan architektúrákon, ahol az adat- és az utasítás-gyorsítótárat nem vonták össze (egyes ARM és MIPS magok), a gyorsítótár-szinkronizációt explicit módon kell elvégezni az utasításkód módosításával (az adatgyorsítótár kiürítésével és a módosított memóriaterület utasítás-gyorsítótárának érvénytelenítésével).

Egyes esetekben modern processzorokon az önmódosító kód rövid szekciói lassabban futnak le. Ennek az az oka, hogy a modern processzorok megpróbálják a kódblokkokat a gyorsítótárukban tartani. Minden alkalommal, amikor a program újraírja valamely részét, az újraírt részt újra be kell tölteni a gyorsítótárba, ami némi késlekedéssel jár abban az esetben, ha a módosított kódrészlet a módosító kóddal ugyanazon a soron osztozik a gyorsítótárban – tehát ha a módosított memóriacím és a módosító kód között csak néhány bájtnyi távolság van.

A gyorsítótár-érvénytelenítési probléma modern processzorokon általában azt jelenti, hogy az önmódosító kód akkor gyorsabb, ha a módosítás ritkán történik meg, mint pl. egy belső ciklus állapotváltásakor.

A legtöbb modern processzor végrehajtás előtt először betölti a gépi kódot, így ha olyan utasítás kerül módosításra, ami túl közel van az utasításszámláló aktuális értékéhez, a processzor nem fogja észrevenni. Bár a PC-k processzorainak kompatibilitási okokból korrektül kellene kezelniük az önmódosító kódokat, ezt nem túl hatékonyan teszik.

Előnyök[szerkesztés | forrásszöveg szerkesztése]

Hátrányok[szerkesztés | forrásszöveg szerkesztése]

Az önmódosító kód nehezebben értelmezhető és karbantartható, mivel a forrásprogram utasításai nem feltétlenül azok az utasítások, amik ténylegesen végrehajtásra fognak kerülni. Ha az önmódosítás csak a függvénymutatók lecserélésére terjed ki, az kevésbé rejtélyes kódot eredményez, hiszen látható, hogy a kódban hivatkozott függvények csak „helyőrzők” a később azonosítandó függvények számára. Az önmódosító kódot újra lehet írni olyan módon, hogy jelzőbiteket használjon és annak értékét tesztelve ugorjon különböző utasításokra, de általában az önmódosító kód gyorsabb ennél.

Modern, utasítás-futószalagot tartalmazó processzorokon a rendszeresen önmódosító kód futása lassabb is lehet, ha a memóriából a futószalagba beolvasott utasításokat is módosít. Ilyen processzorok esetében ugyanis a kód helyes futását csak úgy lehetséges biztosítani, ha a futószalagot újrainicializálva több utasítást újra beolvastatnak a memóriából.

Nem minden környezet engedi meg az önmódosító kód futtatását:

  • A szigorú W^X biztonságot alkalmazó operációs rendszeren futó alkalmazások nem futtathatnak utasításokat olyan memórialapokon, melyekre írási joguk van – csak az operációs rendszer számára engedélyezett utasítások memóriába írása és később azok futtatása.
  • Számos Harvard-architektúrájú mikrokontroller létezik, amik nem képesek a RAM-ban lévő utasítások végrehajtására, csak a számukra nem írható memóriaterületekről, azaz ROM-ból vagy a kontroller eszközeivel nem programozható flashmemóriából képesek programot futtatni.

Kapcsolódó szócikkek[szerkesztés | forrásszöveg szerkesztése]

Jegyzetek[szerkesztés | forrásszöveg szerkesztése]

  1. Self-modifying Batch File by Lars Fosdal
  2. Evolutionary Computing with Push
  3. (1981. szeptember 1.) „The Architecture of IBM’s Early Computers25 (5), 363–376. o. DOI:10.1147/rd.255.0363. Sablon:Citeseerx. „The SSEC was the first operating computer capable of treating its own stored instructions exactly like data, modifying them, and acting on the result.” 
  4. On Self-Modifying Code and the Space Shuttle OS

További információk[szerkesztés | forrásszöveg szerkesztése]