大多数的 flash驱动器的文件系统都采用 FAT 格式。下面介绍下这种系统格式, FAT 系统由三个主要部分构成:保留区域、 表 (FAT 区域 ) 和数据区域。保留区域是 FAT 系统的总体描述。 FAT 区域则保存了文件系统的所有簇的使用状况, 即处于空闲状态的可用、已被使用和已损坏这三种状态。数据区域则保存了文件的数据,如一个文本文件的数据是 "abc" ,这三个数据就保存在这个区域。
FAT 文件系统:
1 保留区域;
2 FAT 区域;
3 数据区域。
因为 FAT版本的原因,总共出现了三种 FAT 格式 : Fat12 , Fat16 ,和 Fat32 。这三种版本的保留区域的差别是从第 37个字节开始的。下表列出了相同区域的字段的意义。
偏移量 |
字段描述 |
必须 的 ? |
0-2 |
启动文件系统载入器 (loader)的 汇编代码 (即转到 loader 的起始位置 ) |
不 |
3-10 |
操作系统名称 ( ASCII 形式 ) |
不 |
11-12 |
磁盘扇区的字节数 |
是 |
13 |
一簇的扇区数。这个数许是 2的指数幂,且不能超过 32 Kbytes 。 |
是 |
14-15 |
保留区域占用的扇区数。 |
是 |
16 |
FAT拷贝数。通常是 2 。 |
是 |
17-18 |
根目录的文件数目,在 FAT32格式中它的值是 NULL ,无实际意义。 |
是 |
19-20 |
文件系统的扇区数。如果这个值超过了 65535 ,则其值是 NULL,第 32 到35 字节存储了文件系统的大小。 |
是 |
21 |
文件系统所在的存储介质的类型, 0xf0 表明其实可移动数据介质。 |
不 |
22-23 |
一个FAT 拷贝所占用的扇区数。在 FAT 32 文件系统中,这区域不用。 |
是 |
24-25 |
一个磁道的扇区数。 |
不 |
26-27 |
磁头数。 |
不 |
28-31 |
进入分区前的扇区数,即系统引导区大小。 |
不 |
32-35 |
文件占用的扇区数。是对 19-20 的补充 。 |
是 |
第三列“必须”的意思是:这个字段对于文件系统的分析是必须的么?
下面看看 FAT12/FAT16 的一些字段的意义。
36 |
BIOS int13h 磁盘号。 |
不 |
37 |
不用。 |
不 |
38 |
如果其值是 0x29 ,则下一个值有效。 |
不 |
39-42 |
卷数 |
不 |
43-53 |
卷标 |
不 |
54-61 |
文件系统标签。 它可以是虚假值。 |
不 |
62-511 |
不用。 |
不 |
FAT3 2文件系统。
40-41 |
更新模式 ( 并不是所有的表都可以更新 ) 。 |
是 |
42-43 |
主要版本号以及其他版本号。 |
是 |
44-47 |
根目录的簇数。 |
是 |
48-49 |
FSINFO 结构的起始扇区。 |
不 |
50-51 |
引导扇区的程序的副本的起始扇区。 |
不 |
52-63 |
保留不用。 |
不 |
64 |
BIOS int13h 磁盘号。 |
不 |
65 |
保留不用。 |
不 |
66 |
如果值为 0x29 ,则后面三个参数可用。 |
不 |
67-70 |
卷序列号 。 |
不 |
71-81 |
卷标。 |
不 |
82-89 |
文件系统标识。 |
不 |
90-511 |
保留不用。 |
不 |
紧邻保留区域的下一个区域是 FAT 区域。这个区域包含一个或多个FAT表。 表的 数目以及它们的大小等信息在保留区域可以查找到。FAT表中记录了每个簇的使用状况。 如果记录数值 是 100 则说明某个文件用到了簇号为100的簇,记录数值为500则说明用到了簇号为500的簇,以此类推。簇号为0和1的簇在数据区域没有使用,但是在FAT表中有它们的定义。在 FAT12, 、 FAT16 和 FAT32 这三个文件系统中,FAT表中每个记录的占用的空间大小分别是 12、 16 和 32 比特。如果表中某个记录的值为NULL,则说明这个簇没有被分配使用。
如果某个记录的值如下表所示,则说明其为坏簇:
FAT格式 |
坏簇值 |
Fat12 |
0xff7 |
Fat16 |
0xfff7 |
Fat32 |
0x0ffffff7 |
一个文件的大小不定,所以就可能占用多个簇。每个文件使用一个FAT表,每个FAT表记录了一个文件占用的所有簇的号,这些簇号按照文件数据存储的顺序连接起来,就构成了一个链表。例如,一个文件1.txt,其大小是1200字节,每个簇的大小是512字节,则这个文件就要占用三个簇。则FAT表就要依次存储这个簇的号(id),这样程序读取文件的数据时就可以根据这些簇号依次把各个簇的数据读取完毕后,这个的数据就加载完毕了。这里还有一个问题,程序根据这个FAT表的簇号依次读取每个簇,则程序怎么知道一个簇读取完毕后这个文件就算读取完毕了?一般地,每个一个记录值为下列表中值,则说明已经读到文件的末尾了。
末尾取值 |
|
Fat12 |
0xfff |
Fat16 |
0xffff |
Fat32 |
0x0fffffff |
数据区域
至于数据区, FAT12/16 和 FAT32 二者之间有着根本的区别。 FAT12/16 文件系统中,文件的的数据区域起始自根目录区,我们很容易地根据根目录的记录的数目以及每个记录的大小计算出数据区域的大小。
FAT32
FAT12/16
1 保留区域;
2 FAT 区域;
3 数据区域;
4 根目录。
每个文件或目录在根目录中都有一个相应的32自己的记录。下表描述了每个字节的意义。
偏移量 |
字段意义 |
0 |
文件名称的第一部分。取值为 0x00 或 0xe5 。 |
1-10 |
文件名称。 |
11 |
属性。 |
12 |
保留未用。 |
13 |
创建时间(一秒的十分之几)。 |
14-15 |
文件创建时间 (hh:mm:ss) 。 |
16-17 |
文件创建日期。 |
18-19 |
最近一次访问时间。 |
20-21 |
第一个簇的两个高字节。 |
22-23 |
最近一次修改时间 (hh:mm:ss) 。 |
24-25 |
最近一次访问时间。 |
26-27 |
第一个簇的两个低字节。 |
28-31 |
文件大小。 |
用于实际数据恢复的代码片段
当需要恢复实际数据时,需要先做一个文件系统的镜像。首先,需要知道与当前计算机连接的flash驱动有几个。下面的代码执行了这个任务:
RemovableDeviceInfo_vt Functions::SearchRemovalDisks()
{
RemovableDeviceInfo_vt devInfos; //result
/*GUID_DEVCLASS_DISKDRIVE*/
CONST CLSID CLSID_DeviceInstance = { 0x4D36E967, 0xE325, 0x11CE, { 0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18 } }; // removable disk guid
HDEVINFO hDevInfo = ::SetupDiGetClassDevs(&CLSID_DeviceInstance, NULL, NULL, DIGCF_PRESENT); //getting all devices with a removable disk guid
if ( INVALID_HANDLE_VALUE == hDevInfo )
{
return devInfos;//exit if there are no devices
}
try
{
std::wstring name;
RemovableDeviceInfo devInfo;
SP_DEVINFO_DATA devInfoData;
devInfoData.cbSize = sizeof(devInfoData);
for ( DWORD dwCount = 0; ::SetupDiEnumDeviceInfo(hDevInfo, dwCount, &devInfoData); ++dwCount ) // enumerating all devices
{
DWORD dwSize = 0;
DWORD dwDataType = 0;
DWORD dwRemovalPolicy = 0;
// [Warning]: only Windows XP and later versions
if ( ::SetupDiGetDeviceRegistryProperty(hDevInfo, &devInfoData, SPDRP_REMOVAL_POLICY, &dwDataType, (PBYTE)&dwRemovalPolicy, sizeof(dwRemovalPolicy), &dwSize) )//Getting information about device from registry
{
if ( CM_REMOVAL_POLICY_EXPECT_NO_REMOVAL != dwRemovalPolicy )
{
RemovableDeviceInfo devInfo;
//Getting information for the current device
devInfo.wsDeviceDesc = GetDeviceRegistryProperty(hDevInfo, &devInfoData, SPDRP_DEVICEDESC );
devInfo.wsEnumeratorName = GetDeviceRegistryProperty(hDevInfo, &devInfoData, SPDRP_ENUMERATOR_NAME);
devInfo.wsCompatibleIDs = GetDeviceRegistryProperty(hDevInfo, &devInfoData, SPDRP_COMPATIBLEIDS );
devInfo.wsHardwareID = GetDeviceRegistryProperty(hDevInfo, &devInfoData, SPDRP_HARDWAREID );
devInfo.wsMFG = GetDeviceRegistryProperty(hDevInfo, &devInfoData, SPDRP_MFG );
devInfo.wsFriendlyName = GetDeviceRegistryProperty(hDevInfo, &devInfoData, SPDRP_FRIENDLYNAME );
devInfo.wsDevInterfaceVolume = GetDevInterfaceVolume(&devInfoData);
devInfo.wsPath = GetDevicePath (&devInfoData);
devInfo.deviceType = GetDeviceType (&devInfoData);
devInfo.vHarddiskIndexes = GetHarddiskIndexes(devInfo.wsDevInterfaceVolume);
if ( !devInfo.vHarddiskIndexes.empty() )
{
devInfo.diskGeometry = GetDeviceGeometry(devInfo.wsDevInterfaceVolume);
}
devInfos.push_back(devInfo);
}
}
}
::SetupDiDestroyDeviceInfoList( hDevInfo );
return devInfos;
}
catch (...)
{
::SetupDiDestroyDeviceInfoList( hDevInfo );
throw;
}
}
这个函数可以得到所有GUID值为 GUID REMOVABLE DISK 的设备信息。然后,从注册表中读取每个设备的信息。最后得到的结果就是一个向量表(vector),表中的每个数据成员描述了相应设备的信息。我们可以通过这样的字符连接到物理磁盘的镜像: //./PhysicalDriveX( X是磁盘的序号 ) 。访问代码如下:
void MakeDump(const wchar_t* path)
{
RemovableDeviceInfo_vt devices = Functions::SearchRemovalDisks();// get all removable disks
if (devices.empty())
{
std::cout << "Mass storage devices was not found/n";
return;
}
std::cout << "Please enter device number/n";
for (size_t i = 0; i < devices.size(); ++i)
{
std::cout << i << ". ";
std::cout << devices.at(i).wsFriendlyName.c_str()<< L"/n";
}
int driveIndex;
std::cin >> driveIndex;//selecting a disk
std::vector<unsigned char> buffer;
//creating a path
std::wstring dumpPath(L"////.//PhysicalDrive");
wchar_t index[MAX_PATH];
_itow(devices.at(driveIndex).vHarddiskIndexes[0], index , MAX_PATH);
dumpPath.append(index);
//opening mass storage for reading
//requires administrator privilege
HANDLE hDump( ::Create File (
dumpPath.c_str(),
GENERIC_READ,
FILE _SHARE_READ | FILE _SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE _ATTRIBUTE_NORMAL | FILE _FLAG_BACKUP_SEMANTICS,
NULL) );
//opening of file to save a dump
//requires administrator privilege
HANDLE h File = ::Create File (path,
GENERIC_WRITE,
0,
0,
CREATE_NEW,
FILE _ATTRIBUTE_NORMAL,
0);
DiskGeometry diskGeometry = devices.at(driveIndex).diskGeometry;
DWORD dwRead = 0;
DWORD dwMb = (1024*1024);
LARGE_INTEGER liFullSize = {0,0};
LARGE_INTEGER liTotalRead = {0,0};
DWORD dwSize = diskGeometry.BytesPerSector * diskGeometry.SectorsPerTrack;
//Getting the size of a removable disk
liFullSize.QuadPart = GetLogicalDriveSize(hDump);
dwSize = diskGeometry.BytesPerSector;
std::vector<unsigned char> tempBuff(dwSize, 0x00);
double dProgrVal = 0.0;
double dProgrStep = 100.0 / (liFullSize.QuadPart) * dwSize;
// reading from removable disk and writing to a dump file
while ( (liFullSize.QuadPart > liTotalRead.QuadPart) && ::Read File(hDump, &tempBuff.at(0), dwSize, &dwRead, NULL) && dwRead )
{
DWORD dwBytesWritten;
if ( ! ::Write File (h File , &tempBuff.front(), tempBuff.size(), &dwBytesWritten, 0) )
{
throw std::exception("Could not write");
}
dProgrVal += dProgrStep;
std::cout << dProgrVal<<'/n';
liTotalRead.QuadPart += dwRead;
}
::CloseHandle(hDump);
::CloseHandle(h File );
}
需要说明的是,函数 GetFileSize 可能返回一个无效值。下面的函数则会返回一个文件大小的有效值:
ULONGLONG GetLogicalDriveSize(HANDLE hDrive)
{
PARTITION_INFORMATION lpOutBuffer;
DWORD lpBytesReturned = 0;
if(!DeviceIoControl(hDrive,
IOCTL_DISK_GET_PARTITION_INFO,
NULL,
0,
&lpOutBuffer,
(DWORD)sizeof(lpOutBuffer),
&lpBytesReturned,
NULL))
throw std::exception("Could not get file size");
return lpOutBuffer.PartitionLength.QuadPart;
}
现在,我们通过上述代码已经得到了文件数据的一个镜像,下面就可以根据这些数据进行分析了。
你可以在一个flash盘上建立一个文件,并在其中写上一些数据,然后删除它。然后利用利用上面的示例把这个磁盘的数据转储出来,或者说做一个镜像加载到内存中,通过分析把这个文件的数据恢复出来。我的flash盘的文件系统格式是FAT16,下面首先从保留区域开始分析。
保留区域。用 FAT文件浏览器观察数据。 如果想以十六进制形式观察数据,可以点击 F3;如果想以十六进制形式观察其内容,点击 F4 。
在上面的图片中,我把我们可能感兴趣的区域都用红圈圈起来了。第一个圈子中的两个字节显示扇区的大小事0x0200 字节 。第二个圈子中的值显示每个簇包含了一个扇区。第三个圈子中的数据显示保留区域占用了两个扇区。紧接着的红圈中的数据显示了磁盘上存在两个FAT表。下一个值说明了我的flash盘的根目录的最大值是 0x0200 。最后一个值显示出每个FAT 表 的大小 是 0x00f3 个扇区。根据这些数据,我们就可以分析出我的flash的整个文件系统了。
1 保留区域;
2 第一个 FAT表;
3 第二个 FAT表;
4 根目录的开始地址;
5 数据区域的开始地址。
启动区域占用了两个扇区,每个扇区的大小事 0x200 字节,所以第一个FAT表的其实地址是 2*0x200=0x400 。每个FAT表的大小是 0xF3 个扇区或 0xF3*0x200= 0x1E600 字节,这意味着第二个FAT表的起始地址是0x400+0x1E600=0x1EA00 ,根目录的起始地址是 0x1EA00+0x1E600=0x3D000 。根目录最多可以存储 0x200 个记录,每个记录大小事 0x20 字节,所以保留区域的大小是 0x20*0x200=0x4000 ,数据区的起始地址是 0x41000 。
上图 FAT的 Hex View 显示了根目录的内容。
让我们从根目录的开始地址处开始分析。可以知道文件系统中有三个文件,但是都被删除掉了。也可以清楚地看到每个记录(文件)的第一个字节。对应没有删除的文件,记录的第一个字节是文件名称的第一个字符,如果这个记录已经删除掉了,则这个字节值为 0xE5 。
我们尝试下恢复第三个文件。第二个高亮显示的数据显示文件的第一个簇的地址是 0x02 ,文件的大小是0x00005600 ,即文件占用了 0x00005600/0x200 = 0x2B 个簇。如果这个文件没有删除,我们就可以根据这个值到FAT表中找到这个簇后面的各个簇,从而把文件的数据给读取出来。但问题是这个簇后面的簇的地址因为文件已经被删除而无法知晓了,换句话说就是这个文件的簇的链表不存在了。删除文件时,文件占用的所有的簇都被标记为可用(处于空闲状态)。这就解释了我们能够获取文件的第一个簇和文件大小。 . 这种情况下有两种方法恢复数据:
1 如果文件的所有簇是顺次相连的,这种情况下只要文件系统还没有被破坏,则文件 是 可恢复的。
2 文件第一个簇紧跟着的后面的所有的可用簇(处于空闲状态的簇)都 被 认为是这个文件的,当然实际情况是从第一个簇开始把后面的处于空闲状态的簇依次读取出来,读到的数据量等于文件的大小 时 停止。如果文件 是刚删除而且 文件的各个簇 还没有写上新的数据,则这种方法是可用的。
如果使用了各种方法都无法恢复文件的数据,最后的方法就是上面所说的第一个方法。第一个簇的地址是 0x41000,则最后的簇的地址是 0x41000+0x5600=0x46600 。
范例运行时的情景
工程运行完后,一个文件被创建:D:/hello.doc 。用 MS Word 打开它:
恢复后的文件数据
至于 FAT32 文件系统的数据恢复,跟上面的方法有一些不同。主要不同的地方在于根目录不是在数据区的开始区域,其起始地址在保留区域的相关字段中保存着。