HTML

Hackstock

Hack the planet! Hackers unite!

Címkék

Címkefelhő

MachO image file-ok linkelése MacOSX rendszerben

2009.12.27. 10:54 :: theshadow

A napokban kezdtem el MacOSX rendszerben a MachO binárisok betöltődését vizsgálni. Látni akartam, hogy mi történik a file-lal, hogyan kerül a memóriába a tartalma és hogyan történik a rendszerbe illesztése.

Szerencsére az Apple nyílt forráskódúvá tette ezeket a részleteket és - hasraütésszerűen - letöltöttem a MacOSX 10.5.1 XNU csomagjának forráskódját. Ezen kívül még érdekes információkat találtam több írásban is szerte a web-en; az "Apple binárisok védelme" című írásban, találtam egy másik írást, ahol szintén az OSX alkalmazásvégrehajtását taglalják. Jó leírások, de mindegyik esetben, amikor a dinamikus linkerhez érkeznek, akkor csak azzal intézik el, hogy az /usr/lib/dyld töltődik be, ez elvégzi a linkelést viszont arra vonatkozóan, hogy milyen fázisban történik ez meg illetve milyen módon, milyen funkció hívódik meg, nem találtam információt. Ilyen és ehhez hasonló kérdések miatt kezdtem foglalkozni a témával.

Csupán megjegyzés szinten érdemes megemlíteni, hogy dinamikus linker univerzális binárisában négy rendszer image-et találtam, ami azért volt meghökkentő első látásra, mert korábban csak két rendszerét találtam (Intel X86 és PowerPC). Ez a négy image Intel X86 32 bit, Intel X86 64 bit, PowerPC 32 bit és Power PC 64 bit. Ráadásul egy 10.4.8-as disztribució került a kezembe. Természetesen ez nem akadálya a kutakodásomnak, hiszen az IDA 5.5 remekül kezeli ezeket az állományokat.

Az egész történet a következő funkció meghívásával indul el (forráskód).

int execve(struct proc *p, struct execve_args *uap, register_t *retval)

Ez a metódus lényegében egy stub metódus a __mac_execve hívásához.

int __mac_execve(proc_t p, struct __mac_execve_args *uap, register_t *retval)

Itt most nem akarom részletezni hogyan történik a környezet ellenőrzése és a változók inicializálása, mert ez most nem érdekes a vizsgálatom szemszögéből. Initialize the common data, audit the path name... blabla... ez itt már érdekes!

error = exec_activate_image(imgp);

Ezen a metóduson belül még történik néhány előkészítő lépés és ellenőrzések, mígnem végre megmozdul valami. 

error = vn_rdwr(UIO_READ, imgp->ip_vp, imgp->ip_vdata, PAGE_SIZE, 0,

UIO_SYSSPACE32, IO_NODELOCKED,

vfs_context_ucred(imgp->ip_vfs_context),

&resid, vfs_context_proc(imgp->ip_vfs_context)); 

A kiszemelt file-ból (imgp->ip_vp) egy lapnyi (PAGESIZE) adat beolvasása történik a bufferbe (imgp->ip_vdata).

encapsulated_binary:

/* Limit the number of iterations we will attempt on each binary */

if (--iterlimit == 0) {

error = EBADEXEC;

goto bad;

}

error = -1;

for(i = 0; error == -1 && execsw[i].ex_imgact != NULL; i++) {

error = (*execsw[i].ex_imgact)(imgp);

switch (error) {

/* case -1: not claimed: continue */

case -2: /* Encapsulated binary */

goto encapsulated_binary;

case -3: /* Interpreter */

// removed just for now

default:

break;

}

}

A következő egységet megrövidítettem, hogy a lényeg látszódjon. Először is a beágyazott, több platform image kezelését irányító (és védő) feltétel látható - annak érdekében, hogy le lehessen sok egymásba ágyazással veremterületeket feltölteni - tehát maximum csak a második egymásba ágyazási szinten helyezhetők el (MachO ABI FatArch formátum).

