http://blog.csdn.net/weinierbian/article/details/45649729
开始先说下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模板信息查看):
- //
- // NTFS 的DBR 数据结构
- //
- typedef struct _BIOS_PARAMETER_BLOCK {
- /*+0x0B*/ uint16 BytesPerSector; // 字节/扇区一般为0x0200 即512
- /*+0x0D*/ uchar SectorsPerCluster; // 扇区/簇
- /*+0x0E*/ uint16 ReservedSectors; // 保留扇区
- /*+0x0F*/ uchar Fats; //
- /*+0x11*/ uint16 RootEntries; //
- /*+0x13*/ uint16 Sectors; //
- /*+0x15*/ uchar Media; // 媒介描述
- /*+0x16*/ uint16 SectorsPerFat; //
- /*+0x18*/ uint16 SectorsPerTrack; // 扇区/磁轨
- /*+0x1A*/ uint16 Heads; // 头
- /*+0x1C*/ uint32 HiddenSectors; // 隐藏扇区
- /*+0x20*/ uint32 LargeSectors; // checked when volume is mounted
- }BIOS_PARAMETER_BLOCK, *pBIOS_PARAMETER_BLOCK;
- typedef struct _NTFS_Boot_Sector{
- /*+0x00*/ uchar JmpCode[3]; // 跳转指令
- /*+0x03*/ char OemID[8]; // 文件系统ID
- /*+0x0B*/ BIOS_PARAMETER_BLOCK PackedBpb; // BPB
- /*+0x24*/ uchar Unused[4]; // 未使用,总是为
- /*+0x28*/ uint64 NumberSectors; // 扇区总数
- /*+0x30*/ lcn MftStartLcn; // 开始C# $MFT (簇) 乘以 BIOS_PARAMETER_BLOCK.SectorsPerCluster 值得到扇区号
- /*+0x38*/ lcn Mft2StartLcn; // 开始C# $MFTMirr (簇)
- /*+0x40*/ uchar ClustersPerFileRecordSegment; // 文件记录大小指示器
- /*+0x41*/ uchar Reserved0[3]; // 未使用
- /*+0x44*/ uchar DefaultClustersPerIndexAllocationBuffer; // 簇/索引块
- /*+0x45*/ uchar Reserved1[3]; // 未使用
- /*+0x48*/ uint64 SerialNumber; // 64位序列号
- /*+0x50*/ uint32 Checksum; // 校验和
- /*+0x54*/ uchar BootStrap[426]; // 启动代码
- /*+0x1FE*/ uint16 RecordEndSign; // 0xAA55 结束标记
- }NTFS_Boot_Sector, *pNTFS_Boot_Sector;
- // 保存 NTFS 的基本信息
- typedef struct _NTFS_INFO
- {
- uint32 BytesPerSector; // 每扇区的字节数
- uint32 SectorsPerCluster; // 每簇的扇区数
- uint32 BytesPerCluster; // 每簇的字节数
- uint64 SectorCount; // 扇区总数
- uint64 MftStart; // MFT表开始簇
- uint64 MftMirrStart; // MFT备份表开始簇
- uint32 BytesPerFileRecord; // 每个文件记录的字节数一般为512*2
- uint16 VolumeLabelLength; // 卷名长度,卷名从MFT第4个项0x60属性得到(与0x30属性相似)
- wchar VolumeLabel[MAXIMUM_VOLUME_LABEL_LENGTH]; // 卷名
- uint16 vcnlen;
- uchar vcn[VCN_LENTH];
- } NTFS_INFO, *PNTFS_INFO;
- #define MAXIMUM_VOLUME_LABEL_LENGTH (32*sizeof(wchar))
- typedef struct _Partition_Stand_Post
- {
- HANDLE handle; // 分区句柄
- uint64 CluNnum; // 簇号
- uint32 BytesPerCluster; // 每簇字节
- uint64 CluCount; // 簇数量
- PNTFS_INFO NtfsInfo; // 指向NTFS_INFO 结构
- }Partition_Stand_Post, *pPartition_Stand_Post;
- // 按簇读取数据,
- // 传入 一个Partition_Stand_Post结构体指针,并指定buf,读取大小
- // 结果返回读取的数据指针
- 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;
- }
首先,看到的是头部,标记为"FILE", 结构体如下定义:
- // 文件记录头
- typedef struct _FILE_RECORD_HEADER
- {
- /*+0x00*/ uint32 Type; // 固定值'FILE'
- /*+0x04*/ uint16 UsaOffset; // 更新序列号偏移, 与操作系统有关
- /*+0x06*/ uint16 UsaCount; // 固定列表大小Size in words of Update Sequence Number & Array (S)
- /*+0x08*/ uint64 Lsn; // 日志文件序列号(LSN)
- } FILE_RECORD_HEADER, *PFILE_RECORD_HEADER;
- // 文件记录体
- typedef struct _FILE_RECORD{
- /*+0x00*/ FILE_RECORD_HEADER Ntfs; // MFT表头
- /*+0x10*/ uint16 SequenceNumber; // 序列号(用于记录文件被反复使用的次数)
- /*+0x12*/ uint16 LinkCount; // 硬连接数
- /*+0x14*/ uint16 AttributeOffset; // 第一个属性偏移
- /*+0x16*/ uint16 Flags; // falgs, 00表示删除文件,01表示正常文件,02表示删除目录,03表示正常目录
- /*+0x18*/ uint32 BytesInUse; // 文件记录实时大小(字节) 当前MFT表项长度,到FFFFFF的长度+4
- /*+0x1C*/ uint32 BytesAllocated; // 文件记录分配大小(字节)
- /*+0x20*/ uint64 BaseFileRecord; // = 0 基础文件记录 File reference to the base FILE record
- /*+0x28*/ uint16 NextAttributeNumber; // 下一个自由ID号
- /*+0x2A*/ uint16 Pading; // 边界
- /*+0x2C*/ uint32 MFTRecordNumber; // windows xp中使用,本MFT记录号
- /*+0x30*/ uint32 MFTUseFlags; // MFT的使用标记
- }FILE_RECORD, *pFILE_RECORD;
这里主要关注的就是文件头大小(0x38)可以找到第一个属性地址,紧跟在后面的是文件类型,00表示被删除的文件,01表示正常文件,02表示删除目录,03表示正常目录.再后面就是这个MFT记录的数据大小(很奇怪,为什么数据大小是从头到0xFFFFFFFF的大小+4,这个值为什么不是直接从头到0xFFFFFFFF的大小呢?).
根据FILE头部数据找到下面的一个个属性,接下来分析的就是一个个属性了.
属性由属性头跟属性体组成,属性头的结构定义如下:
- // 属性头
- typedef struct
- {
- /*+0x00*/ ATTRIBUTE_TYPE AttributeType; // 属性类型
- /*+0x04*/ uint16 RecordLength; // 总长度(Header+body长度)
- /**0x06*/ uint16 unknow0;
- /*+0x08*/ uchar Nonresident; // 非常驻标志
- /*+0x09*/ uchar NameLength; // 操作属性名长度
- // 0X0001为压缩标记
- // 0X4000为加密标记
- // 0X8000为系数文件标志
- /*+0x0A*/ uint16 NameOffset; // 属性名偏移(从属性起始位置的偏移)
- // NameLength 如果不为零,则用这个值去寻址数据偏移
- /*+0x0C*/ uint16 Flags; // ATTRIBUTE_xxx flags.
- /*+0x0E*/ uint16 AttributeNumber; // The file-record-unique attribute instance number for this attribute.
- } ATTRIBUTE, *PATTRIBUTE;
- // 属性头
- typedef struct _RESIDENT_ATTRIBUTE
- {
- /*+0x00*/ ATTRIBUTE Attribute; // 属性
- /*+0x10*/ uint32 ValueLength; // Data部分长度
- /*+0x14*/ uint16 ValueOffset; // Data内容起始偏移
- /*+0x16*/ uchar Flags; // 索引标志
- /*+0x17*/ 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;
- // 基础信息ATTRIBUTE.AttributeType == 0x10
- 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;
- // 属性列表ATTRIBUTE.AttributeType == 0x20
- typedef struct _ATTRIBUTE_LIST
- {
- ATTRIBUTE_TYPE AttributeType;
- uint16 Length;
- uchar NameLength;
- uchar NameOffset;
- uint64 StartVcn; // LowVcn
- uint64 FileReferenceNumber;
- uint16 AttributeNumber;
- uint16 AlignmentOrReserved[3];
- } ATTRIBUTE_LIST, *PATTRIBUTE_LIST;
- // 文件属性ATTRIBUTE.AttributeType == 0x30
- typedef struct
- {
- /*+0x00*/ uint64 DirectoryFile:48; // 父目录记录号(前个字节)
- /*+0x06*/ uint64 ReferenceNumber:16; // +序列号(与目录相关)
- /*+0x08*/ uint64 CreationTime; // 文件创建时间
- /*+0x10*/ uint64 ChangeTime; // 文件修改时间
- /*+0x18*/ uint64 LastWriteTime; // MFT更新的时间
- /*+0x20*/ uint64 LastAccessTime; // 最后一次访问时间
- /*+0x28*/ uint64 AllocatedSize; // 文件分配大小
- /*+0x30*/ uint64 DataSize; // 文件实际大小
- /*+0x38*/ uint32 FileAttributes; // 标志,如目录\压缩\隐藏等
- /*+0x3C*/ uint32 AlignmentOrReserved; // 用于EAS和重解析
- /*+0x40*/ uchar NameLength; // 以字符计的文件名长度,没字节占用字节数由下一字节命名空间确定
- // 文件名命名空间, 0 POSIX大小写敏感,1 win32空间,2 DOS空间, 3 win32&DOS空间
- /*+0x41*/ uchar NameType;
- /*+0x42*/ wchar Name[1]; // 以Unicode方式标识的文件名
- } FILENAME_ATTRIBUTE, *PFILENAME_ATTRIBUTE;
- // 数据流属性 ATTRIBUTE.AttributeType == 0x80
- typedef struct _NONRESIDENT_ATTRIBUTE
- {
- /*+0x00*/ ATTRIBUTE Attribute;
- /*+0x10*/ uint64 StartVcn; // LowVcn 起始VCN 起始簇号
- /*+0x18*/ uint64 LastVcn; // HighVcn 结束VCN 结束簇号
- /*+0x20*/ uint16 RunArrayOffset; // 数据运行的偏移
- /*+0x22*/ uint16 CompressionUnit; // 压缩引擎
- /*+0x24*/ uint32 Padding0; // 填充
- /*+0x28*/ uint32 IndexedFlag; // 为属性值分配大小(按分配的簇的字节数计算)
- /*+0x30*/ uint64 AllocatedSize; // 属性值实际大小
- /*+0x38*/ uint64 DataSize; // 属性值压缩大小
- /*+0x40*/ uint64 InitializedSize; // 实际数据大小
- /*+0x48*/ 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"), //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; // 簇流长度 *8*512 才是得到的文件字节数
- uchar StartVcnLen:4; // 簇流起始位置--簇号
- uchar Data[1]; // 簇流长度&Data + 簇流起始位置&Data+VcnLen 数据部分
- }VCN_FLASH, *PVCN_FLASH;
- uint64 make_uint64(uchar* buf, int lenth)
- {
- int64 ui=0;
- if (lenth > 8)
- {
- return (int64)0;
- }
- for (int i=0; i<lenth; 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<lenth; i++)
- {
- ui = (buf[i]<<8*i)|ui;
- }
- // 判断符号位,为负则需减取反
- if (buf[lenth-1] >= 0x80)
- {
- int64 xorval = 0;
- for (i=0; i<lenth; i++)
- {
- xorval = xorval|(0xFF<<8*i);
- }
- ui = -((ui - 1)^xorval);
- }
- return ui;
- }
0x90 AttributeIndexRoot 属性 ( 目录索引B+树结构 )
这个属性,我也不是很清楚,等弄清楚了,再接着完善.................