HTML

Hackstock

Hack the planet! Hackers unite!

Címkék

Címkefelhő

Dinamikus linkelés (1) - /usr/lib/dyld

2009.12.31. 10:13 :: theshadow

 Bizonyára nem csak bennem maradtak kérdések az előző bejegyzés kapcsán. Az szép és jó, hogy a program bináris állománya betöltődött, de ezen kívül más nem történt igazán, ha a veremszegmens, a taszk státuszának feltöltését és a dinamikus linker betöltését nem számítjuk. Egy igen fontos elem hiányzik a kirakóból, ez pedig az importált függvények betöltés, feloldása, csatolása vagy linkelése.

Tehát most azt kell megvizsgálnunk hogy mi történik akkor, amikor a kernel zöld utat ad és a thread elkezd működni az /usr/lib/dyld belépési pontjától. Szerencsére ennek a modulnak a forráskódja is szabadon hozzáférhető,ezért töltsük is le.

Az eredeti binárist IDA 5.5-tel fejtettem vissza és az ott látottak segítségével találtam meg a megfelelő forrásállományt, de a neve is elég beszédes: dyldStartup.s

A megjegyzésekben láthatjuk a stack tartalmát a hívás pillanatában. Ez hasznos információ.

 *  | STRING AREA |

 *  +-------------+

 *  |      0      |

 *  +-------------+

 *  |  apple[n]   |

 *  +-------------+

 *       :

 *  +-------------+

 *  |  apple[0]   | 

 *  +-------------+ 

 *  |      0      |

 *  +-------------+

 *  |    env[n]   |

 *  +-------------+

 *       :

 *       :

 *  +-------------+

 *  |    env[0]   |

 *  +-------------+

 *  |      0      |

 *  +-------------+

 *  | arg[argc-1] |

 *  +-------------+

 *       :

 *       :

 *  +-------------+

 *  |    arg[0]   |

 *  +-------------+

 *  |     argc    |

 *  +-------------+

 * sp->|      mh     | address of where the a.out's file offset 0 is in memory

 *  +-------------+

 *

 * Where arg[i] and env[i] point into the STRING AREA

A modul belépési ponta a __dyld_start cimkénél található.

__dyld_start:

pushl $0        # push a zero for debugger end of frames marker

movl %esp,%ebp # pointer to base of kernel frame

andl    $-16,%esp       # force SSE alignment

# call dyldbootstrap::start(app_mh, argc, argv, slide)

call    L__dyld_start_picbase

 

L__dyld_start_picbase:

popl %ebx # set %ebx to runtime value of picbase

 

     movl __dyld_start_static_picbase-L__dyld_start_picbase(%ebx), %eax

subl    %eax, %ebx      # slide = L__dyld_start_picbase - [__dyld_start_static_picbase]

pushl   %ebx # param4 = slide

lea     12(%ebp),%ebx

pushl   %ebx # param3 = argv

movl 8(%ebp),%ebx

pushl   %ebx # param2 = argc

movl 4(%ebp),%ebx

pushl   %ebx # param1 = mh

call __ZN13dyldbootstrap5startEPK11mach_headeriPPKcl

 

     # clean up stack and jump to result

movl %ebp,%esp # restore the unaligned stack pointer

 

addl $8,%esp # remove the mh argument, and debugger end

#  frame marker

 

movl $0,%ebp # restore ebp back to zero

jmp *%eax # jump to the entry point

 

A verembe kerülő nulla érték a debugger számára érdekes frame vége jelzés. A veremmutató EBP regiszterbe mentése és egy veremigazítás után egy függvényhívással a verem tetejére kerül a L__dyld_start_picbase rutin címe. Ez az EBX regiszterbe kerül.

A következő utasítás érdekes, vizsgáljuk meg részletesebben is!

movl __dyld_start_static_picbase-L__dyld_start_picbase(%ebx), %eax

