Los binarios de Mac OS generalmente se compilan como binarios universales. Un binario universal puede soportar múltiples arquitecturas en el mismo archivo.
Estos binarios siguen la estructura Mach-O que está compuesta básicamente de:
#defineFAT_MAGIC0xcafebabe#defineFAT_CIGAM0xbebafeca /* NXSwapLong(FAT_MAGIC) */struct fat_header {uint32_t magic; /* FAT_MAGIC o FAT_MAGIC_64 */uint32_t nfat_arch; /* número de estructuras que siguen */};struct fat_arch {cpu_type_t cputype; /* especificador de cpu (int) */cpu_subtype_t cpusubtype; /* especificador de máquina (int) */uint32_t offset; /* desplazamiento del archivo a este archivo objeto */uint32_t size; /* tamaño de este archivo objeto */uint32_t align; /* alineación como una potencia de 2 */};
El encabezado tiene los bytes mágicos seguidos por el número de archs que el archivo contiene (nfat_arch) y cada arch tendrá una estructura fat_arch.
Como puedes estar pensando, generalmente un binario universal compilado para 2 arquitecturas duplica el tamaño de uno compilado para solo 1 arch.
Encabezado Mach-O
El encabezado contiene información básica sobre el archivo, como bytes mágicos para identificarlo como un archivo Mach-O e información sobre la arquitectura objetivo. Puedes encontrarlo en: mdfind loader.h | grep -i mach-o | grep -E "loader.h$"
#defineMH_MAGIC0xfeedface /* the mach magic number */#defineMH_CIGAM0xcefaedfe /* NXSwapInt(MH_MAGIC) */struct mach_header {uint32_t magic; /* mach magic number identifier */cpu_type_t cputype; /* cpu specifier (e.g. I386) */cpu_subtype_t cpusubtype; /* machine specifier */uint32_t filetype; /* type of file (usage and alignment for the file) */uint32_t ncmds; /* number of load commands */uint32_t sizeofcmds; /* the size of all the load commands */uint32_t flags; /* flags */};#defineMH_MAGIC_640xfeedfacf /* the 64-bit mach magic number */#defineMH_CIGAM_640xcffaedfe /* NXSwapInt(MH_MAGIC_64) */struct mach_header_64 {uint32_t magic; /* mach magic number identifier */int32_t cputype; /* cpu specifier */int32_t cpusubtype; /* machine specifier */uint32_t filetype; /* type of file */uint32_t ncmds; /* number of load commands */uint32_t sizeofcmds; /* the size of all the load commands */uint32_t flags; /* flags */uint32_t reserved; /* reserved */};
Tipos de archivos Mach-O
Hay diferentes tipos de archivos, puedes encontrarlos definidos en el código fuente, por ejemplo aquí. Los más importantes son:
MH_OBJECT: Archivo de objeto relocatable (productos intermedios de la compilación, aún no ejecutables).
MH_EXECUTE: Archivos ejecutables.
MH_FVMLIB: Archivo de biblioteca VM fija.
MH_CORE: Volcados de código.
MH_PRELOAD: Archivo ejecutable pre-cargado (ya no es compatible en XNU).
MH_DYLIB: Bibliotecas dinámicas.
MH_DYLINKER: Vínculo dinámico.
MH_BUNDLE: "Archivos de plugin". Generados usando -bundle en gcc y cargados explícitamente por NSBundle o dlopen.
MH_DYSM: Archivo compañero .dSym (archivo con símbolos para depuración).
MH_KEXT_BUNDLE: Extensiones del kernel.
# Checking the mac header of a binaryotool-archarm64e-hv/bin/lsMachheadermagiccputypecpusubtypecapsfiletypencmdssizeofcmdsflagsMH_MAGIC_64ARM64EUSR00EXECUTE191728NOUNDEFSDYLDLINKTWOLEVELPIE
El código fuente también define varios flags útiles para cargar bibliotecas:
MH_NOUNDEFS: Sin referencias indefinidas (totalmente enlazado)
MH_DYLDLINK: Enlace Dyld
MH_PREBOUND: Referencias dinámicas preenlazadas.
MH_SPLIT_SEGS: El archivo divide segmentos r/o y r/w.
MH_WEAK_DEFINES: El binario tiene símbolos definidos débiles
MH_BINDS_TO_WEAK: El binario utiliza símbolos débiles
MH_ALLOW_STACK_EXECUTION: Hacer que la pila sea ejecutable
MH_NO_REEXPORTED_DYLIBS: Biblioteca no tiene comandos LC_REEXPORT
MH_PIE: Ejecutable independiente de la posición
MH_HAS_TLV_DESCRIPTORS: Hay una sección con variables locales de hilo
MH_NO_HEAP_EXECUTION: Sin ejecución para páginas de heap/datos
MH_HAS_OBJC: El binario tiene secciones de oBject-C
MH_SIM_SUPPORT: Soporte para simuladores
MH_DYLIB_IN_CACHE: Usado en dylibs/frameworks en la caché de bibliotecas compartidas.
Comandos de carga de Mach-O
El diseño del archivo en memoria se especifica aquí, detallando la ubicación de la tabla de símbolos, el contexto del hilo principal al inicio de la ejecución y las bibliotecas compartidas requeridas. Se proporcionan instrucciones al cargador dinámico (dyld) sobre el proceso de carga del binario en memoria.
Utiliza la estructura load_command, definida en el mencionado loader.h:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
Hay alrededor de 50 tipos diferentes de comandos de carga que el sistema maneja de manera diferente. Los más comunes son: LC_SEGMENT_64, LC_LOAD_DYLINKER, LC_MAIN, LC_LOAD_DYLIB y LC_CODE_SIGNATURE.
LC_SEGMENT/LC_SEGMENT_64
Básicamente, este tipo de comando de carga define cómo cargar el __TEXT (código ejecutable) y __DATA (datos para el proceso) segmentos de acuerdo con los desplazamientos indicados en la sección de datos cuando se ejecuta el binario.
Estos comandos definen segmentos que son mapeados en el espacio de memoria virtual de un proceso cuando se ejecuta.
Hay diferentes tipos de segmentos, como el __TEXT segmento, que contiene el código ejecutable de un programa, y el __DATA segmento, que contiene datos utilizados por el proceso. Estos segmentos se encuentran en la sección de datos del archivo Mach-O.
Cada segmento puede ser dividido en múltiples secciones. La estructura del comando de carga contiene información sobre estas secciones dentro del segmento respectivo.
En el encabezado primero encuentras el encabezado del segmento:
struct segment_command_64 { /* for 64-bit architectures */uint32_t cmd; /* LC_SEGMENT_64 */uint32_t cmdsize; /* incluye sizeof section_64 structs */char segname[16]; /* nombre del segmento */uint64_t vmaddr; /* dirección de memoria de este segmento */uint64_t vmsize; /* tamaño de memoria de este segmento */uint64_t fileoff; /* desplazamiento del archivo de este segmento */uint64_t filesize; /* cantidad a mapear desde el archivo */int32_t maxprot; /* protección máxima de VM */int32_t initprot; /* protección inicial de VM */uint32_t nsects; /* número de secciones en el segmento */uint32_t flags; /* banderas */};
Ejemplo de encabezado de segmento:
Este encabezado define el número de secciones cuyos encabezados aparecen después de él:
struct section_64 { /* for 64-bit architectures */char sectname[16]; /* name of this section */char segname[16]; /* segment this section goes in */uint64_t addr; /* memory address of this section */uint64_t size; /* size in bytes of this section */uint32_t offset; /* file offset of this section */uint32_t align; /* section alignment (power of 2) */uint32_t reloff; /* file offset of relocation entries */uint32_t nreloc; /* number of relocation entries */uint32_t flags; /* flags (section type and attributes)*/uint32_t reserved1; /* reserved (for offset or index) */uint32_t reserved2; /* reserved (for count or sizeof) */uint32_t reserved3; /* reserved */};
Ejemplo de encabezado de sección:
Si agregas el desplazamiento de sección (0x37DC) + el desplazamiento donde comienza la arquitectura, en este caso 0x18000 --> 0x37DC + 0x18000 = 0x1B7DC
También es posible obtener información de encabezados desde la línea de comandos con:
otool-lv/bin/ls
Segmentos comunes cargados por este cmd:* **`__PAGEZERO`:** Instruye al kernel para **mapear** la **dirección cero** de modo que **no se pueda leer, escribir o ejecutar**. Las variables maxprot y minprot en la estructura se establecen en cero para indicar que **no hay derechos de lectura-escritura-ejecución en esta página**.* Esta asignación es importante para **mitigar vulnerabilidades de desreferencia de puntero NULL**. Esto se debe a que XNU aplica una página cero dura que asegura que la primera página (solo la primera) de la memoria sea inaccesible (excepto en i386). Un binario podría cumplir con estos requisitos creando un pequeño \_\_PAGEZERO (usando `-pagezero_size`) para cubrir los primeros 4k y tener el resto de la memoria de 32 bits accesible tanto en modo usuario como en modo kernel.
* **`__TEXT`**: Contiene **código****ejecutable** con permisos de **lectura** y **ejecución** (no escribible)**.** Secciones comunes de este segmento:* `__text`: Código binario compilado* `__const`: Datos constantes (solo lectura)* `__[c/u/os_log]string`: Constantes de cadenas C, Unicode o de os logs* `__stubs` y `__stubs_helper`: Involucrados durante el proceso de carga de la biblioteca dinámica* `__unwind_info`: Datos de deshacer la pila.* Tenga en cuenta que todo este contenido está firmado pero también marcado como ejecutable (creando más opciones para la explotación de secciones que no necesariamente necesitan este privilegio, como secciones dedicadas a cadenas).* **`__DATA`**: Contiene datos que son **legibles** y **escribibles** (no ejecutables)**.*** `__got:` Tabla de Desplazamiento Global* `__nl_symbol_ptr`: Puntero de símbolo no perezoso (vinculación al cargar)* `__la_symbol_ptr`: Puntero de símbolo perezoso (vinculación al usar)* `__const`: Debería ser datos de solo lectura (no realmente)* `__cfstring`: Cadenas de CoreFoundation* `__data`: Variables globales (que han sido inicializadas)* `__bss`: Variables estáticas (que no han sido inicializadas)* `__objc_*` (\_\_objc\_classlist, \_\_objc\_protolist, etc): Información utilizada por el tiempo de ejecución de Objective-C* **`__DATA_CONST`**: \_\_DATA.\_\_const no está garantizado que sea constante (permisos de escritura), ni otros punteros y la GOT. Esta sección hace que `__const`, algunos inicializadores y la tabla GOT (una vez resuelta) sean **solo lectura** usando `mprotect`.* **`__LINKEDIT`**: Contiene información para el enlazador (dyld) como, entradas de tabla de símbolos, cadenas y reubicación. Es un contenedor genérico para contenidos que no están en `__TEXT` o `__DATA` y su contenido se describe en otros comandos de carga.* Información de dyld: Rebase, opcodes de vinculación no perezosa/perezosa/débil e información de exportación* Inicios de funciones: Tabla de direcciones de inicio de funciones* Datos en código: Islas de datos en \_\_text* Tabla de símbolos: Símbolos en binario* Tabla de símbolos indirectos: Punteros/símbolos stub* Tabla de cadenas* Firma de código* **`__OBJC`**: Contiene información utilizada por el tiempo de ejecución de Objective-C. Aunque esta información también podría encontrarse en el segmento \_\_DATA, dentro de varias secciones en \_\_objc\_\*.* **`__RESTRICT`**: Un segmento sin contenido con una sola sección llamada **`__restrict`** (también vacía) que asegura que al ejecutar el binario, ignorará las variables ambientales de DYLD.Como se pudo ver en el código, **los segmentos también admiten banderas** (aunque no se utilizan mucho):* `SG_HIGHVM`: Solo núcleo (no utilizado)* `SG_FVMLIB`: No utilizado* `SG_NORELOC`: El segmento no tiene reubicación* `SG_PROTECTED_VERSION_1`: Cifrado. Usado, por ejemplo, por Finder para cifrar el segmento de texto `__TEXT`.### **`LC_UNIXTHREAD/LC_MAIN`****`LC_MAIN`** contiene el punto de entrada en el **atributo entryoff.** En el momento de la carga, **dyld** simplemente **agrega** este valor a la **base del binario** (en memoria), luego **salta** a esta instrucción para comenzar la ejecución del código del binario.**`LC_UNIXTHREAD`** contiene los valores que el registro debe tener al iniciar el hilo principal. Esto ya fue desaprobado, pero **`dyld`** aún lo utiliza. Es posible ver los valores de los registros establecidos por esto con:
Contiene información sobre la firma de código del archivo Macho-O. Solo contiene un desplazamiento que apunta al blob de firma. Esto suele estar al final del archivo.
Sin embargo, puedes encontrar algo de información sobre esta sección en esta publicación del blog y en este gist.
LC_ENCRYPTION_INFO[_64]
Soporte para la encriptación de binarios. Sin embargo, por supuesto, si un atacante logra comprometer el proceso, podrá volcar la memoria sin encriptar.
LC_LOAD_DYLINKER
Contiene la ruta al ejecutable del enlazador dinámico que mapea bibliotecas compartidas en el espacio de direcciones del proceso. El valor siempre está configurado como /usr/lib/dyld. Es importante notar que en macOS, el mapeo de dylib ocurre en modo de usuario, no en modo kernel.
LC_IDENT
Obsoleto, pero cuando se configura para generar volcado en caso de pánico, se crea un volcado de núcleo Mach-O y la versión del kernel se establece en el comando LC_IDENT.
LC_UUID
UUID aleatorio. Es útil para cualquier cosa directamente, pero XNU lo almacena en caché con el resto de la información del proceso. Puede ser utilizado en informes de fallos.
LC_DYLD_ENVIRONMENT
Permite indicar variables de entorno al dyld antes de que se ejecute el proceso. Esto puede ser muy peligroso, ya que puede permitir ejecutar código arbitrario dentro del proceso, por lo que este comando de carga solo se utiliza en dyld construido con #define SUPPORT_LC_DYLD_ENVIRONMENT y restringe aún más el procesamiento solo a variables de la forma DYLD_..._PATH especificando rutas de carga.
LC_LOAD_DYLIB
Este comando de carga describe una dependencia de bibliotecadinámica que instruye al cargador (dyld) a cargar y vincular dicha biblioteca. Hay un comando de carga LC_LOAD_DYLIBpara cada biblioteca que el binario Mach-O requiere.
Este comando de carga es una estructura de tipo dylib_command (que contiene una estructura dylib, describiendo la biblioteca dinámica dependiente real):
struct dylib_command {
uint32_t cmd; /* LC_LOAD_{,WEAK_}DYLIB */
uint32_t cmdsize; /* includes pathname string */
struct dylib dylib; /* the library identification */
};
struct dylib {
union lc_str name; /* library's path name */
uint32_t timestamp; /* library's build time stamp */
uint32_t current_version; /* library's current version number */
uint32_t compatibility_version; /* library's compatibility vers number*/
};
También puedes obtener esta información desde la línea de comandos con:
Algunas bibliotecas potencialmente relacionadas con malware son:
DiskArbitration: Monitoreo de unidades USB
AVFoundation: Captura de audio y video
CoreWLAN: Escaneos de Wifi.
Un binario Mach-O puede contener uno o másconstructores, que serán ejecutadosantes de la dirección especificada en LC_MAIN.
Los desplazamientos de cualquier constructor se mantienen en la sección __mod_init_func del segmento __DATA_CONST.
Datos de Mach-O
En el núcleo del archivo se encuentra la región de datos, que está compuesta por varios segmentos como se define en la región de comandos de carga. Una variedad de secciones de datos puede estar contenida dentro de cada segmento, con cada sección conteniendo código o datos específicos de un tipo.
Los datos son básicamente la parte que contiene toda la información que es cargada por los comandos de carga LC_SEGMENTS_64
Esto incluye:
Tabla de funciones: Que contiene información sobre las funciones del programa.
Tabla de símbolos: Que contiene información sobre la función externa utilizada por el binario
También podría contener funciones internas, nombres de variables y más.
Para verificarlo, podrías usar la herramienta Mach-O View:
O desde la cli:
size-m/bin/ls
Secciones Comunes de Objetive-C
En el segmento __TEXT (r-x):
__objc_classname: Nombres de clases (cadenas)
__objc_methname: Nombres de métodos (cadenas)
__objc_methtype: Tipos de métodos (cadenas)
En el segmento __DATA (rw-):
__objc_classlist: Punteros a todas las clases de Objetive-C
__objc_nlclslist: Punteros a clases de Objetive-C no perezosas
__objc_catlist: Puntero a Categorías
__objc_nlcatlist: Puntero a Categorías no perezosas