ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Windows用户态物理扇区读写:从DeviceIoControl到MBR实战

Windows用户态物理扇区读写:从DeviceIoControl到MBR实战 简介这是一份基于Visual C与MFC类库编写的硬盘扇区直接读写演示程序面向系统底层编程初学者、数据恢复从业者及安全分析人员旨在演示如何在Windows环境中通过API调用绕过文件系统直接对物理扇区执行读写。资源压缩包共含18个文件主要包含C源代码.h/.cpp、MFC工程配置.vcproj/.sln/.rc以及引导扇区、恢复数据等二进制样本总大小仅31KB整体轻量且便于做源码级分析。目前已有387人学习或下载该资源。通过学习代码可以掌握CreateFile、DeviceIoControl等Windows API在磁盘操作中的实际用法理解扇区512B/4KB作为最小数据读写单位的底层机制结合附带的WinHex十六进制工具能够对读写前后的扇区内容进行直观对比和验证。该案例对数据恢复、磁盘驱动分析以及系统调试都有直接的参考价值适合希望提升底层系统编程能力的开发者深入学习。1. 扇区读写不是老古董是数据恢复和取证的基本功一个 MFC 对话框程序配合几个 bin 文件和一份 WinHex 压缩包就能在 Windows 下直接读写硬盘物理扇区。这套东西在今天依然有用数据恢复要绕过文件系统抓原始字节安全分析要看引导扇区有没有被改写自研磁盘工具要确认底层读写路径是否正确。很多人以为这类操作必须写内核驱动实际上用户态用 CreateFile 打开\\.\PhysicalDrive0再走 DeviceIoControl 发控制码就能完成扇区级的读和写。这个演示工程的价值就是把这条路完整走了一遍而且留了 boot.bin、sector.bin、restored.bin 三个关键样本适合正在做系统级编程、想搞懂磁盘布局或需要给恢复类软件补底层能力的开发者反复拆。2. 打开物理盘的那一步CreateFile 与设备路径命名2.1 设备路径表\\.\PhysicalDrive0和\\.\C:根本不是一回事Windows 把磁盘设备也暴露成文件打开方式跟读写普通 txt 一样只是路径变了。这个包里的 MFC 工程之所以能读到引导扇区关键就是路径用对了。路径写法指向对象典型用途注意点\\.\PhysicalDrive0第一块物理硬盘整体读引导扇区、整盘镜像、改分区表从 0 开始编号绕过文件系统\\.\PhysicalDrive1第二块物理硬盘整体多盘环境按序定位交换过盘符后编号不变\\.\C:卷分区按文件系统视角读分区域只能看到分区起始之后的空间\\.\Floppy0、\\.\CdRom0软驱、光驱老设备兼容现在很少用到\\.\GLOBALROOT\Device\Harddisk0\Partition0对象管理器路径过滤驱动、调试器里用用户态一般不直接写这个工程里读的是PhysicalDrive0的 0 号扇区也就是 MBR 所在位置。boot.bin 就是从这个整盘句柄读出来的不是从C:盘读的因为引导扇区属于整块磁盘不属于某个逻辑卷。这一点如果没搞清很多人会在读分区偏移的时候得到一堆看似乱码的数据还不知哪错了。2.2 为什么用 MFC 对话框而不是控制台程序打开工程先看工程结构MFC_001.vcproj、MFC_001.sln、MFC_001.suo、MFC_001.rc、resource.h这是 VC2008 时代典型的 MFC 对话框工程。一个扇区读写工具交互本质就是“选磁盘、输入扇区号、点读取、点写入”对话框比控制台适合多了每个按钮对应一个 OnBnClicked 消息读出的数据可以直接填进 Edit 控件或者写成 bin 文件不用在控制台里来回敲命令。stdafx.h和stdafx.cpp是预编译头MFC 框架类和 Windows API 都在这里统一 includeMFC_001Dlg.cpp里包含MFC_001Dlg.h所以实际扇区读写代码基本集中在对话框类里。文件清单里的MFC_001.manifest控制的是通用控件样式避免按钮和进度条显示成旧版风格这个在 Vista 之后很关键。.suo是当前用户的工程选项缓存记录断点和窗口布局正常不需要提交到版本库。.vcproj是 VS2008 之前的工程格式后来的 VS 系列会自动升级但如果团队有人还在用老版本 IDE这个文件反而比.vcxproj更好往下兼容。2.3 打开物理盘的最小可运行代码不管后面读多少扇区第一步都是拿到设备句柄。这个工程里最常见的写法如下HANDLE OpenRawDisk(int nDiskNo, BOOL bWrite) { CString strPath; strPath.Format(_T(\\\\.\\PhysicalDrive%d), nDiskNo); HANDLE hDevice CreateFile( strPath, bWrite ? (GENERIC_READ | GENERIC_WRITE) : GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hDevice INVALID_HANDLE_VALUE) { CString strErr; strErr.Format(_T(打开设备失败错误码: %d), GetLastError()); AfxMessageBox(strErr); return INVALID_HANDLE_VALUE; } return hDevice; }代码逻辑不复杂但有三处直接决定成败。第一处是访问权限只用 GENERIC_READ 能读要写回 boot.bin 就必须 GENERIC_READ | GENERIC_WRITE。第二处是共享模式FILE_SHARE_READ | FILE_SHARE_WRITE 同时放读和写否则磁盘被文件系统占用时创建会直接失败Windows 对磁盘设备句柄的占用检查比普通文件严得多。第三处是 OPEN_EXISTING对设备文件来说这是固定写法填 CREATE_ALWAYS 会报参数错误。这里用的是非 Unicode 版本的 CreateFile 思路。VC2008 时代的 MFC 工程一般默认多字节字符集用字符串拼接设备路径时如果项目切到 Unicode_T宏会自动换到宽字符版本代码本身不用改。但磁盘路径里几乎不会出现非 ASCII 字符所以就算工程里混用CreateFileA也不会出问题关键是自己要清楚当前工程是什么字符集否则后面读写缓冲区时容易被char和wchar_t坑到。3. 先探测再读写IOCTL_DISK_GET_DRIVE_GEOMETRY_EX 与扇区大小3.1 不能写死 512 字节的行业现实老式硬盘每个扇区 512 字节但今天的大容量盘普遍是 4096 字节原生扇区4KN还有一部分是 512e物理 4096、逻辑 512。如果代码把扇区大小写死成 512读写 4KN 硬盘时会遇到两种问题缓冲区申请小了读一半数据被截断偏移算错了写回的数据落在错误位置。这个演示工程虽然可能只在某个老盘或虚拟机上跑通过但代码上不能把扇区大小当常量处理必须先从设备查询真实值。3.2 用 DeviceIoControl 拿到 BytesPerSectorWindows 提供IOCTL_DISK_GET_DRIVE_GEOMETRY_EX控制码读取磁盘几何参数返回的结构体里直接包含扇区大小和磁盘总容量。代码只有十几行DISK_GEOMETRY_EX geom {0}; DWORD dwBytesReturned 0; BOOL bOk DeviceIoControl( hDevice, IOCTL_DISK_GET_DRIVE_GEOMETRY_EX, NULL, 0, geom, sizeof(geom), dwBytesReturned, NULL); if (bOk) { DWORD dwSectorSize geom.Geometry.BytesPerSector; ULONGLONG ullDiskSize geom.DiskSize.QuadPart; LONGLONG llSectorCount (LONGLONG)(ullDiskSize / dwSectorSize); }DeviceIoControl 是这类底层工具的核心入口输入输出缓冲区怎么组织完全取决于控制码。IOCTL_DISK_GET_DRIVE_GEOMETRY_EX不需要输入数据输出缓冲区就是一个DISK_GEOMETRY_EX里面的Geometry又是嵌套的DISK_GEOMETRY结构。调用完成后dwBytesReturned会写入实际返回的字节数可以用来校验结构体是否被完整填充。如果DeviceIoControl返回 FALSE 且GetLastError()是 87ERROR_INVALID_PARAMETER先检查结构体大小是不是sizeof(DISK_GEOMETRY_EX)少一个字节都不行。3.3 DISK_GEOMETRY 结构里值得关注的字段字段含义调试时怎么用Cylinders柱面数老工具按 CHS 寻址用现在基本只做展示MediaType固定盘还是可移动盘FixedMedia代表硬盘RemovableMedia多半是 U 盘TracksPerCylinder每柱面磁道数由设备固件决定不需要自己算SectorsPerTrack每磁道扇区数CHS 转 LBA 时才用得到BytesPerSector每个扇区字节数读写缓冲区和偏移计算的唯一依据DiskSize磁盘总字节数用DiskSize / BytesPerSector得总扇区数工程里的sector.bin就是按这个流程读出来的打开PhysicalDrive0查询得到BytesPerSector默认从 0 号扇区开始读一个扇区内存缓冲大小取BytesPerSector读出来后用CFile写成sector.bin。这个文件是十六进制的原始字节流直接双击用 WinHex 打开就能看到引导代码和分区表。如果读出来的扇区大小只有 512 字节但sector.bin实际是 4096 字节说明当时跑的环境是 4KN 盘或程序把扇区数参数调成了 8排查时用磁盘几何信息对一下就知道。物理盘的容量可能超过 2TBLBA 偏移必须用 64 位整数计算。很多人第一次写这类工具时用int存偏移读 1TB 往后的扇区就会溢出成负数读出来的数据完全不可信。凡是涉及扇区号的乘法统一用LONGLONG或ULONGLONG这是底层磁盘代码的硬性习惯。4. 读写扇区的两条路IOCTL_DISK_READ 与 SetFilePointerEx 的取舍4.1 控制码和数据传输码的分工DeviceIoControl不是只能查参数它本质上是一个通用的设备控制通道。对磁盘来说控制类操作发IOCTL_DISK_GET_DRIVE_GEOMETRY_EX、IOCTL_DISK_UPDATE_PROPERTIES数据传输类操作发IOCTL_DISK_READ和IOCTL_DISK_WRITE。后者会直接把扇区数据填进你提供的缓冲区不需要像普通文件那样先 Seek 再 Read。控制码输入输出用途IOCTL_DISK_GET_DRIVE_GEOMETRY_EX无DISK_GEOMETRY_EX查询扇区大小、总容量IOCTL_DISK_READDISKIO结构DISK_BUFFER结构按字节偏移读取扇区IOCTL_DISK_WRITEDISKIO 数据无按字节偏移写入扇区IOCTL_DISK_IS_WRITABLE无无检查磁盘是否可写IOCTL_DISK_UPDATE_PROPERTIES无无让系统刷新磁盘属性缓存也有工程师用SetFilePointerEx加ReadFile读物理盘那个方案在逻辑卷上问题不大但在PhysicalDrive上对偏移的管理更绕而且某些设备过滤驱动对文件指针支持不好。做扇区级工具我一般直接用IOCTL_DISK_READ语义和扇区这个概念严格对应出问题也好定位。4.2 读扇区代码DISKIO 和 DISK_BUFFER 的关系读一个扇区到内存完整代码如下BOOL ReadSectors(HANDLE hDevice, LONGLONG nStartSector, DWORD nSectorCount, BYTE* pBuffer) { DWORD dwSectorSize 4096; // 正式代码中由 IOCTL 查询得到 DWORD dwBufSize dwSectorSize * nSectorCount; DISKIO dio; ZeroMemory(dio, sizeof(dio)); dio.dwLowOffset (DWORD)(nStartSector * dwSectorSize); dio.dwHighOffset (DWORD)((nStartSector * dwSectorSize) 32); dio.nNumberOfBytesToTransfer dwBufSize; DISK_BUFFER db; ZeroMemory(db, sizeof(db)); db.Data pBuffer; db.DataSize dwBufSize; DWORD dwBytesReturned 0; BOOL bOk DeviceIoControl( hDevice, IOCTL_DISK_READ, dio, sizeof(dio), db, sizeof(db), dwBytesReturned, NULL); if (!bOk) { DWORD dwErr GetLastError(); // 87: DISKIO 偏移或长度不是扇区整数倍 // 1: 缓冲区地址未对齐或长度不合法 } return bOk; }DISKIO是告诉驱动程序“我要从哪读”DISK_BUFFER是告诉它“读出来的数据放哪”。dwLowOffset和dwHighOffset拼成一个 64 位字节偏移所以读第 100 个扇区、每扇区 4096 字节时偏移是 409600拆成低 32 位和高 32 位分别写入。nNumberOfBytesToTransfer是本次传输的总字节数必须是扇区整数倍。DISK_BUFFER.Data指向的是接收数据的缓冲区首地址这个地址建议用VirtualAlloc申请能得到页对齐的内存避免某些存储驱动对缓冲区首地址有对齐要求时返回 ERROR_INVALID_PARAMETER。用new或malloc也通常能跑但越底层的接口越保守越好。4.3 写扇区代码与 boot.bin 的回写流程写入方向的代码结构与读取几乎对称但多了两步保险。第一步是写之前调用IOCTL_DISK_IS_WRITABLE确认磁盘没有被系统标记为只读否则DeviceIoControl会直接返回拒绝访问。第二步是写成功后调用IOCTL_DISK_UPDATE_PROPERTIES通知系统重新读取磁盘属性否则分区表或引导代码的变更可能被文件系统缓存盖掉。BOOL WriteSectors(HANDLE hDevice, LONGLONG nStartSector, DWORD nSectorCount, BYTE* pData) { DWORD dwSectorSize 4096; DWORD dwBufSize dwSectorSize * nSectorCount; DISKIO dio; ZeroMemory(dio, sizeof(dio)); dio.dwLowOffset (DWORD)(nStartSector * dwSectorSize); dio.dwHighOffset (DWORD)((nStartSector * dwSectorSize) 32); dio.nNumberOfBytesToTransfer dwBufSize; DISK_BUFFER db; ZeroMemory(db, sizeof(db)); db.Data pData; db.DataSize dwBufSize; DWORD dwBytesReturned 0; BOOL bOk DeviceIoControl( hDevice, IOCTL_DISK_WRITE, dio, sizeof(dio), db, sizeof(db), dwBytesReturned, NULL); if (bOk) { DeviceIoControl(hDevice, IOCTL_DISK_UPDATE_PROPERTIES, NULL, 0, NULL, 0, dwBytesReturned, NULL); } return bOk; }包里的 boot.bin 回写实验本质就是把 boot.bin 用CFile.Read读进字节数组然后调用WriteSectors(0, 1, bootBuf)。写 0 号扇区之前必须确认当前磁盘的分区表是自己能接受的否则把 MBR 写坏整个盘的分区在 Windows 磁盘管理器里会变成“未初始化”数据虽然还在但访问路径全断。实际做这个实验时找一块闲置 U 盘比拿系统盘试安全得多U 盘的MediaType是RemovableMedia\\.\PhysicalDrive编号靠后写坏了也不影响系统启动。4.4 打开的是卷还是整盘决定了你读到的内容用\\.\C:打开卷句柄后再调IOCTL_DISK_READ读到的第一个扇区是分区起始位置不是整块硬盘的 0 号扇区。很多人读完sector.bin发现内容跟引导扇区对不上就是因为用卷句柄做了整盘读取。区分两种句柄有一个直观方法读出来的数据如果在偏移 0x1FE 处是 55 AA说明这是有效的 MBR 引导扇区大概率来自整盘句柄如果开头是 NTFS 的跳转指令EB 52 90或 FAT 的EB 3C 90那就是分区起始的引导扇区 BPB属于卷句柄。这个工程里boot.bin是专门从整盘读出来的所以它对应的是 MBR不是某个分区的 BPB。5. 用 WinHex 交叉验证sector.bin、boot.bin 与 restored.bin 的完整链路5.1 WinHex 在这个工程里到底起什么作用包里附带 winhex.zip不是摆设。WinHex 是扇区级数据的可视化工具程序读出来的 sector.bin 到底是不是磁盘上的真实字节光靠程序自己没法证明因为读和显示在同一个进程里错误可能互相掩盖。用 WinHex 直接打开物理磁盘、跳到同一个扇区人工比对十六进制内容才是独立验证。另一个用途是手工构造测试样本把 sector.bin 复制一份改掉几个字节再取名为test.bin让程序写回磁盘再读出来对比就能确认写路径真实生效。5.2 WinHex 打开物理盘和扇区跳转的固定操作验证流程在 WinHex 里通常是四步步骤固定做顺手之后都是肌肉记忆。操作意图菜单路径快捷方式打开物理磁盘Tools - Open DiskF3跳转到指定扇区Navigation - Go to SectorCtrlG查看偏移 0x1FE 的引导标志直接滚动到窗口底部无导出当前扇区为 binFile - Export无打开磁盘时WinHex 会列出Physical Drive Media和Logical Drive Letters两类选前者再选对应编号才能看到 MBR。CtrlG 里输入的是扇区号不是字节偏移这一点和程序代码里的nStartSector对应。导出后的 bin 跟sector.bin在哈希层面完全一致才算验证通过。5.3 用哈希比对替代人眼检查人眼对比两个扇区的十六进制太累而且偏移一大就容易看串行。程序读出来的结果和 WinHex 导出的结果用哈希做一致性判断最快。Windows 自带certutil不需要额外装工具certutil -hashfile sector.bin MD5 certutil -hashfile winhex_export.bin MD5两行命令的输出分别是两串 32 位十六进制摘要逐位相同就是读路径没毛病。工程施工环境里没有 certutil 或者想跑批量比对就用脚本做for /f delims %%i in (certutil -hashfile sector.bin MD5 ^| findstr /v hash) do set H1%%i for /f delims %%i in (certutil -hashfile winhex_export.bin MD5 ^| findstr /v hash) do set H2%%i if %H1%%H2% (echo MATCH) else (echo MISMATCH)这段批处理通过findstr过滤掉certutil输出的“hash”标题行和空行只保留真正的哈希值字符串然后直接比较。注意certutil输出的哈希字母大小写是固定的所以字符串直接比较可信。做数据恢复时这种“读两次、比对哈希”的做法能排除缓存导致读到旧数据的可能。5.4 restored.bin 是如何参与验证的压缩包里的 restored.bin从命名看是“恢复后”的数据。在这个工程里有两种可能的生产路径。第一种程序把某个扇区备份到 boot.bin然后对原位置写入修改后的数据再重新读出来存成 restored.bin用于确认写入没有造成意外破坏。第二种程序先读出原始扇区修改一部分字节后先存 sample完成后把原始备份写回去restored.bin 就是恢复原状的结果。无论哪种三个 bin 文件合起来构成了“原盘数据 - 中间样本 - 写回结果”的完整校验闭环。实际做实验时建议把读出的 0 号扇区同时保存为orig.bin和sector.bin两份一份留着对比一份用于修改。WinHex 打开orig.bin在偏移 0x1BE 处能看到 64 字节的分区表 DPT0x1FE 处是 55 AA。用 WinHex 编辑保存为modified.bin后再通过WriteSectors写回最后读出来和modified.bin做 hash 对比。这个流程里每个环节都有独立证据不像直接在程序里写个“写入成功”就算完。6. 提示会导致失败的常见盘点从错误码反推问题底层磁盘工具调试时错误码比界面提示可靠得多。整理一份常见的错误对应关系遇到问题时先查GetLastError()再动手改代码。错误码含义常见诱因处理方式5拒绝访问没有用管理员权限运行右键“以管理员身份运行”32文件被占用磁盘上有分区处于活动状态换一块无挂载分区的磁盘87参数错误偏移或长度不是扇区整数倍检查 dwLowOffset 与高 32 位拼接1功能错误缓冲区未按扇区对齐用 VirtualAlloc 申请缓冲50设备不支持请求硬件不支持该 IOCTL换用卷句柄或换设备第 4 章写过写成功之后系统缓存可能让后续读取返回旧数据所以有一个更稳的验证手法值得专门说明。写扇区完成后紧跟一次IOCTL_DISK_READ把同一个扇区读回内存用memcmp和写入的缓冲逐字节比较BYTE* pVerify (BYTE*)VirtualAlloc(NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE); ReadSectors(hDevice, nStartSector, nSectorCount, pVerify); BOOL same (memcmp(pVerify, pData, dwBufSize) 0); if (!same) { // 写路径可能走了缓存等待几秒再读第二次 Sleep(200); ReadSectors(hDevice, nStartSector, nSectorCount, pVerify); same (memcmp(pVerify, pData, dwBufSize) 0); } VirtualFree(pVerify, 0, MEM_RELEASE);读完立即对比如果一致说明写路径和读路径都没有被缓存干扰。如果不一致等 200ms 再读数据就变了基本可以判定是磁盘控制器固件或驱动在中间做了缓冲真正的落盘有延迟。这个技巧在做固件级调试时要频繁用因为不少 SATA/USB 桥接芯片对写入有自己的回写策略DeviceIoControl返回值只能证明设备接受了数据证明不了数据已经躺在盘片上。后面这个 200ms 延迟重读的验证方式在排查不稳定的写入问题时往往比抓驱动日志更快定位问题。本文还有配套的精品资源点击获取
返回列表