Számoljuk ki, hogy mi lesz az eredménye a következő műveletnek (a cimkék helyére azon címeit kell helyettesítenünk): __dyld_start_static_picbase - L__dyld_start_picbase + EBX = 0x8FE4A000 - 0x8FE0101C + 0x8FE0101C = 0x8FE4A000. Látszólag ennek semmi értelme, de képzeljük el azt a szituációt, hogy a modul 0x1000-rel alacsonyabb címre töltődik be, ekkor a képletünkben csakis az EBX fog változni: __dyld_start_static_picbase - L__dyld_start_picbase + EBX = 0x8FE4A000 - 0x8FE0101C + 0x8FE0001C = 0x8FE49000. Ebből következik, hogy a lefordított programkódban a __dyld_start_static_picbase és L__dyld_start_picbase relatív offszete kerül, amihez a L__dyld_start_picbase jelenlegi címét adjuk hozzá, ami a __dyld_start_static_picbase mindenkori címét fogja eredményezni függetlenül attól, hogy a modul eredetileg milyen címre töltődött be. Mellesleg a __dyld_start_static_picbase címen a L__dyld_start_picbase eredeti (relokáalizálatlan) címe helyezkedik el. A számítás eredménye pedig az EAX regiszterben kerül tárolásra.

A következő utasításnak megintcsak látszólag nincs értelme.

subl    %eax, %ebx      # slide = L__dyld_start_picbase - [__dyld_start_static_picbase]

Tehát EBX értékéből kivonjuk EAX-et vagyis L__dyld_start_picbase címéből kivonjuk az előbb kiolvasott értéket. Ez a mi esetünkben 0-t fog eredményezni, de ha - megintcsak - nem az eredeti címre töltődött be, akkor a relatív offszetét fogja megmutatni; ezt fogjuk a későbbiekben slide értéknek hívni azaz ez mutatja meg, hogy ha relokációra van szükség, akkor milyen értékkel kell dolgozni.

A következő, hogy a verem a slide értékkel, EBP+12 (argumentum értékek), EBP+2 (argumentumok száma), EBP+4 (modul báziscíme) értékekkel töltődik fel és a __ZN13dyldbootstrap5startEPK11mach_headeriPPKcl rutin hívódik meg. Megjegyzésből láthatjuk, hogy ez a call dyldbootstrap::start(app_mh, argc, argv, slide) hívásnak felel meg. Mielőtt ezzel folytatnánk a barangolásunkat, fejezzük be ennek a rutinnak az elemzését.

Az utolsó lépésekben EBP regiszterből helyreállítjuk a hívás előtti veremmutatót, eltávolítjuk a verem keretjelzőjét és az eredeti image címét (egyszerűen a veremmutató értékének módosításával). EBP-be nullát töltünk és ugrás az EAX-ben tárolt címre. Nyílván az előző rutin fogja eredményül adni az eredeti program belépési pontját és így lesz kerek ez a történet.

Nem kell megijedni, nem assemlby szinten fogjuk vizsgálni az egész dyld modult, csak inicializáló stub rész forrása tartalmazott alacsony szintű programozást. A dyldbootstrap::start már C forrásokat tartalmaz.

// _mh_dylinker_header is magic symbol defined by static linker (ld), see <mach-o/ldsyms.h>

const struct macho_header* dyldsMachHeader =  (const struct macho_header*)(((char*)&_mh_dylinker_header)+slide);

// if kernel had to slide dyld, we need to fix up load sensitive locations

// we have to do this before using any global variables

if ( slide != 0 ) {

rebaseDyld(dyldsMachHeader, slide);

}

A Dinamikus Linker relokalizációja és inicializálása

 

A legelső és legfontosabb tennivaló a Dinamikus Linker relokalizációja, amennyiben szükséges. A teljes forrás mellékelése nélkül tekintsük át a lépéseket.

A Dinamikus Linker minden LoadCommand-jánnak minden szekcióján végrehajtuk a következő műveleteket.

const uint8_t type = sect->flags & SECTION_TYPE;

if ( type == S_NON_LAZY_SYMBOL_POINTERS ) {

// rebase non-lazy pointers (which all point internal to dyld, since dyld uses no shared libraries)

const uint32_t pointerCount = sect->size / sizeof(uintptr_t);

uintptr_t* const symbolPointers = (uintptr_t*)(sect->addr + slide);

for (uint32_t j=0; j < pointerCount; ++j) {

symbolPointers[j] += slide;

}

}

