C#: Detect encrypted volumes

Determine if a given path/filename is pointing to an encrypted volume. This C# code analyses a specified file path and determines if it is pointing to a mounted encryption container (e.g. PGP disk, TrueCrypt, VeraCrypt volume).

Determine if a given path/filename is pointing to an encrypted volume

One task that I was recently dealing with, was to ensure that confidential data that is needed “in-the-field” can only be stored in an encrypted container on the laptop. The policy states that disk drive encryption is not enough since when logged in, the encryption is transparent.
Therefore the procedures recommend to use in addition encrypted containers, like PGP disks, TrueCrypt (which is not supported anymore) or VeraCrypt. Those containers are explicitly mounted as disk drives when classified data needs to be accessed and either automatically closed (dismounted) after a defined inactivity period or explicitly closed by the user when work is finished.
The challenge was now to provide a means for software applications to determine if the target directory onto which the data is supposed to be stored is part of an encrypted container or not – whereas storage to any other location should be prohibited by the software application.
Thus, I’ve created a small software library written in C# / .NET that uses forensic techniques to determine whether a specified directory is part of such a container or not.
In this article I’m explaining the idea and the solution to this problem.

Forensic background

Microsoft Windows keeps track of all (currently) mounted devices in the registry hive HKLM\SYSTEM\MountedDevices. Each partition/device known to Windows will have here at least one entry in the form of \??\Volume{GUID}. If a drive letter is also assigned, a second entry will show up in the form of \DosDevices\DriveLetter:.

 
The binary information provided in the data column allows to determine if the associated volume is an encrypted container. This is done by analyzing the binary data since this data will contain (or start with) signatures or magic numbers that can be used to determine the nature of the device:

Product Signature ASCII Representation Remarks
PGP Disk 50 00 47 00 50 00 64 00 69 00 73 00 6B 00 56 00 6F 00 6C 00 75 00 6D 00 65 00 P.G.P.d.i.s.k.V.o.l.u.m.e.
(the.represents a null-byte)
The following bytes are used by PGP
TrueCrypt 54 72 75 65 43 72 79 70 74 56 6F 6C 75 6D 65 TrueCryptVolume The following byte specifies the drive letter
VeraCrypt 56 65 72 61 43 72 79 70 74 56 6F 6C 75 6D 65 VeraCryptVolume The following byte specifies the drive letter

 
So by analyzing the first n bytes according to the values in the table above, we can tell already if a certain volume is a mounted encryption container.
 

Determine the Volume ID

The here presented registry values are identified by the volume’s ID (as the registry key), so it is required to derive the volume’s ID from a given path.
Unfortunately, the \DosDevices\DriveLetter: key is not reliable as well since PGP as well as Windows allows mounting volumes anywhere within the file system structure (directories).
The following illustrates this behavior where a virtual disk (Disk 1) is mounted under the directory C:\Temp\Virtual Disk. Anything within this directory is stored on the virtual disk and will only be available as long as the virtual disk is mounted.

In the here shown scenario, no\DosDevicesentry will show up in the registry but a\??\Volume{Guid}will be present!
This actually means that we need to determine the volume’s ID from a given path. To get the job done, several Win32 API functions must be called. Using .NET, this can be easily achieved using Platform Invokes (PInvokes).

#region Windows API P/Invokes
[DllImport("kernel32.dll", SetLastError = true)]
static extern uint GetFullPathName(string lpFileName, uint nBufferLength, [Out] StringBuilder lpBuffer, out StringBuilder lpFilePart);

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GetVolumePathName(string lpszFileName, [Out] StringBuilder lpszVolumePathName, uint ccBufferLength);

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GetVolumeNameForVolumeMountPoint(string lpszVolumeMountPoint, [Out] StringBuilder lpszVolumeName,  uint cchBufferLength);
#endregion

The Win32 API functionGetVolumePathNameis used to get a directory’s mount point. This function takes as a parameter

  • the target file or directory name(string lpszFileName)
  • and fills up a buffer with the path pointing to the correct mount point([Out] StringBuilder lpszVolumePathName).

In order to determine the required size of the output buffer, the Win32 API functionGetFullPathNameis used. This function returns the size in bytes of the buffer that would be required to hold the path’s name and the terminating null character.
 
The following functionGetVolumeMountPointForPathcombines these two API calls and returns the correct mount point for any given path:

