When developing an application for Windows, you’re likely to need some sort of functionality from the Windows API library. The Windows API is basically the backend of the Windows operating system. Since Windows APIs are mostly written in C/C++, you may think that you’ll need to write your program in C/C++ for it to work with it. This isn’t the case; C# (and other programming languages) can be used. In the fourth and final blog post in my series on C#, I will go through using marshaling and P/Invoke to read the NTFS (New Technology File System) boot sector.

Contents

Equivalent Data Types

Before going into P/Invoke and Marshal, it is good to go over the equivalent data types in C/C++ and C#. There are basically an infinite number of ways the data types in C/C++ can be defined.

C/C++ C# Size (bits) Size (bytes)
char SByte 8 1
unsigned char
BYTE
UCHAR
byte
short
short int
signed short
signed short int
short 16 2
unsigned short
unsigned short int
WORD
USHORT
ushort
int
signed
signed int
BOOL1
int 32 4
unsigned
unsigned int
DWORD
uint
long long
long long int
signed long long
signed long long int
__int64
LONGLONG
long 64 8
unsigned long
unsigned __int64
ULONG
ULONGLONG
ulong
void * IntPtr
UIntPtr2
32 or 64 (depending on operating system) 4 or 8 (depending operating system)
char *
wchar_t *
string3 Length x 8 or Length x 16 (depending on character set) Length x 1 or Length x 2 (depending on character set)

Notes

  1. Not to be confused with “bool” (all lower case) which is 8 bits (or 1 byte).
  2. The major difference between IntPtr and UIntPtr is the former is CLS compliant while the latter is not.
  3. CharSet should be set to CharSet.Unicode if using a wide character set (CharSet.ANSI is set by default).

Breakdown

To start, we have the main entry point to the C# program. The code that will be put together by the end of this blog post will be formatted as follows.

class Program
{
    static void Main(string[] args)
    {
        // Open handle to volume using CreateFile

        // Read data with from volume with ReadFile

        // Parse data using Marshal
    }
}

P/Invoke

P/Invoke is short for Platform Invocation Services and it allows code in C# to call other external code that is usually in another programming language (like C/C++). A P/Invoke signature starts with the DllImport attribute at the top and then the signature below it. How the attribute and signature is made up is explained below. Anytime we have a P/Invoke signature, we need to reference the System.Runtime.InteropServices namespace at the top of the file.

CreateFile

Putting together the signature

The first function from the Windows API we’ll need is CreateFile. I’m going to go through looking at the MSDN documentation to get the P/Invoke signature of it in C#.

At the top of the page, we have the signature:

HANDLE WINAPI CreateFile(
  _In_     LPCTSTR               lpFileName,
  _In_     DWORD                 dwDesiredAccess,
  _In_     DWORD                 dwShareMode,
  _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  _In_     DWORD                 dwCreationDisposition,
  _In_     DWORD                 dwFlagsAndAttributes,
  _In_opt_ HANDLE                hTemplateFile
);

At the bottom of the page, lets take note of a few things from the “Requirements” section… the first is CreateFile is available from the dynamically linked library (DLL) file “Kernel32.dll”. Another is there’s two different names for the function, “CreateFileA” and “CreateFileW”. It is common to see this since there are two variations of how characters in C/C++ can be represented, either ANSI or Unicode. An ANSI character is made up of one byte while a Unicode character is made up of two bytes.

With the above information, we can put together a P/Invoke signature. The following is a diagram I came up with that documents how this function can be translated to work in C#.

The result would be this:

[DllImport(“Kernel32.dll”, CharSet = CharSet.Auto)]
private static extern IntPtr CreateFile(
	[In]			string	lpFileName, 
	[In]			uint		dwDesiredAccess, 
	[In]			uint		dwShareMode, 
	[In, Optional]	IntPtr	lpSecurityAttributes, 
	[In]			uint		dwCreationDisposition, 
	[In]			uint		dwFlagsAndAttributes, 
	[In, Optional]	IntPtr	hTemplateFile
);

