macOS Universal binaries & Mach-O Format
Last updated
Last updated
Aprenda e pratique Hacking AWS: Treinamento HackTricks AWS Red Team Expert (ARTE) Aprenda e pratique Hacking GCP: Treinamento HackTricks GCP Red Team Expert (GRTE)
Os binários do Mac OS geralmente são compilados como binários universais. Um binário universal pode suportar várias arquiteturas no mesmo arquivo.
Esses binários seguem a estrutura Mach-O que é basicamente composta por:
Cabeçalho
Comandos de Carregamento
Dados
Procure pelo arquivo com: mdfind fat.h | grep -i mach-o | grep -E "fat.h$"
O cabeçalho tem os bytes de magic seguidos pelo número de arquiteturas que o arquivo contém (nfat_arch
) e cada arquitetura terá uma estrutura fat_arch
.
Verifique com:
ou usando a ferramenta Mach-O View:
Como você pode estar pensando, geralmente um binário universal compilado para 2 arquiteturas dobra o tamanho de um compilado para apenas 1 arquitetura.
O cabeçalho contém informações básicas sobre o arquivo, como bytes mágicos para identificá-lo como um arquivo Mach-O e informações sobre a arquitetura de destino. Você pode encontrá-lo em: mdfind loader.h | grep -i mach-o | grep -E "loader.h$"
Existem diferentes tipos de arquivos, você pode encontrá-los definidos no código-fonte, por exemplo, aqui. Os mais importantes são:
MH_OBJECT
: Arquivo de objeto relocável (produtos intermediários da compilação, ainda não executáveis).
MH_EXECUTE
: Arquivos executáveis.
MH_FVMLIB
: Arquivo de biblioteca VM fixa.
MH_CORE
: Despejos de código.
MH_PRELOAD
: Arquivo executável pré-carregado (não mais suportado no XNU).
MH_DYLIB
: Bibliotecas Dinâmicas.
MH_DYLINKER
: Linker Dinâmico.
MH_BUNDLE
: Arquivos de "plugin". Gerados usando -bundle no gcc e carregados explicitamente por NSBundle
ou dlopen
.
MH_DYSM
: Arquivo .dSym
companheiro (arquivo com símbolos para depuração).
MH_KEXT_BUNDLE
: Extensões de Kernel.
Ou usando Mach-O View:
O código fonte também define várias flags úteis para carregar bibliotecas:
MH_NOUNDEFS
: Sem referências indefinidas (totalmente vinculado)
MH_DYLDLINK
: Vinculação Dyld
MH_PREBOUND
: Referências dinâmicas predefinidas.
MH_SPLIT_SEGS
: Arquivo divide segmentos r/o e r/w.
MH_WEAK_DEFINES
: Binário possui símbolos definidos fracos
MH_BINDS_TO_WEAK
: Binário usa símbolos fracos
MH_ALLOW_STACK_EXECUTION
: Torna a pilha executável
MH_NO_REEXPORTED_DYLIBS
: Biblioteca sem comandos LC_REEXPORT
MH_PIE
: Executável Independente de Posição
MH_HAS_TLV_DESCRIPTORS
: Há uma seção com variáveis locais de thread
MH_NO_HEAP_EXECUTION
: Sem execução para páginas de heap/dados
MH_HAS_OBJC
: Binário possui seções oBject-C
MH_SIM_SUPPORT
: Suporte ao simulador
MH_DYLIB_IN_CACHE
: Usado em dylibs/frameworks na cache de biblioteca compartilhada.
A disposição do arquivo na memória é especificada aqui, detalhando a localização da tabela de símbolos, o contexto da thread principal no início da execução e as bibliotecas compartilhadas necessárias. Instruções são fornecidas ao carregador dinâmico (dyld) sobre o processo de carregamento do binário na memória.
O uso da estrutura load_command, definida no mencionado loader.h
:
Existem cerca de 50 tipos diferentes de comandos de carga que o sistema trata de forma diferente. Os mais comuns são: LC_SEGMENT_64
, LC_LOAD_DYLINKER
, LC_MAIN
, LC_LOAD_DYLIB
e LC_CODE_SIGNATURE
.
Basicamente, este tipo de Comando de Carga define como carregar o __TEXT (código executável) e __DATA (dados para o processo) segmentos de acordo com os deslocamentos indicados na seção de Dados quando o binário é executado.
Esses comandos definem segmentos que são mapeados no espaço de memória virtual de um processo quando ele é executado.
Existem diferentes tipos de segmentos, como o segmento __TEXT, que contém o código executável de um programa, e o segmento __DATA, que contém dados usados pelo processo. Esses segmentos estão localizados na seção de dados do arquivo Mach-O.
Cada segmento pode ser dividido em várias seções. A estrutura do comando de carga contém informações sobre essas seções dentro do respectivo segmento.
No cabeçalho, primeiro você encontra o cabeçalho do segmento:
Exemplo de cabeçalho de segmento:
Este cabeçalho define o número de seções cujos cabeçalhos aparecem após ele:
Exemplo de cabeçalho de seção:
Se você adicionar o deslocamento da seção (0x37DC) + o deslocamento onde o arquivo começa, neste caso 0x18000
--> 0x37DC + 0x18000 = 0x1B7DC
Também é possível obter informações de cabeçalho a partir da linha de comando com:
Segmentos comuns carregados por este comando:
__PAGEZERO
: Instrui o kernel a mapear o endereço zero para que ele não possa ser lido, escrito ou executado. As variáveis maxprot e minprot na estrutura são definidas como zero para indicar que não há direitos de leitura-escrita-execução nesta página.
Essa alocação é importante para mitigar vulnerabilidades de referência de ponteiro nulo. Isso ocorre porque o XNU impõe uma página zero rígida que garante que a primeira página (apenas a primeira) da memória seja inacessível (exceto no i386). Um binário poderia atender a esses requisitos criando um pequeno __PAGEZERO (usando o -pagezero_size
) para cobrir os primeiros 4k e tendo o restante da memória de 32 bits acessível tanto no modo usuário quanto no modo kernel.
__TEXT
: Contém código executável com permissões de leitura e execução (não gravável). Seções comuns deste segmento:
__text
: Código binário compilado
__const
: Dados constantes (somente leitura)
__[c/u/os_log]string
: Constantes de string C, Unicode ou os logs
__stubs
e __stubs_helper
: Envolvidos durante o processo de carregamento de biblioteca dinâmica
__unwind_info
: Dados de desenrolamento de pilha.
Note que todo esse conteúdo é assinado, mas também marcado como executável (criando mais opções para exploração de seções que não necessariamente precisam desse privilégio, como seções dedicadas a strings).
__DATA
: Contém dados que são legíveis e graváveis (não executáveis).
__got:
Tabela de Deslocamento Global
__nl_symbol_ptr
: Ponteiro de símbolo não preguiçoso (vinculado no carregamento)
__la_symbol_ptr
: Ponteiro de símbolo preguiçoso (vinculado no uso)
__const
: Deveria ser dados somente leitura (não realmente)
__cfstring
: Strings CoreFoundation
__data
: Variáveis globais (que foram inicializadas)
__bss
: Variáveis estáticas (que não foram inicializadas)
__objc_*
(__objc_classlist, __objc_protolist, etc): Informações usadas pelo tempo de execução Objective-C
__DATA_CONST
: __DATA.__const não é garantido ser constante (permissões de escrita), assim como outros ponteiros e a GOT. Esta seção torna __const
, alguns inicializadores e a tabela GOT (uma vez resolvida) somente leitura usando mprotect
.
__LINKEDIT
: Contém informações para o linker (dyld) como, símbolos, strings e entradas de tabela de realocação. É um contêiner genérico para conteúdos que não estão nem em __TEXT
nem em __DATA
e seu conteúdo é descrito em outros comandos de carregamento.
Informações do dyld: Rebase, opcodes de ligação não preguiçosa/preguiçosa/fraca e informações de exportação
Início de funções: Tabela de endereços de início de funções
Dados no Código: Ilhas de dados em __text
Tabela de Símbolos: Símbolos no binário
Tabela de Símbolos Indiretos: Símbolos de ponteiro/stub
Tabela de Strings
Assinatura de Código
__OBJC
: Contém informações usadas pelo tempo de execução Objective-C. Embora essas informações também possam ser encontradas no segmento __DATA, dentro de várias seções em __objc_*.
__RESTRICT
: Um segmento sem conteúdo com uma única seção chamada __restrict
(também vazia) que garante que ao executar o binário, ele irá ignorar variáveis ambientais DYLD.
Como foi possível ver no código, os segmentos também suportam flags (embora não sejam muito utilizadas):
SG_HIGHVM
: Apenas núcleo (não utilizado)
SG_FVMLIB
: Não utilizado
SG_NORELOC
: Segmento sem realocação
SG_PROTECTED_VERSION_1
: Criptografia. Usado, por exemplo, pelo Finder para criptografar o segmento de texto __TEXT
.
LC_UNIXTHREAD/LC_MAIN
LC_MAIN
contém o ponto de entrada no atributo entryoff. No momento do carregamento, o dyld simplesmente adiciona esse valor à (em memória) base do binário, e então salta para esta instrução para iniciar a execução do código binário.
LC_UNIXTHREAD
contém os valores que os registradores devem ter ao iniciar a thread principal. Isso já foi descontinuado, mas o dyld
ainda o utiliza. É possível ver os valores dos registradores definidos por isso com:
LC_CODE_SIGNATURE
Contém informações sobre a assinatura de código do arquivo Mach-O. Ele contém apenas um deslocamento que aponta para o bloco de assinatura. Geralmente, isso está no final do arquivo. No entanto, você pode encontrar algumas informações sobre esta seção neste post de blog e neste gists.
LC_ENCRYPTION_INFO[_64]
Suporte para criptografia binária. No entanto, é claro, se um atacante conseguir comprometer o processo, ele poderá despejar a memória sem criptografia.
LC_LOAD_DYLINKER
Contém o caminho para o executável do link dinâmico que mapeia bibliotecas compartilhadas no espaço de endereço do processo. O valor é sempre definido como /usr/lib/dyld
. É importante observar que no macOS, o mapeamento de dylib acontece no modo de usuário, não no modo kernel.
LC_IDENT
Obsoleto, mas quando configurado para gerar despejos em caso de pânico, um despejo principal Mach-O é criado e a versão do kernel é definida no comando LC_IDENT
.
LC_UUID
UUID aleatório. Não é útil diretamente, mas o XNU o armazena com o restante das informações do processo. Pode ser usado em relatórios de falhas.
LC_DYLD_ENVIRONMENT
Permite indicar variáveis de ambiente ao dyld antes que o processo seja executado. Isso pode ser muito perigoso, pois pode permitir a execução de código arbitrário dentro do processo, portanto, este comando de carregamento é usado apenas no dyld construído com #define SUPPORT_LC_DYLD_ENVIRONMENT
e restringe ainda mais o processamento apenas a variáveis no formato DYLD_..._PATH
especificando caminhos de carregamento.
LC_LOAD_DYLIB
Este comando de carregamento descreve uma dependência de biblioteca dinâmica que instrui o carregador (dyld) a carregar e vincular a biblioteca. Há um comando de carregamento LC_LOAD_DYLIB
para cada biblioteca que o binário Mach-O requer.
Este comando de carregamento é uma estrutura do tipo dylib_command
(que contém uma struct dylib, descrevendo a biblioteca dinâmica dependente real):
Você também pode obter essas informações a partir da linha de comando com:
Algumas bibliotecas potencialmente relacionadas a malware são:
DiskArbitration: Monitorando unidades USB
AVFoundation: Captura de áudio e vídeo
CoreWLAN: Escaneamento de Wifi.
Um binário Mach-O pode conter um ou mais construtores, que serão executados antes do endereço especificado em LC_MAIN. Os deslocamentos de quaisquer construtores são mantidos na seção __mod_init_func do segmento __DATA_CONST.
No cerne do arquivo está a região de dados, composta por vários segmentos conforme definido na região de comandos de carga. Uma variedade de seções de dados pode ser alojada dentro de cada segmento, com cada seção mantendo código ou dados específicos para um tipo.
Os dados são basicamente a parte que contém todas as informações carregadas pelos comandos de carga LC_SEGMENTS_64
Isso inclui:
Tabela de funções: Que contém informações sobre as funções do programa.
Tabela de símbolos: Que contém informações sobre a função externa usada pelo binário
Também pode conter funções internas, nomes de variáveis e mais.
Para verificar, você pode usar a ferramenta Mach-O View:
Ou a partir da linha de comando:
No segmento __TEXT
(r-x):
__objc_classname
: Nomes das classes (strings)
__objc_methname
: Nomes dos métodos (strings)
__objc_methtype
: Tipos dos métodos (strings)
No segmento __DATA
(rw-):
__objc_classlist
: Ponteiros para todas as classes do Objetive-C
__objc_nlclslist
: Ponteiros para classes do Objetive-C não preguiçosas
__objc_catlist
: Ponteiro para Categorias
__objc_nlcatlist
: Ponteiro para Categorias não preguiçosas
__objc_protolist
: Lista de protocolos
__objc_const
: Dados constantes
__objc_imageinfo
, __objc_selrefs
, objc__protorefs
...
_swift_typeref
, _swift3_capture
, _swift3_assocty
, _swift3_types, _swift3_proto
, _swift3_fieldmd
, _swift3_builtin
, _swift3_reflstr