A kiemelt kódrész második felében látható ciklus egy struktúra tömbön lépdel végig, aminek az ex_imgact mezejében tárolt metódust hívja meg az imgp struktúrával paraméterezve. A visszatérési értéknek megfelelően, ha Fat formátumot olvasott be (error = -2), akkor újra kell próbálkozni - nyílván a buffer újratöltése megtörtént, vagy  script értelmezőt kell indítani (error = -3) vagy ki kell lépni a ciklusból minden más esetben.

struct execsw {

int (*ex_imgact)(struct image_params *);

const char *ex_name;

} execsw[] = {

{ exec_mach_imgact, "Mach-o Binary" },

{ exec_fat_imgact, "Fat Binary" },

#ifdef IMGPF_POWERPC

{ exec_powerpc32_imgact, "PowerPC binary" },

#endif /* IMGPF_POWERPC */

{ exec_shell_imgact, "Interpreter Script" },

{ NULL, NULL}

};

Ez a struktúra tábla vezérli az iménti ciklust; exec_mach_imgact felelős az úgynevezett MachO 1.0 file-ok betöltéséért, exec_fat_imgact a MachO 2.0 file-okat kezeli (több platform programkódja egy állományba ágyazva), exec_powerpc32_imgact tölti be a PowerPC 32 bites állományokat akkor, ha annak a fordítása is megtörtént és végül exec_shell_imgact végzi a script értelmezővel kapcsolatos tennivalókat. Ahogy sejthető nekünk most az első két eset érdekes.

Nézzük meg először a második metódust, mert az sokkal egyszerűbb (és hamar kipipálhatjuk).

for (f = 0; f < nfat_arch; f++) {

cpu_type_t archtype = OSSwapBigToHostInt32(

arches[f].cputype);

cpu_type_t archsubtype = OSSwapBigToHostInt32(

arches[f].cpusubtype) & ~CPU_SUBTYPE_MASK;

if (pref == archtype &&

grade_binary(archtype, archsubtype)) {

/* We have a winner! */

fat_arch.cputype = archtype; 

fat_arch.cpusubtype = archsubtype; 

fat_arch.offset = OSSwapBigToHostInt32(

arches[f].offset);

fat_arch.size = OSSwapBigToHostInt32(

arches[f].size);

fat_arch.align = OSSwapBigToHostInt32(

arches[f].align);

goto use_arch;

}

// ...

use_arch:

/* Read the Mach-O header out of fat_arch */

error = vn_rdwr(UIO_READ, imgp->ip_vp, imgp->ip_vdata,

PAGE_SIZE, fat_arch.offset,

UIO_SYSSPACE32, (IO_UNIT|IO_NODELOCKED),

cred, &resid, p);

// ...

error = -2;

imgp->ip_arch_offset = (user_size_t)fat_arch.offset;

imgp->ip_arch_size = (user_size_t)fat_arch.size;

Egyre hosszabbak a programkód részek. :) A ciklus a jelenlegi platformnak megfelelő FatArch struktúrát választja ki. Ha nincs ilyen, akkor a ciklus utáni sorok elvégzik a hibaflag-ek beállítását és a kilépést. Azonban ha megtaláltuk, amit keresünk ("We have a winner", vannak néha ilyen vicces kommentek a forráskódokban), akkor újra feltöltjük a buffert, de most a fat_arch.offset pozícióról olvassuk a 4KB-ot. Flag és a megfelelő mezők beállítása után visszatérünk a hívóhoz, hogy az megismételhesse a vizsgálatot.

Megjegyzés: Vajon scriptet is el lehetne rejteni egy MachO 2.0 file image-ében? Az eddigiek alapján  akár még ez is lehetséges, de nem merek rá mérget venni.

Az exec_mach_imgact az első lépések során számos környezeti beállítást végez, amelyek ismét nem fontosan számunkra. Ott viszont már lassabban rögressük a sorokat, ahol a következőt látjuk.

/*

 * Actually load the image file we previously decided to load.

 */

lret = load_machfile(imgp, mach_header, thread, map, &load_result); 

Ezen a ponton ne menjünk tovább, hanem nézzük meg mi történik a hívott függvényen belül. A deklaráció a bsd/kern/mach_loader.c fileban található. Mondhatni ez is egy unalmas metódus, hiszen az ellenőrzések mellett nem sok lényegi történik.

