I’ve been messing around with disk I/O in C# lately, attempting to read/modify the boot sectors and such. I’ve used the lower-level functions for accessing disks like files, from the Win32 API. Most of the signatures were adapted from the pinvoke.net website as well as some examples from MSDN.
What I’ve done is made one file/class to hold all the unsafe/unmanaged code and another to wrap those functions up nicely in a .NET-style class named DiskStream. The DiskStream class inherits from the Stream object and so exposes an interface similar to a FileStream. You can open up a DiskStream and read, write, and seek it just like a regular file.
There are a few gotcha’s associated with performing disk access like this:
- You need to access the disk in multiples of the sector size (ie. 512 bytes).
- I don’t believe this will work at all in any Windows below Win2K.
- Currently has odd behaviour on disks with mounted volumes, if there’s a mounted volume, it appears only the MBR can be written. I’m going to fix this in a future version so that the volumes get locked and/or dismounted before hand.
So here’s the code for accessing the Win32 API from .NET. I think this is pretty useful, since I had a hard time tracking down this stuff and making it work. Here’s the first class, which I call DeviceIO:
using System; using System.IO; using System.Runtime.InteropServices; using System.Threading; using Microsoft.Win32.SafeHandles; namespace DiskLib { /// /// P/Invoke wrappers around Win32 functions and constants. /// internal class DeviceIO { #region Constants used in unmanaged functions public const uint FILE_SHARE_READ = 0x00000001; public const uint FILE_SHARE_WRITE = 0x00000002; public const uint FILE_SHARE_DELETE = 0x00000004; public const uint OPEN_EXISTING = 3; public const uint GENERIC_READ = (0x80000000); public const uint GENERIC_WRITE = (0x40000000); public const uint FILE_FLAG_NO_BUFFERING = 0x20000000; public const uint FILE_FLAG_WRITE_THROUGH = 0x80000000; public const uint FILE_READ_ATTRIBUTES = (0x0080); public const uint FILE_WRITE_ATTRIBUTES = 0x0100; public const uint ERROR_INSUFFICIENT_BUFFER = 122; #endregion #region Unamanged function declarations [DllImport("kernel32.dll", SetLastError = true)] public static unsafe extern SafeFileHandle CreateFile( string FileName, uint DesiredAccess, uint ShareMode, IntPtr SecurityAttributes, uint CreationDisposition, uint FlagsAndAttributes, IntPtr hTemplateFile); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle(SafeFileHandle hHandle); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool DeviceIoControl( SafeFileHandle hDevice, uint dwIoControlCode, IntPtr lpInBuffer, uint nInBufferSize, [Out] IntPtr lpOutBuffer, uint nOutBufferSize, ref uint lpBytesReturned, IntPtr lpOverlapped); [DllImport("kernel32.dll", SetLastError = true)] public static extern unsafe bool WriteFile( SafeFileHandle hFile, void* pBuffer, int NumberOfBytesToWrite, int* pNumberOfBytesWritten, int Overlapped); [DllImport("kernel32.dll", SetLastError = true)] public static extern unsafe bool ReadFile( SafeFileHandle hFile, void* pBuffer, int NumberOfBytesToRead, int* pNumberOfBytesRead, int Overlapped); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool SetFilePointerEx( SafeFileHandle hFile, long liDistanceToMove, out long lpNewFilePointer, uint dwMoveMethod); [DllImport("kernel32.dll")] public static extern bool FlushFileBuffers( SafeFileHandle hFile); #endregion } }
With that out of the way, I wrote a Stream class to make accessing the disk more “,NET-ish”. Here’s the code for the stream class:
using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; using Microsoft.Win32.SafeHandles; using System.Runtime.InteropServices; namespace DiskLib { public class DiskStream : Stream { public const int DEFAULT_SECTOR_SIZE = 512; private const int BUFFER_SIZE = 4096; private string diskID; private DiskInfo diskInfo; private FileAccess desiredAccess; private SafeFileHandle fileHandle; public DiskInfo DiskInfo { get { return this.diskInfo; } } public int SectorSize { get { return (int)this.diskInfo.BytesPerSector; } } public DiskStream(string diskID, FileAccess desiredAccess) { this.diskID = diskID; this.diskInfo = new DiskInfo(diskID); this.desiredAccess = desiredAccess; // if desiredAccess is Write or Read/Write // find volumes on this disk // lock the volumes using FSCTL_LOCK_VOLUME // unlock the volumes on Close() or in destructor this.fileHandle = this.openFile(diskID, desiredAccess); } private SafeFileHandle openFile(string id, FileAccess desiredAccess) { uint access; switch (desiredAccess) { case FileAccess.Read: access = DeviceIO.GENERIC_READ; break; case FileAccess.Write: access = DeviceIO.GENERIC_WRITE; break; case FileAccess.ReadWrite: access = DeviceIO.GENERIC_READ | DeviceIO.GENERIC_WRITE; break; default: access = DeviceIO.GENERIC_READ; break; } SafeFileHandle ptr = DeviceIO.CreateFile( id, access, DeviceIO.FILE_SHARE_READ, IntPtr.Zero, DeviceIO.OPEN_EXISTING, DeviceIO.FILE_FLAG_NO_BUFFERING | DeviceIO.FILE_FLAG_WRITE_THROUGH, IntPtr.Zero); if (ptr.IsInvalid) { Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); } return ptr; } public override bool CanRead { get { return (this.desiredAccess == FileAccess.Read || this.desiredAccess == FileAccess.ReadWrite) ? true : false; } } public override bool CanWrite { get { return (this.desiredAccess == FileAccess.Write || this.desiredAccess == FileAccess.ReadWrite) ? true : false; } } public override bool CanSeek { get { return true; } } public override long Length { get { return this.diskInfo.Size; } } public override long Position { get { long n = 0; if (!DeviceIO.SetFilePointerEx(this.fileHandle, 0, out n, (uint)SeekOrigin.Current)) Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); return n; } set { if (value > (this.Length - 1)) throw new EndOfStreamException("Cannot set position beyond the end of the disk."); long n = 0; if (!DeviceIO.SetFilePointerEx(this.fileHandle, value, out n, (uint)SeekOrigin.Begin)) Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); } } public override void Flush() { // not required, since FILE_FLAG_WRITE_THROUGH and FILE_FLAG_NO_BUFFERING are used //if (!Unmanaged.FlushFileBuffers(this.fileHandle)) // Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); } public override void Close() { DeviceIO.CloseHandle(this.fileHandle); base.Close(); } public override void SetLength(long value) { throw new NotSupportedException("Setting the length is not supported with DiskStream objects."); } public override unsafe int Read(byte[] buffer, int offset, int count) { int n = 0; fixed (byte* p = buffer) { if (!DeviceIO.ReadFile(this.fileHandle, p + offset, count, &n, 0)) Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); } return n; } public override unsafe void Write(byte[] buffer, int offset, int count) { int n = 0; fixed (byte* p = buffer) { if (!DeviceIO.WriteFile(this.fileHandle, p + offset, count, &n, 0)) Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); } } public override long Seek(long offset, SeekOrigin origin) { long n = 0; if (!DeviceIO.SetFilePointerEx(this.fileHandle, offset, out n, (uint)origin)) Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); return n; } public int ReadSector(DiskSector sector) { return this.Read(sector.Data, 0, sector.SectorSize); } public void WriteSector(DiskSector sector) { this.Write(sector.Data, 0, sector.SectorSize); } public void SeekSector(DiskSector sector) { this.Seek(sector.Offset, SeekOrigin.Begin); } } public struct DiskSector { public const int DEFAULT_SECTOR_SIZE = 512; private long offset; private byte[] data; private int sectorSize; public int SectorSize { get { return this.sectorSize; } set { if ((value % 2) != 0) throw new ArgumentException("Sector size must be a multiple of 2."); this.sectorSize = value; } } public long Offset { get { return this.offset; } set { if ((value % this.SectorSize) != 0) throw new ArgumentException("Sector offset must be a multiple of SectorSize."); this.offset = value; } } public byte[] Data { get { return this.data; } set { if (value.Length != this.SectorSize) throw new ArgumentException("Data length must be the same as SectorSize."); this.data = value; } } public DiskSector(byte[] sectorData, long sectorOffset, int sectorSize) { if ((sectorSize % 2) != 0) throw new ArgumentException("Sector size must be a multiple of 2."); this.sectorSize = sectorSize; if (sectorData.Length != sectorSize) throw new ArgumentException("Data length must be the same as SectorSize."); this.data = sectorData; if ((sectorOffset % sectorSize) != 0) throw new ArgumentException("Sector offset must be a multiple of SectorSize."); this.offset = sectorOffset; } public DiskSector(byte[] sectorData, long sectorOffset) { int sectorSize = DEFAULT_SECTOR_SIZE; this.sectorSize = sectorSize; if (sectorData.Length != sectorSize) throw new ArgumentException("Data length must be the same as SectorSize."); this.data = sectorData; if ((sectorOffset % sectorSize) != 0) throw new ArgumentException("Sector offset must be a multiple of SectorSize."); this.offset = sectorOffset; } public DiskSector(long sectorOffset) { int sectorSize = DEFAULT_SECTOR_SIZE; this.sectorSize = sectorSize; this.data = new byte[sectorSize]; if ((sectorOffset % sectorSize) != 0) throw new ArgumentException("Sector offset must be a multiple of SectorSize."); this.offset = sectorOffset; } } }
You might’ve noticed the DiskInfo class, I’m not going to post the code here, but I’ll provide a download link. It’s currently just a wrapper around WMI/Win32_DiskDrive, although I’m planning on removing the WMI code and replacing it with more platform invoke calls. I’ve also added a DiskSector struct to (attempt to) make accessing the disk using sectors easier. The DiskSector struct can be used with the ReadDiskSector(), WriteDiskSector() and SeekDiskSector() methods of the DiskStream class.
And finally a WARNING to anyone intending to use this code: messing around with disks like this is EXTREMELY dangerous and you can hose your whole system very easily. I HIGHLY recommend you play with this code within a Virtual Machine (VirtualBox, Qemu, etc) or on a separate system that you don’t mind losing.
It’s also important to note that this is very early code, and it has not at all been thoroughly tested in any way. I’ve used it to toggle some bits in the MBR, read the partition table, and to wipe a disk clean, and that’s the extent of my testing. Proceed with caution and at your own risk.
I’ll update this post as I improve the code here.
Here’s a bunch of source files which could be useful in Zip format.
