
This is the main repository for metasm, a free assembler / disassembler / compiler written in ruby
作者:Yoann Guillot
基本概述:
Metasm 允许你与可执行文件格式(ExeFormat)交互: PE、ELF、Mach-O、Shellcode 等。 对 ExeFormat 有三种使用方式:
可直接使用的脚本可以在 samples/ 子目录中找到,请查看脚本头部的注释。你也可以试试 --help 参数,说不定会有惊喜。
更多信息请查看 doc/ 子目录。可以使用 misc/txt2html.rb 脚本将文本文件编译为 html。
以下是 Metasm 内部机制的简要概述。
汇编:
编译时,你从一个源代码文本(ruby String,主要由一系列指令/数据/填充指令组成)开始,该文本会被解析。
这个字符串会被交给一个 Preprocessor 实例(它处理 #if、#ifdef、#include、#define、/* */ 等,应该与 gcc -E 100% 兼容),该实例被封装在用于汇编源码的 AsmPreprocessor 中(以处理 asm 宏定义、'equ' 和 asm 的 ';' 注释)。 完成这一操作的接口是 ExeFormat#parse(text[, filename, lineno]) 或 ExeFormat.assemble(后者会调用 .new、#parse 和 #assemble)。
(Asm)Preprocessor 将 token 返回给 ExeFormat,ExeFormat 将它们解析为数据、填充、标签或解析器指令。解析器指令总是以点开头。 它们可以是通用的(.pad、.offset...)或 ExeFormat 特定的(.section、.import、.entrypoint...)。这些指令由 #parse_parser_instruction() 处理。 如果 ExeFormat 无法识别某个单词,它会被交给其 CPU 实例,由该实例负责解析指令(或抛出异常)。 所有这些 token 都存储在 ExeFormat 的 @source 属性的一个或多个数组中(Shellcode 的 @source 是一个 Array;对于 PE/ELF,它是一个 hash [section name] => [Array of parsed data])。 每个立即数都可以是任意的 Expression(见后文)。
然后你可以使用 ExeFormat#assemble 将源码汇编为二进制节区。
一旦节区二进制数据可用,就可以使用 ExeFormat#encode_file(filename[, format]) 将整个二进制可执行文件写入磁盘。
PE 和 ELF 包含一个自动导入(autoimport)功能,可以为已知的特定操作系统函数自动创建与导入相关的数据(例如,对 'strcpy' 的未解析调用会生成数据,使二进制文件在运行时与 libc 库链接)。
samples/{exe,pe,elf}encode.rb 可以接受一个 asm 源文件作为参数,并将其编译为可运行的可执行文件。
CPU 类负责解析和编码单条指令。当前的 Ia32 解析器使用 Intel 语法(例如 mov eax, 42)。 通用解析器将标签识别为行首字符串后跟冒号(例如 'some_label:')。可以使用 GCC 风格的局部标签(例如 '1:',通过 '1b'(向后)或 '1f'(向前)来引用;可以按需多次重新定义)。 数据使用 'db' 风格表示法指定(例如 'dd 42h'、'db "blabla", 0')。 参见 samples/asmsyntax.rb
EncodedData:
在 Metasm 中,所有二进制数据都存储为 EncodedData。 EncodedData 有 3 个主要属性:
EncodedData 还有一个 #virtsize(例如用于 .bss 节区)和一个 #ptr(解码时使用的内部偏移量)。
你可以用一个 Hash 变量名 => 值(value 应为 Expression 或数值)来 fixup EncodedData。执行该操作时,每个 relocation 的 target 都会使用该 binding 进行绑定;如果结果可计算(Expression 中没有使用外部变量名),则使用 relocation 的大小/符号/字节序信息对结果进行编码。如果发生溢出(例如试图在 8 位有符号 relocation 中存储 128),则会抛出 EncodeError 异常。使用 :a32 类型可以静默截断以允许溢出。 如果 relocation 的 target 不是数值,使用 EncodedData#fixup 时 target 保持不变;使用 #fixup! 时,它会被替换为绑定后的 target。
反汇编:
这段代码位于 metasm/decode.rb 源文件中,该文件定义了 Disassembler 类。
反汇编器需要一个已解码的 ExeFormat(以便确定哪个虚拟地址处有什么数据)和一个入口点(entrypoint,虚拟地址或导出名称)。 然后它就可以开始反汇编指令。当遇到标记为 :setip 的 Opcode 时,它会向 CPU 询问跳转目标(一个可能涉及寄存器值的 Expression,例如 jmp eax),并回溯指令,直到找到数值。
解码时,Disassembler 维护一个 #decoded hash,将地址(表达式/整数 #normalize()d 后的结果)关联到 DecodedInstruction。
反汇编会生成一个 InstructionBlock 图。每个块保存一个 DecodedInstruction 列表,以及指向下一个/上一个块(按地址)的指针。
反汇编器还会跟踪指令对数据的访问,并为其存储 Xref(交叉引用)。 回溯参数可以调整;对于 :r/:w 回溯(指令内存 xref),可以使用 #backtrace_maxblocks_data 专门修改要考虑的最大深度。 当回溯一个 Expression 时,走过的每个块都会被标记,以便检测循环;并且如果发现一条通向已有块的新代码路径,就可以利用这条新路径恢复回溯。
反汇编器很少做假设,尤其不会假定函数会返回;只有在 'ret' 指令的回溯能够得出结论时,函数才会被视为返回。这非常强大,但也意味着回溯过程中的任何错误都可能导致完全停止;同时还意味着反汇编器相当慢。
当已知代码结构良好时(即假设所有调用都会返回),可以使用特殊的 #disassemble_fast 方法来规避这个问题。
当发现子函数时,会创建一个特殊的 DecodedFunction,其中保存了该函数效果的摘要(就像增强版的 DecodedInstruction)。这使得回溯器能够“跨过”子函数,从而大幅提高速度。DecodedFunction 可以基于回调,以实现非常动态的行为。 外部函数调用会创建专用的 DecodedFunction,其中保存一些 API 信息(例如栈修复信息、基本参数访问等)。 这些信息可以来自事先解析过的 C 头文件。 如果没有可用的 C 函数原型,则使用一个特殊的 'default' 条目,该条目假定函数具有标准 ABI。
Ia32 实现了一个特殊的 :default 条目,它通过假定最后一个 'call' 指令会返回来处理自动栈修复。这可能会导致意外结果;为了获得最大精度,建议使用一个包含所有外部函数信息的 C 头文件(有关从完整 Visual Studio 安装和目标二进制文件生成此类头文件的脚本,请参阅 samples/factorize-headers-peimports)。
Ia32 还实现了一个特定的 GetProcAddress/dlsym 回调,如果参数可以被回溯,它会给出正确的返回值。
实现完整反汇编器的脚本是 samples/disassemble{-gui}.rb。 有关 GUI 按键绑定的说明,请参阅注释。
ExeFormat 操作:
你可以编码/解码 ExeFormat(即解码节区、导入、头等)。
构造函数:ExeFormat.decode_file(str)、ExeFormat.decode_file_header(str) 方法:ExeFormat#encode_file(filename)、ExeFormat#encode_string
PE 和 ELF 文件有对应的 LoadedPE/LoadedELF,它们能够处理这些格式的内存映射版本(例如用于调试正在运行的进程)。
VirtualString:
VirtualString 是一种类似 String 的对象:你可以读取并可能重写它的切片。它可以用作 EncodedData#data,因此可以实现大多数 Metasm 算法的虚拟化。 你不能改变 VirtualString 的长度。 获取 VirtualString 的切片会返回一个 String(对于较小的尺寸)或另一个 VirtualString(前者的一个“窗口”)。你可以使用 #dup(offset, length) 方法强制获取一个小的 VirtualString。 任何未实现的方法被调用时,都会转发给一个冻结的 String,该 String 是 VirtualString 的完整副本(应尽可能避免这种情况,底层字符串可能非常大,访问速度也很慢)。
目前实现了 3 种 VirtualString:
Win/Lin 版本非常强大,可以轻松进行实时进程反汇编/修补等操作(使用 LoadedPE/LoadedELF 作为 ExeFormat)。
调试:
Metasm 包含几个用于处理调试的接口。 WinOS 和 LinOS 类提供对底层 OS 进程的访问(例如 OS.current.find_process('foobar') 会检索其文件名中包含 foobar 的运行中进程;然后可以使用 process.mem 访问其内存。)
Windows 和 Linux 的低层调试 API 有一个基本的 ruby 接口(PTrace 和 WinAPI);这些接口被统一的高层 Debugger 类所使用。 通过 GDB server 通信协议(wire protocol)支持远程调试。
可以使用以下 ruby 代码行创建高层调试器: Metasm::OS.current.create_debugger('foo')
同一时间只能存在一种宿主调试器类;要调试多个进程,请使用现有类附加到其他进程。这是由于 Windows 和 Linux 上 OS 调试 API 的工作方式决定的。
低层后端定义在 os/ 子目录中,前端定义在 debug.rb 中。
samples/lindebug.rb 中提供了一个 Linux 控制台调试界面;它使用(简化的)类似 SoftICE 的外观和感觉。 它可以与 gdb-server socket 通信;使用 [udp:]host:port 作为目标。
反汇编器 GUI 示例在将 'live:' 作为目标时,允许与实时进程交互。
C 解析器:
Metasm 包含一个手写的 C 解析器。 它处理我所知道的所有结构,除了十六进制浮点数:
解析 C 文件应该使用现有的 ExeFormat,通过 parse_c_file 方法进行。这可以确保格式特定的宏/ABI 被正确定义(例如 'long' 类型的大小、向函数传递参数的 ABI 等)。
当你使用 C::Parser.parse(text) 解析 C String 时,你会收到一个 Parser 对象。它有一个 #toplevel 字段,是一个 C::Block,其中包含 #structs、#symbols 和 #statements。顶层函数位于 #symbol hash 中,其键是符号名,关联到保存函数信息的 C::Variable 对象。函数的参数/属性可以通过 func.type 访问,代码位于 func.initializer 中,它本身是一个 C::Block。 在它之下,你会找到树状的 C::Statements 结构(If、While、Asm、CExpressions...)。
C::Parser 可以被 #precompiled,转换为更易于编译的简化版本:会移除 typedef,控制序列会转换为 'if (XX) goto YY;' 等。
要编译 C 程序,请使用 PE/ELF.compile_c,它会创建一个定义了特定于可执行文件宏(例如 PE 或 ELF)的 C::Parser。
特定厂商的头文件可能需要使用 #pragma prepare_visualstudio(用于解析 Microsoft Visual Studio 头文件)或 prepare_gcc(用于 gcc);后者可能会被自动检测(也可能不会)。 经过测试的厂商头文件包括 VS2003(含 DDK)和 gcc4;效果视情况而定。
目前,CPU#compilation 在编译 C 代码时会生成 asm 源码(文本),然后可以将其解析并汇编为二进制代码。
参见 ExeFormat#compile_c 和 samples/exeencode.rb