
Este é o repositório principal do metasm, um montador / desmontador / compilador gratuito escrito em ruby
Autor: Yoann Guillot
Visão geral básica:
O Metasm permite que você interaja com formatos de executáveis (ExeFormat): PE, ELF, Mach-O, Shellcode, etc. Existem três abordagens para um ExeFormat:
Scripts prontos para uso podem ser encontrados no subdiretório samples/; verifique os comentários nos cabeçalhos dos scripts. Você também pode tentar o argumento --help se estiver se sentindo sortudo.
Para mais informações, consulte o subdiretório doc/. Os arquivos de texto podem ser compilados para html usando o script misc/txt2html.rb.
Aqui está uma breve visão geral do funcionamento interno do Metasm.
Assembly:
Ao compilar, você começa a partir de um texto-fonte (String ruby, consistindo principalmente em uma sequência de instruções/dados/diretivas de preenchimento), que é analisado.
A string é entregue a uma instância de Preprocessor (que lida com #if, #ifdef, #include, #define, /* */ etc, e deve ser 100% compatível com gcc -E), que é encapsulada em um AsmPreprocessor para fontes de assembly (para lidar com definições de macros de asm, 'equ' e comentários ';' de asm). A interface para fazer isso é ExeFormat#parse(text[, filename, lineno]) ou ExeFormat.assemble (que chama .new, #parse e #assemble).
O (Asm)Preprocessor retorna tokens para o ExeFormat, que os analisa como Data, Padding, Labels ou diretivas de parser. Diretivas de parser sempre começam com um ponto. Elas podem ser genéricas (.pad, .offset...) ou específicas do ExeFormat (.section, .import, .entrypoint...). Elas são tratadas por #parse_parser_instruction(). Se o ExeFormat não reconhecer uma palavra, ela é entregue à sua instância de CPU, que é responsável por analisar Instructions (ou lançar uma exceção). Todos esses tokens são armazenados em um ou mais arrays no atributo @source do ExeFormat (o @source do Shellcode é um Array; para PE/ELF é um hash [nome da seção] => [Array de dados analisados]). Todo valor imediato pode ser um Expression arbitrário (veja adiante).
Você pode então montar a fonte em seções binárias usando ExeFormat#assemble.
Uma vez que os binários das seções estejam disponíveis, o executável binário completo pode ser gravado em disco usando ExeFormat#encode_file(filename[, format]).
PE e ELF incluem um recurso de autoimportação que permite a criação automática de dados relacionados a importações para funções conhecidas específicas do SO (por exemplo, chamadas não resolvidas para 'strcpy' gerarão dados para que o binário seja vinculado à biblioteca libc em tempo de execução).
Os arquivos samples/{exe,pe,elf}encode.rb podem receber um arquivo de código-fonte asm como argumento e compilá-lo em um executável funcional.
As classes de CPU são responsáveis por analisar e codificar instruções individuais. O parser Ia32 atual usa a sintaxe Intel (por exemplo, mov eax, 42). O parser genérico reconhece labels como uma string no início de uma linha seguida por dois-pontos (por exemplo, 'some_label:'). Labels locais no estilo GCC podem ser usadas (por exemplo, '1:', referidas usando '1b' (para trás) ou '1f' (para frente); podem ser redefinidas quantas vezes forem necessárias). Dados são especificados usando notação estilo 'db' (por exemplo, 'dd 42h', 'db "blabla", 0') Veja samples/asmsyntax.rb
EncodedData:
No Metasm, todos os dados binários são armazenados como um EncodedData. EncodedData tem 3 atributos principais:
EncodedData também tem um #virtsize (para, por exemplo, seções .bss) e um #ptr (offset interno usado ao decodificar coisas).
Você pode aplicar fixup em um EncodedData, com um Hash nome de variável => valor (o valor deve ser um Expression ou um valor numérico). Ao fazer isso, o alvo de cada realocação é vinculado usando o binding, e se o resultado for calculável (nenhum nome de variável externo usado no Expression), o resultado é codificado usando as informações de tamanho/sinal/endianness da realocação. Se houver overflow (tentar armazenar 128 em uma realocação de 8 bits com sinal), uma exceção EncodeError é lançada. Use o tipo :a32 para permitir truncamento de overflow silencioso. Se o alvo da realocação não for numérico, o alvo permanece inalterado se você usar EncodedData#fixup, ou é substituído pelo alvo vinculado com #fixup!.
Desmontagem:
Este código é encontrado no arquivo-fonte metasm/decode.rb, que define a classe Disassembler.
O desmontador precisa de um ExeFormat decodificado (para poder dizer quais dados estão em qual endereço virtual) e de um entrypoint (um endereço virtual ou nome de exportação). Ele pode então começar a desmontar instruções. Quando encontra um Opcode marcado como :setip, ele pergunta à CPU o destino do salto (um Expression que pode envolver valores de registradores, por exemplo, jmp eax) e faz backtrace das instruções até encontrar o valor numérico.
Ao decodificar, o Disassembler mantém um hash #decoded associando endereços (expressões/inteiros #normalize()d) a DecodedInstructions.
A desmontagem gera um grafo de InstructionBlock. Cada bloco contém uma lista de DecodedInstruction e ponteiros para o próximo/bloco anterior (por endereço).
O desmontador também rastreia acessos a dados por instruções e armazena Xrefs para eles. Os parâmetros de backtrace podem ser ajustados, e a profundidade máxima a considerar pode ser alterada especificamente para backtraces :r/:w (xrefs de memória de instrução) usando #backtrace_maxblocks_data. Quando um Expression é rastreado via backtrace, cada bloco percorrido é marcado para que loops sejam detectados, e para que, se um novo caminho de código for encontrado para um bloco existente, os backtraces possam ser retomados usando esse novo caminho.
O desmontador faz muito poucas suposições e, em particular, não supõe que as funções retornarão; elas só retornarão se o backtrace das instruções 'ret' for conclusivo. Isso é bastante poderoso, mas também implica que qualquer erro no processo de backtracking pode levar a uma parada total; e também significa que o desmontador é bastante lento.
O método especial #disassemble_fast pode ser usado para contornar isso quando o código é conhecido por ser bem-formado (ou seja, ele assume que todas as chamadas retornam).
Quando uma subfunção é encontrada, uma DecodedFunction especial é criada, que contém um resumo dos efeitos da função (como uma DecodedInstruction turbinada). Isso permite que o backtracker 'pule' subfunções, o que melhora muito a velocidade. As DecodedFunctions podem ser baseadas em callbacks, para permitir um comportamento muito dinâmico. Chamadas de funções externas criam DecodedFunctions dedicadas, que contêm algumas informações de API (por exemplo, informações de stack fixup, acessos básicos a parâmetros...) Essas informações podem ser derivadas de um cabeçalho C analisado anteriormente. Se nenhum protótipo de função C estiver disponível, uma entrada especial 'default' é usada, que assume que a função tem uma ABI padrão.
Ia32 implementa uma entrada :default específica, que lida com a resolução automática de stack fixup, assumindo que a última instrução 'call' retorna. Isso pode levar a resultados inesperados; para máxima precisão, um cabeçalho C contendo informações para todas as funções externas é recomendado (veja samples/factorize-headers-peimports para um script que gera tal cabeçalho a partir de uma instalação completa do Visual Studio e do binário alvo).
Ia32 também implementa um callback específico GetProcAddress/dlsym, que produzirá o valor de retorno correto se os parâmetros puderem ser rastreados via backtrace.
Os scripts que implementam um desmontador completo são samples/disassemble{-gui}.rb Veja os comentários para os atalhos de teclado da GUI.
Manipulação de ExeFormat:
Você pode codificar/decodificar um ExeFormat (ou seja, decodificar seções, importações, cabeçalhos etc)
Construtor: ExeFormat.decode_file(str), ExeFormat.decode_file_header(str) Métodos: ExeFormat#encode_file(filename), ExeFormat#encode_string
Arquivos PE e ELF têm uma contraparte LoadedPE/LoadedELF, que é capaz de trabalhar com versões mapeadas em memória (mmap) desses formatos (por exemplo, para depurar processos em execução)
VirtualString:
Uma VirtualString é um objeto semelhante a String: você pode ler e pode reescrever partes dela. Ela pode ser usada como EncodedData#data e, assim, permite a virtualização da maioria dos algoritmos do Metasm. Você não pode alterar o comprimento de uma VirtualString. Pegar uma parte (slice) de uma VirtualString retornará ou uma String (para tamanhos pequenos) ou outra VirtualString (uma 'janela' para a outra). Você pode forçar a obtenção de uma VirtualString pequena usando o método #dup(offset, length). Qualquer método não implementado chamado nela é encaminhado para uma String congelada (frozen), que é uma cópia completa da VirtualString (isso deve ser evitado se possível, pois a string subjacente pode ser muito grande e lenta de acessar).
Atualmente, existem 3 VirtualStrings implementadas:
As versões Win/Lin são bastante poderosas e permitem coisas como desmontagem/patches de processos em execução facilmente (usando LoadedPE/LoadedELF como ExeFormat)
Depuração:
O Metasm inclui algumas interfaces para lidar com depuração. As classes WinOS e LinOS oferecem acesso aos processos do SO subjacente (por exemplo, OS.current.find_process('foobar') recuperará um processo em execução com foobar em seu nome de arquivo; então, process.mem pode ser usado para acessar sua memória.)
As APIs de depuração de baixo nível do Windows e do Linux têm uma interface ruby básica (PTrace e WinAPI), que são usadas pela classe Debugger unificada de alto nível. A depuração remota é suportada por meio do protocolo wire do GDB server.
Depuradores de alto nível podem ser criados com a seguinte linha ruby: Metasm::OS.current.create_debugger('foo')
Apenas um tipo de classe de depurador de host pode existir por vez; para depurar vários processos, anexe-se a outros processos usando a classe existente. Isso se deve à forma como a API de depuração do SO funciona no Windows e no Linux.
Os backends de baixo nível são definidos no subdiretório os/; o front-end é definido em debug.rb.
Uma interface de depuração por console Linux está disponível em samples/lindebug.rb; ela usa uma aparência e comportamento (simplificados) no estilo SoftICE. Ela pode se comunicar com um socket gdb-server; use um alvo [udp:]host:port.
O exemplo disassembler-gui permite interação ao vivo com processos ao usar como alvo 'live:'.
C Parser:
O Metasm inclui um C Parser escrito manualmente. Ele lida com todas as construções que eu conheço, exceto floats hexadecimais:
O parsing de arquivos C deve ser feito usando um ExeFormat existente, com o método parse_c_file. Isso garante que macros/ABI específicos do formato sejam definidos corretamente (ex: tamanho do tipo 'long', ABI para passar parâmetros a funções, etc)
Quando você faz o parse de uma String C usando C::Parser.parse(text), você recebe um objeto Parser. Ele contém um campo #toplevel, que é um C::Block, que contém #structs, #symbols e #statements. As funções de nível superior são encontradas no hash #symbol, cujas chaves são os nomes dos símbolos, associadas a um objeto C::Variable que contém as funções. Os parâmetros/atributos da função são acessíveis por meio de func.type, e o código está em func.initializer, que é ele próprio um C::Block. Abaixo dele, você encontrará uma estrutura semelhante a uma árvore de C::Statements (If, While, Asm, CExpressions...)
Um C::Parser pode ser #precompiled para transformá-lo em uma versão simplificada que é mais fácil de compilar: os typedefs são removidos, as sequências de controle são transformadas em 'if (XX) goto YY;' etc.
Para compilar um programa C, use PE/ELF.compile_c, que criará um C::Parser com macros específicas do exe definidas (por exemplo, PE ou ELF).
Cabeçalhos específicos de fornecedores podem precisar usar #pragma prepare_visualstudio (para analisar os cabeçalhos do Microsoft Visual Studio) ou prepare_gcc (para gcc); este último pode ser auto-detectado (ou não). Os cabeçalhos de fornecedores testados são VS2003 (incl. DDK) e gcc4; os resultados podem variar.
Atualmente, a CPU#compilation de um código C gera uma fonte asm (texto), que pode então ser analisada e montada em código binário.
Veja ExeFormat#compile_c e samples/exeencode.rb