Ir para o conteúdo
AZ Tools

Inspetor de binários Mach-O

Lê um binário Mach-O como otool e lipo fazem, direto dos bytes e inteiramente no seu navegador. Um arquivo de macOS ou iOS é fino — uma única arquitetura — ou universal: uma tabela big-endian de arquiteturas em que cada entrada aponta para uma imagem completa. Os dois casos são tratados, e num arquivo universal você vê primeiro a tabela de arquiteturas com o deslocamento, o tamanho e o alinhamento de cada slice, e depois escolhe um slice para ler o resto. Dentro de um slice, o cabeçalho dá o tipo e o subtipo de CPU (arm64, arm64e, x86_64, i386, ppc), o tipo de arquivo — executável, biblioteca dinâmica, bundle, objeto relocável, dSYM — e os flags que interessam: MH_PIE, MH_TWOLEVEL, MH_DYLDLINK, MH_ALLOW_STACK_EXECUTION e MH_NO_HEAP_EXECUTION. Os load commands trazem o resto: cada segmento com seu endereço virtual, tamanho em memória, deslocamento e tamanho no arquivo, as seções dentro dele (__TEXT,__text ou __DATA,__const), cada LC_LOAD_DYLIB e LC_LOAD_WEAK_DYLIB com as versões atual e compatível, as entradas LC_RPATH que o carregador coloca no lugar de @rpath, o install name que uma biblioteca publica em LC_ID_DYLIB, o UUID que casa um binário com o seu dSYM, e a plataforma, o sistema mínimo e o SDK de LC_BUILD_VERSION ou do antigo LC_VERSION_MIN_MACOSX. Os tamanhos dos segmentos são a resposta para “por que este binário tem 40 MB” — mas leia com atenção, porque o tamanho em memória pode ser muito maior que o tamanho no arquivo: seções preenchidas com zeros e os quatro gigabytes de __PAGEZERO não ocupam disco nenhum. Duas coisas que ele não faz. Não desmonta o código. E não verifica a assinatura: um LC_CODE_SIGNATURE significa que o arquivo carrega um bloco de assinatura, enquanto saber se ela é válida, de quem é o certificado e se o binário foi notarizado é coisa que só codesign e spctl num Mac respondem. A ordem dos bytes e o tamanho da palavra saem do magic em vez de serem presumidos, então um binário PowerPC de 32 bits big-endian de vinte anos atrás abre tão bem quanto um arm64e, e um arquivo cuja tabela de load commands promete mais bytes do que ele tem é recusado com um erro próprio em vez de ser lido pela metade.

Como usar

  1. Solte o binário na caixa: uma ferramenta de linha de comando sem extensão, um .dylib, um .bundle, um .o, o DWARF de um dSYM ou o executável de dentro de um .app.
  2. Se o arquivo for universal, veja primeiro a tabela de arquiteturas — é a resposta para “tem arm64?” — e depois clique numa arquitetura para ver cabeçalho, segmentos e bibliotecas.
  3. Confira as proteções: PIE deveria ser Sim em qualquer binário moderno, e uma pilha executável merece explicação antes de você distribuir o arquivo.
  4. Olhe Bibliotecas vinculadas para o que ele vai precisar ao iniciar, Caminhos de busca para os diretórios que @rpath expande, e Install name para o caminho que uma biblioteca reivindica para si.
  5. Abra a lista de seções quando estiver caçando tamanho: __TEXT é código, __DATA são dados graváveis, __LINKEDIT guarda as tabelas de símbolos e a assinatura, e as seções __DWARF de um dSYM costumam ser o maior item em disco.

Perguntas frequentes

Como sei se um aplicativo traz um slice arm64 (Apple silicon)?
Abra o binário de dentro do .app — Contents/MacOS/<nome> — e olhe a tabela de arquiteturas. Um arquivo universal começa com um cabeçalho fat big-endian que lista cada arquitetura, seu deslocamento dentro do arquivo e o alinhamento em que o slice foi colocado; o que não estiver ali não está no arquivo, então um app só com x86_64 roda sob Rosetta e não nativamente. Cada slice é uma imagem Mach-O completa, com cabeçalho e load commands próprios, e é por isso que um binário universal pesa aproximadamente a soma das partes e o lipo consegue afiná-lo copiando um único slice.
Ter assinatura de código significa que o binário é confiável?
Não, e esta ferramenta faz questão da diferença. LC_CODE_SIGNATURE diz apenas que existe um bloco de assinatura em algum deslocamento dentro de __LINKEDIT; não diz que os hashes ainda batem com as páginas que cobrem, que o certificado encadeia até a Apple, que ele não foi revogado ou que o binário foi notarizado. Verificar isso exige a cadeia de certificados e, para a notarização, os servidores da Apple: ou seja, codesign --verify --deep --strict e spctl --assess num Mac. O hardened runtime também não está no cabeçalho: fica nos entitlements e flags da assinatura, então o cabeçalho sozinho não diz se ele está ligado.
Por que o binário é bem maior do que o código que ele contém?
Leia a tabela de segmentos com as duas colunas de tamanho lado a lado. __TEXT tem o código e os dados somente leitura e costuma ser mapeado como leitura-execução; __DATA e __DATA_CONST têm dados graváveis; __LINKEDIT guarda a tabela de símbolos, os function starts, os fixups do dyld e a assinatura, e muitas vezes é a maior parte de um binário distribuído. Tamanho em memória e tamanho no arquivo são números diferentes de propósito: seções de zeros como __bss ocupam espaço de endereçamento mas nenhum byte em disco, e __PAGEZERO reserva quatro gigabytes que nunca serão mapeados do arquivo. Se for universal, lembre que você carrega todas as arquiteturas ao mesmo tempo.
O que é um install name e por que vejo @rpath em tudo?
Uma biblioteca dinâmica registra em LC_ID_DYLIB o caminho onde espera ser encontrada, e todo programa que liga contra ela copia essa string para o próprio LC_LOAD_DYLIB. Se for um caminho absoluto, a biblioteca precisa estar exatamente ali em cada máquina. Frameworks modernos usam @rpath/Algo.framework/Algo e deixam a resolução para o programa: as entradas LC_RPATH listadas aqui são os diretórios que o dyld tenta, na ordem, no lugar de @rpath, e costumam ser escritas em relação ao binário com @executable_path ou @loader_path, para que o bundle possa ser movido. Um rpath faltando é a causa clássica do “image not found” na inicialização.
O que significa a marca Criptografado num binário de iOS?
Binários de iOS distribuídos pela App Store trazem um LC_ENCRYPTION_INFO (ou a forma de 64 bits) com cryptid igual a 1, marcando um intervalo de bytes — normalmente quase todo o __TEXT — criptografado com uma chave aplicada pela loja. O kernel descriptografa esse intervalo ao carregar, então em disco aquela região é ilegível: strings não acha nada útil e um desmontador vê ruído. Um binário que você mesmo compilou, ou tirado de um IPA antes de a loja reassinar, tem cryptid 0 e está em claro. O comando existe com valor zero em muitos arquivos, e por isso aqui se relata o cryptid, não apenas a presença do comando.
O binário é enviado para algum lugar?
Não. O arquivo é lido com a File API do navegador e analisado por JavaScript dentro da página. Nada é enviado a um servidor e não existe componente de servidor para onde enviar, o que aqui importa mais do que na maioria das ferramentas: uma build não lançada ou o app de um cliente não é algo que você queira entregar a um site. Você pode desconectar da rede depois que a página carregar e tudo continua funcionando.

Ferramentas relacionadas