Quali tipi di file possono essere eseguiti su macOS?

Un processo viene creato come risultato del caricamento di un file opportunamente predisposto in memoria. Ma quali tipi di file possiamo mandare in esecuzione su macOS?

file

Preambolo

Il requisito fondamentale nell’esecuzione di un file è che sia in un formato comprensibile dal sistema operativo, che a sua volta può analizzarlo, impostare le dipendenze richieste (come le librerie), inizializzare l’ambiente di runtime e avviare l’esecuzione.

In Unix, qualsiasi file può essere contrassegnato come eseguibile con il semplice comando chmod +x; questo tuttavia, non garantisce che il file possa effettivamente essere eseguito, piuttosto si limita a dire al kernel di leggere questo file in memoria e cercare una delle diverse firme di intestazione per mezzo della quale sarà possibile determinare il formato esatto dell’eseguibile.

Dei vari formati eseguibili supportati da macOS, discuteremo fondamentalmente solo di due tipi: Universal Binaries e Mach-O.

Universal Binaries

Con macOS Apple ha sbandierato a tutti il concetto di Universal Binaries, spiegando che l’idea è di fornire un formato binario che sia completamente portatile e che possa essere eseguito su qualsiasi architettura. Ricordiamo che macOS, originariamente, era costruito sull’architettura PowerPPC ed è stato portato successivamente su architettura Intel (a partire da macOS Tiger). I binari universali, consentono l’esecuzione dei binari su entrambi i processori PPC e x86.

Sostanzialmente, i binari universali non sono altro che archivi che contengono le rispettive architetture che possono supportare. Contengono un’intestazione abbastanza semplice, seguita da copie back-to-back del file binario per ogni architettura supportata.

Nell’immagine sottostante è mostrata la struttura del fat header

file

Per verificare quanto dico, vi basta eseguire il comando file /usr/bin/perl  per rendervi conto che sono supportare due architetture: la versione Intel a 32 bit (i386) e la versione Intel a 64 bit (x86_64). Se eseguiste ancora macOS Snow Leopard, trovereste addirittura una versione di PowerPC (ppc).

file

Come immaginerete, realizzare più copie degli stessi binari per le varie architetture aumenta notevolmente la dimensione dei binari stessi. In effetti, i binari universali sono molto “gonfiati”, il che li rendo meno appetibili all’uso, ed inoltre hanno preso l’alias di “fat” binaries.

Anche se occupano molto spazio su disco, la loro struttura consente a macOS di selezionare automaticamente il binario migliore per la piattaforma sul quale sarà eseguito. Quando viene richiamato un binario, il loader (colui che carica nella memoria centrale, a partire da un determinato indirizzo fisico stabilito dal Sistema Operativo, il binario) Mach analizza l’intestazione del fat binaries e determina le architetture disponibili. Procede quindi a caricare solo l’architettura adatta. Le architetture in questa fase non sono rilevanti, non occupano memoria perchè il kernel carica solo la prima pagina del binario per leggere l’intestazione, procedendo velocemente a caricare l’immagine appropriata.

Mach-O Binaries

Unix, in questo settore è ampiamente standardizzato su formato binario chiamato Executable and Library Format, o ELF. Questo formato è ben documentato, ha un gran numero di binutils per manutenzione e debug e consente anche la portabilità binaria tra macchine Unix con uguale architettura della CPU. macOS, tuttavia, mantiene il proprio formato binario, Mach-Object (Mach-O), in memoria delle sue origini NeXTSTEP.

file

Il formato Mach-O,  inizia con un’intestazione fissa, dettagliata in <mach-o/loader.h> che a sua volta inizia con il valore “Magic” che consente al loader di determinare rapidamente se è destinato ad un’architettura a 32 bit (MH_MAGIC) o architettura a 64 bit (MH_MAGIC_64).

Dopo questo valore “Magic” ci sono una serie di campi che identificano la CPU il CPUSubtype: hanno le stesse funzionalità dell’intestazione degli Universal Binaries e assicurano che il binario sia adatto per essere eseguito su questa architettura. Poiché lo stesso formato binario viene utilizzato per più tipi di file oggetto (eseguibili, librerie, file core o estensione kernel), il campo successivo, filetype, è un int con valori definiti in <mach-o/loader.h> come macro, che specifica il tipo di eseguibile.

Una nota molto importante riguarda due possibili parametri che il campo filetype può accettare e sono:

  • MH_ALLOW_STACK_EXECUTION: consente allo stack di essere eseguibile; è valido solo per i file eseguibili e generalmente è una cattiva idea perché si previene la possibilità di code injection e quindi si mitigano le possibilità di un attacco basato su buffer overflow: generalmente, lo stack è non eseguibile proprio per il motivo sopra descritto.
  • MH_NO_HEAP_EXECUTION: rende l’heap non eseguibile, il che è un’ottima mossa se si vogliono combattere attacchi basati su heap overflow: ad esempio, quando in un programma abbiamo più funzioni che eseguono la stessa azione, solitamente si usano memorizzare dei puntatori a funzione nell’area heap: questi puntatori contengono gli indirizzi iniziali delle funzioni che vogliamo eseguire. Sfruttando l’overflow di un buffer allocato nello heap si vanno quindi a sovrascrivere i puntatori delle funzioni con un puntatore ad esempio ad uno shellcode iniettato attraverso l’overflow: la successiva chiamata a una delle funzioni comporta il trasferimento del controllo allo shellcode invece che alla funzione che ci aspettavamo di eseguire.

In macOS lo stack è non eseguibile di default, mentre lo heap è eseguibile; in iOS sia stack che heap sono non eseguibili.

Anche per questo articolo è tutto. Per eventuali domande, curiosità o feedback potete lasciare un commento qui in basso, a presto!

HotAcquista iPhone 15 su Amazon!
Approfondimenti