http://blog.csdn.net/jha334201553/article/details/9089119
开始先说下DBR, DBR是继MBR 之后最先访问的地方,MBR利用int 13h 读取MBR并将之加载到物理地址0x7c00的地方. 然后将段地址:代码地址入栈后retf跳过去运行.
MBR利用BIOS中断int 13h读取数据加载到内存指定位置..传统的int 13h调用最多只能识别1024个磁头:
前面MBR讲解MBR的时候,有结构如下
/*+0x01*/ uchar StartHead; // 分区起始磁头号 (1磁头 = 63 扇区,取值 0~255 之间)
/*+0x02*/ uint16 StartSector:10; // 启始柱面号 10位 (1柱面 = 255 磁头,取值 0~1023 之间)
/*+0x02*/ uint16 StartCylinder:6; // 启始扇区号 6位 (取值 1 到 63 之间)
此结构可容纳最大值为FF FF FF (现在这个值基本都写成FE FF FF, 而废弃不用), 即最大能寻址的就是255柱面, 1023磁头, 63扇区,计算扇区个数为:
1023*255*63+255*63+63 = 16450623
再按每扇区 512 字节算, 那么它容量为 8 GB ≈ 512*16450623 B = 7.84 GB
传统的int 13h中断就受限于8G的限制, Microsoft等多家公司制定了int 13h扩展标准,让int 13h读写磁盘中断可以突破8G限制. 现在的计算机BIOS都是按扩展int 13h标准编写的代码.(具体详细内容可参考"扩展 int 13h规范").
按MBR分区表里面的 SectionPrecedingPartition 逻辑扇区偏移(注意,这个逻辑扇区偏移是从0开始算的,读取出来值为63,而物理扇区是从1开始计算的,逻辑扇区转换物理扇区的时候必须+1才是正确的) 可以找到DBR的位置.可以看看winhex的显示
以下就偷懒不从MBR寻址分区的DBR了,而是直接打开盘符读取 (这样打开的第一个扇区就是DBR),这样做有个缺点,就是你用这个handle值将不能进行内存映射,只能一次多读取几个扇区来加快分析磁盘的速度(当前用的是一次读取20M数据然后分析)。
- HANDLE handle = CreateFile ( TEXT("\\\\.\\C:") ,
- GENERIC_READ,
- FILE_SHARE_READ|FILE_SHARE_WRITE,
- NULL,
- OPEN_EXISTING,
- FILE_ATTRIBUTE_NORMAL,
- NULL);
DBR结构定义为(对照winhex模板信息查看):
-
-
-
-
-
- typedef struct _BIOS_PARAMETER_BLOCK {
-
- uint16 BytesPerSector;
- uchar SectorsPerCluster;
- uint16 ReservedSectors;
- uchar Fats;
- uint16 RootEntries;
- uint16 Sectors;
- uchar Media;
- uint16 SectorsPerFat;
- uint16 SectorsPerTrack;
- uint16 Heads;
- uint32 HiddenSectors;
- uint32 LargeSectors;
-
- }BIOS_PARAMETER_BLOCK, *pBIOS_PARAMETER_BLOCK;
-
- typedef struct _NTFS_Boot_Sector{
- uchar JmpCode[3];
- char OemID[8];
- BIOS_PARAMETER_BLOCK PackedBpb;
- uchar Unused[4];
- uint64 NumberSectors;
- lcn MftStartLcn;
- lcn Mft2StartLcn;
- uchar ClustersPerFileRecordSegment;
- uchar Reserved0[3];
- uchar DefaultClustersPerIndexAllocationBuffer;
- uchar Reserved1[3];
- uint64 SerialNumber;
- uint32 Checksum;
- uchar BootStrap[426];
- uint16 RecordEndSign;
- }NTFS_Boot_Sector, *pNTFS_Boot_Sector;
在读取DBR的时候,一些数据以后经常会用到,那么需要根据DBR里面的信息保存以后会用到的信息,下面定义一个常用的保存信息结构:
-
- typedef struct _NTFS_INFO
- {
- uint32 BytesPerSector;
- uint32 SectorsPerCluster;
- uint32 BytesPerCluster;
- uint64 SectorCount;
- uint64 MftStart;
- uint64 MftMirrStart;
- uint32 BytesPerFileRecord;
-
- uint16 VolumeLabelLength;
- wchar VolumeLabel[MAXIMUM_VOLUME_LABEL_LENGTH];
-
- uint16 vcnlen;
- uchar vcn[VCN_LENTH];
- } NTFS_INFO, *PNTFS_INFO;
其中 MAXIMUM_VOLUE_LABEL_LENGTH定义为
- #define MAXIMUM_VOLUME_LABEL_LENGTH (32*sizeof(wchar))
NTFS_Boot_Sector .MftStartLcn*NTFS_Boot_Sector. PackedBpb .SectorsPerCluster得到MFT所在扇区号,这里为 786432*8 = 6291456扇区(字节偏移为 6291456*512= 3221225472 ( 十六进制0xC0000000))。然后MFT表里面的内容是根据簇号来读取数据的,那么可以定义一个根据簇号,读取数据的函数,如下形式:
- typedef struct _Partition_Stand_Post
- {
- HANDLE handle;
- uint64 CluNnum;
- uint32 BytesPerCluster;
- uint64 CluCount;
- PNTFS_INFO NtfsInfo;
- }Partition_Stand_Post, *pPartition_Stand_Post;
-
-
-
-
- PBYTE ReadClues(pPartition_Stand_Post post, PBYTE buf, DWORD lenth)
- {
- DWORD dwbytes = 0;
- LARGE_INTEGER li = {0};
- li.QuadPart = post->CluNnum*post->BytesPerCluster;
- SetFilePointer(post->handle, li.LowPart, &li.HighPart, FILE_BEGIN);
- ReadFile(post->handle, buf, lenth, &dwbytes, NULL);
- if (lenth == dwbytes)
- {
- return buf;
- }
- return NULL;
- }
下面先说MFT表的结构:
首先,看到的是头部,标记为"FILE", 结构体如下定义:
-
- typedef struct _FILE_RECORD_HEADER
- {
- uint32 Type;
- uint16 UsaOffset;
- uint16 UsaCount;
- uint64 Lsn;
- } FILE_RECORD_HEADER, *PFILE_RECORD_HEADER;
-
-
- typedef struct _FILE_RECORD{
- FILE_RECORD_HEADER Ntfs;
- uint16 SequenceNumber;
- uint16 LinkCount;
- uint16 AttributeOffset;
- uint16 Flags;
- uint32 BytesInUse;
- uint32 BytesAllocated;
- uint64 BaseFileRecord;
- uint16 NextAttributeNumber;
- uint16 Pading;
- uint32 MFTRecordNumber;
- uint32 MFTUseFlags;
- }FILE_RECORD, *pFILE_RECORD;
这里主要关注的就是文件头大小(0x38)可以找到第一个属性地址,紧跟在后面的是文件类型,00表示被删除的文件,01表示正常文件,02表示删除目录,03表示正常目录.再后面就是这个MFT记录的数据大小(很奇怪,为什么数据大小是从头到0xFFFFFFFF的大小+4,这个值为什么不是直接从头到0xFFFFFFFF的大小呢?).
根据FILE头部数据找到下面的一个个属性,接下来分析的就是一个个属性了.
属性由属性头跟属性体组成,属性头的结构定义如下:
-
- typedef struct
- {
- ATTRIBUTE_TYPE AttributeType;
- uint16 RecordLength;
- uint16 unknow0;
- uchar Nonresident;
- uchar NameLength;
-
-
-
-
- uint16 NameOffset;
-
- uint16 Flags;
- uint16 AttributeNumber;
- } ATTRIBUTE, *PATTRIBUTE;
-
-
- typedef struct _RESIDENT_ATTRIBUTE
- {
- ATTRIBUTE Attribute;
- uint32 ValueLength;
- uint16 ValueOffset;
- uchar Flags;
- uchar Padding0;
- } RESIDENT_ATTRIBUTE, *PRESIDENT_ATTRIBUTE;
其中 ATTRIBUTE_TYPE 是一个枚举类型,里面定义了可能出现的所有类型。查看这个结构主要是看AttributeType(上图中,染上绿色的为属性类型,跟在后面的的红色框框内数据为属性头+属性体的大小(这个值必须是大于0x10,小于512的,程序中必须判断),由这个值可以得到下一个属性的地址),这个类型是什么,然后再跟去类型定义的Data部分去解析下面的属性体。遍历属性的时候可以根据属性类型来判断是否已经到达结尾,如果属性类型为0xFFFFFFFF则表示已经到达末尾(注意遍历的时候还是要结合FILE头部指定的大小来遍历,这样程序健壮性好很多,还有如果属性头后面跟着的大小值小于0x10也要结束遍历,因为这时候这个MFT项已经不可靠了,再继续下去程序出错可能性比较大(0x00值可能出现死循环))。
属性类型定义,及各个类型属性的结构(缺少无关紧要的结构,其他结构可参考nt4里面的源码并对照winhex分析):
-
- typedef enum _ATTRIBUTE_TYPE
- {
- AttributeStandardInformation = 0x10,
- AttributeAttributeList = 0x20,
- AttributeFileName = 0x30,
- AttributeObjectId = 0x40,
- AttributeSecurityDescriptor = 0x50,
- AttributeVolumeName = 0x60,
- AttributeVolumeInformation = 0x70,
- AttributeData = 0x80,
- AttributeIndexRoot = 0x90,
- AttributeIndexAllocation = 0xA0,
- AttributeBitmap = 0xB0,
- AttributeReparsePoint = 0xC0,
- AttributeEAInformation = 0xD0,
- AttributeEA = 0xE0,
- AttributePropertySet = 0xF0,
- AttributeLoggedUtilityStream = 0x100
- } ATTRIBUTE_TYPE, *PATTRIBUTE_TYPE;
-
-
- typedef struct _STANDARD_INFORMATION
- {
- uint64 CreationTime;
- uint64 ChangeTime;
- uint64 LastWriteTime;
- uint64 LastAccessTime;
- uint32 FileAttribute;
- uint32 AlignmentOrReserved[3];
- #if 0
- uint32 QuotaId;
- uint32 SecurityId;
- uint64 QuotaCharge;
- USN Usn;
- #endif
- } STANDARD_INFORMATION, *PSTANDARD_INFORMATION;
-
-
-
- typedef struct _ATTRIBUTE_LIST
- {
- ATTRIBUTE_TYPE AttributeType;
- uint16 Length;
- uchar NameLength;
- uchar NameOffset;
- uint64 StartVcn;
- uint64 FileReferenceNumber;
- uint16 AttributeNumber;
- uint16 AlignmentOrReserved[3];
- } ATTRIBUTE_LIST, *PATTRIBUTE_LIST;
-
-
-
- typedef struct
- {
- uint64 DirectoryFile:48;
- uint64 ReferenceNumber:16;
- uint64 CreationTime;
- uint64 ChangeTime;
- uint64 LastWriteTime;
- uint64 LastAccessTime;
- uint64 AllocatedSize;
- uint64 DataSize;
- uint32 FileAttributes;
- uint32 AlignmentOrReserved;
- uchar NameLength;
-
-
- uchar NameType;
- wchar Name[1];
- } FILENAME_ATTRIBUTE, *PFILENAME_ATTRIBUTE;
-
-
- typedef struct _NONRESIDENT_ATTRIBUTE
- {
- ATTRIBUTE Attribute;
-
- uint64 StartVcn;
- uint64 LastVcn;
-
- uint16 RunArrayOffset;
- uint16 CompressionUnit;
- uint32 Padding0;
- uint32 IndexedFlag;
- uint64 AllocatedSize;
- uint64 DataSize;
- uint64 InitializedSize;
- uint64 CompressedSize;
- } NONRESIDENT_ATTRIBUTE, *PNONRESIDENT_ATTRIBUTE;
以下特别要说明就是数据恢复中要使用的一些结构
0x30 AttributeFileName属性 (如果文件名很长,那么有多个0x30属性,一个记录短文件名,一个记录长文件名),记录了很多文件信息,可能使用到的有文件创建时间、文件修改时间、最后写入时间、文件最后一次访问时间。这里面的几个值相当于用GetFileTime 或者GetFileInformationByHandle得到的几个FILETIME值,可以调用FileTimeToSystemTime转换时间。其中DataSize为实际文件使用的大小,在0x80属性中寻找簇恢复数据的时候会用到这个值。最后就是wchar的文件名,此名在cmd中显示需用WideCharToMultiByte转换后用printf显示,如果用wprintf显示中文会出现乱码问题。数据恢复的时候如果需要目录结构可由此属性中的DirectoryFile值得到,此值为父目录的记录号(相对于$MFT元所在扇区的偏移,即:$MFT + DirectoryFile*2)例如:
上图,父目录号为0x0000000002A4,从DBR中得到$MFT起始簇为786432(上面图一簇为8扇区),则父目录的MFT表项扇区为: 786432*8+0x0000000002A4*2 = 6292808,再查看6292808扇区:
所以这个文件为 X:\windows\notepad.exe(其中X表示为根目录,具体看前面CreateFile参数值是什么了).
0x80 AttributeData属性( 注意:如果是目录的话, 此结构属性是0xA0 )
如果有多个0x80属性,则应该认为这个文件里面存在数据流文件,数据流表示形式为"0x30记录文件名:流文件名",并且在explorer浏览中查看不到这个文件,NTFS刚出来的时候,文件流属性进程被病毒作者使用,比如如果要将hack.exe数据加到 1.txt 数据流里面,那么可以如下方式:
- void CrateDataStream()
- {
- HANDLE hfile = CreateFile( TEXT("1.txt:DataStream.exe"),
- GENERIC_WRITE,
- 0,
- NULL,
- CREATE_ALWAYS,
- FILE_ATTRIBUTE_NORMAL,
- NULL);
- if (hfile == INVALID_HANDLE_VALUE)
- {
- return ;
- }
- HANDLE hExeFile = CreateFile( TEXT("hack.exe"),
- GENERIC_READ,
- 0,
- NULL,
- OPEN_EXISTING,
- FILE_ATTRIBUTE_NORMAL,
- NULL);
- if (hExeFile == INVALID_HANDLE_VALUE)
- {
- CloseHandle(hfile);
- return ;
- }
- DWORD dwsize = GetFileSize(hExeFile, NULL);
- BYTE* buf = new BYTE[dwsize];
- DWORD wbytes;
- ReadFile(hExeFile, buf, dwsize, &wbytes, NULL);
- WriteFile(hfile, buf, wbytes, &wbytes, NULL);
- CloseHandle(hExeFile);
- CloseHandle(hfile);
- }
一般是病毒作者将这个 1.txt 添加到压缩文件(高级里面选上保存文件流数据), 然后搞成自解压包, 在解压后命令中写入1.txt: DataStream.exe, 在用户双击解压的时候就会运行里面的数据流程序。不过现在杀毒软件对这种数据流病毒基本都能杀了。
再说数据恢复的关键点:数据内容由NONRESIDENT_ATTRIBUTE. RunArrayOffset偏移指定DATA数据地址。如果属性头 ATTRIBUTE.Nonresident标记为1表示非常驻,则下面会解析怎么需要解析DATA部分, 如果为0则表示DATA就是文件内容, 比如一个txt文件里面记录的数据很少, 则此标记为0, 记事本里面数据就保存在DATA中。上图因为查看的是MFT自身,$MFT比较特殊,非常驻属性设置成0(即FALSE)但是却要像非常驻属性一样去分析。
上图蓝色的部分,
看不太清数据,原始数据如下:
最开始的数据为32,其中高4bits表示数据起始簇地址占用字节数,后4bits为数据大小(簇个数)占用字节数..这里要特别注意,第一个起始簇信息是无符号的,后面第二个开始就是相对于前面一个簇的偏移,是有正负的,查了很多资料,这点基本上都没提及,这也是数据恢复最容易出错的地方,辛辛苦苦写好了程序,一测试发现恢复出来的数据有问题。
上面第一个扇区地址为52604144(0x64559E*8,相对去当前分区的扇区偏移),第二个扇区地址为(0x64559E - 0x 77)*8 = 6575399 扇区(这里值0x89为-119)。
恢复数据的时候可以根据簇大小读取文件,然后保存,再根据前面的NONRESIDENT_ATTRIBUTE. DataSize值去SetFilePointer设置文件大小继而调用SetEndOfFile。
定义一个结构体表示这个结构:
- typedef struct _VCN_FLASH
- {
- uchar VcnLen:4;
- uchar StartVcnLen:4;
-
- uchar Data[1];
- }VCN_FLASH, *PVCN_FLASH;
再定义2个函数计算有符号与无符号数:
- uint64 make_uint64(uchar* buf, int lenth)
- {
- int64 ui=0;
- if (lenth > 8)
- {
- return (int64)0;
- }
-
- for (int i=0; i
- {
- ui = (buf[i]<<8*i)|ui;
- }
- return ui;
- }
-
-
- int64 make_int64(uchar* buf, int lenth)
- {
- int64 ui=0;
- if (lenth > 8)
- {
- return (int64)0;
- }
-
- for (int i=0; i
- {
- ui = (buf[i]<<8*i)|ui;
- }
-
-
- if (buf[lenth-1] >= 0x80)
- {
- int64 xorval = 0;
- for (i=0; i
- {
- xorval = xorval|(0xFF<<8*i);
- }
- ui = -((ui - 1)^xorval);
- }
- return ui;
- }
0x90 AttributeIndexRoot 属性 ( 目录索引B+树结构 )
这个属性,我也不是很清楚,等弄清楚了,再接着完善.................