/// <summary>
/// Uses the Windows API to determine the volume mount point of a specified path.
/// </summary>
/// <param name="path">The path for which the mount point needs to be determined.</param>
/// <returns>The volume mount point or null if the mount point could not be determined.</returns>
private static string GetVolumeMountPointForPath(string path)
{
    StringBuilder tmp = new StringBuilder(); // a temporary buffer that is required as parameter.
    // if the passed buffer size is smaller than the required size, the function returns the required buffer size.
    // see Microsoft Documentation @ https://docs.microsoft.com/de-de/windows/desktop/api/fileapi/nf-fileapi-getfullpathnamea
    uint requiredBufferSize = GetFullPathName(path, (uint)1, new StringBuilder(), out tmp);
    if (requiredBufferSize >= 1)
    {
        requiredBufferSize++; // make sure there is at least 1 byte more!
        // Buffer of correct size that contains the volume of the path.
        StringBuilder b = new StringBuilder((int)requiredBufferSize);
        if (GetVolumePathName(path, b, requiredBufferSize))
        {
            // return the volume mount point
            return b.ToString();
        }
    }
    return null; // return null if an error occurred
}

As the debugger shows, the method returns the mount point for a file within an in-directory mounted volume.

 
This information is now used to retrieve the volume’s GUID path for the volume that is associated with the specified volume mount point. And this in turn is then used to query the correct key in the registry as shown in the beginning.
 
To retrieve the volume’s GUID path, the Win32 API functionGetVolumeNameForVolumeMountPointis used.

StringBuilder volName = new StringBuilder(50); // Buffer for the volume name (can be max. 50 chars)

if (GetVolumeNameForVolumeMountPoint(mntPoint, volName, (uint)volName.Capacity))
{
    string volumeID = volName.ToString(); // the volume's GUID path
    // Code continues here...

 
But there is a small problem with what the Win32 API returns. If you run the code and debug into thevolumeIDvariable, you will see that it has a value similar to\\?\Volume{GUID}\whereas the registry key has a value similar to\??\Volume{GUID}.
This actually means that we need to fix that formatting problem of Windows Management Instrumentation in our code:

    // Code continues here...
    // Fix a Microsoft Windows Management Instrumentation Formatting Problem
    if (volumeID.ToString().StartsWith(@"\\?\Volume{"))
    {
        volumeID = volumeID.Replace(@"\\?\Volume{", @"\??\Volume{");
    }

    if (volumeID.EndsWith("\\"))
        volumeID = volumeID.Substring(0, volumeID.Length - 1);

 

Query the registry for forensic data

Now it is time to query the registry using the retrieved volume’s ID in the correct format, read the associated data and compare it to known signatures / magic numbers to determine possible crypto-containers.

// open the registry hive HKLM\SYSTEM\MountedDevices. 
// Make sure you open it read-only, otherwise the code must be run with admin privileges.
var registry = Registry.LocalMachine.OpenSubKey("SYSTEM\\MountedDevices", false); 
var val = registry.GetValue(volumeID); // read the value for the given volume's GUID path

var col = System.Console.ForegroundColor;
if (val is byte[])                     // should be a byte array...
{
    string hex = ByteArrayToString(val as byte[]); // convert the byte array to a string

    if (hex.ToUpper().StartsWith("5000470050006400690073006B0056006F006C0075006D006500"))
    {
        // yeah, a PGP drive was used :-).
        System.Console.ForegroundColor = ConsoleColor.Green;
        System.Console.WriteLine("  {0} is on a PGP encrypted volume.", path);
    }
    else if (hex.ToUpper().StartsWith("566572614372797074566F6C756D65"))
    {
        // yeah, a VeraCrypt drive was used :-).
        System.Console.ForegroundColor = ConsoleColor.Green;
        System.Console.WriteLine("  {0} is on a VeraCrypt encrypted volume.", path);
    }
    else if (hex.ToUpper().StartsWith("547275654372797074566F6C756D65"))
    {
        // yeah, a TrueCrypt drive was used :-).
        System.Console.ForegroundColor = ConsoleColor.Yellow;
        System.Console.WriteLine("  {0} is on a TrueCrypt encrypted volume.", path);
    }
    else
    {
        System.Console.ForegroundColor = ConsoleColor.Red;
        System.Console.WriteLine("  {0} is not on an encrypted volume.", path);
    }
}
else
{
    System.Console.ForegroundColor = ConsoleColor.Red;
    // propably a virtual disk mounted within the file system
    System.Console.WriteLine("  No device information could be retrieved for {0}.{1}  This normally indicates a mounted, non-encrypted virtual hard disk file.", path, System.Environment.NewLine);
}

 

The result

The information obtained can now be used in many ways. I’ve extended my library so that it either works with a list of user-specified paths (as shown in this article) or determine all currently existing mount points using a WMI call and returns a list of paths that point to encrypted storage areas.

Leave a Reply

Your email address will not be published. Required fields are marked *