Ha a szekció S_NON_LAZY_SYMBOL_POINTERS flag-je be van állítva, akkor minden pointerhez a szekcióban hozzáadjuk a kiszámított slide értéket.

Mellesleg a LoadCommand-ek feldolgozása során feljegyezzük a "__LINKEDIT" nevű szegmens, az első írható szegmens referenciáját és a Dinamikus Szimbólum tábla (LC_DYSYMTAB) referenciáját. Az utolsó lépés során ezeket használjuk fel.

// use reloc's to rebase all random data pointers

#if __x86_64__

const uintptr_t relocBase = firstWritableSeg->vmaddr + slide;

#else

const uintptr_t relocBase = (uintptr_t)mh;

#endif

const relocation_info* const relocsStart = (struct relocation_info*)(linkEditSeg->vmaddr + slide + dynamicSymbolTable->locreloff - linkEditSeg->fileoff);

const relocation_info* const relocsEnd = &relocsStart[dynamicSymbolTable->nlocrel];

for (const relocation_info* reloc=relocsStart; reloc < relocsEnd; ++reloc) {

#if __ppc__ || __ppc64__ || __i36__

if ( (reloc->r_address & R_SCATTERED) != 0 )

throw "scattered relocation in dyld";

#endif

if ( reloc->r_length != RELOC_SIZE ) 

throw "relocation in dyld has wrong size";

 

if ( reloc->r_type != POINTER_RELOC ) 

throw "relocation in dyld has wrong type";

// update pointer by amount dyld slid

*((uintptr_t*)(reloc->r_address + relocBase)) += slide;

}

A Dinamikus Szimbólum Tábla minden elemén végiglépdelve a relokalizációs címen (reloc->r_address + relocBase) talált értékekhez hozzáadjuk a slide értéket, így kialakítva a mostmár valós címértékeket.

Ezt követően néhány alrendszer inicializálása következik (_pthread_keys_init, dyld_exceptions_init, mach_init, segmentProtectDyld), ami szerintem most nem lényeges. Ha mégis, akkor később frissítem ezt az írást a szükséges magyarázatokkal.

// kernel sets up env pointer to be just past end of agv array

const char** envp = &argv[argc+1];

// kernel sets up apple pointer to be just past end of envp array

const char** apple = envp;

while(*apple != NULL) { ++apple; }

++apple;

Itt egy saját pointert hozunk létre a Apple változóknak; már van egy az argumentumoknak, környezeti változóknak. Ez utóbbiakat egy NULL érték zárja le és ezt használjuk a következő tömb kezdetének megállapítására.

// run all C++ initializers inside dyld

runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);

A Dinamikus Linker C++ inicializáló eljárásainak meghívára történik itt a különféle környezeti változó átadásával.

typedef void (*Initializer)(int argc, const char* argv[], const char* envp[], const char* apple[]);

 

...

 

const uint8_t type = sect->flags & SECTION_TYPE;

if ( type == S_MOD_INIT_FUNC_POINTERS ){

Initializer* inits = (Initializer*)(sect->addr + slide);

const uint32_t count = sect->size / sizeof(uintptr_t);

for (uint32_t i=0; i < count; ++i) {

Initializer func = inits[i];

func(argc, argv, envp, apple);

}

}

Minden szekción megvizsgáljuk, hogy az S_MOD_INIT_FUNC_POINTERS flag be van-e állítva. Ha igen, akkor egy mutató tömbként végiglépkedünk a szekció tartalmán és Initializer funkciókként meghívjuk a hivatkozott rutinokat.

Visszatérve a főprogram ághoz még az alkalmazás betöltési címének randomizálást találjuk.

dyld főprogram

Itt már tényleg csak a betöltendő állománnyal foglalkozunk.

