
Este es el repositorio principal de metasm, un ensamblador / desensamblador / compilador gratuito escrito en ruby
Autor: Yoann Guillot
Visión general básica:
Metasm te permite interactuar con formatos de ejecutables (ExeFormat): PE, ELF, Mach-O, Shellcode, etc. Hay tres enfoques para un ExeFormat:
Puedes encontrar scripts listos para usar en el subdirectorio samples/; revisa los comentarios en las cabeceras de los scripts. También puedes probar el argumento --help si te sientes con suerte.
Para más información, consulta el subdirectorio doc/. Los archivos de texto se pueden compilar a html usando el script misc/txt2html.rb.
Aquí tienes una breve visión general del funcionamiento interno de Metasm.
Ensamblado:
Al compilar, se parte de un texto fuente (String de Ruby, que consiste principalmente en una secuencia de instrucciones/datos/directivas de relleno), que se analiza.
La cadena se pasa a una instancia de Preprocessor (que gestiona #if, #ifdef, #include, #define, /* */, etc., y debería ser 100% compatible con gcc -E), que está encapsulada en un AsmPreprocessor para fuentes de ensamblador (para manejar definiciones de macros asm, 'equ' y comentarios asm ';'). La interfaz para hacerlo es ExeFormat#parse(text[, filename, lineno]) o ExeFormat.assemble (que llama a .new, #parse y #assemble).
El (Asm)Preprocessor devuelve tokens al ExeFormat, que los analiza como Data, Padding, Labels o directivas del analizador. Las directivas del analizador siempre comienzan con un punto. Pueden ser genéricas (.pad, .offset...) o específicas de ExeFormat (.section, .import, .entrypoint...). Se gestionan mediante #parse_parser_instruction(). Si el ExeFormat no reconoce una palabra, se la pasa a su instancia de CPU, que es responsable de analizar Instructions (o lanzar una excepción). Todos esos tokens se almacenan en uno o más arrays en el atributo @source del ExeFormat (el @source de Shellcode es un Array; para PE/ELF es un hash [nombre de sección] => [Array de datos analizados]). Cada valor inmediato puede ser una Expression arbitraria (ver más adelante).
A continuación puedes ensamblar el fuente en secciones binarias usando ExeFormat#assemble.
Una vez que los binarios de sección están disponibles, el ejecutable binario completo se puede escribir en disco usando ExeFormat#encode_file(filename[, format]).
PE y ELF incluyen una función de importación automática (autoimport) que permite la creación automática de datos relacionados con la importación para funciones conocidas específicas del sistema operativo (p. ej., las llamadas no resueltas a 'strcpy' generarán datos para que el binario se enlace contra la biblioteca libc en tiempo de ejecución).
Los samples/{exe,pe,elf}encode.rb pueden tomar un archivo fuente asm como argumento y compilarlo a un ejecutable funcional.
Las clases CPU son responsables de analizar y codificar instrucciones individuales. El analizador Ia32 actual usa la sintaxis de Intel (p. ej. mov eax, 42). El analizador genérico reconoce las etiquetas como una cadena al comienzo de una línea seguida de dos puntos (p. ej. 'some_label:'). Se pueden usar etiquetas locales de estilo GCC (p. ej. '1:', a las que se hace referencia usando '1b' (hacia atrás) o '1f' (hacia adelante); pueden redefinirse tantas veces como sea necesario). Los datos se especifican mediante notación de estilo 'db' (p. ej. 'dd 42h', 'db "blabla", 0'). Ver samples/asmsyntax.rb
EncodedData:
En Metasm todos los datos binarios se almacenan como un EncodedData.
EncodedData tiene 3 atributos principales:
Un objeto Relocation tiene un endianness (:little/:big), un tipo (:u32 para 32 bits sin signo) y un target (el valor previsto almacenado aquí). El target es una Expression aritmética/lógica arbitraria.
EncodedData también tiene un #virtsize (p. ej., para secciones .bss) y un #ptr (desplazamiento interno usado al decodificar cosas).
Puedes hacer fixup de un EncodedData con un Hash nombre de variable => valor (el valor debe ser una Expression o un valor numérico). Al hacerlo, el target de cada reubicación se vincula usando el binding y, si el resultado es calculable (no se usa ningún nombre de variable externa en la Expression), el resultado se codifica usando la información de tamaño/signo/endianness de la reubicación. Si se desborda (intentar almacenar 128 en una reubicación con signo de 8 bits), se lanza una excepción EncodeError. Usa el tipo :a32 para permitir el truncamiento silencioso por desbordamiento. Si el target de la reubicación no es numérico, el target no cambia si usas EncodedData#fixup, o se reemplaza con el target vinculado usando #fixup!.
Desensamblado:
Este código se encuentra en el archivo fuente metasm/decode.rb, que define la clase Disassembler.
El desensamblador necesita un ExeFormat decodificado (para poder decir qué datos hay en cada dirección virtual) y un punto de entrada (una dirección virtual o nombre de exportación). A continuación puede empezar a desensamblar instrucciones. Cuando encuentra un Opcode marcado como :setip, le pide a la CPU el destino del salto (una Expression que puede involucrar valores de registros, por ejemplo jmp eax), y hace backtrace de las instrucciones hasta encontrar el valor numérico.
Al decodificar, el Disassembler mantiene un hash #decoded que asocia direcciones (expresiones/enteros #normalize()d) con DecodedInstructions.
El desensamblado genera un grafo InstructionBlock. Cada bloque contiene una lista de DecodedInstruction y punteros al bloque siguiente/anterior (por dirección).
El desensamblador también rastrea los accesos a datos de las instrucciones y almacena Xrefs para ellos. Los parámetros de backtrace se pueden ajustar, y la profundidad máxima a considerar puede cambiarse específicamente para los backtraces :r/:w (xrefs de memoria de instrucciones) usando #backtrace_maxblocks_data. Cuando se hace backtrace de una Expression, cada bloque recorrido se marca para que se detecten bucles, y para que si se encuentra una nueva ruta de código hacia un bloque existente, los backtraces puedan reanudarse usando esta nueva ruta.
El desensamblador hace muy pocas suposiciones y, en particular, no supone que las funciones vayan a retornar; solo lo harán si el backtrace de las instrucciones 'ret' es concluyente. Esto es bastante potente, pero también implica que cualquier error en el proceso de backtracking puede provocar una parada total; y también significa que el desensamblador es bastante lento.
El método especial #disassemble_fast se puede usar para solucionar esto cuando se sabe que el código está bien formado (es decir, asume que todas las llamadas retornan).