This would work; however, a quicker way is to find the P/Invoke function on pinvoke.net. Simply locate the DLL that this function is located in (Kernel32) on the left dropdown menu and expand it. Once expanded, click on “CreateFile”. Let’s go with the signature located in the third box on the webpage:

[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern SafeFileHandle CreateFile(
string lpFileName,
 	[MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess,
	[MarshalAs(UnmanagedType.U4)] FileShare dwShareMode,
	IntPtr lpSecurityAttributes,
	[MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition,
	[MarshalAs(UnmanagedType.U4)] FileAttributes dwFlagsAndAttributes,
	IntPtr hTemplateFile);

The signature here utilizes some built-in types in C#. SafeFileHandle (in Microsoft.Win32.SafeHandles) is wrapper class for disposing a IntPtr that represents a HANDLE properly. FileAccess, FileShare, FileMode, and FileAttributes in the System.IO namespace are enumerations that contain all the possible values that CreateFile would normally accept. The attribute [MarshalAs(UnmanagedType.U4)] tells C# to treat those parameters it’s specified on as an unsigned 4 byte integer.

Opening a handle to the volume

I am going to need to get a handle to the volume. Once I have this handle, I can then read from the volume. I should note that I can’t write to the volume because of Windows restrictions. We’re going to add in the signature of CreateFile from above. Don’t forget, we also need the namespaces that it uses and always, always, always the System.Runtime.InteropServices namespace.

using System;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

class Program
{
    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern SafeFileHandle CreateFile(
        string lpFileName,
        [MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess,
        [MarshalAs(UnmanagedType.U4)] FileShare dwShareMode,
        IntPtr lpSecurityAttributes,
        [MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition,
        [MarshalAs(UnmanagedType.U4)] FileAttributes dwFlagsAndAttributes,
        IntPtr hTemplateFile);
	
        static void Main(string[] args)
        {
            // Open handle to volume using CreateFile

            // Read data with from volume with ReadFile

            // Parse data using Marshal
	}
}

With the P/Invoke signature added, we can call the function just like a method. Below I explain each parameter we’ll use:

  • lpFileName – The file path format for a volume is “\\.\[Drive]:”, where [Drive] is the drive letter (between A and Z) in uppercase. The drive letter on most systems is C and that’s what we’re going to use, so the file path is “\\.\C:”.
  • dwDesiredAccess – As I mentioned above, we can only read the volume and even if we could write to it, that would be a very bad thing to do. Therefore, the desired access is just FileAccess.Read (which represents GENERIC_READ in C/C++). I should note, the FileAccess.Write flag will work in the sense that CreateFile returns a valid handle, however, trying to write to the file handle (with WriteFile) will give error code 5 (ERROR_ACCESS_DENIED).
  • dwShareMode – This is a volume that is constantly being accessed and here we want to allow other programs to access the volume while we’re reading it. Setting this to FileShare.ReadWrite (which represents “FILE_SHARE_READ|FILE_SHARE_WRITE” in C/C++) is a good choice.
  • lpSecurityAttributes – We don’t need to worry about allowing child processes to inherit handle. This is set to IntPtr.Zero which represents a “nullptr” in C++.
  • dwCreationDisposition – The file path should already exist and we don’t have to create it nor do we truncate (or erase) it. The better choice here is FileMode.Open (which represents “OPEN_EXISTING” in C/C++).
  • dwFlagsAndAttributes – We’re going to set this to 0 because there are no file attributes or flags to worry about. Technically, this is a directory that’s being opened and the MSDN documentation states that FILE_FLAG_BACKUP_SEMANTICS (0x02000000) should be specified, but, in my tests it made no difference.
  • hTemplateFile – An existing file is being opened, therefore, this can be set to IntPtr.Zero.

Here is the code that calls CreateFile:

using System;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

class Program
{
	[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
	private static extern SafeFileHandle CreateFile(
		string lpFileName,
		[MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess,
		[MarshalAs(UnmanagedType.U4)] FileShare dwShareMode,
		IntPtr lpSecurityAttributes,
		[MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition,
		[MarshalAs(UnmanagedType.U4)] FileAttributes dwFlagsAndAttributes,
		IntPtr hTemplateFile);

	static void Main(string[] args)
	{
		// Open handle to volume using CreateFile
		var handle = CreateFile(
			"\\\\.\\C:", 
			FileAccess.Read, 
			FileShare.ReadWrite, 
			IntPtr.Zero, 
			FileMode.Open, 
			0,
			IntPtr.Zero);

		if (handle.IsClosed || handle.IsInvalid)
		{
			Console.WriteLine("An error occurred trying to open file. Error: {0}", Marshal.GetLastWin32Error());
			return;
		}

		// Read data with from volume with ReadFile

		// Parse data using Marshal

 		handle.Close();
	}
}

The if statement below checks if the handle was opened and if it is closed or invalid, outputs a message with the error code (created by the CreateFile function using Marshal.GetLastWin32Error) and exits. This program requires administrator privileges and the handle can be closed or invalid if it’s not ran as administrator. The last line in the Main method closes the handle.

ReadFile

Signature

For this, let’s get a signature for ReadFile using the handy pinvoke.net. There are four different signatures listed and the parameters can be combined to make one signature that’ll do the same but with less code.

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool ReadFile(
	SafeFileHandle				hFile,					// Used since CreateFile returns SafeFileHandle
	IntPtr						pBuffer,				// Can be byte[], but for our purposes, it's easier to read to a IntPtr
	uint						NumberOfBytesToRead,	// Shouldn't be int because this isn't going to be negative
	out uint					pNumberOfBytesRead,		// Could be IntPtr but easier to just use this
	[In] ref NativeOverlapped	lpOverlapped			// Can also be IntPtr
);

Preparing for reading the file

Before calling ReadFile in C#, some preparation is going to need to be done using marshalling.

A buffer is needed so ReadFile has somewhere to store the bytes that are read. But how do we create a buffer for a IntPtr data type? The answer is the Marshal.AllocHGlobal method. The length of the buffer is 512 bytes because a volume is split up into sectors and you must read from it sectors at time, which are (usually) 512 bytes on NTFS volumes.

var bufferPtr = Marshal.AllocHGlobal(512);

This will allocate 512 bytes in a fixed memory address in the heap and return that memory address. C# does not manage this buffer and it is always good practice to free it once we’re done using it to prevent memory leaks. Use the Marshal.FreeHGlobal method to free the memory…

Marshal.FreeHGlobal(bufferPtr);

NativeOverlapped is in the System.Threading namespace and is used to specify where in the file we want to read. If it is set to null, it reads from the current position in the file. We could leave this as null and it would read from the current position (which is the beginning), but let’s set it anyway.

var nativeOverlapped = new NativeOverlapped {OffsetLow = 0, OffsetHigh = 0};

Notice there are two fields for the offset, OffsetLow and OffsetHigh, and they’re both set to 0. If you don’t understand what this is, look at this webpage on high and low bits.

Here is the code to prepare for the method call:

var bufferPtr = Marshal.AllocHGlobal(512);
var nativeOverlapped = new NativeOverlapped {OffsetLow = 0, OffsetHigh = 0};

// Do stuff

Marshal.FreeHGlobal(bufferPtr);

Reading the file

The following uses what I explained above except for one other variable, which is needed to receive the number of bytes that were read.

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32.SafeHandles;

class Program
{
	[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
	private static extern SafeFileHandle CreateFile(
		string lpFileName,
		[MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess,
		[MarshalAs(UnmanagedType.U4)] FileShare dwShareMode,
		IntPtr lpSecurityAttributes,
		[MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition,
		[MarshalAs(UnmanagedType.U4)] FileAttributes dwFlagsAndAttributes,
		IntPtr hTemplateFile);

	[DllImport("kernel32.dll", SetLastError = true)]
	private static extern bool ReadFile(
		SafeFileHandle hFile, // Used since CreateFile returns SafeFileHandle
		IntPtr pBuffer, // Can be byte[], but for our purposes, it’s easier to read to a IntPtr
		uint NumberOfBytesToRead, // Shouldn’t be int because this isn’t going to be negative
		out uint pNumberOfBytesRead, // Could be IntPtr but easier to just use this
		[In] ref NativeOverlapped lpOverlapped // Can also be IntPtr 
	);


	static void Main(string[] args)
	{
		// Open handle to volume using CreateFile
		var handle = CreateFile(
		  "\\\\.\\C:",
		  FileAccess.Read,
		  FileShare.ReadWrite,
		  IntPtr.Zero,
		  FileMode.Open,
		  0,
		  IntPtr.Zero);

		if (handle.IsClosed || handle.IsInvalid)
		{
			Console.WriteLine("An error occurred trying to open file. Error code: {0}", Marshal.GetLastWin32Error());
			return;
		}

		// Read data with from volume with ReadFile
		var bufferPtr = Marshal.AllocHGlobal(512);
		var nativeOverlapped = new NativeOverlapped { OffsetLow = 0, OffsetHigh = 0 };
		if (!ReadFile(handle, bufferPtr, 512, out uint bytesRead, ref nativeOverlapped))
		{
			Console.WriteLine("Unable to read volume. Error code: {0}", Marshal.GetLastWin32Error());

			Marshal.FreeHGlobal(bufferPtr);
			handle.Close();

			return;
		}

		// Parse data using Marshal

		Marshal.FreeHGlobal(bufferPtr);
		handle.Close();
	}
}

A few things you may have noticed… The bufferPtr isn’t freed until closer to the end of code since we’ll be needing it. A nice thing in C# is an out parameter doesn’t need to be declared before it’s used and can be declared right in the method call. I added an if check on ReadFile because it might fail and there’s no use continuing without our needed data (don’t forget to free the allocated buffer and handle).

Marshalling

The data has been read and next, we want to break it down. With the Marshal class, the IntPtr that we got previous can be changed into a struct in C#. A struct is a group of variables and are like classes, except usually only with fields inside them.

The NTFS Boot Sector

I put together this table which is the structure for the boot sector of the NTFS from a known website with information about the filesystem.

Offset Length Sample Value Name
0x00 3 bytes Jump Instruction
0x03 LONGLONG
(8 bytes)
OEM ID
0x0B WORD
(2 bytes)
0x0002 Bytes Per Sector
0x0D BYTE 0x08 Sectors Per Cluster
0x0E WORD
(2 bytes)
0x0000 Reserved Sectors
0x10 3 bytes 0x000000 Always zero
0x13 WORD
(2 bytes)
0x0000 Not used by NTFS
0x15 BYTE 0xF8 Media Descriptor
0x16 WORD
(2 bytes)
0x0000 Always zero
0x18 WORD
(2 bytes)
0x3F00 Sectors Per Track
0x1A WORD
(2 bytes)
0xFF00 Number of Heads
0x1C DWORD
(4 bytes)
0x3F000000 Hidden Sectors
0x20 DWORD
(4 bytes)
0x00000000 Not used by NTFS
0x24 DWORD
(4 bytes)
0x80008000 Not used by NTFS
0x28 LONGLONG
(8 bytes)
0x4AF57F0000000000 Total Sectors
0x30 LONGLONG
(8 bytes)
0x0400000000000000 Logical Cluster Number for the file $MFT
0x38 LONGLONG
(8 bytes)
0x54FF070000000000 Logical Cluster Number for the file $MFTMirr
0x40 DWORD
(4 bytes)
0xF6000000 Clusters Per File Record Segment
0x44 BYTE 0x01 Clusters Per Index Buffer
0x45 3 BYTES 0x000000 Not used by NTFS
0x48 LONGLONG
(8 bytes)
0x14A51B74C91B741C Volume Serial Number
0x50 DWORD
(4 bytes)
0x00000000 Checksum
0x54 426 bytes Bootstrap Code
0x01FE WORD
(2 bytes)
End of Sector Marker

Next, a struct can be created using the table on the equivalent data types above. This will be added below the P/Invoke signatures included earlier.

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct NtfsBootSector
{
	[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
	public readonly byte[] JMPInstruction;
	public readonly ulong OEMID;
	public readonly ushort BytesPerSector;
	public readonly byte SectorsPerCluster;
	public readonly ushort ReservedSectors;
	[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
	public readonly byte[] AlwaysZero1;
	public readonly ushort NotUsed1;
	public readonly byte MediaDescriptor;
	public readonly ushort AlwaysZero2;
	public readonly ushort SectorsPerTrack;
	public readonly ushort NumberOfHeads;
	public readonly uint HiddenSectors;
	public readonly uint NotUsed2;
	public readonly uint NotUsed3;
	public readonly ulong TotalSectors;
	public readonly ulong MFTLCN;
	public readonly ulong MFTMirrLCN;
	public readonly byte ClustersPerMFTRecord;
	[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
	public readonly byte[] NotUsed4;
	public readonly uint ClustersPerIndexBuffer;
	public readonly ulong VolumeSerialNumber;
	public readonly uint NTFSChecksum;
	[MarshalAs(UnmanagedType.ByValArray, SizeConst = 426)]
	public readonly byte[] BootStrapCode;
	public readonly ushort Signature;
}

Somethings to note:

  • At the top of the struct, the attribute “StructLayout” is used. LayoutKind.Sequential is set by default and not important. What is important is “Pack”. I’m not going to go into great detail on this, however, data types in structs are aligned using the size of the largest data type (not including the size of arrays) in it (in our case, 8 bytes because of ulong). When there’s a data type that isn’t 8 bytes, a padding is added to it to make it be 8 bytes in memory. We don’t want this, so “Pack” is set to 1 cause the data is byte by byte (rather than ulong by ulong).
  • The fields with an odd number of bytes. For these, the MarshalAs attribute is attached with the UnmanagedType set to UnmanagedType.ByValArray and SizeConst set to the length of the field. It is not until an array data type is assigned that the length is known, so setting UnmanagedType.ByValArray means there’s an array data type that needs to be marshalled and the size of it should be what SizeConst is set to.
  • Changing the visibility modifier (to public, private, etc.) doesn’t effect marshalling.
  • We’re only reading the data so “readonly” is set.

Marshalling to a Struct

The operation of marshalling data to a struct (that we created above) is very simple. We first need to declare the struct variable. Then the Marshal.PtrToStructure method is called and the return value is assigned to this variable. This method as a couple different ways of calling it. Both need the IntPtr as a parameter but the difference is one needs the datatype to return specified as a generic and the other needs the data type to return in the parameters. We’ll use the former because the latter returns a object (which then needs to be casted). I simplified the code using “var” rather than “NtfsBootSector” in the declaration (cause the data type is determined by what it’s assigned to).

var bootSector = Marshal.PtrToStructure<NtfsBootSector>(bufferPtr);

Final Code

The above code can be included and we’ll have done everything to get the boot sector from a NTFS volume. The Debug.Assert is added after parsing the data to ensure that we have a NTFS (and not a FAT) volume.

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32.SafeHandles;

class Program
{
	[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
	private static extern SafeFileHandle CreateFile(
		string lpFileName,
		[MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess,
		[MarshalAs(UnmanagedType.U4)] FileShare dwShareMode,
		IntPtr lpSecurityAttributes,
		[MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition,
		[MarshalAs(UnmanagedType.U4)] FileAttributes dwFlagsAndAttributes,
		IntPtr hTemplateFile);

	[DllImport("kernel32.dll", SetLastError = true)]
	private static extern bool ReadFile(
		SafeFileHandle hFile, // Used since CreateFile returns SafeFileHandle
		IntPtr pBuffer, // Can be byte[], but for our purposes, it’s easier to read to a IntPtr
		uint NumberOfBytesToRead, // Shouldn’t be int because this isn’t going to be negative
		out uint pNumberOfBytesRead, // Could be IntPtr but easier to just use this
		[In] ref NativeOverlapped lpOverlapped // Can also be IntPtr 
	);
	
	[StructLayout(LayoutKind.Sequential, Pack = 1)]
	public struct NtfsBootSector
	{
		[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
		public readonly byte[] JMPInstruction;
		public readonly ulong OEMID;
		public readonly ushort BytesPerSector;
		public readonly byte SectorsPerCluster;
		public readonly ushort ReservedSectors;
		[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
		public readonly byte[] AlwaysZero1;
		public readonly ushort NotUsed1;
		public readonly byte MediaDescriptor;
		public readonly ushort AlwaysZero2;
		public readonly ushort SectorsPerTrack;
		public readonly ushort NumberOfHeads;
		public readonly uint HiddenSectors;
		public readonly uint NotUsed2;
		public readonly uint NotUsed3;
		public readonly ulong TotalSectors;
		public readonly ulong MFTLCN;
		public readonly ulong MFTMirrLCN;
		public readonly byte ClustersPerMFTRecord;
		[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
		public readonly byte[] NotUsed4;
		public readonly uint ClustersPerIndexBuffer;
		public readonly ulong VolumeSerialNumber;
		public readonly uint NTFSChecksum;
		[MarshalAs(UnmanagedType.ByValArray, SizeConst = 426)]
		public readonly byte[] BootStrapCode;
		public readonly ushort Signature;
	}

	static void Main(string[] args)
	{

		// Open handle to volume using CreateFile
		var handle = CreateFile(
		  "\\\\.\\C:",
		  FileAccess.Read,
		  FileShare.ReadWrite,
		  IntPtr.Zero,
		  FileMode.Open,
		  0,
		  IntPtr.Zero);

		if (handle.IsClosed || handle.IsInvalid)
		{
			Console.WriteLine("An error occurred trying to open file. Error code: {0}", Marshal.GetLastWin32Error());
			return;
		}

		// Read data with from volume with ReadFile
		var bufferPtr = Marshal.AllocHGlobal(512);
		var nativeOverlapped = new NativeOverlapped { OffsetLow = 0, OffsetHigh = 0 };
		if (!ReadFile(handle, bufferPtr, 512, out uint bytesRead, ref nativeOverlapped))
		{
			Console.WriteLine("Unable to read volume. Error code: {0}", Marshal.GetLastWin32Error());

			Marshal.FreeHGlobal(bufferPtr);
			handle.Close();

			return;
		}

		// Parse data using Marshal
		var bootSector = Marshal.PtrToStructure<NtfsBootSector>(bufferPtr);

		// Is this a NTFS boot sector?
		Debug.Assert(bootSector.OEMID == 0x202020205346544e); // Represents 'NTFS    '

	    Marshal.FreeHGlobal(bufferPtr);
        handle.Close();
    }
}

But wait, there’s more!

If you’re trying to run this and the handle is not being opened (as I mentioned before) the program needs to be ran as administrator. With the boot sector read, are we going to just assign it to a variable and never use it? I doubt that. There’s much more that can be done here. In the Gist that I created, the NTFS boot sector fields are outputted. Using the size of sectors and number of sectors inside a cluster, the offset for a LCN (Logical Cluster Number) on the hard drive can be calculated.

Conclusion

With the understanding of how to read the boot sector of a NTFS volume, you can do other various things with a NTFS volume. There’s a project that I’ve been working on recently which does much more than what was explained here. The project called NtfsSharp is hosted on GitHub. It allows a program to read a NTFS volume just like Windows does. Some may say it’s reinventing the wheel. However, if you’re into forensics, you can find information that is hidden in different parts of the file system and an average user would have no idea it’s there. Defragmenter tools also use this information to determine what files are fragmented and where they can be relocated to. I hope my blog series on C# has opened the door to what you can do with this popular programming language!