A setContext függvény a LinkContext struktúra mezeit tölti fel. Sok érdekeset jelen pillanatban nem találtam benne, számos callback függvény címe szerepel a mezők értékeiben, amelyeknek a későbbiekben lehet/lesz értelme.

Ha relatív útvonal szerepel a futtatható állomány helyeként, akkor abszolútra cseréljük azt. Ezt követően a pruneEnvironmentVariables eljárás minden "DYLD_" kezdetű és "LD_LIBRARY_PATH" nevű környezeti változót eltávolít bár van olyan eset, amikor a checkEnvironmentVariables keretein belül eljutunk ezeknek a változóknak a feldolgozásáig is.

A linker futtató környezetében található vektorok inicializálása (all images, image roots, add és remove image callbacks, stb.) itt történik meg, majd ezt követően a instantiateFromLoadedImage eljárás kerül meghívásra.

// try mach-o loader

if ( isCompatibleMachO((const uint8_t*)mh) ) {

ImageLoader* image = new ImageLoaderMachO(mh, slide, path, gLinkContext);

addImage(image);

return image;

}

 

throw "main executable not a known format";

Ha kompatibilis MachO file-lal dolgozunk (isCompatibleMachO - CPU típus és bizonyos esetekben altípus ellenőrzés is történhet), akkor egy ImageLoaderMachO objektumot hozunk létre. Vegyük észre, hogy az ImageLoaderMachO meglehetősen nagy és összetett osztály, amely az ImageLoader osztályból öröklődik. Ezen az összetettségen próbálunk változtatni a továbbiakban. Próbáljuk megérteni, hogy milyen feladatokat lát el a konstruktor!

ImageLoaderMachO konstruktor

Figyeljünk oda, hogy a konstruktor annak rende és módja szerint az ős osztály konstruktorát is meghívja. Szerencsére ott nem történik más, mint a privát változók kezdeti értékekkel feltöltése NULL, 0 és hamis értékekkel, valamint a MachO image file-jának nevével. Vegyük észre, hogy a filenévből egy egyszerű hash számolódik, ami a sztring összehasonlítások (strcmp) számának csökkentésében tesz jó szolgáltatot.

Ezzel ellentétben az ImageLoaderMachO konstruktorban több minden történik amellett, hogy itt is megtalálható az imént látott inicializáló metódus - értelemszerűen ez csak a leszármazó osztály privát adatjaival dolgozik.

Ez a metódus azokra a LoadSegment parancsokra hoz létre egy SegmentMachO objektumpéldányt, amelyek virtális mérete nem nulla. Az eredmény egy tömb formájában a fSegmentsArray osztályváltozóba kerül, míg a példányok számát a fSegmentsArrayCount változóba mentjük.

A slide értéket is tároljuk a randomizált betöltési című modulok (PIE) számára (nyilván a későbbiekben szerepe lesz a relokalizáció során).

A következő nagyobb egységet a parseLoadCmds metódus képviseli, ahol a fontosabb LoadCommand-ek referenciáinak üsszegyüjtése történik. A "__LINKEDIT" nevű szegmens tényleges betöltési címéből kivonjuk a file offszetét, aminek eredményét a fLinkEditBase változóba töltjük - jegyezzük meg ugyanakkor, hogy ez egyenlő a MachO image báziscímével. A fTextSegmentWithFixups változóba mentjük a "__TEXT" szegmens referenciáját, mert ez mindíg a file elején található és magában foglalja a fejlécet és a betöltő parancsokat. A "__IMPORT" szegmens referenciáját fReadOnlyImportSegment változóba mentjük. Nem utolsósorban pedig fMachOData változóba kerül annak a szegmensnek a hivatkozása, amelyik a file 0 offszetén kezdődik és nem nulla a file-ban tárolt adatblokk mérete.

