通过之前的学习,相信大家已经对磁盘的引导区有了充分的认识。但是我们之前的学习都是利用现成的工具来对引导区进行解析的,而对于一名反病毒工程师而言,不单单需要有扎实的逆向分析功底,同时也需要有很强的编程能力来解决实际问题。对于我们本次的课程来说,就需要大家亲自动手,利用程序来实现引导区的解析。这样做的目的,一方面是为了提高大家的编程能力,而另一方面则有助于我们更好地理解引导区的内容。
对于学习过PE文件格式解析的朋友来说,解析MBR可能不会有太大的问题,毕竟二者的原理还是非常相似的。但是虽然解析过PE文件,还是有一些微小的差别。首先造成解析困难的一点是MBR没有给出具体的结构体。如果大家分析过PE文件结构,那就知道,各个结构体在WinNt.h头文件中都有给出现成的定义,而MBR的定义是没有给出的。因此我们上次课在解析MBR时,并没有对照着结构体给大家介绍。再一个问题是,解析PE文件时,我们会打开具体的可执行文件去按照PE文件结构的定义进行解析,而硬盘的引导区并不属于某一个文件。用WinHex打开的是物理硬盘,那么我们如何通过程序来打开物理硬盘呢?这就是比较困惑的地方。不过,这些都不是太大的问题。本次的课程只要解决了这两个问题,那么,我们的进行编程解析时就容易多了。
typedef struct _MBR { unsigned char BootRecord[440]; // 引导程序 unsigned char ulSigned[4]; // Windows磁盘签名 unsigned char sReserve[2]; // 保留位 unsigned char Dpt[64]; // 分区表 unsigned char EndSign[2]; // 结束标志 }MBR, *PMBR;这就是定义的MBR了,引导程序共440个字节,Windows签名共4个字节,保留字节共2个字节,分区表共64个字节,再加上2个结束标志,一共512个字节。不过这样定义并不好,因为里面的常量比较多,下面修改一下,定义如下:
#define BOOTRECORDSIZE 440 #define DPTSIZE 64 typedef struct _MBR { unsigned char BootRecord[BOOTRECORDSIZE]; // 引导程序 unsigned char ulSigned[4]; // Windows磁盘签名 unsigned char sReserve[2]; // 保留位 unsigned char Dpt[DPTSIZE]; // 分区表 unsigned char EndSign[2]; // 结束标志 } MBR, *PMBR;
这样定义后,可以很方便地获得引导程序的大小和分区表的大小。虽然这样定义直观一些,但是还不能算太直观,因为定义的都是unsigned char类型,无法真正反映出每个成员变量的具体含义。下面再次进行修改,定义如下:
#define BOOTRECORDSIZE 440 typedef struct _BOOTRECORD { unsigned char BootRecord[BOOTRECORDSIZE]; }BOOTRECORD, *PBOOTRECORD; #define DPTSIZE 64 typedef struct _DPT { unsigned char Dpt[DPTSIZE]; }DPT, *PDPT; typedef struct _MBR { BOOTRECORD BootRecord; unsigned char ulSigned[4]; unsigned char sReserve[2]; DPT Dpt; unsigned char EndSign[2]; }MBR, *PMBR;这次修改后,可以很容易地从MBR这个结构体中看出主要两个成员变量的含义了。虽然直观了,但还是有问题。Dpt其实是一个有4条记录的表,也就是说它其实是一个数组,这样的定义当解析它的时候并不方便。这样的定义方便我们一次性将DPT读出,只要再定义一个DP的结构体来对DPT进行转换,就可以方便地对DPT进行解析了。下面再次定义一个结构体,定义如下:
#define DPTNUMBER 4 typedef struct _DP { unsigned char BootSign; // 引导标志 unsigned char StartHsc[3]; // 分区的起始磁头号、扇区号、柱面号 unsigned char PartitionType; // 分区类型 unsigned char EndHsc[3]; // 分区的结束磁头号、扇区号、柱面号 ULONG SectorsPreceding; // 本分区之前使用的扇区数 ULONG SectorsInPartition; // 分区的总扇区数 }DP, *PDP;
有了这个结构体,就可以方便地对DPT进行解析了。最后两个定义就是对MBR各结构体的完整定义。之所以如此反复介绍如何进行MBR结构体的定义,是想告诉大家一个在没有相关数据结构定义的情况下如何通过自己的分析来定义数据结构的思路和方法。
大家可以思考一下,如果不定义这些结构体是不是就无法对MBR进行解析,定义了这些结构体后对于解析MBR有哪些影响。对于MBR的解析,可以完全不定义这些结构体,定义这些结构体的目的是方便对程序的后期维护,并使程序在整体上有一个良好的格式。定义数据结构可以清晰地表达各个数据结构之间的关系,让我们在写程序的过程中有一个清晰的思路,让看程序的人也可以一目了然。
通过上图可以找到硬盘设备的设备名称,例如可以通过\Device\Harddisk0\DR0这个设备名称再去查找相应的设备符号链接。我们再依次打开WinObj左边的树形控件,如下图所示:
由上图可知,硬盘的设备符号链接为PhysicalDrive0,那么在使用时就应该书写为\\.\PhysicalDrive0。
这里给大家简单地介绍一下设备名和设备符号链接。每个设备在Windows的内核中都有对应的驱动模块,在驱动模块中会为设备提供一个名字来对设备进行操作,驱动模块中提供的名字即为“设备名”。设备名只能在内核模块中使用。如果想要在应用程序下对设备进行操作,不能直接使用设备名称,应该使用设备符号链接。设备符号链接就是驱动模块为应用程序提供的操作设备的一个符号,通过这个符号可与设备进行对应。
#include "windows.h" #include "stdio.h" // 显示MBR数据 VOID ShowMbr (HANDLE hDevice, PMBR pMbr) { DWORD dwRead = 0; ReadFile(hDevice, (LPVOID)pMbr, sizeof(MBR), &dwRead, NULL); int i; for(i= 0; i < 512; i++) { printf("%02X", ((BYTE*)pMbr)[i]); if ((i+1)%16 == 0) { printf("\r\n"); } } } // 解析MBR VOID ParseMbr(MBR Mbr) { printf("引导记录: \r\n"); for( int i = 0; i < BOOTRECORDSIZE; i++ ) { printf("%02X ", Mbr.BootRecord.BootRecord[i]); if((i+1)%16 == 0) { printf("\r\n"); } } printf("\r\n"); printf("磁盘签名: \r\n"); for(i = 0; i < 4; i ++) { printf("%02X ", Mbr.ulSigned[i]); } printf("\r\n"); printf("解析分区表: \r\n"); for(i = 0; i < DPTSIZE; i++) { printf("%02X ", Mbr.Dpt.Dpt[i]); if((i+1)%16 == 0) { printf("\r\n"); } } printf("\r\n"); // 获取分区表的地址,并将指向分区表的指针转换为PDP的类型,最后赋给pDp PDP pDp = (PDP)&(Mbr.Dpt.Dpt); for(i = 0; i < DPTNUMBER; i++) { printf("引导标志:%02X ",pDp[i].BootSign); printf("分区类型:%02X ",pDp[i].PartitionType); printf("\r\n"); printf("本分区之前扇区数:%d ",pDp[i].SectorsPreceding); printf("本分区的总扇区数:%d ",pDp[i].SectorsInPartition); printf("\r\n"); printf("该分区的大小:%f \r\n", (double)pDp[i].SectorsInPartition/1024*512/1024/1024); printf("\r\n \r\n"); } printf("结束标志:\r\n"); for (i = 0; i < 2; i ++) { printf("%02X ", Mbr.EndSign[i]); } printf("\r\n"); } int main(int argc, char* argv[]) { // 打开物理硬盘设备 HANDLE hDevice = CreateFile("\\\\.\\PhysicalDrive0", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (hDevice == INVALID_HANDLE_VALUE) { printf("CreateFile Error %d \r\n", GetLastError()); return -1; } MBR Mbr = { 0 }; ShowMbr(hDevice, &Mbr); ParseMbr(Mbr); CloseHandle(hDevice); getchar(); return 0; }
代码非常短,也不复杂,看起来跟读写文件没什么太大的差别,其实就是在读写文件。前面介绍过,Windows将各种设备都当作文件来看待,因此打开硬盘设备的时候直接使用CreateFile()函数就可以了。