*result = load_result_null;

lret = parse_machfile(vp, map, thread, header, file_offset, macho_size,

     0, result); 

 Paraméterek előkészítése után a parser egység kerül végrehajtásra.

error = vn_rdwr(UIO_READ, vp, addr, size, file_offset,

   UIO_SYSSPACE32, 0, kauth_cred_get(), &resid, p); 


Filetípusra és formátumra vonatkozó ellenőrzések után a LoadCommand struktúrák bufferbe töltése történik meg.

/*

 * Scan through the commands, processing each one as necessary.

 */

for (pass = 1; pass <= 2; pass++) {

/*

* Loop through each of the load_commands indicated by the

* Mach-O header; if an absurd value is provided, we just

* run off the end of the reserved section by incrementing

* the offset too far, so we are implicitly fail-safe.

*/

offset = mach_header_sz;

ncmds = header->ncmds;

while (ncmds--) {

/*

* Get a pointer to the command.

*/

lcp = (struct load_command *)(addr + offset);

oldoffset = offset;

offset += lcp->cmdsize;

 

/*

* Perform prevalidation of the struct load_command

* before we attempt to use its contents.  Invalid

* values are ones which result in an overflow, or

* which can not possibly be valid commands, or which

* straddle or exist past the reserved section at the

* start of the image.

*/

if (oldoffset > offset ||

   lcp->cmdsize < sizeof(struct load_command) ||

   offset > header->sizeofcmds + mach_header_sz) {

ret = LOAD_BADMACHO;

break;

}

 

/*

* Act on struct load_command's for which kernel

* intervention is required.

*/

switch(lcp->cmd) {

case LC_SEGMENT_64:

if (pass != 1)

break;

ret = load_segment_64(

      (struct segment_command_64 *)lcp,

  pager,

  file_offset,

  macho_size,

  ubc_getsize(vp),

  map,

  result);

break;

case LC_SEGMENT:

if (pass != 1)

break;

ret = load_segment(

      (struct segment_command *) lcp,

  pager,

  file_offset,

  macho_size,

  ubc_getsize(vp),

  map,

  result);

break;

case LC_THREAD:

if (pass != 2)

break;

ret = load_thread((struct thread_command *)lcp,

  thread,

 result);

break;

case LC_UNIXTHREAD:

if (pass != 2)

break;

ret = load_unixthread(

(struct thread_command *) lcp,

  thread,

result);

break;

case LC_LOAD_DYLINKER:

if (pass != 2)

break;

if ((depth == 1) && (dlp == 0)) {

dlp = (struct dylinker_command *)lcp;

dlarchbits = (header->cputype & CPU_ARCH_MASK);

} else {

ret = LOAD_FAILURE;

}

break;

case LC_CODE_SIGNATURE:

/* CODE SIGNING */

if (pass != 2)

break;

/* pager -> uip ->

  load signatures & store in uip

  set VM object "signed_pages"

*/

ret = load_code_signature(

(struct linkedit_data_command *) lcp,

vp,

file_offset,

macho_size,

header->cputype,

(depth == 1) ? result : NULL);

if (ret != LOAD_SUCCESS) {

printf("proc %d: load code signature error %d "

      "for file \"%s\"\n",

      p->p_pid, ret, vp->v_name);

ret = LOAD_SUCCESS; /* ignore error */

} else {

got_code_signatures = TRUE;

}

break;

default:

/* Other commands are ignored by the kernel */

ret = LOAD_SUCCESS;

break;

}

if (ret != LOAD_SUCCESS)

break;

}

if (ret != LOAD_SUCCESS)

break;

}

Ez a nagyobb darab kódrészlet, ez végzi el a LoadCommand-ek megfelelő feldolgozását és végrehajtását. Lényegét tekintve két menetben történik meg (pass). Az első menet során - én csak így nevezem - az első szintű parancsok hajtódnak végre. A szegmensek betöltése - logikus - az első feladat, hiszen minden más parancs a betöltött adatokon dolgozhat, tehát nekik elő kell készíteni a terepet és csak utána lehet őket feldolgozni.

