在数字时代,数据已成为我们生活和工作中不可或缺的一部分。无论是珍贵的家庭照片、重要的工作文档,还是多年的研究成果,它们的意外丢失都可能带来无法估量的损失和困扰。U盘(USB闪存驱动器)作为一种便捷的存储介质,因其便携性而被广泛使用,但也常常成为数据丢失的“重灾区”。不当的插拔、病毒攻击、文件系统损坏等原因都可能导致U盘中的数据无法访问,甚至出现大量的 FILExxxx.CHK
文件,让用户束手无策。此外,误删除文件也是一个常见的数据丢失场景。
当Windows的磁盘检查工具(CHKDSK
)在修复文件系统错误时,它可能会将找到的但无法归属到正确文件或目录的数据片段保存为 .CHK
文件,存放在 FOUND.xxx
文件夹中。这些 CHK
文件本身包含了原始数据的残片,但失去了文件名和目录结构,使得直接识别和使用它们变得异常困难。同样,当文件被“删除”时,操作系统通常只是在文件系统中做了一些标记(例如,在FAT文件系统中将目录条目的首字节改为0xE5
,在NTFS中标记MFT条目为未使用),而实际的数据块在被新数据覆盖之前仍然存在于磁盘上。
传统的商业数据恢复软件虽然功能强大,但往往价格不菲,且其内部工作原理对用户来说是一个黑箱。而Python作为一种功能强大、语法简洁且拥有丰富库支持的编程语言,为我们提供了一个 уникальный视角和工具集,使我们能够深入理解数据恢复的底层机制,并亲手编写脚本来尝试恢复丢失的数据。通过Python,我们可以直接与磁盘的原始字节流交互,解析文件系统结构,识别文件签名,甚至尝试重建损坏的文件片段。
在尝试恢复任何数据之前,我们必须对数据是如何被存储以及如何被管理的有一个清晰且深入的理解。这包括了物理存储介质的基本工作方式,以及操作系统用于组织和访问这些数据的逻辑结构——即文件系统。
尽管我们主要关注U盘,但其底层存储原理与其他基于闪存的设备(如SSD)以及传统的机械硬盘(HDD)有共通之处,也有其特性。
机械硬盘 (HDD - Hard Disk Drive):
固态驱动器 (SSD - Solid State Drive) 与 U盘 (USB Flash Drive):
文件系统是操作系统用于明确存储设备(或分区)上的文件的方法和数据结构;即在存储设备上组织文件的方法。它使得用户能够以文件名和目录(文件夹)的层次结构来创建、访问、修改和删除数据,而无需关心数据在物理介质上的具体存储位置和细节。
文件系统的主要功能包括:
我们将重点关注在U盘上常见的文件系统:FAT32、exFAT,并简要提及NTFS,因为NTFS也可能出现在大容量U盘或移动硬盘上。
FAT是最早也是最简单的文件系统之一,因其良好的兼容性,在U盘、存储卡等移动存储设备上仍被广泛使用。主要有FAT12, FAT16, FAT32和exFAT等版本。
核心组件:
引导扇区 (Boot Sector) / DBR (DOS Boot Record):
文件分配表 (FAT - File Allocation Table):
0x0000
(FAT12/16) or 0x00000000
(FAT32): 表示该簇是空闲的,可用于存储新数据。0xFFF7
(FAT12/16) or 0x0FFFFFF7
(FAT32): 表示该簇是一个坏簇,不应使用。0xFFF8
- 0xFFFF
(FAT12/16) or 0x0FFFFFF8
- 0x0FFFFFFF
(FAT32): 表示该簇是文件中最后一个簇(EOF - End Of File)。根目录区 (Root Directory Area):
数据区 (Data Area):
目录条目 (Directory Entry):
无论是根目录还是子目录,它们的内容都是一系列32字节的目录条目。
短文件名目录条目 (SFN - Short File Name):
0x01
: 只读 (Read-only)0x02
: 隐藏 (Hidden)0x04
: 系统 (System)0x08
: 卷标 (Volume Label) - 特殊条目,表示分区名0x10
: 子目录 (Subdirectory)0x20
: 存档 (Archive)长文件名目录条目 (LFN - Long File Name):
0x0F
(只读 | 隐藏 | 系统 | 卷标)。0x0F
的条目,只读取SFN条目。文件存储与分配:
文件删除 (FAT):
0xE5
(一个特殊的标记,表示已删除)。对于LFN条目,它们的首字节也会被标记为 0xE5
,但它们的校验和仍然有效。0xE5
),我们就能知道它的起始簇号和文件大小。FAT32的特点:
exFAT (Extended File Allocation Table):
NTFS是现代Windows操作系统(如Windows NT, 2000, XP, Vista, 7, 8, 10, 11)默认的文件系统。它比FAT复杂得多,提供了许多高级特性,如日志记录、安全性(ACLs)、压缩、加密、硬链接、稀疏文件等。大容量U盘或移动硬盘也可能格式化为NTFS。
核心组件:
引导扇区 (Boot Sector) / VBR (Volume Boot Record):
主文件表 (MFT - Master File Table):
$MFT
)。FILE
或BAAD
表示坏记录)、更新序列号(用于保证写入一致性)、此记录的MFT条目号、硬链接计数、序列号(每次重用MFT记录时递增)等。$STANDARD_INFORMATION (0x10)
: 包含文件的基本信息,如创建时间、修改时间、最后访问时间、文件属性(只读、隐藏、系统、存档等)、所有者ID、安全ID。$ATTRIBUTE_LIST (0x20)
: (可选) 如果一个文件的所有属性无法容纳在一个MFT记录中,此属性会列出其他包含该文件属性的MFT记录。$FILE_NAME (0x30)
: 包含文件名(Unicode)、父目录的MFT引用、文件的时间戳(可能与$STANDARD_INFORMATION
中的不同步)、文件分配大小、实际大小、文件属性等。一个文件可以有多个$FILE_NAME
属性(例如,一个长文件名和一个兼容DOS的短文件名,或者硬链接)。$VOLUME_NAME (0x60)
: (用于$Volume
文件) 卷标名。$DATA (0x80)
: 包含文件的实际内容。这是最重要的属性。它可以是常驻的(对于非常小的文件)或非常驻的。$INDEX_ROOT (0x90)
和 $INDEX_ALLOCATION (0xA0)
: 用于实现目录(B+树索引)。目录的内容(即其包含的文件和子目录的列表)被组织成索引。小目录的索引可能完全常驻在$INDEX_ROOT
中;大目录则使用$INDEX_ALLOCATION
指向数据区中的索引块。$BITMAP (0xB0)
: (用于$Bitmap
文件和目录的索引) 位图,用于跟踪簇的分配情况或索引条目的使用情况。NTFS的$Bitmap
文件(MFT记录号通常是6)就使用此属性来记录卷上所有簇的分配状态(已用或空闲)。$
开头的特殊文件(元数据文件)组成,它们都有自己的MFT记录。例如:
$MFT
(记录0): 主文件表本身。$MftMirr
(记录1): MFT的前几个(通常是4个)记录的副本,用于灾难恢复。$LogFile
(记录2): 日志文件,用于记录文件系统操作,保证文件系统的一致性(类似于事务日志)。$Volume
(记录3): 包含卷名、NTFS版本和卷状态等信息。$AttrDef
(记录4): 定义了卷上所有允许的属性类型及其特性。$Bitmap
(记录6): 卷簇分配位图。$Boot
(记录7): 引导扇区(VBR)。$BadClus
(记录8): 坏簇表。$Secure
(记录9): 安全描述符数据库。$UpCase
(记录10): Unicode字符大写转换表。$Extend
(目录,记录11): 包含如$Quota
, $ObjId
, $Reparse
等可选的扩展文件系统功能文件。文件存储与分配 (NTFS):
$STANDARD_INFORMATION
, $FILE_NAME
, $DATA
)被添加到MFT记录中。$DATA
属性是非常驻的,NTFS会查询$Bitmap
文件找到空闲的簇,并将这些簇分配给文件,更新$Bitmap
,然后在$DATA
属性中记录数据运行信息。$INDEX_ROOT
和$INDEX_ALLOCATION
属性实现的B+树。当向目录添加文件时,会更新这个索引。文件删除 (NTFS):
$FILE_NAME
属性中的文件名通常保持不变,但其在目录索引中的链接断开了。$FILE_NAME
属性可以提供原始文件名和路径信息。$DATA
属性的数据运行信息是定位实际文件内容的关键。无论使用何种文件系统,数据恢复都遵循一些基本原则:
理解了这些基础知识,我们就能更有针对性地去分析CHK文件是如何产生的,以及如何尝试恢复它们和其它已删除的文件。
当Windows用户遇到文件系统错误,例如U盘无法正常读取、提示需要格式化、或者文件和目录出现异常时,一个常用的工具就是 CHKDSK
(Check Disk)。这个命令行工具会扫描磁盘分区的文件系统结构,查找并尝试修复错误。在这个修复过程中,CHKDSK
可能会创建 FOUND.xxx
文件夹,并在其中生成一系列名为 FILExxxx.CHK
的文件。
CHKDSK
是Windows操作系统内置的一个磁盘检查和修复工具。它可以用于检查FAT (FAT12, FAT16, FAT32), exFAT, 和NTFS文件系统的完整性。
启动方式:
chkdsk <驱动器盘符>: [参数]
例如 chkdsk E: /f
主要参数:
<驱动器盘符>:
: 指定要检查的驱动器,例如 E:
。/f
: 修复磁盘上的错误。如果省略此参数,CHKDSK
只会报告错误,不进行修复。如果驱动器正在被使用(例如系统盘或包含打开的文件),/f
参数可能需要重启计算机才能执行。/r
: 定位坏扇区并恢复可读信息。此参数包含了 /f
的功能。扫描坏扇区是一个非常耗时的过程。/x
: (与 /f
一起使用时) 强制卸载卷(如果需要)。/v
: (在FAT/FAT32上) 显示磁盘上每个文件的完整路径和名称。(在NTFS上) 显示清除消息(如果有)。/scan
: (NTFS专用) 运行联机扫描,不需要卸载卷。/spotfix
: (NTFS专用) 运行联机点修复,同样不需要卸载卷。CHKDSK 的工作阶段 (以NTFS为例,FAT类似但简化):
CHKDSK
在修复模式下通常会经历多个阶段:
$MFT
, $Bitmap
等)的一致性。/r
参数): 扫描用户文件数据中的坏簇。/r
参数): 验证未分配簇的列表是否准确。当CHKDSK
在扫描和修复过程中发现文件系统存在以下类型的错误时,可能会生成CHK文件:
丢失的分配单元 (Lost Allocation Units / Lost Clusters):
$Bitmap
中标记某些簇为“已使用”,但在任何目录条目或MFT记录中都找不到对这些簇的引用时,这些簇就被认为是“丢失的”。这意味着数据存在,但文件系统不知道它们属于哪个文件。CHKDSK
无法确定这些丢失簇的原始文件名或它们在文件中的逻辑顺序(如果是多个不连续的丢失簇)。CHKDSK
会将每个找到的连续的丢失簇序列(或单个丢失簇)恢复为一个单独的 .CHK
文件。交叉链接的文件 (Cross-linked Files):
CHKDSK
可能会尝试解决交叉链接。一种做法是复制共享的簇,为其中一个文件创建新的副本,然后将另一个文件指向原始簇。或者,它可能会将共享的簇数据保存为一个 .CHK
文件,并修复两个文件的分配信息,使其不再共享这些簇(这可能导致其中一个或两个文件数据不完整)。无效的目录条目或MFT记录:
CHKDSK
发现一个目录条目或MFT记录引用了一些数据簇,但该条目/记录本身已损坏到无法完全重建其原始文件关联,它可能会将这些数据簇恢复为 .CHK
文件。损坏的目录结构:
$INDEX_ROOT
/ $INDEX_ALLOCATION
)损坏,CHKDSK
可能无法正确遍历目录树。.CHK
文件。本质: FILExxxx.CHK
文件 不是 某种特殊格式的文件。它们是 CHKDSK
从磁盘上直接提取出来的 原始数据块。这些数据块可能是任何类型文件的片段,例如文本文件的一部分、JPEG图像的一部分、程序代码的一部分、数据库文件的一部分,甚至是文件系统元数据本身的片段。
.CHK
文件通常对应于 CHKDSK
找到的一个或多个连续的簇。CHKDSK
恢复的那些簇的总大小。命名:
FILE0000.CHK
, FILE0001.CHK
, FILE0002.CHK
,以此类推,序号递增。存储位置:
CHKDSK
会在被检查驱动器的根目录下创建一个或多个名为 FOUND.xxx
的隐藏文件夹(例如 FOUND.000
, FOUND.001
等),并将生成的 .CHK
文件存放在这些文件夹中。CHKDSK
并且每次都生成了CHK文件,可能会看到多个 FOUND.xxx
文件夹。直接使用这些 .CHK
文件通常是不可行的,因为:
FILE0000.CHK
原本是一个文档、一张图片,还是一个可执行文件。双击打开它,系统可能会提示选择程序,或者用错误的程序打开导致乱码。.CHK
文件可能只包含原始文件的一部分,特别是如果原始文件是碎片化的,而 CHKDSK
只找到了其中一个片段。.CHK
文件,你无法知道这些 .CHK
文件的正确顺序来将它们拼接回原始文件。CHKDSK
可能会生成成百上千个 .CHK
文件,手动检查它们是不现实的。因此,恢复CHK文件的核心任务是 识别这些原始数据块的内容类型,并尝试将它们恢复到可用的状态。
CHKDSK
的“修复”?虽然 CHKDSK
的目标是修复文件系统使其恢复可用状态,但它的“修复”有时是以牺牲部分数据为代价的。
CHKDSK
将数据保存为 .CHK
文件时,它实际上已经承认无法将这些数据完美地放回原始的文件结构中。CHKDSK
的修复可能会导致其中一个或多个文件内容损坏或不完整。CHKDSK
可能无法找到所有的文件数据,或者错误地“修复”它们。因此,如果数据非常重要,在运行 CHKDSK /f
或 CHKDSK /r
之前,强烈建议先尝试使用数据恢复软件(或我们即将学习的Python脚本)对原始状态的驱动器(或其镜像)进行数据提取。 如果已经运行了 CHKDSK
并生成了 .CHK
文件,那么这些 .CHK
文件就成了我们最后的希望之一。
理解了 CHKDSK
和 CHK
文件的这些背景知识后,我们就可以开始思考如何用Python来处理这些“孤儿”数据了。其核心思路是通过分析 CHK
文件自身的内容来推断其原始类型。
要用Python进行数据恢复,我们首先需要学习如何让Python程序直接访问和读取存储设备的原始数据,而不是通过操作系统提供的文件和目录这种高级抽象。这种底层的磁盘I/O能力是进行文件签名分析、数据雕刻以及文件系统结构解析的基础。
在操作系统层面,物理磁盘和逻辑分区有其特定的表示方法。
Windows:
\\.\PhysicalDriveX
,其中 X
是从0开始的磁盘编号 (例如 \\.\PhysicalDrive0
, \\.\PhysicalDrive1
)。访问物理磁盘通常需要管理员权限。\\.\C:
,\\.\D:
等,其中 C:
或 D:
是分配给该分区的盘符。访问分区的原始数据同样需要管理员权限。直接打开盘符(如open('C:', 'rb')
)通常只能访问文件系统层面的内容,而不是原始扇区。\
是转义字符,所以路径字符串需要写为 \\\\.\\PhysicalDrive0
或 r'\\.\PhysicalDrive0'
。Linux:
/dev/sdX
(对于SATA/SCSI/USB磁盘) 或 /dev/nvmeXnY
(对于NVMe SSD),其中 X
是字母 (a, b, c, …),Y
是数字。例如 /dev/sda
, /dev/sdb
。/dev/sda1
, /dev/sda2
。disk
组)。Python的内置 open()
函数可以用来打开这些设备文件,就像打开普通文件一样,但需要以二进制模式 ('rb'
) 打开进行读取。
io.open()
或内置 open()
'rb'
(二进制读取)。写入 ('wb'
, 'ab'
) 原始设备非常危险,可能导致数据完全丢失,除非你非常清楚自己在做什么(例如,写回恢复的数据到另一个磁盘的镜像文件)。buffering=0
(仅二进制模式) 会禁用缓冲,直接进行系统调用,但这可能不是最高效的。通常,让操作系统处理块设备的缓冲是合理的,或者使用 io.BufferedReader
.在Windows上,由于权限和设备命名的特殊性,直接使用 open()
可能不够,或者需要特定的API调用来获取设备句柄。更可靠的方式是使用 ctypes
模块调用Windows API函数 CreateFileW
。
import ctypes # 导入ctypes模块,用于调用C库函数
import os # 导入os模块,提供操作系统相关功能
# 定义Windows API常量
GENERIC_READ = 0x80000000 # 定义通用读权限常量
GENERIC_WRITE = 0x40000000 # 定义通用写权限常量 (恢复时一般不用,除非写镜像)
FILE_SHARE_READ = 0x00000001 # 定义文件共享读权限常量
FILE_SHARE_WRITE = 0x00000002 # 定义文件共享写权限常量
OPEN_EXISTING = 3 # 定义打开已存在文件的常量
FILE_ATTRIBUTE_NORMAL = 0x80 # 定义普通文件属性常量
FILE_FLAG_NO_BUFFERING = 0x20000000 # 定义无缓冲标志 (读取时可能需要对齐)
FILE_FLAG_SEQUENTIAL_SCAN = 0x04000000 # 定义顺序扫描标志 (提示系统优化)
INVALID_HANDLE_VALUE = -1 # 定义无效句柄值的常量
# Kernel32函数原型定义
# CreateFileW 用于打开或创建文件或I/O设备
CreateFileW = ctypes.windll.kernel32.CreateFileW # 获取CreateFileW函数的引用
CreateFileW.argtypes = [ # 定义CreateFileW函数的参数类型
ctypes.c_wchar_p, # lpFileName: 文件名或设备路径 (宽字符字符串指针)
ctypes.c_uint32, # dwDesiredAccess: 访问权限 (读、写等)
ctypes.c_uint32, # dwShareMode: 共享模式
ctypes.c_void_p, # lpSecurityAttributes: 安全属性 (通常为None)
ctypes.c_uint32, # dwCreationDisposition: 创建方式
ctypes.c_uint32, # dwFlagsAndAttributes: 文件属性和标志
ctypes.c_void_p # hTemplateFile: 模板文件句柄 (通常为None)
]
CreateFileW.restype = ctypes.c_void_p # 定义CreateFileW函数的返回值类型 (句柄)
# ReadFile 用于从文件或I/O设备读取数据
ReadFile = ctypes.windll.kernel32.ReadFile # 获取ReadFile函数的引用
ReadFile.argtypes = [ # 定义ReadFile函数的参数类型
ctypes.c_void_p, # hFile: 文件句柄
ctypes.c_void_p, # lpBuffer: 读取缓冲区指针
ctypes.c_uint32, # nNumberOfBytesToRead: 要读取的字节数
ctypes.POINTER(ctypes.c_uint32), # lpNumberOfBytesRead: 实际读取的字节数 (指针)
ctypes.c_void_p # lpOverlapped: 异步操作结构体指针 (通常为None用于同步)
]
ReadFile.restype = ctypes.c_bool # 定义ReadFile函数的返回值类型 (布尔值,成功或失败)
# SetFilePointerEx 用于设置文件指针位置 (64位偏移)
SetFilePointerEx = ctypes.windll.kernel32.SetFilePointerEx # 获取SetFilePointerEx函数的引用
SetFilePointerEx.argtypes = [ # 定义SetFilePointerEx函数的参数类型
ctypes.c_void_p, # hFile: 文件句柄
ctypes.c_int64, # liDistanceToMove: 要移动的距离 (64位整数)
ctypes.POINTER(ctypes.c_int64), # lpNewFilePointer: 新的文件指针位置 (指针,可选)
ctypes.c_uint32 # dwMoveMethod: 移动方法 (0: 文件开头, 1: 当前位置, 2: 文件末尾)
]
SetFilePointerEx.restype = ctypes.c_bool # 定义SetFilePointerEx函数的返回值类型 (布尔值)
# CloseHandle 用于关闭一个打开的对象句柄
CloseHandle = ctypes.windll.kernel32.CloseHandle # 获取CloseHandle函数的引用
CloseHandle.argtypes = [ctypes.c_void_p] # 定义CloseHandle函数的参数类型 (句柄)
CloseHandle.restype = ctypes.c_bool # 定义CloseHandle函数的返回值类型 (布尔值)
# 尝试获取设备大小的IOCTL (可选,但有助于确定读取范围)
# IOCTL_DISK_GET_LENGTH_INFO = 0x0007405C # 定义获取磁盘长度信息的IOCTL码
# class GET_LENGTH_INFORMATION(ctypes.Structure): # 定义GET_LENGTH_INFORMATION结构体
# _fields_ = [("Length", ctypes.c_int64)] # 结构体字段:Length (64位整数)
# DeviceIoControl = ctypes.windll.kernel32.DeviceIoControl # 获取DeviceIoControl函数的引用
def open_windows_device(device_path): # 定义打开Windows设备的函数
"""
使用CreateFileW打开Windows上的物理驱动器或分区。
例如: r'\\.\PhysicalDrive0' 或 r'\\.\C:'
需要管理员权限。
返回设备句柄,如果失败则返回INVALID_HANDLE_VALUE。
"""
print(f"Attempting to open device: {
device_path} with CreateFileW") # 打印尝试打开设备的信息
handle = CreateFileW(
device_path, # 设备路径
GENERIC_READ, # 请求读权限
FILE_SHARE_READ | FILE_SHARE_WRITE, # 允许其他进程读写该设备 (重要,否则可能打开失败)
None, # 安全属性,通常为None
OPEN_EXISTING, # 只打开已存在的设备
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, # 普通属性并提示顺序扫描
# FILE_FLAG_NO_BUFFERING, # 如果使用无缓冲,读取大小和偏移必须是扇区大小的倍数
None # 模板文件句柄,通常为None
)
if handle == INVALID_HANDLE_VALUE or handle is None: # 如果获取句柄失败
error_code = ctypes.GetLastError() # 获取最后一个错误代码
print(f"Failed to open device {
device_path}. Error code: {
error_code}") # 打印打开失败信息及错误码
# 可以根据error_code提供更具体的错误信息,例如5是权限不足
if error_code == 5: # 如果错误码是5 (拒绝访问)
print("Error 5: Access Denied. Please ensure you are running this script with Administrator privileges.") # 打印权限不足提示
elif error_code == 2: # 如果错误码是2 (找不到文件)
print(f"Error 2: The system cannot find the file specified ({
device_path}). Check the device path.") # 打印找不到文件提示
return INVALID_HANDLE_VALUE # 返回无效句柄
print(f"Successfully opened device {
device_path}. Handle: {
handle}") # 打印成功打开设备的信息
return handle # 返回设备句柄
def close_windows_device(handle): # 定义关闭Windows设备的函数
"""关闭通过CreateFileW打开的设备句柄。"""
if handle != INVALID_HANDLE_VALUE and handle is not None: # 如果句柄有效
print(f"Closing device handle: {
handle}") # 打印关闭句柄信息
closed = CloseHandle(handle) # 调用CloseHandle关闭句柄
if not closed: # 如果关闭失败
error_code = ctypes.GetLastError() # 获取错误码
print(f"Failed to close device handle {
handle}. Error code: {
error_code}") # 打印关闭失败信息
# else:
# print(f"Device handle {handle} closed successfully.") # 打印成功关闭信息 (可省略)
# 示例使用 (需要管理员权限运行此脚本):
# if __name__ == "__main__":
# # 重要: 请极端小心选择device_path_win,错误的操作可能损坏数据
# # 建议先使用U盘的盘符,例如 r'\\.\E:' (假设E盘是你的U盘)
# # 或者物理驱动器号,例如 r'\\.\PhysicalDrive1' (你需要先确定哪个是你U盘)
# # 不要轻易尝试操作你的系统盘 (如 PhysicalDrive0 或 C:)
# device_path_win = r'\\.\E:' # 将E替换为你的U盘盘符
# # device_path_win = r'\\.\PhysicalDriveX' # 将X替换为正确的物理驱动器编号
# print("Requesting Administrator privileges if not already granted...") # 打印请求管理员权限信息
# # 在实际脚本中,你可能需要更复杂的逻辑来检测和请求管理员权限
# # 或者直接提示用户以管理员身份运行
# h_device = open_windows_device(device_path_win) # 调用函数打开设备
# if h_device != INVALID_HANDLE_VALUE: # 如果设备成功打开
# print(f"Device {device_path_win} opened. Ready for I/O operations.") # 打印设备已打开信息
# # 在这里可以进行读取操作,例如读取第一个扇区
# # close_windows_device(h_device) # 操作完成后关闭设备
# else:
# print(f"Could not open device {device_path_win}.") # 打印无法打开设备信息
# print("Make sure the device path is correct and you have Administrator privileges.") # 提示检查路径和权限
关于 FILE_FLAG_NO_BUFFERING
: 如果使用此标志,那么 ReadFile
的缓冲区地址、读取的字节数以及文件指针的偏移量都必须是磁盘扇区大小的倍数。这增加了编程的复杂性,但可以避免操作系统缓存带来的影响,在某些取证场景下可能需要。对于一般的数据恢复读取,使用操作系统的默认缓冲(不设置此标志)通常是可以接受且更简单的。FILE_FLAG_SEQUENTIAL_SCAN
是一个给操作系统的提示,如果打算顺序读取大量数据,它可能有助于优化性能。
在Linux上,过程相对简单,可以直接使用内置的 open()
函数,但同样需要适当的权限(通常是root权限,或用户属于disk
组)。
import os # 导入os模块
def open_linux_device(device_path): # 定义打开Linux设备的函数
"""
打开Linux上的块设备文件,例如 /dev/sdb 或 /dev/sdb1。
需要root权限或用户在disk组中。
返回文件对象,如果失败则返回None。
"""
print(f"Attempting to open device: {
device_path} with os.open") # 打印尝试打开设备的信息
try:
# os.O_RDONLY: 以只读方式打开
# os.O_BINARY: 在某些系统上确保二进制模式 (虽然在Linux上对块设备影响不大)
# os.O_SYNC: (可选) 确保写入同步,但我们是只读。某些情况下用于确保直接IO。
# os.O_DIRECT: (可选) 尝试禁用内核缓冲,类似于FILE_FLAG_NO_BUFFERING。
# 使用O_DIRECT时,读取的偏移、大小和内存缓冲区地址通常需要对齐到扇区边界。
# 这会增加复杂性,我们暂时不使用它。
fd = os.open(device_path, os.O_RDONLY | getattr(os, 'O_BINARY', 0)) # 以只读二进制方式打开设备文件,获取文件描述符
print(f"Successfully opened device {
device_path}. File Descriptor: {
fd}") # 打印成功打开设备的信息
# 将文件描述符包装成Python文件对象,以便使用read, seek等方法
# buffering=0 表示不使用Python层面的缓冲,直接操作文件描述符,但系统仍有其缓冲
# 更常见的做法是让Python管理一些缓冲,例如使用默认的缓冲大小
device_file = os.fdopen(fd, 'rb') # 将文件描述符包装成文件对象,以二进制读取模式
return device_file # 返回文件对象
except FileNotFoundError: # 捕获文件未找到错误
print(f"Error: Device {
device_path} not found.") # 打印设备未找到信息
return None # 返回None
except PermissionError: # 捕获权限错误
print(f"Error: Permission denied to open {
device_path}. Try running as root or with sudo.") # 打印权限被拒绝信息
return None # 返回None
except Exception as e: # 捕获其他异常
print(f"An unexpected error occurred while opening {
device_path}: {
e}") # 打印意外错误信息
return None # 返回None
def close_linux_device(device_file): # 定义关闭Linux设备的函数
"""关闭通过os.fdopen打开的文件对象。"""
if device_file: # 如果文件对象存在
print(f"Closing device file: {
device_file.name}") # 打印关闭文件信息
try:
device_file.close() # 关闭文件对象
# print(f"Device file {device_file.name} closed.") # 打印成功关闭信息 (可省略)
except Exception as e: # 捕获异常
print(f"Error closing device file {
device_file.name}: {
e}") # 打印关闭文件错误信息
# 示例使用 (需要root权限或适当的用户组权限):
# if __name__ == "__main__":
# # 重要: 小心选择device_path_linux,确保它是你的目标U盘
# # 可以使用命令如 `lsblk` 或 `fdisk -l` 来确定正确的设备路径
# # 例如: /dev/sdb (整个U盘), /dev/sdb1 (U盘的第一个分区)
# device_path_linux = "/dev/sdXN" # 将XN替换为你的U盘分区,例如 /dev/sdb1
# if device_path_linux == "/dev/sdXN": # 如果路径是占位符
# print("Please replace '/dev/sdXN' with the actual device path of your USB drive.") # 提示替换路径
# else:
# print(f"Attempting to access Linux device: {device_path_linux}") # 打印尝试访问设备信息
# usb_device_file = open_linux_device(device_path_linux) # 调用函数打开设备
# if usb_device_file: # 如果设备成功打开
# print(f"Device {device_path_linux} opened. Ready for I/O operations.") # 打印设备已打开信息
# # 在这里可以进行读取操作
# # close_linux_device(usb_device_file) # 操作完成后关闭设备
# else:
# print(f"Could not open device {device_path_linux}.") # 打印无法打开设备信息
关于 os.O_DIRECT
(Linux): 类似于Windows的 FILE_FLAG_NO_BUFFERING
,它尝试绕过内核的页缓存。使用它时,Python的 read()
操作的参数(如读取大小、内存缓冲区的对齐)必须满足特定的对齐要求(通常是逻辑块大小的倍数)。这会使得编程更复杂,但可以获得更直接的磁盘访问。对于大多数恢复场景,标准的缓冲读取已经足够,除非有特别的性能或取证需求。
一旦成功打开了原始设备/分区,我们就可以使用文件对象的 seek()
方法定位到特定的偏移量,然后使用 read()
方法读取一定数量的字节。扇区通常是512字节或4096字节(4KB)。在进行数据恢复时,了解扇区大小很重要。
DeviceIoControl
和 IOCTL_DISK_GET_DRIVE_GEOMETRY_EX
来获取磁盘的几何信息,包括每个扇区的字节数。/sys/block//queue/logical_block_size
(例如 /sys/block/sdb/queue/logical_block_size
) 或使用 blockdev --getss /dev/sdb
命令来确定。在Python中,也可以尝试读取一个已知大小(如512字节),如果文件系统元数据(如引导扇区)中包含扇区大小信息,则优先使用那个。如果无法动态获取,可以先假设为512字节,因为这是非常常见的值。
我们可以创建一个包装函数来处理Windows和Linux的差异。
import platform # 导入platform模块,用于获取操作系统信息
# (接续之前的Windows API和Linux函数定义)
DEFAULT_SECTOR_SIZE = 512 # 定义默认扇区大小为512字节
def read_sectors_from_device(device_handle_or_file, start_sector, num_sectors, sector_size=DEFAULT_SECTOR_SIZE): # 定义从设备读取扇区的函数
"""
从打开的设备句柄(Windows)或文件对象(Linux)读取指定数量的扇区。
:param device_handle_or_file: Windows设备句柄 或 Linux文件对象。
:param start_sector: int, 要开始读取的扇区号 (从0开始)。
:param num_sectors: int, 要读取的扇区数量。
:param sector_size: int, 每个扇区的大小 (字节)。
:return: bytes, 读取到的数据;如果失败则返回None。
"""
offset = start_sector * sector_size # 计算起始偏移量(字节)
bytes_to_read = num_sectors * sector_size # 计算总共要读取的字节数
data = None # 初始化数据为None
current_os = platform.system() # 获取当前操作系统名称
if current_os == "Windows": # 如果是Windows系统
if device_handle_or_file == INVALID_HANDLE_VALUE or device_handle_or_file is None: # 如果句柄无效
print("Error: Invalid device handle for Windows.") # 打印无效句柄错误
return None # 返回None
# Windows: 使用 SetFilePointerEx 和 ReadFile
new_pointer_long = ctypes.c_int64() # 创建一个64位整数用于存储新的文件指针位置
# FILE_BEGIN = 0
if not SetFilePointerEx(device_handle_or_file, ctypes.c_int64(offset), ctypes.byref(new_pointer_long), 0): # 设置文件指针
error_code = ctypes.GetLastError() # 获取错误码
print(f"Windows: SetFilePointerEx failed to seek to offset {
offset}. Error: {
error_code}") # 打印设置指针失败信息
return None # 返回None
# print(f"Windows: Seeked to offset {offset}, new pointer at {new_pointer_long.value}") # 打印寻址成功信息 (调试用)
buffer = ctypes.create_string_buffer(bytes_to_read) # 创建一个指定大小的字符缓冲区
bytes_read_long = ctypes.c_uint32() # 创建一个无符号32位整数用于存储实际读取的字节数
if ReadFile(device_handle_or_file, buffer, bytes_to_read, ctypes.byref(bytes_read_long), None): # 读取文件
if bytes_read_long.value == bytes_to_read: # 如果实际读取的字节数等于期望读取的字节数
data = buffer.raw[:bytes_read_long.value] # 获取读取到的原始字节数据
# print(f"Windows: Read {bytes_read_long.value} bytes successfully.") # 打印读取成功信息 (调试用)
elif bytes_read_long.value > 0 : # 如果读取到的字节数大于0但小于期望值 (可能到达文件末尾)
data = buffer.raw[:bytes_read_long.value] # 获取部分数据
print(f"Windows: Read {
bytes_read_long.value} bytes (less than expected {
bytes_to_read}). Possibly EOF.") # 打印部分读取信息
else: # 如果实际读取的字节数为0
print(f"Windows: ReadFile succeeded but read 0 bytes from offset {
offset}.") # 打印读取0字节信息
data = b'' # 返回空字节串
else: # 如果ReadFile失败
error_code = ctypes.GetLastError() # 获取错误码
print(f"Windows: ReadFile failed to read {
bytes_to_read} bytes from offset {
offset}. Error: {
error_code}") # 打印读取失败信息
# 常见的错误码23: 数据错误(循环冗余检查)。表示磁盘扇区可能损坏。
if error_code == 23:
print("ReadFile Error 23: Data error (cyclic redundancy check). Possible bad sector.")
return None # 返回None
elif current_os == "Linux": # 如果是Linux系统
if device_handle_or_file is None: # 如果文件对象无效
print("Error: Invalid device file object for Linux.") # 打印无效文件对象错误
return None # 返回None
try:
device_handle_or_file.seek(offset) # 移动文件指针到指定偏移量
# print(f"Linux: Seeked to offset {offset}") # 打印寻址成功信息 (调试用)
data = device_handle_or_file.read(bytes_to_read) # 读取指定字节数的数据
if len(data) < bytes_to_read and len(data) > 0: # 如果读取到的数据长度小于期望但大于0
print(f"Linux: Read {
len(data)} bytes (less than expected {
bytes_to_read}). Possibly EOF.") # 打印部分读取信息
elif len(data) == 0 and bytes_to_read > 0: # 如果期望读取但读到0字节
print(f"Linux: Read 0 bytes from offset {
offset} when {
bytes_to_read} were expected.") # 打印读取0字节信息
# else:
# print(f"Linux: Read {len(data)} bytes successfully.") # 打印读取成功信息 (调试用)
except IOError as e: # 捕获IO错误
# IOError可能是由于坏道等原因
print(f"Linux: IOError during seek/read from offset {
offset}. Error: {
e}") # 打印IO错误信息
return None # 返回None
except Exception as e: # 捕获其他异常
print(f"Linux: Unexpected error during seek/read: {
e}") # 打印意外错误信息
return None # 返回None
else: # 如果是不支持的操作系统
print(f"Error: Unsupported operating system '{
current_os}' for raw disk access.") # 打印不支持的操作系统信息
return None # 返回None
return data # 返回读取到的数据
# --- 示例如何使用 read_sectors_from_device ---
# if __name__ == "__main__":
# current_os_name = platform.system() # 获取当前操作系统名称
# device_handle = None # 初始化设备句柄/文件对象
#
# # 根据操作系统选择设备路径和打开方式
# if current_os_name == "Windows":
# # !!! 再次强调: 确保这是你要操作的U盘,而不是系统盘 !!!
# # 例如: r'\\.\E:' (E盘是U盘) 或 r'\\.\PhysicalDrive1' (PhysicalDrive1是U盘)
# win_dev_path = r'\\.\E:' # 修改为你的U盘对应的盘符或物理驱动器
# if win_dev_path == r'\\.\E:' and not os.path.exists('E:\\'): # 简单检查盘符是否存在 (不完全可靠)
# print(f"Path {win_dev_path} seems invalid as drive E: does not exist. Please check.")
# else:
# device_handle = open_windows_device(win_dev_path) # 打开Windows设备
# elif current_os_name == "Linux":
# # !!! 确保这是你要操作的U盘 !!!
# # 例如: /dev/sdb1 (U盘的第一个分区)
# linux_dev_path = "/dev/sdb1" # 修改为你的U盘对应的设备路径
# if not os.path.exists(linux_dev_path): # 检查设备文件是否存在
# print(f"Path {linux_dev_path} does not exist. Please check.")
# else:
# device_handle = open_linux_device(linux_dev_path) # 打开Linux设备
# else:
# print(f"OS {current_os_name} not directly supported by this example for raw access.") # 打印不支持的操作系统
#
# if device_handle and (device_handle != INVALID_HANDLE_VALUE if current_os_name == "Windows" else True): # 如果设备成功打开
# print("-" * 30) # 打印分隔线
# print("Attempting to read the first sector (Boot Sector)...") # 打印尝试读取第一个扇区信息
# # 读取第一个扇区 (扇区0),读取1个扇区,假设扇区大小为512字节
# boot_sector_data = read_sectors_from_device(device_handle, start_sector=0, num_sectors=1, sector_size=DEFAULT_SECTOR_SIZE) # 读取扇区数据
#
# if boot_sector_data: # 如果成功读取到数据
# print(f"Successfully read {len(boot_sector_data)} bytes for the boot sector.") # 打印成功读取字节数
# # 可以使用hexdump来查看扇区内容
# import hexdump # 导入hexdump模块
# print("Hexdump of the first 64 bytes of the boot sector:") # 打印引导扇区前64字节的十六进制转储
# hexdump.hexdump(boot_sector_data[:64]) # 十六进制转储数据的前64字节
#
# # 尝试读取FAT表或MFT的开始部分 (这需要知道文件系统类型和布局)
# # 例如,对于一个FAT32分区,引导扇区会告诉我们FAT表的位置
# # 假设引导扇区显示保留扇区数为32,每个FAT表占2000个扇区
# # 则第一个FAT表从扇区32开始
# # fat_start_sector = 32 # (这只是一个例子,需要从引导扇区解析)
# # print(f"\nAttempting to read first sector of FAT (example sector {fat_start_sector})...")
# # fat_sector_data = read_sectors_from_device(device_handle, fat_start_sector, 1)
# # if fat_sector_data:
# # print(f"Read {len(fat_sector_data)} bytes from presumed FAT start.")
# # hexdump.hexdump(fat_sector_data[:64])
#
# else: # 如果读取失败
# print("Failed to read the boot sector.") # 打印读取引导扇区失败信息
#
# print("-" * 30) # 打印分隔线
# # 关闭设备
# if current_os_name == "Windows": # 如果是Windows系统
# close_windows_device(device_handle) # 关闭Windows设备
# elif current_os_name == "Linux": # 如果是Linux系统
# close_linux_device(device_handle) # 关闭Linux设备
# else: # 如果打开设备失败
# print("Device handle/file object is not valid. Cannot proceed with reading.") # 打印设备句柄/文件对象无效信息
#
代码解释与注意事项:
read_sectors_from_device
函数内部通过 platform.system()
判断操作系统,然后调用相应的API(Windows的 SetFilePointerEx
/ReadFile
或 Linux的 seek
/read
)。GetLastError()
可以提供具体的错误原因。Linux的 IOError
尤其需要关注,因为它可能表示磁盘坏道。sudo
)运行。open_windows_device
或 open_linux_device
的设备路径是准确无误的,指向你希望进行数据恢复的U盘或其他非系统关键设备。操作错误的设备可能导致灾难性的数据丢失。DEFAULT_SECTOR_SIZE
设为512。对于现代磁盘(尤其是4K Native磁盘或使用4K模拟扇区的U盘),真实的扇区大小可能是4096字节。如果文件系统(如引导扇区)中明确指出了扇区大小,应优先使用该值。读取操作的 bytes_to_read
和 offset
都应该基于正确的扇区大小计算,以确保读取到的是完整的逻辑单元。ReadFile
(Windows) 或 read()
(Linux) 遇到物理损坏的扇区(坏道)时,它们通常会失败并返回错误或抛出异常(如Windows的错误码23,Linux的IOError: [Errno 5] Input/output error
)。一个健壮的数据恢复程序需要能够处理这种情况,例如:
如前所述,直接在原始故障设备上进行恢复操作是有风险的。创建一个逐位的磁盘镜像是更安全的方法。Python也可以用来创建这样的镜像文件,尽管对于非常大的驱动器,使用专门的工具如 dd
(Linux) 或 FTK Imager
, dd for Windows
等可能更高效。
def create_disk_image(source_device_handle_or_file, output_image_path, sector_size=DEFAULT_SECTOR_SIZE, chunk_sectors=128, device_size_bytes=None): # 定义创建磁盘镜像的函数
"""
从源设备创建一个逐位拷贝的镜像文件。
:param source_device_handle_or_file: 打开的源设备句柄(Win)或文件对象(Lin)。
:param output_image_path: str, 输出镜像文件的路径。
:param sector_size: int, 扇区大小。
:param chunk_sectors: int, 每次读取/写入的扇区数量 (块大小)。
:param device_size_bytes: int, (可选) 源设备的总大小(字节)。如果为None,会尝试读取直到EOF。
:return: bool, True如果成功,False如果失败。
"""
current_os = platform.system() # 获取当前操作系统名称
print(f"Starting to create image of device to '{
output_image_path}'...") # 打印开始创建镜像信息
print(f"Sector size: {
sector_size} bytes, Chunk size: {
chunk_sectors} sectors ({
chunk_sectors * sector_size} bytes)") # 打印扇区大小和块大小信息
try:
with open(output_image_path, 'wb') as img_file: # 以二进制写入模式打开输出镜像文件
total_bytes_written = 0 # 初始化总写入字节数
start_sector = 0 # 初始化起始扇区
read_errors = 0 # 初始化读取错误计数
while True: # 无限循环,直到读取完成或遇到问题
# print(f"Imaging: Reading chunk starting at sector {start_sector}...") # 打印正在读取的块信息 (调试用,会产生大量输出)
data_chunk = read_sectors_from_device(source_device_handle_or_file, start_sector, chunk_sectors, sector_size) # 从源设备读取一块数据
if data_chunk is None: # 如果读取数据块失败 (可能遇到坏道)
print(f"Warning: Failed to read chunk at sector {
start_sector}. Filling with zeros.") # 打印读取块失败警告
# 用零填充这个块,并继续 (或者可以选择终止)
img_file.write(b'\x00' * (chunk_sectors * sector_size)) # 向镜像文件写入零字节
total_bytes_written += (chunk_sectors * sector_size) # 累加写入字节数 (即使是零)
read_errors += 1 # 读取错误计数加1
elif not data_chunk: # 如果读取到空数据 (通常表示到达设备末尾)
print("Reached end of source device (or read empty chunk).") # 打印到达设备末尾信息
break # 跳出循环
else: # 如果成功读取到数据块
img_file.write(data_chunk) # 将数据块写入镜像文件
total_bytes_written += len(data_chunk) # 累加实际写入的字节数
start_sector += chunk_sectors # 更新下一个起始扇区号
# 打印进度 (可以根据需要调整频率)
if start_sector % (chunk_sectors * 10) == 0: # 每10个块打印一次进度
progress_mb = total_bytes_written / (1024 * 1024) # 计算已处理的MB数
print(f"Progress: {
progress_mb:.2f} MB written. Current sector: {
start_sector}. Read errors: {
read_errors}") # 打印进度信息
if device_size_bytes is not None and total_bytes_written >= device_size_bytes: # 如果指定了设备大小且已达到
print(f"Reached specified device size of {
device_size_bytes} bytes.") # 打印达到指定设备大小信息
break # 跳出循环
print(f"Disk imaging finished. Total bytes written: {
total_bytes_written} ({
total_bytes_written / (1024*1024):.2f} MB).") # 打印镜像完成信息
if read_errors > 0: # 如果有读取错误
print(f"Warning: Encountered {
read_errors} read error(s) during imaging. Image may be incomplete or contain zeroed blocks.") # 打印读取错误警告
return True # 返回成功标志
except IOError as e: # 捕获IO错误 (通常是写入镜像文件时发生)
print(f"IOError during disk imaging (likely writing to output file '{
output_image_path}'): {
e}") # 打印IO错误信息
return False # 返回失败标志
except Exception as e: # 捕获其他异常
print(f"An unexpected error occurred during disk imaging: {
e}") # 打印意外错误信息
return False # 返回失败标志
# --- 示例如何使用 create_disk_image ---
# if __name__ == "__main__":
# # ... (与之前相同的设备打开逻辑) ...
# current_os_name = platform.system()
# device_handle = None
# source_device_path = ""
# if current_os_name == "Windows":
# source_device_path = r'\\.\E:' # 修改为你的U盘对应的盘符或物理驱动器
# device_handle = open_windows_device(source_device_path)
# elif current_os_name == "Linux":
# source_device_path = "/dev/sdb1" # 修改为你的U盘对应的设备路径
# device_handle = open_linux_device(source_device_path)
# # else ...
# output_image_file_path = "usb_image.dd" # 定义输出镜像文件名
# if device_handle and (device_handle != INVALID_HANDLE_VALUE if current_os_name == "Windows" else True):
# print(f"\nStarting imaging process for {source_device_path} to {output_image_file_path}") # 打印开始镜像进程信息
# # 你可能需要一种方法来获取源设备的大小,以避免读取超出范围或无限循环
# # 对于Windows,可以使用DeviceIoControl与IOCTL_DISK_GET_LENGTH_INFO
# # 对于Linux,可以使用 os.fstat(device_handle.fileno()).st_size (如果适用) 或 blockdev --getsize64
# # 这里我们暂时不实现动态获取大小,假设你知道大致范围或让它读到EOF
# # device_actual_size = get_device_size_in_bytes(device_handle) # (这是一个需要你实现的函数)
# device_actual_size = None # 暂时不指定大小,让它尝试读到EOF
# success = create_disk_image(device_handle, output_image_file_path,
# sector_size=DEFAULT_SECTOR_SIZE,
# chunk_sectors=2048, # 每次读写 2048 * 512 B = 1MB
# device_size_bytes=device_actual_size)
# if success: # 如果镜像成功
# print(f"Image creation to '{output_image_file_path}' completed successfully (or with logged read errors).") # 打印镜像创建成功信息
# print("It is recommended to perform recovery operations on this image file instead of the original device.") # 建议在镜像文件上操作
# else: # 如果镜像失败
# print(f"Image creation to '{output_image_file_path}' failed.") # 打印镜像创建失败信息
# # 关闭设备
# if current_os_name == "Windows":
# close_windows_device(device_handle)
# elif current_os_name == "Linux":
# close_linux_device(device_handle)
# else:
# print("Device could not be opened. Imaging aborted.")
镜像函数解释:
create_disk_image
循环读取源设备的数据块(chunk_sectors
* sector_size
字节),并将这些块写入到输出的镜像文件中。read_sectors_from_device
返回 None
(表示读取错误,可能是坏道),它会向镜像文件写入等量的零字节,并记录错误。这是一种常见的做法,叫做“用零填充坏道”,以保持镜像的完整性和偏移量正确,但损坏区域的数据会丢失。device_size_bytes
): 如果能预先知道设备的总大小,可以作为循环的终止条件之一。否则,函数会一直读取,直到 read_sectors_from_device
返回空字节串(表示到达设备末尾)或 None
。动态获取设备大小是一个依赖于操作系统的复杂任务,这里未完全实现。chunk_sectors
): 选择一个合适的块大小(例如1MB到64MB)可以平衡读取效率和内存使用。太小的块会导致过多的读写调用,太大的块可能消耗过多内存。一旦我们有了打开原始设备(或其镜像文件)并从中读取数据的能力,下一步就是解析这些数据,以识别文件系统的结构、查找CHK文件内容、或者扫描已删除文件的痕迹。这将涉及到对特定文件系统(如FAT32, NTFS)的引导扇区、文件分配表、目录条目、MFT等关键数据结构的深入理解和编程解析。
掌握了这些底层的磁盘I/O操作,我们就为后续更复杂的数据恢复任务打下了坚实的基础。接下来的部分将开始聚焦于如何利用这些能力来识别和恢复CHK文件。
FILExxxx.CHK
文件本质上是原始数据簇的集合,它们失去了文件名和明确的文件类型信息。恢复它们的核心思想是 通过分析文件内容本身来识别其原始类型,并尝试将其重命名或转换为可用的格式。
主要的恢复策略可以分为以下几种,并且常常结合使用:
基于文件签名 (File Signature / Magic Number) 的识别:
FF D8 FF E0 xx xx 4A 46 49 46 00
(ÿØÿà..JFIF.
) 或 FF D8 FF E1 xx xx 45 78 69 66 00
(ÿØÿá..Exif.
) 开头。89 50 4E 47 0D 0A 1A 0A
(.PNG....
) 开头。%PDF-
(即 25 50 44 46 2D
) 开头。PK
(即 50 4B 03 04
) 开头。PK
开头,但其内部结构包含特定的XML文件可以进一步确认。ID3
字节序列) 或帧同步字 (例如 FFFx
)。基于文件结构和内容特征的分析:
<
和 >
标签。ftyp
, moov
, mdat
等),可以尝试解析这些结