Egy újabb iteráció során a következő LoadCommand-ekkel foglalkozunk: Szimbolóm Tábla - feljegyezzük a szimbólumnevek és a link edit tábla báziscímét, mentjük a Dinamikus Szimbólumtábla, TwoLevel Hint és ID Dylib parancs címét, flag-et állítunk be ha SubUmbrella, SubFramework, SubLibary vagy Routines töltő parancsokat találunk és "__DATA" nevű szegmens esetén megvizsgáljuk a szekciókat, hogy S_MOD_INIT_FUNC_POINTERS, S_MOD_TERM_FUNC_POINTERS vagy S_DTRACE_DOF flag-ek be vannak-e állítva, és "__image_notify" nevű szekció jelenléte esetén is egy állapotjelzőt billentünk be.

Az adjustSegments eljárás azért felelős, hogy a létrehozott SegmentMachO objektumok számára beállítsa a töltőparanccsokra mutató pointert - csak egyszerűen egy-egy referencia létrehozása.

Csak olvasható "__IMPORT" szegmens esetén ideiglenesen írható jogosultságot állítunk be, mert a kapcsolatok felépítése során erre szükség lesz.

Vissza a dyld főprogramhoz

A fő futtatható modul ImageLoaderMachO objektumpéldányának létrehozása után sorra betöltődik minden dinamikus könyvtár, amelyik a DYLD_INSERT_LIBRARIES környezeti változóvan kettősponttal elválasztva vannak rögzítve. Itt még nem szeretnék belemenni a loadInsertedDylib rutin által hívott load eljárásba, mert szerteágazósága miatt nagyon távolra kerülnénk a dyld főprogramtól. Minden bizonnyal a későbbiekben még találkozunk ezzel az rutinnal. Most csak elégedjünk meg annyival, hogy ez is csak egy ImageLoaderMacho objektumpéldányt hoz létre minden egyes betöltött elemre.

// link main executable

gLinkContext.linkingMainExecutable = true;

link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, ImageLoader::RPathChain( NULL, NULL));

gLinkContext.linkingMainExecutable = false;

A főprogramnak ebben a szakaszában érkeztünk el a betöltött futtatható állomány import linkeinek feloldásához.

image->link(gLinkContext, forceLazysBound, false, loaderRPaths); 

Ez a rutin pedig némi előkészítés után az ImageLoadMachO link metódusát hívja meg. Tudom, hogy megint, amikor kezd érdekes lenni, akkor javaslom, hogy ezt halasszuk későbbre. Szóval most vissza a főághoz...

result = (uintptr_t)sMainExecutable->getMain();

Itt a Unix Thread parancsból szerezzük meg az állományunk belépési pontját. Emlékezzünk, hogy a DYLD_INSERT_LIBRARIES könyvtárakat még csak betöltöttük, de az ő linkelésük nem történt meg.

if ( sInsertedDylibCount > 0 ) {

for(unsigned int i=0; i < sInsertedDylibCount; ++i) {

ImageLoader* image = sAllImages[i+1];

link(image, sEnv.DYLD_BIND_AT_LAUNCH, ImageLoader::RPathChain(NULL, NULL));

}

}

Ezt most elintézzük itt (az sAllImages tömbben a futtatható állományunk után helyezkednek el közvetlenül). Emlékezhetünk a Dinamikus Linker modul betöltési folyamatából, hogy a legutolsó lépés az inicializáló függvények futtatása volt. Most sincs ez másképpen, hiszen a initializeMainExecutable ezt végzi el.

// run initialzers for any inserted dylibs

const int rootCount = sImageRoots.size();

if ( rootCount > 1 ) {

for(int i=1; i < rootCount; ++i)

sImageRoots[i]->runInitializers(gLinkContext);

}

 

// run initializers for main executable and everything it brings up 

sMainExecutable->runInitializers(gLinkContext);

Minden betöltött dinamikus könyvtár ImageLoaderMachO példányán meghívjuk a runInitializers eljárást, ahogy a fő futtatható állományon is, ami továbbítja a kérést a recursiveInitialization eljárás felé.

// initialize lower level libraries first

for(unsigned int i=0; i < fLibrariesCount; ++i){

DependentLibrary& libInfo = fLibraries[i];

// don't try to initialize stuff "above" me

if ( (libInfo.image != NULL) && (libInfo.image->fDepth >= fDepth) )

libInfo.image->recursiveInitialization(context, this_thread);

}

