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
- 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.
- 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.
- 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.
- 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.
- 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
Inspetor de binários ELF
Abra um executável Linux, um .so ou um .o no navegador: arquitetura, bibliotecas necessárias, Build ID e se tem PIE, NX e RELRO.
Inspetor PE: visualizador de EXE e DLL
Abra um .exe, .dll, .sys ou .efi do Windows no navegador: arquitetura, subsistema, importações, exportações, seções e se tem ASLR, DEP e CFG.
Inspetor de arquivos .class do Java
Analise um .class compilado no navegador: versão do arquivo e do Java, sinalizadores de acesso, pool de constantes, campos, métodos e dependências.
Inspetor de arquivos de fonte
Abra um .ttf, .otf ou .woff e leia o nome de família real, a contagem de glifos e caracteres, a versão, o texto da licença e as permissões de incorporação.
Inspetor de arquivos gzip (.gz)
Leia o cabeçalho de um .gz campo a campo, percorra todos os membros e verifique o trailer recalculando o CRC32 e o tamanho real no navegador.
Inspetor de fluxos xz (.xz)
Leia um contêiner .xz: cabeçalho do fluxo, cada cabeçalho de bloco e sua cadeia de filtros, o índice e o rodapé, com o tamanho real sem descomprimir nada.