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.
\DosDevices
entry 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 functionGetVolumePathName
is 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 functionGetFullPathName
is 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 functionGetVolumeMountPointForPath
combines 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 function
GetVolumeNameForVolumeMountPoint
is 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 thevolumeID
variable, 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