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...