- 献词
- 致谢
- 前言
- 第一部分 IDA 简介
- 第 1 章 反汇编简介
- 第 2 章 逆向与反汇编工具
- 第 3 章 IDA Pro 背景知识
- 第二部分 IDA 基本用法
- 第 4 章 IDA 入门
- 第 5 章 IDA 数据显示窗口
- 第 6 章 反汇编导航
- 第 7 章 反汇编操作
- 第 8 章 数据类型与数据结构
- 第 9 章 交叉引用与绘图功能
- 第 10 章 IDA 的多种面孔
- 第三部分 IDA 高级应用
- 第 11 章 定制 IDA
- 第 12 章 使用 FLIRT 签名来识别库
- 第 13 章 扩展 IDA 的知识
- 第 14 章 修补二进制文件及其他 IDA 限制
- 第四部分 扩展 IDA 的功能
- 第 15 章 编写 IDA 脚本
- 第 16 章 IDA 软件开发工具包
- 第 17 章 IDA 插件体系结构
- 第 18 章 二进制文件与 IDA 加载器模块
- 第 19 章 IDA 处理器模块
- 第五部分 实际应用
- 第 20 章 编译器变体
- 第 21 章 模糊代码分析
- 第 22 章 漏洞分析
- 第 23 章 实用 IDA 插件
- 第六部分 IDA 调试器
- 第 24 章 IDA 调试器
- 第 25 章 反汇编器/ 调试器集成
- 第 26 章 其他调试功能
- 附录 A 使用 IDA 免费版本 5.0
- 附录 B IDC/SDK 交叉引用
18.4 使用 SDK 编写 IDA 加载器
IDA 通过每个加载器必须声明和导出的全局 loader_t
对象访问加载器模块。 loader_t
结构体类似于所有插件模块使用的 plugin_t
类。在 loader.hpp 中定义的 loader_t
结构体的布局如下面的代码清单所示。
struct loader_t { ulong version; // api version, should be IDP_INTERFACE_VERSION ulong flags; // loader flags //check input file format. if recognized, int (idaapi *accept_file)(linput_t *li, char fileformatname[MAX_FILE_FORMAT_NAME], int n); //load file into the database. void (idaapi *load_file)(linput_t *li, ushort neflags, const char *fileformatname); //create output file from the database, this function may be absent. int (idaapi *save_file)(FILE *fp, const char *fileformatname); //take care of a moved segment (fix up relocations, for example) //this function may be absent. int (idaapi *move_segm)(ea_t from, ea_t to, asize_t size, const char *fileformatname); //initialize user configurable options based on the input file. //Called only when loading is done via File->New, not File->Open //this function may be absent. bool (idaapi *init_loader_options)(linput_t *li); };
和 plugin_t
类一样, loader_t
对象的行为由它的成员指向的函数(由加载器作者创建)定义。每个加载器必须导出一个名为 LDSC
(指加载器说明)的 loader_t
对象。loader.hpp 文件负责导出 LDSC
对象,然后由你声明和初始化。需要注意的是,有几个函数接受一个 linput_t
(指加载器输入类型)类型的输入参数。 linput_t
是一个内部 SDK 类,它为 C 标准 FILE
类型提供不依赖于编译器的包装器。为 linput_t
执行标准输入操作的函数在 diskio.hpp 中声明。
要成功创建加载器,你必须正确初始化 LDSC
对象。下面简要说明这个对象的每个成员的作用。
version
。这个成员的作用和plugin_t
类中的version
成员的作用相同。请参阅第 17 章中对它的描述。flags
。加载器识别的唯一一个标志为LDRF_RELOAD
,该标志在 loader.hpp 中定义。对许多加载器来说,把这个字段赋值为零就够了。accept_file
。这个函数的作用是为新选择的输入文件提供基本的识别功能。这个函数应利用所提供的linput_t
对象,从一个文件中读取足够的信息,以决定加载器是否能够解析该文件。如果该函数能够识别这个文件,加载器应将文件格式名称复制到file- formatname
输出缓冲区中。如果无法识别文件格式,这个函数应返回 0;如果它能够识别文件格式,则返回非零值。用ACCEPT_FIRST
标志对返回值进行OR
处理,可要求 IDA 在文件加载对话框中首先列出这个加载器。如果几个加载器都标有ACCEPT_FIRST
,则首先列出最后查询的加载器。load_file
。这个成员是另一个函数指针。如果用户选择用你的加载器加载新选择的文件,IDA 将调用相关联的函数。这个函数接受一个应被用于读取所选文件的linput_t
对象。neflags
参数包含对在 loader.hpp 中定义的各种NEF_XXX
标志的按位OR
操作。这其中的几个标志反映了文件加载对话框中各种复选框设置的状态。load_file
函数负责执行必需的任务,如解析输入文件内容,加载和映射一些或全部文件内容到新建的数据库中。如果发现一个无法修复的错误条件,load_file
应调用loader_failure
,终止加载过程。save_file
。这个成员选择性地指向一个函数,该函数能够响应 File ▶ Produce File ▶ Create EXE File 命令,生成一个 EXE 文件。严格来说,“EXE ”有些不恰当,因为执行save_file
可以生成你想要的任何类型的文件。由于加载器负责将一个文件映射到数据库中,它应该也能够将这个数据库映射到原来的文件中。实际上,加载器并没有从初始输入文件中加载足够的信息来根据数据库内容生成一个有效的输出文件。例如,IDA 自带的 PE 文件加载器无法由一个数据库文件重新生成一个 EXE 文件。如果你的加载器不能生成输出文件,那么,你应该将save_file
成员设置为 NULL。move_segm
。这个成员是一个指向函数的指针,当用户尝试移动数据库中一个使用这个加载器加载的段时,IDA 将调用该函数。由于加载器可能知道原始二进制文件中包含的重定位信息,因此,在移动段时,这个函数可能会考虑到重定位信息。这个函数是可选的,如果不需要这个函数(例如,如果在这种文件格式中没有重定位或修复地址),则该指针应设置为 NULL。init_loader_options
。这个成员是一个指向函数的指针,该函数用于通过 File ▶ New 命令完成的基于向导的加载过程,设置用户指定的选项。此函数只能在 IDA 的 Windows 本机 GUI 版本(idag )中使用,因为该版本是唯一提供这些向导的 IDA 版本。在调用oad_file
之前,一旦用户选择一个加载器,这个函数即被调用。如果在调用load_file
之前,不需要对加载器进行配置,那么,你完全可以将这个成员指针设置为 NULL。init_loader_options
函数值得我们额外说明。需要记住的是,如果使用 File ▶ Open 命令打开一个文件,这个函数绝不会被调用。在更加复杂的加载器(如 IDA 的 PE 加载器)中,这个函数用于初始化基于 XML 的向导,这个向导帮助用户完成整个加载过程。/cfg 目录保存了几个这类向导的 XML 模板。但是,除了现有的模板外,没有文档资料说明如何创建你自己的向导模板。
在本章的剩余部分,我们将开发两个示例加载器,以分析一些常用的加载器操作。
18.4.1 “傻瓜式”加载器
为了说明 IDA 加载器的基本操作,我们引入一个完全虚拟的“傻瓜式”文件格式,它由下面的 C 结构体定义(所有值采用小端字节顺序):
struct simpleton { uint32_t magic; //simpleton magic number: 0x1DAB00C uint32_t size; //size of the code array uint32_t base; //base virtual address and entry point uint8_t code[size]; //the actual program code };
这个文件的格式非常简单:一个幻数文件标识符和两个描述文件结构的整数,后面是文件中的所有代码。这个文件从 code
块的第一个字节开始执行。
一个小型“傻瓜式”文件的十六进制代码如下所示:
0000000: 0cb0 da01 4900 0000 0040 0000 31c0 5050 ....I....@..1.PP 0000010: 89e7 6a10 5457 50b0 f350 cd91 5859 4151 ..j.TWP..P..XYAQ 0000020: 50cd 9166 817f 0213 8875 f16a 3e6a 025b P..f.....u.j>j.[ 0000030: 5853 6a09 516a 3ecd 914b 79f4 5068 6e2f XSj.Qj>..Ky.Ph// 0000040: 7368 682f 2f62 6989 e350 5389 e150 5153 shh/bin..PS..PQS 0000050: b03b 50cd 91 .;P..
SDK 提供了几个样本加载器,它们位于/ldr 目录中。我们选择在样本加载器目录下的子目录中构建加载器,这里我们使用/ldr/simpleton。加载器采用以下设置:
#include "../idaldr.h" #define SIMPLETON_MAGIC 0x1DAB00C struct simpleton { uint32_t magic; //simpleton magic number: 0x1DAB00C uint32_t size; //size of the code array uint32_t base; //base virtual address and entry point };
SDK 自带的 idaldr.h 头文件(/ldr/idaldr.h)是一个便捷文件,其中包含其他几个头文件,并定义了几个宏,它们常常用在加载器模块中。
下一步是声明所需的 LDSC
对象,它指向各种实现加载器行为的函数:
int idaapi accept_simpleton_file(linput_t *, char[MAX_FILE_FORMAT_NAME], int); void idaapi load_simpleton_file(linput_t *, ushort, const char *); int idaapi save_simpleton_file(FILE *, const char *); loader_t LDSC = { IDP_INTERFACE_VERSION, 0, // loader flags accept_simpleton_file, // test simpleton format. load_simpleton_file, // load file into the database. save_simpleton_file, // simpleton is an easy format to save NULL, // no special handling for moved segments NULL, // no special handling for File->New };
接下来,我们按照调用的顺序描述这个加载器所使用的函数,首先是 accept_simpleton_ loader
函数:
int idaapi accept_simpleton_file(linput_t *li, char fileformatname[MAX_FILE_FORMAT_NAME], int n) { uint32 magic; if (n || lread4bytes(li, &magic, false)) return 0; if (magic != SIMPLETON_MAGIC) return 0; //bad magic number found qsnprintf(fileformatname, MAX_FILE_FORMAT_NAME, "Simpleton Executable"); return 1; //simpleton format recognized }
这个函数的唯一目的是判断被打开的文件是否是一个“傻瓜式”文件。参数 n
是一个计数器,它统计 accept_file
函数在当前的加载过程中被调用的次数。通过使用这个参数,加载器将能够识别多种相关文件格式。IDA 将用递增的 n
值调用 accept_file
函数,直到该函数返回 0 。对于加载器识别的第一种特殊格式,你应填入 fileformatname
数组并返回非零值。这里,我们通过立即返回 0 ,忽略除第一次调用(当时 n
为 0 )以外的其他调用。在 diskio.hpp 中定义的 lread4bytes
函数用于读取 4 字节幻数。如果成功读取到幻数,这个函数将返回 0 。 lread4bytes
函数的一个有用特性在于它能够根据它的第三个布尔参数的值,以大端或小端顺序读取字节(值为 false
则读取小端,值为 true
则读取大端)。这个特性有助于我们减少调用在加载过程中所需的字节交换函数的次数。如果已经确定所需幻数的位置,那么最后, accept_simpleton_file
函数会将文件格式的名称复制到 fileformatname
输出参数中,然后返回 1 ,表示已识别文件格式。
对“傻瓜式”加载器而言,如果用户选择使用 File ▶ New 而非 File ▶ Open 加载一个“傻瓜式”文件,那么不需要任何特殊处理,也就不需要使用 init_loader_options
函数。因此,下一个被调用的函数将是 load_simpleton_file
,如下所示:
void idaapi load_simpleton_file(linput_t *li, ushort neflags, const char *) { simpleton hdr; //read the program header from the input file lread(li, &hdr, sizeof(simpleton)); //load file content into the database file2base(li, sizeof(simpleton), hdr.base, hdr.base + hdr.size, FILEREG_PATCHABLE); //create a segment around the file's code section if (!add_segm(0, hdr.base, hdr.base + hdr.size, NAME_CODE, CLASS_CODE)) { loader_failure(); } //retrieve a handle to the new segment segment_t *s = getseg(hdr.base); //so that we can set 32 bit addressing mode on (x86 has 16 or 32 bit modes) set_segm_addressing(s, 1); //set 32 bit addressing //tell IDA to create the file header comment for us. Do this //only once. This comment contains license, MD5, // and original input file name information. create_filename_cmt(); //Add an entry point so that the processor module knows at least one //address that contains code. This is the root of the recursive descent //disassembly process add_entry(hdr.base, hdr.base, "_start", true); }
加载器的 load_file
函数完成大部分加载工作。我们的“傻瓜式”加载器执行以下任务。
使用 diskio.hpp 中的
lread
函数从文件中读取“傻瓜式”头部,lread
函数非常类似于 POSIX read 函数。使用 loader.hpp 中的
file2base
函数将文件中的代码节加载到数据库中的适当地址空间。使用 segment.hpp 中的
add_segm
函数创建一个新数据库段,其中包含新加载的字节。通过调用 segment.hpp 中的
getseg
和set_segm_addressing
函数,为我们的新代码段指定 32 位寻址。使用 loader.hpp 中的
create_filename_cmt
函数生成一段数据库头部注释。使用 entry.hpp 中的
add_entry
函数添加一个程序入口点,为处理器模块的反汇编过程提供一个起点。
对加载器而言, file2base
是一个“主力”函数,它的原型如下所示:
int ida_export file2base(linput_t *li, long pos, ea_t ea1, ea_t ea2, int patchable);
这个函数从所提供的 linput_t
中读取字节, linput_t
以 pos
指定的文件位置为起始地址。这些字节被加载到数据库中地址 ea1
与 ea2
(不包括 ea2
)之间的空间中。所读取的总字节数由 ea2-ea1
计算得出。 patchable
参数指明 IDA 是否应维护文件偏移量与它们在数据库中的对应位置之间的内部映射。要维护这样一个映射,应将这个参数设置为 FILEREG_PATCHABLE
,以生成 IDA 的.dif 文件,如第 14 章所述。
add_entry
函数是加载过程中另外一个重要的函数。反汇编过程只能从已知包含指令的地址开始。通常,对递归下降反汇编器来说,通过解析一个文件的入口点(如导出函数),即可获得这类地址。 add_entry
函数的原型如下所示:
bool ida_export add_entry(uval_t ord, ea_t ea, const char *name, bool makecode);
参数 ord
供按序号(而不仅是函数名)导出的导出函数使用。如果入口点没有相关的序号,应设置 ord
使用和 ea
参数相同的值。 ea
参数指定入口点的有效地址,而 name
参数则指定与入口点有关的名称。通常,IDA 会对程序的初始执行地址使用符号名称 _start
。布尔型 makecode
参数规定是(真)否(假)将指定的地址作为代码处理。导出的数据项(如加载器模块中的 LSDC
)就属于非代码进入点。
在“傻瓜式”加载器中,我们执行的最后一个函数是 save_simpleton_file
,它用于根据数据库内容创建一个“傻瓜式”文件。执行过程如下所示:
int idaapi save_simpleton_file(FILE *fp, const char *fileformatname) { uint32 magic = SIMPLETON_MAGIC; if (fp == NULL) return 1; //special case, success means we can save files segment_t *s = getnseg(0); //get segment zero, the one and only segment if (s) { uint32 sz = s->endEA - s->startEA; //compute the segment size qfwrite(fp, &magic, sizeof(uint32)); //write the magic value qfwrite(fp, &sz, sizeof(uint32)); //write the segment size qfwrite(fp, &s->startEA, sizeof(uint32)); //write the base address base2file(fp, sizeof(simpleton), s->startEA, s->endEA); //dump the segment return 1; //return success } else { return 0; //return failure } }
loader_t
的 save_file
函数接受一个 FILE
流指针 fp
, save_file
函数应向这个指针写入它的输出。 fileformatname
参数的名称与加载器的 accept_file
函数的参数的名称相同。如前所述,调用 save_file
函数,是为了响应 IDA 的 File ▶ Produce File ▶ Create EXE File 命令。为了响应这个命令,最初,IDA 会调用 save_file
函数,并将 fp
设置为 NULL。如果以这种方式被调用, save_file
函数将接受查询,以确定它是否能够生成 fileformatname
指定的输出文件类型。这时,如果 save_file
无法创建指定的文件类型,它应返回 0;否则,它应返回 1。例如,只有在数据库中存在特定的信息时,加载器才能创建一个有效的输出文件。
如果使用有效的(非 NULL ) FILE
指针调用, save_file
应将一个有效的输出文件写入到所提供的 FILE
流中。遇到这类情况,IDA 将在向用户显示“保存文件”对话框后创建 FILE
流。
IDA 和 FILE 指针
如果你开发用于 Windows 版本的 IDA 的模块,fpro.h 文件提到 IDA
FILE
流的一个非常重要的行为,这一行为也源于一个事实,即 IDA 的核心 DLL——ida_wll.dll 是使用 Borland 工具构建的。简而言之,程序模块之间不能共享 BorlandFILE
指针,否则,可能会导致访问冲突,甚至会令 IDA 崩溃。为解决这个问题,IDA 以qfxxx
(如qfprintf
,在fpro.h
中声明)的形式提供全套的包装函数,以替代标准的 C 风格FILE
操纵例程(如fprintf
)。在使用这些函数时,需要注意的是,qfxxx
函数并不总是和它们的 C 风格对应函数(例如qfwrite
和fwrite
)使用相同的参数。如果希望使用 C 风格的 FILE 操纵函数,请务必遵循以下规则。
将 fpro.h 包含到你的模块中之前,必须定义
USE_STANDARD_FILE_FUNCTIONS
宏。禁止在 C 风格的 FILE 函数中使用 IDA 提供的 FILE 指针。
禁止在 IDA 的 qfxxx 函数中使用从 C 库函数中获得的 FILE 指针。
回到 save_simpleton_file
函数,在实现 save_file
的功能时,唯一真正有用的函数是 base2file
函数,它与我们在 load_simpleton_file
中使用的 file2base
函数相对应。 base2file
函数只是将一系列数据库值写入到所提供的一个 FILE
流中的指定位置。
虽然“傻瓜式”文件格式几乎没有任何用处,但它可用于一个目的:我们可通过它展示 IDA 加载器模块的核心功能。该“傻瓜式”加载器的源代码可在本书网站上找到。
18.4.2 构建 IDA 加载器模块
除了一些细微的差别外,构建和安装 IDA 加载器模块的过程与第 17 章讨论的构建 IDA 插件模块的过程几乎完全相同。首先,Windows 和 Liunx 平台使用的加载器文件扩展名分别为.ldw/.l64 和.llx/.llx64 。其次,在构建加载器时,我们将新建的加载器存储在/bin/loaders 目录中(这属于个人喜好)。最后,通过将已编译的加载器二进制文件复制到/loaders 目录中,我们可以安装加载器模块。对代码清单 17-1 中的生成文件稍作修改,将 PLUGIN_EXT
变量更改为一个反映正确的加载器文件扩展名的 LOADER_EXT
变量,将 idabook_plugin
的全部引用更改为 simpleton
,使 OUTDIR
变量指向 $(IDA)/bin/loaders
,即可用修改后的生成文件构建“傻瓜式”加载器。
18.4.3 IDA pcap 加载器
可以说,绝大多数网络数据包并不包含可被反汇编的代码。但是,如果一个数据包碰巧包含一个破解程序的证据,那么该数据包可能包含需要进行反汇编(以对数据包进行准确的分析)的二进制代码。为了证明 IDA 加载器可以用于多种用途,现在我们描述如何创建一个能够将 pcap1 格式的数据包捕获文件加载到 IDA 数据库中的加载器。虽然这样做可能有点小题大做,但是,我们将逐渐证实 IDA SDK 的其他一些功能。在这里,我们不会匹配 Wireshark2 之类的工具的功能。
1. 参见 http://www.tcpdump.org/ 。
2. 参见 http://www.wireshark.org/
开发这种加载器需要我们对 pcap 文件格式有一定的研究。研究表明,pcap 文件由以下简单的语法构成:
pcap_file: pcap_file_header (pcap_packet)* pcap_packet: pcap_packet_header pcap_content pcap_content: (byte)+
pcap_file_header
包含一个 32 位幻数字段和描述文件内容的其他字段,包括文件所包含的数据包的类型。为了简化,这里假设仅处理 DLT_EN10MB
(10Mb 以太网数据包)。在开发 pcap 加载器的过程中,我们的一个目标是识别尽可能多的头部数据,以帮助读者集中精力处理数据包内容,特别是应用层的内容。要完成这个目标,首先需要为每一个文件头部创建一个单独的段,将它们与数据包分离开来;然后再从每个段中识别出尽可能多的头部结构体,以便用户不需要手动解析文件内容。下面的讨论将主要集中于 pcap 加载器的 load_file
组件,因为这里的 accept_file
函数只是对 accept_simpleton_file
进行了简单的修改,使它能够识别 pcap 幻数即可。
为了识别头部结构体,在加载过程中,我们需要在 IDA 的“结构体”窗口中定义一些常用的结构体。这样,如果已知一些字节的数据类型,加载器将自动把它们格式化成结构体。IDA 的 GNU C++ Unix 类型库定义了 pcap 头部结构体和各种描述以太网、IP 、TCP 和 UDP 头部的、与网络有关的结构体。但是,在 IDA 5.3 之前的版本中,对 IP 头部结构体( iphdr
)的定义并不正确。 load_pcap_file
采取的第一个步骤是调用我们编写的一个名为 add_types
的帮助函数,将结构体导入到新数据库中。共有两个版本的 add_types
,其中一个版本使用了在 IDA 的 GNU C++ Unix 类型库中声明的类型,另一个版本则自己声明全部所需的结构体。
第一个版本的 add_types
首先加载 GNU C++ Unix 类型库,然后从这个新加载的类型库中提取出类型标识符。这个版本的 add_types
如下所示:
void add_types() { #ifdef ADDTIL_DEFAULT add_til2("gnuunx.til", ADDTIL_SILENT); #else add_til("gnuunx.til"); #endif pcap_hdr_struct = til2idb(-1, "pcap_file_header"); pkthdr_struct = til2idb(-1, "pcap_pkthdr"); ether_struct = til2idb(-1, "ether_header"); ip_struct = til2idb(-1, "iphdr"); tcp_struct = til2idb(-1, "tcphdr"); udp_struct = til2idb(-1, "udphdr"); }
在 typinf.hpp 中定义的 add_til
函数用于将一个现有的类型库文件加载到数据库中。为了支持随 IDA 5.1 版本引入的 add_til2
函数,使用 add_til
函数的做法遭到反对。这些 SDK 函数的功能等同于第 8 章中讨论的使用“类型”窗口加载一个.til 文件。加载一个类型库后,就可以利用 til2idb
函数将各个类型导入到当前数据库中。这种编程操作等同于第 8 章中讨论的将一个标准结构体添加到“结构体”窗口中。 til2idb
函数返回一个类型标识符,在我们希望将一系列字节转换成特定的结构体数据类型时,会用到这个标识符。我们已经将这些类型标识符保存在全局变量( tid_t
类型)中,以便在后面的加载过程中更快地访问各种类型。
第一个版本的 add_types
存在两个缺点。第一,仅仅为了访问 6 种数据类型,我们需要导入整整一个类型库。第二,如前所述,IDA 对于 IP 头部结构体的内建定义并不正确,因此,在后面的加载过程中,如果尝试应用这些结构体,可能会导致问题。
第二个版本的 add_types 说明如何通过解析 C 风格的结构体声明,动态创建一个类型库。这个版本如下所示:
void add_types() { til_t *t = new_til("pcap.til", "pcap header types"); //empty type library parse_decls(t, pcap_types, NULL, HTI_PAK1); //parse C declarations into library sort_til(t); //required after til is modified pcap_hdr_struct = import_type(t, -1, "pcap_file_header"); pkthdr_struct = import_type(t, -1, "pcap_pkthdr"); ether_struct = import_type(t, -1, "ether_header"); ip_struct = import_type(t, -1, "iphdr"); tcp_struct = import_type(t, -1, "tcphdr"); udp_struct = import_type(t, -1, "udphdr"); free_til(t); //free the temporary library }
这个版本的 add_types
使用 new_til
函数创建了一个临时的空类型库。它通过解析一个包含有效 C 结构体定义的字符串( pcap_types
),获得加载器所需的类型,从而填充新的类型库。 pcap_types
字符串的前几行代码如下所示:
char *pcap_types = "struct pcap_file_header {\n" "int magic;\n" "short version_major;\n" "short version_minor;\n" "int thiszone;\n" "int sigfigs;\n" "int snaplen;\n" "int linktype;\n" "};\n" ...
pcap_types
还声明了其他内容,包括 pcap 加载器所需的全部结构体的定义。为了简化解析过程,我们选择更改结构体定义中的所有数据声明,以利用标准的 C 数据类型。
HTI_PAK1
常量在 typeinf.hpp 中定义,是用于控制内部 C 解析器行为的许多 HTI_XXX
值中的一个。在这个例子中,代码请求的是 1 字节对齐的结构体。经过修改后,这个类型库将使用 sort_til
排序,这时它即可供我们使用。 import_type
函数以类似于 til2idb
的方式,从指定的类型库中提取被请求的结构体类型,将它们加载到数据库中。在这个版本中,我们同样将返回的类型标识符保存到全局变量中,以方便在后面的加载过程中使用。最后, add_types
使用 free_til
函数删除临时的类型库,释放被该类型库占用的内存。使用这个版本的 add_types
与使用第一个版本不同,这时我们可以完全控制被选择导入到数据库中的数据类型,不需要导入整个结构体库,从而避免导入那些不需要用到的结构体。
顺便提一下,我们还可以使用 store_til
函数(在此之前,应调用 compact_til
)将临时的类型库文件保存到磁盘中。在当前的例子中,由于需要创建的类型很少,这样做几乎没有什么益处。因为在每次加载器执行时构建结构体,与构建并分发一个必须正确安装的专用类型库同样容易,因而也不会为我们节省大量时间。
将注意力转到 load_pcap_file
函数上,它调用 add_types
初始化数据类型(如前所述),创建一个文件注释,将 pcap 文件头加载到数据库中,创建一个头部字节大小的节,并将头部字节转换成一个 pcap_file_header
结构体:
void idaapi load_pcap_file(linput_t *li, ushort, const char *) { ssize_t len; pcap_pkthdr pkt; add_types(); //add structure templates to database create_filename_cmt(); //create the main file header comment //load the pcap file header from the database into the file file2base(li, 0, 0, sizeof(pcap_file_header), FILEREG_PATCHABLE); //try to add a new data segment to contain the file header bytes if (!add_segm(0, 0, sizeof(pcap_file_header), ".file_header", CLASS_DATA)) { loader_failure(); } //convert the file header bytes into a pcap_file_header doStruct(0, sizeof(pcap_file_header), pcap_hdr_struct); //... continues
再一次, load_pcap_file
使用 file2base
将新打开的磁盘文件的内容加载到数据库中。pcap 文件头的内容被加载后,它将在数据库中获得自己的节,并且 pcap_file_header
结构体将通过在 bytes.hpp 中声明的 doStruct
函数应用于所有头部字节。 doStruct
函数的作用等同于使用 Edit ▶Struct Var 将一组相邻的字节转换成一个结构体。这个函数需要一个地址、一个大小和一个类型标识符,并将给定地址的指定大小的字节转换成给定的类型。
然后, load_pcap_file
继续读取所有数据包内容,并为数据包内容创建一个 .packets
节,如下所示:
//...continuation of load_pcap_file uint32 pos = sizeof(pcap_file_header); //file position tracker while ((len = qlread(li, &pkt, sizeof(pkt))) == sizeof(pkt)) { mem2base(&pkt, pos, pos + sizeof(pkt), pos); //transfer header to database pos += sizeof(pkt); //update position pointer point to packet content //now read packet content based on number of bytes of packet that are //present file2base(li, pos, pos, pos + pkt.caplen, FILEREG_PATCHABLE); pos += pkt.caplen; //update position pointer to point to next header } //create a new section around the packet content. This section begins where //the pcap file header ended. if (!add_segm(0, sizeof(pcap_file_header), pos, ".packets", CLASS_DATA)) { loader_failure(); } //retrieve a handle to the new segment segment_t *s = getseg(sizeof(pcap_file_header)); //so that we can set 32 bit addressing mode on set_segm_addressing(s, 1); //set 32 bit addressing //...continues
在前面的代码中, mem2base
函数是一个新函数,用于将已经加载到内存中的内容传送到数据库中。
最后, load_pcap_file
在数据库中任何可能的地方应用结构体模板。我们必须在创建段后应用结构体模板,否则,创建段的操作将删除所有已应用的结构体模板,使我们的所有辛苦劳动白白浪费。这个函数的第三部分(也是最后一部分)如下所示:
//...continuation of load_pcap_file //apply headers structs for each packet in the database for (uint32 ea = s->startEA; ea < pos;) { uint32 pcap = ea; //start of packet //apply pcap packet header struct doStruct(pcap, sizeof(pcap_pkthdr), pkthdr_struct); uint32 eth = pcap + sizeof(pcap_pkthdr); //apply Ethernet header struct doStruct(eth, sizeof(ether_header), ether_struct); //Test Ethernet type field uint16 etype = get_word(eth + 12); etype = (etype >> 8) | (etype 8); //htons if (etype == ETHER_TYPE_IP) { uint32 ip = eth + sizeof(ether_header); //Apply IP header struct doStruct(ip, sizeof(iphdr), ip_struct); //Test IP protocol uint8 proto = get_byte(ip + 9); //compute IP header length uint32 iphl = (get_byte(ip) & 0xF) * 4; if (proto == IP_PROTO_TCP) { doStruct(ip + iphl, sizeof(tcphdr), tcp_struct); } else if (proto == IP_PROTO_UDP) { doStruct(ip + iphl, sizeof(udphdr), udp_struct); } } //point to start of next pcak_pkthdr ea += get_long(pcap + 8) + sizeof(pcap_pkthdr); } }
前面的代码只是以一次一个数据包的方式简单浏览了数据库,并分析每个数据包头部中的几个字段,以确定需要应用的结构体的类型,以及应用该结构体的起始位置。下面的输出是一个已经使用 pcap 加载器加载到数据库中的 pcap 文件的前几行:
.file_header:0000 _file_header segment byte public 'DATA' use16 .file_header:0000 assume cs:_file_header .file_header:0000 pcap_file_header <0A1B2C3D4h, 2, 4, 0, 0, 0FFFFh, 1> .file_header:0000 _file_header ends .file_header:0000 .packets:00000018 ; ========================================================= .packets:00000018 .packets:00000018 ; Segment type: Pure data .packets:00000018 _packets segment byte public 'DATA' use32 .packets:00000018 assume cs:_packets .packets:00000018 ;org 18h .packets:00000018 pcap_pkthdr <<47DF275Fh, 1218Ah>, 19Ch, 19Ch> .packets:00000028 db 0, 18h, 0E7h, 1, 32h, 0F5h; ether_dhost .packets:00000028 db 0, 50h, 0BAh, 0B8h, 8Bh, 0BDh; ether_shost .packets:00000028 dw 8 ; ether_type .packets:00000036 iphdr <45h, 0, 8E01h, 0EE4h, 40h, 80h, 6, 9E93h, 200A8C0h, 6A00A8C0h> .packets:0000004A tcphdr <901Fh, 2505h, 0C201E522h, 6CE04CCBh, 50h, 18h, 0E01Ah, 3D83h, 0> .packets:0000005E db 48h ; H .packets:0000005F db 54h ; T .packets:00000060 db 54h ; T .packets:00000061 db 50h ; P .packets:00000062 db 2Fh ; / .packets:00000063 db 31h ; 1 .packets:00000064 db 2Eh ; . .packets:00000065 db 30h ; 0
以这种方式应用结构体模板,可以展开和折叠任何头部,显示或隐藏它的每一个成员字段。如上所示,我们可以轻易确定,地址 0000005E
处的字节是一个 HTTP 响应数据包的第一个字节。
了解了基本的 pcap 文件加载功能,就为我们开发执行更加复杂的任务(如 TCP 流重组和其他各种形式的数据提取)的插件打下基础。另外,在格式化各种与网络有关的结构体时,我们还可以使用对用户更加友好的方式,如显示一个 IP 地址的可读版本,为每个头部中的其他字段提供按字节排序的显示。这些改进将作为挑战留给读者来解决。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论