# 六、调试与逆向工程 调试器是用于逆向工程的主要工具。使用调试器,我们可以在运行时执行分析以理解程序。我们可以识别呼叫链并跟踪间接呼叫。通过调试器,我们可以分析和观察程序运行时,以指导我们的逆向工程。在本章中,我们将学习如何在脚本中使用调试器。 本章涵盖的主题如下: * 可移植可执行分析 * 顶石分解 * 顶石锉 * 使用 PyDBG 进行调试 # 逆向工程 逆向工程分析主要有三种: * **静态分析**:对二进制文件内容的分析。这有助于确定可执行部分的结构并打印出可读部分,以获得关于可执行部分用途的更多细节。 * **动态分析**:该类型将执行二进制文件,无论是否附加调试器,以发现其用途以及可执行文件的工作方式。 * **混合分析**:这是静态和动态分析的混合。在静态分析之间重复,然后进行动态调试,可以更好地直观地了解程序。 # 便携式可执行分析 任何 UNIX 或 Windows 二进制可执行文件都有一个头来描述其结构。这包括其代码的基址、数据段和可从可执行文件导出的函数列表。当操作系统执行可执行文件时,首先操作系统读取其头信息,然后从二进制文件加载二进制数据,以填充相应进程地址的代码和数据部分的内容。 **可移植可执行文件**(**PE**文件)是 Windows 操作系统可以执行或运行的文件类型。我们在 Windows 系统上运行的文件是 Windows PE 文件;它们可以有 EXE、DLL(动态链接库)和 SYS(设备驱动程序)扩展。此外,它们还包含 PE 文件格式。 Windows 上的二进制可执行文件具有以下结构: * DOS 标头(64 字节) * PE 头部 * 章节(代码和数据) 现在,我们将详细研究每一个问题。 ## DOS 头 DOS 头以幻数`4D 5A 50 00`(前两个字节为字母`MZ`)开头,最后四个字节(`e_lfanew`)表示 PE 头在二进制可执行文件中的位置。所有其他字段都不相关。 ## PE 割台 PE 标题包含更多有趣的信息。以下是 PE 标头的结构: ![PE header](images/image_06_001-2.jpg) PE 总管由三部分组成: * 4 字节幻码 * 20 字节文件头,数据类型为**图像文件头** * 224 字节可选头,数据类型为**图像\可选\头 32** 另外,可选标题有两部分。前 96 个字节包含主要操作系统和入口点等信息。第二部分由 16 个条目组成,每个条目中有 8 个字节,以形成一个 128 字节的数据目录。 ### 注 有关 PE 文件的更多信息,请访问:[http://www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx](http://www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx) 文件头中使用的结构:[http://msdn2.microsoft.com/en-gb/library/ms680198.aspx](http://msdn2.microsoft.com/en-gb/library/ms680198.aspx) 。 我们可以使用`pefile`模块(一个用于处理 PE 文件的多平台完整 Python 模块)在 Python 中获取这些文件头的所有详细信息。 ### 加载 PE 文件 加载文件非常简单,只需在模块中创建 PE 类的实例,并将可执行文件的路径作为参数。 首先,导入模块`pefile`: ``` Import pefile ``` 使用可执行文件启动实例: ``` pe = pefile.PE('path/to/file') ``` ## 检查集管 在交互式终端中,我们可以对 PE 文件头进行基本检查。 照常导入`pefile`并加载可执行文件: ``` >>>import pefile >>>pe = pefile.PE('md5sum.exe') >>> dir(pe) ``` 这将打印对象。为了更好地理解,我们可以使用`pprint`模块以可读的格式打印此对象: ``` >>> pprint.pprint(dir(pe)) ``` 这将以可读格式列出所有内容,如下所示: ![Inspecting headers](images/image_06_002-2.jpg) 我们还可以打印特定标题的内容,如下所示: ``` >>> pprint.pprint(dir(pe.OPTIONAL_HEADER)) ``` 可以使用 hex()获取每个标头的十六进制值: ``` >>>hex( pe.OPTIONAL_HEADER.ImageBase) ``` ## 检查路段 要检查可执行文件中的节,我们必须迭代`pe.sections`: ``` >>>for section in pe.sections: print (section.Name,       hex(section.VirtualAddress),       hex(section.Misc_VirtualSize),       section.SizeOfRawData) ``` ## 聚乙烯封隔器 **打包机**是用于压缩 PE 文件的工具。这将减小文件的大小,并为静态反向工程的文件添加另一层模糊处理。尽管创建打包程序是为了减小可执行文件的总体文件大小,但后来,许多恶意软件作者利用了模糊处理的好处。打包器将压缩后的数据包装在工作 PE 文件结构中,并将 PE 文件数据解压缩到内存中,并在执行时运行。 如果可执行文件被打包,我们可以使用签名数据库来检测使用的打包器。可以通过搜索 Internet 找到签名数据库文件。 为此,我们需要另一个模块`peutils`,它与`pefile`模块一起提供。 您可以从本地文件或 URL 加载签名数据库: ``` Import peutils signatures = peutils.SignatureDatabase('/path/to/signature.txt') ``` 您还可以使用以下选项: ``` signatures = peutils.SignatureDatabase('handlers.sans.org/jclausing/userdb.txt') ``` 加载签名数据库后,我们可以使用此数据库运行 PE 实例,以识别所用封隔器的签名: ``` matches = signatures.match(pe, ep_only = True) print matches ``` 这将输出可能使用的封隔器。 此外,如果我们检查打包的可执行文件中的节名称,它们将有轻微的差异。例如,一个用 UPX 打包的可执行文件,其节名将是`UPX0`、`UPX1`等等。 # 列出所有导入和导出的符号 进口可列示如下: ``` for entry in pe.DIRECTORY_ENTRY_IMPORT:   print entry.dll   for imp in entry.imports:     print '\t', hex(imp.address), imp.name ``` 同样,我们不能列出出口: ``` for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:   print hex(pe.OPTIONAL_HEADER.ImageBase + exp.address), exp.name, exp.ordinal ``` # 顶石分解 **拆卸**是与组装相反的过程。反汇编程序试图从二进制机器代码创建汇编代码。为此,我们使用了一个名为**Capstone**的 Python 模块。Capstone 是一个自由、多平台和多体系结构的反汇编引擎。 安装后,我们可以在 Python 脚本中使用此模块。 首先,我们需要运行一个简单的测试脚本: ``` from capstone import * cs = Cs(CS_ARCH_X86, CS_MODE_64) for i in cs.disasm('\x85\xC0', 0x1000)    print("0x%x:\t%s\t%s" %(i.address, i.mnemonic, i.op_str)) ``` 脚本的输出如下所示: ``` 0x1000:     test  eax, eax ``` 第一行导入模块,然后使用`Cs`启动`capstone`Python 类,该类包含两个参数:硬件架构和硬件模式。在这里,我们说明如何为 x86 体系结构反汇编 64 位代码。 下一行迭代代码列表,并将代码传递给`capstone`实例`cs`中的`disasm()`。`disasm()`的第二个参数是第一次安装的地址。`disasm()`的输出是`Cslnsn`类型的装置列表。 最后,我们打印出一些输出。`Cslnsn`公开有关已拆卸装置的所有内部信息。 其中一些措施如下: * **Id**:该指令的指令 Id * **地址**:指令的地址 * **助记符**:指令的助记符 * **op_str**:指令的操作数 * **大小**:指令的大小 * **字节**:指令的字节序列 像这样,我们可以用顶点分解二进制文件。 # 带顶石的平锉 接下来,我们使用`capstone`反汇编程序对我们用`pefile`提取的代码进行反汇编,得到汇编代码。 像往常一样,我们从导入所需的模块开始。这里是`capstone`和`pefile`: ``` from capstone import * import pefile pe = pefile.PE('md5sum.exe') entryPoint = pe.OPTIONAL_HEADER.AddressOfEntryPoint data = pe.get_memory_mapped_image()[entryPoint:] cs = Cs(CS_ARCH_X86, CS_MODE_32) for i in cs.disasm(data, 0x1000):     print("0x%x:\t%s\t%s" %(i.address, i.mnemonic, i.op_str)) ``` `IMAGE_OPTIONAL_HEADER`中的`AddressofEntryPoint`值是指向相对于映像基址的入口点函数的指针。对于可执行文件,这正是应用程序代码开始的位置。因此,我们借助于`pefile` 作为`pe.OPTIONAL_HEADER.AddressOfEntryPoint`获得代码的开头,并将其传递给反汇编程序。 # 调试 调试是修复程序中错误的过程。调试器是那些可以运行并监视另一个程序执行的程序。因此,调试器可以控制目标程序的执行,并可以监视或更改目标程序的内存和变量 ## 断点 断点有助于在我们选择的位置停止调试器中目标程序的执行。此时,执行停止,控制权传递给调试器。 断点有两种不同的形式: * **硬件断点**:硬件断点需要 CPU 的硬件支持。它们使用特殊的调试寄存器。这些寄存器包含断点地址、控制信息和断点类型。 * **软件断点**:软件断点将原始指令替换为捕获调试器的指令。这只能在执行时中断。它们之间的主要区别在于可以在内存上设置硬件断点。但是,无法在内存上设置软件断点。 # 使用 PyDBG 我们可以使用 PyDBG 模块在运行时调试可执行文件。我们可以使用 PyDBG 浏览一个基本脚本,以了解它是如何工作的。 首先,我们导入模块: ``` from pydbg import * import sys ``` 然后我们定义一个函数来处理断点。另外,它以`pydbg`实例作为参数。在该函数中,它打印出流程的执行上下文,并指示`pydbg`继续: ``` define breakpoint_handler(dbg):    print dbg.dump_context()    return DBG_CONTINUE ``` 然后初始化`pydbg`实例,设置`handler_breakpoint`函数处理断点异常: ``` dbg = pydbg() dbg.set_callback(EXEPTION_BREAKPOINT, breakpoint_handler) ``` 然后使用`pydbg`附加我们需要调试的流程的流程 ID: ``` dbg.attach(int(sys.argv[1])) ``` 接下来,我们将设置触发断点的地址。这里,我们使用`bp_set()`函数,它接受三个参数。第一个是设置断点的地址,第二个是可选描述,第三个参数表示`pydbg`是否恢复该断点: ``` dbg.bp_set(int(sys.argv[1], 16), "", 1) ``` 最后,在事件循环中启动`pydbg`: ``` dbg.debug_event_loop() ``` 在本例中,我们将断点作为参数传递给此脚本。因此,我们可以按如下方式运行此脚本: ``` $ python debug.py 1234 0x00001fa6 ``` ### 注 `pydbg`包含许多其他有用的功能,可在以下文档中找到:[http://pedramamini.com/PaiMei/docs/PyDbg/public/pydbg.pydbg.pydbg-class.html](http://pedramamini.com/PaiMei/docs/PyDbg/public/pydbg.pydbg.pydbg-class.html) 。 # 总结 我们已经讨论了一些基本工具,这些工具可用于以编程方式使用 Python 对二进制文件进行反向工程和调试。现在,您将能够编写自定义脚本来调试和反向工程可执行文件,这将有助于恶意软件分析。在下一章中,我们将用 Python 讨论一些加密、哈希和转换函数。