Mielőtt a saját inicializáló eljárások hajtódnának végre, előbb a "mélyebben" elhelyezkedő import modulokatat kell előkészíteni.

// let objc know we are about to initalize this image

fState = dyld_image_state_dependents_initialized;

oldState = fState;

context.notifySingle(dyld_image_state_dependents_initialized, this->machHeader(), fPath, fLastModified);

 

// initialize this image

this->doInitialization(context);

 

// let anyone know we finished initalizing this image

fState = dyld_image_state_initialized;

oldState = fState;

context.notifySingle(dyld_image_state_initialized, this->machHeader(), fPath, fLastModified); 

Látható, ahogy az Objective C környezetet értesítjük arról, hogy a függő import könyvtárak inicializálása megtörtént, majd a saját inicializáló rutin kerül meghívásra és végül értesítést küldünk, hogy ez is megtörtént.

// mach-o has -init and static initializers

doImageInit(context);

doModInitFunctions(context);

A doInitialization-ön belül a statikus és a dinamikus inicializációt történnek meg.

for (unsigned long i = 0; i < cmd_count; ++i) {

switch (cmd->cmd) {

case LC_ROUTINES_COMMAND:

Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide);

if ( context.verboseInit )

dyld::log("dyld: calling -init function 0x%p in %s\n", func, this->getPath());

func(context.argc, context.argv, context.envp, context.apple, &context.programVars);

break;

}

cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);

}

A doImageInit-ben Routines tötlőparancs segítségével összegyüjtjük az inicialiáló metódusok címét és rendre meghívjuk őket az argumentumokkal paraméterezve.

for (unsigned long i = 0; i < cmd_count; ++i) {

if ( cmd->cmd == LC_SEGMENT_COMMAND ) {

const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;

const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));

const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];

for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {

const uint8_t type = sect->flags & SECTION_TYPE;

if ( type == S_MOD_INIT_FUNC_POINTERS ) {

Initializer* inits = (Initializer*)(sect->addr + fSlide);

const uint32_t count = sect->size / sizeof(uintptr_t);

for (uint32_t i=0; i < count; ++i) {

Initializer func = inits[i];

if ( context.verboseInit )

dyld::log("dyld: calling initializer function %p in %s\n", func, this->getPath());

func(context.argc, context.argv, context.envp, context.apple, &context.programVars);

}

}

}

cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);

}

}

A doModInitFunctions-ban olyan Szegmens töltőparancsot keresünk, ahol egy szekciónak a S_MOD_INIT_FUNC_POINTERS flag-je be van állítva, mert ez a terület fogja tartalmazni az inicializáló függvények címét, melyeket az elöbbiekben látott módot paraméterezve hívunk meg.

Az inicializációt végeztével a Dyld főprogram abetöltőtt program belépési címével tér vissza és emlékezzünk vissza, hogy honnan indultunk a dyldInitialization.cpp start metódusából...

return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple);

...amit a dyldStartup.s végefelé hívtunk meg...

call __ZN13dyldbootstrap5startEPK11mach_headeriPPKcl

 

# clean up stack and jump to result

movl %ebp,%esp # restore the unaligned stack pointer

addl $8,%esp # remove the mh argument, and debugger end

#  frame marker

movl $0,%ebp # restore ebp back to zero

jmp *%eax # jump to the entry point

Vagyis a főprogram adja át a belépési címet és így az ugró utasítás adja át a vezérlést a betöltött és előkészített programmodulnak.

Szólj hozzá!

Címkék: macosx

A bejegyzés trackback címe:

https://hackstock.blog.hu/api/trackback/id/tr461634535

Kommentek:

A hozzászólások a vonatkozó jogszabályok  értelmében felhasználói tartalomnak minősülnek, értük a szolgáltatás technikai  üzemeltetője semmilyen felelősséget nem vállal, azokat nem ellenőrzi. Kifogás esetén forduljon a blog szerkesztőjéhez. Részletek a  Felhasználási feltételekben és az adatvédelmi tájékoztatóban.

Nincsenek hozzászólások.
süti beállítások módosítása