A "page zero"-t természetesen nem tölti be, hiszen nincs file offset és size mezők tartalma nulla.

initprot = (scp->initprot) & VM_PROT_ALL;

maxprot = (scp->maxprot) & VM_PROT_ALL;

/*

 * Map a copy of the file into the address space.

 */

ret = vm_map(map,

&map_addr, map_size, (vm_offset_t)0,

       VM_FLAGS_FIXED, pager, map_offset, TRUE,

initprot, maxprot,

VM_INHERIT_DEFAULT);

Ha a lapokra (4KB) felfele kerekített mérete aszegmensnek nagyobb, mint nulla, akkor a megfelelő virtuális címre történő betöltés történik meg. Amennyiben a szegmens virtuális mérete nagyobb, mint a file-ban tárolt adatmennyiség, a fennmaradó rész nullával kerül feltöltésre.

A LoadUnixThreadCommand vagy LoadThreadCommand a megfelelő regisztertartalommal tölti fel a thread státuszt. Ennek a részletezésébe nem megyek bele, mert több metódus mélységében megy végbe és nem is kapcsolódik szorosan a témánkhoz.

Itt történik meg a Dinamikus Linker nevének betöltése is (a DYLINKER LoadCommand-ből).

Végül még érdemes megjegyezni, hogy a kód aláírás betöltése is itt történik meg.

if (dlp != 0)

ret = load_dylinker(dlp, dlarchbits, map, thread, depth, result, abi64);

 A következő említésre érdemes mozzanat, hogy ha Dinamikus Linker név betöltése megtörtént, akkor annak a modulnak a betöltése is megkezdődik ezen a ponton.

ret = parse_machfile(vp, map, thread, &header, file_offset, macho_size,

depth, &myresult);

A már korábban látott parse_machfile metódus segítségével történik meg a Dinamikus Linker betöltése az aktuális címterületre. Ha valamilyen okból a kiszemelt címtartomány foglalt lenne, akkor a betöltő megkísérli más területen elhelyezni.

if (ret == LOAD_SUCCESS) {

result->dynlinker = TRUE;

result->entry_point = myresult.entry_point;

(void)ubc_map(vp, PROT_READ | PROT_EXEC);

}

Sikeres betöltés esetén megtörténik a Dinamikus Linker flag beállítása és egy nagyon fontos momentum, hogy a belépési pont a linker belépési pontjára fog mutatni(!).

Ezek végeztével egy nagy ugrással térjünk vissza a exec_mach_imgact metódus azon pontjára, ahol a load_machfile hívás történt.

if (load_result.unixproc &&

create_unix_stack(get_task_map(task),

 load_result.user_stack,

 load_result.customstack,

 p) != KERN_SUCCESS) {

error = load_return_to_errno(LOAD_NOSPACE);

goto badtoolate;

Több flag beállítása után a veremterület létrehozása is megtörténik, majd a processz környezeti változóinak és paramétereinek a másolását végzi el a exec_copyout_strings eljárás és a verem címének beállítása is megtörténik a thread_setuserstack segítségével.

/* Set the entry point */

thread_setentrypoint(thread, load_result.entry_point);

Legvégül emlékezzünk vissza, hogy a Dinamikus Linker betöltése során a load_result struktúrában annak a belépési pontját mentettük le és itt a thread belépési pontjaként jelenik meg.

/*

 * mark as execed, wakeup the process that vforked (if any) and tell

 * it that it now has it's own resources back

 */

OSBitOrAtomic(P_EXEC, (UInt32 *)&p->p_flag);

if (p->p_pptr && (p->p_lflag & P_LPPWAIT)) {

proc_lock(p);

p->p_lflag &= ~P_LPPWAIT;

proc_unlock(p);

wakeup((caddr_t)p->p_pptr);

}

...és végre mozgásba lendül a gépezet...

Szólj hozzá!

Címkék: macosx

A bejegyzés trackback címe:

http://hackstock.blog.hu/api/trackback/id/tr71625755

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.

Nincsenek hozzászólások.