Ever since I wrote my function to locate a file’s MFT (ChangeTime) timestamp, I wanted to take on an extra challenge by taking all of the C# code that included using pinvoke and rewrite it using reflection in PowerShell after seeing fellow PowerShell MVP Matt Graeber’s article on using reflection. While there may not have been a need to keep this particular code resident in memory, it will server as a fun exercise in rewriting all of the C# code and avoid the use of Add-Type to compile the code (which does write to disk).
The plan is to piece through each part of code and show the translation from C# to using reflection to create the Structs, Enums and pinvoke methods in memory by showing the C# code followed by the PowerShell code. If you want to check out the code in its entirety, you can find the majority of it on the link that I referenced at the beginning of this article.
The following code is the initial part of the C# code that defines my environment and new class. Nothing here will be translated over to PowerShell using reflection.
using System; using System.Runtime.InteropServices; // for DllImport and friends using Microsoft.Win32.SafeHandles; // for SafeHandle using System.Collections.Generic; // for ParseFileAttributes helper function (List<FileAttributes> only using System.IO; // for test main (FileStream) only using System.Text; // for test main (Encoding) only using System.Threading; // for test main (Thread.Sleep) only using System.Diagnostics; // for test main (Trace.Write[Line]) only public class Nt {
First off, I am going to build my dynamic assembly and module which will be used the rest of the time to help define other types that will be required at different points.
#region Module Builder $Domain = [AppDomain]::CurrentDomain $DynAssembly = New-Object System.Reflection.AssemblyName('TestAssembly') # Only run in memory by specifying [System.Reflection.Emit.AssemblyBuilderAccess]::Run $AssemblyBuilder = $Domain.DefineDynamicAssembly( $DynAssembly, [System.Reflection.Emit.AssemblyBuilderAccess]::Run ) $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('TimeStampModule', $False) #endregion Module Builder
What I’ve done here is creating a new assembly called TestAssembly that is used later to build a dynamic module that is used to build out our required components (structs, enums, pinvoke, etc…).
I then hook into the current AppDomain and use a method called DefineAssembly() to configure the custom assembly (stored in memory by specifying [System.Reflection.Emit.AssemblyBuilderAccess]::Run). Once that is done, I build my dynamic module using DefineDynamicModule() and specify a module name and and whether the symbol information should be emitted.
Up next is where I define a Struct for IO_STATUS_BLOCK which is used to determine the status of using the win32 function, NTQueryInformationFile.
struct IO_STATUS_BLOCK { internal uint status; internal ulong information; }
And now for the PowerShell reflection approach:
#region STRUCTs #region IOStatusBlock #Define STRUCT $Attributes = 'AutoLayout, AnsiClass, Class, Public, SequentialLayout, Sealed, BeforeFieldInit' $TypeBuilder = $ModuleBuilder.DefineType('IOStatusBlock', $Attributes, [System.ValueType], 1, 0x40) [void]$TypeBuilder.DefineField('status', [UInt64], 'Public') [void]$TypeBuilder.DefineField('information', [UInt64], 'Public') #Create STRUCT Type [void]$TypeBuilder.CreateType() #endregion IOStatusBlock
I use my $ModuleBuilder that I defined earlier to build a new Struct using the DefineType() method in which I specify a name for the Struct as well as specifying some attributes to make it more like a Struct (I found out what these attributes were by looking at my previously compiled C# code and using Get-Member against the Struct). From there I take the Type I have created and use the DefineField() method to build out the fields to match what is required of the IO_STATUS_BLOCK. The Define() method requires that I supply a name, a type and the FieldAttribute of the field. Once completed, I simply call CreateType() to complete the build of the type so it is available to me.
The next Struct is for FILE_BASIC_INFORMATION and is a little different to build out due to some unique requirements:
[StructLayout(LayoutKind.Explicit)] struct FILE_BASIC_INFORMATION { [FieldOffset(0)] internal long CreationTime; [FieldOffset(8)] internal long LastAccessTime; [FieldOffset(16)] internal long LastWriteTime; [FieldOffset(24)] internal long ChangeTime; [FieldOffset(32)] internal ulong FileAttributes; }
Here I have a StructLayout attribute as well as FieldOffsets which define the physical position of the field in the struct. Have on fear though, we can easily put this one together!
#region FileBasicInformation #Define STRUCT $Attributes = 'AutoLayout, AnsiClass, Class, ExplicitLayout, Sealed, BeforeFieldInit,public' $TypeBuilder = $ModuleBuilder.DefineType('FileBasicInformation', $Attributes, [System.ValueType], 8, 0x40) $CreateTimeField = $TypeBuilder.DefineField('CreationTime', [UInt64], 'Public') $CreateTimeField.SetOffset(0) $LastAccessTimeField = $TypeBuilder.DefineField('LastAccessTime', [UInt64], 'Public') $LastAccessTimeField.SetOffset(8) $LastWriteTimeField = $TypeBuilder.DefineField('LastWriteTime', [UInt64], 'Public') $LastWriteTimeField.SetOffset(16) $ChangeTimeField = $TypeBuilder.DefineField('ChangeTime', [UInt64], 'Public') $ChangeTimeField.SetOffset(24) $FileAttributesField = $TypeBuilder.DefineField('FileAttributes', [UInt64], 'Public') $FileAttributesField.SetOffset(32) #Create STRUCT Type [void]$TypeBuilder.CreateType() #endregion FileBasicInformation
Same as before, I use my ModuleBuilder to define the type with specific attributes (I have added ExplicitLayout to match the [StructLayout(LayoutKind.Explicit)] attribute defined in C#) and then start adding the fields. The thing you might notice is that I save each field from DefineField() to a variable. This is because I must then call the SetOffset() method and supply an integer which will define the physical position of the field. This is how I do my FieldOffset!
That does it for the Structs! Time to do an Enum.
I only have one Enum to do for FILE_INFORMATION_CLASS. I didn’t need everything, just the following items that are listed below in the C# code.
enum FILE_INFORMATION_CLASS { FileDirectoryInformation = 1, // 1 FileBasicInformation = 4, // 4 FileHardLinkInformation = 46 // 46 }
The refactored code for PowerShell is below:
#region ENUMs $EnumBuilder = $ModuleBuilder.DefineEnum('FileInformationClass', 'Public', [UInt32]) # Define values of the enum [void]$EnumBuilder.DefineLiteral('FileDirectoryInformation', [UInt32] 1) [void]$EnumBuilder.DefineLiteral('FileBasicInformation', [UInt32] 4) [void]$EnumBuilder.DefineLiteral('FileModeInformation', [UInt32] 16) [void]$EnumBuilder.DefineLiteral('FileHardLinkInformation', [UInt32] 46) #Create ENUM Type [void]$EnumBuilder.CreateType() #endregion ENUMs
Unlike the previous items, I will be calling DefineEnum() vs. DefineType() and supplying the name of the enum, the type attributes and underlying type for the enum. From there I start adding the enum values using DefineLiteral() and giving values for the name of the value name, the underlying type (must be UInt32 because UInt16 will work on PowerShell V3, but V4+ will fail when using the pinvoke method) and the numeric value of the name. Once that has completed, I call the familiar CreateType() method and away we go!
Now to hook into the Win32 API using pinvoke and grabbing the NTQueryInformationFile() function.
[DllImport("ntdll.dll", SetLastError = true)] static extern IntPtr NtQueryInformationFile(SafeFileHandle fileHandle, ref IO_STATUS_BLOCK IoStatusBlock, IntPtr pInfoBlock, uint length, FILE_INFORMATION_CLASS fileInformation);
The PowerShell code is somewhat larger as we have to build everything out, but it is by no means impossible. I will step through by splitting up some parts of the code to better explain what is happening here.
#region DllImport $TypeBuilder = $ModuleBuilder.DefineType('ntdll', 'Public, Class') #region NtQueryInformationFile Method $PInvokeMethod = $TypeBuilder.DefineMethod( 'NtQueryInformationFile', #Method Name [Reflection.MethodAttributes] 'PrivateScope, Public, Static, HideBySig, PinvokeImpl', #Method Attributes [IntPtr], #Method Return Type [Type[]] @([Microsoft.Win32.SafeHandles.SafeFileHandle], [IOStatusBlock], [IntPtr] ,[UInt64], [FileInformationClass]) #Method Parameters )
Once again, I use my ModuleBuilder object to define my type. This time I am specifying a name of the dll that the function resides on (I don’t have to do this, but it makes sense to make it something that relates to what I am using) as well as specifying the type attributes.
From there I can begin to define my NTQueryInformationFile method (the win32 function). To do this I use the DefineMethod() method and supply the name of the function, the method attributes (I found this by reviewing the method attributes on the compiled C# code), the return type that the function provides when used (in this case an IntPtr) and lastly I supply the required parameter types that are required for this to actually work. Fortunately, I can get all of this information from the MSDN page. This is a array of Types that consist of a SafeFileHandle, the IOStatusBlock type, an IntPtr, an UInt64 and lastly the FileInformationClass type.
$DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String])) $FieldArray = [Reflection.FieldInfo[]] @( [Runtime.InteropServices.DllImportAttribute].GetField('EntryPoint'), [Runtime.InteropServices.DllImportAttribute].GetField('SetLastError') ) $FieldValueArray = [Object[]] @( 'NtQueryInformationFile', #CASE SENSITIVE!! $True ) $SetLastErrorCustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder( $DllImportConstructor, @('ntdll.dll'), $FieldArray, $FieldValueArray )
This piece is where I build out and define the attributes of the DLLImport attribute. In this case, I only need to worry about setting the EntryPoint (which is the actual Win32 function that this will use – AND IT IS CASE SENSITIVE) and the SetLastError field that I can use to get any error messages if this happens to fail.
$PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute) #endregion NtQueryInformationFile Method [void]$TypeBuilder.CreateType() #endregion DllImport
I add that custom attribute to my method and then create the type.
Now I have a working method that will call a Win32 function! But that is not all! I had some extra stuff coded in C# (that really wasn’t needed, but I did it in C# just to learn more about it) that needs to be moved to PowerShell.
public static bool GetFourFileTimes(string path2file, out DateTime creationTime, out DateTime lastAccessTime, out DateTime lastWriteTime, out DateTime changeTime, out string errMsg) { bool brc = false; creationTime = default(DateTime); lastAccessTime = default(DateTime); lastWriteTime = default(DateTime); changeTime = default(DateTime); errMsg = string.Empty; IntPtr p_fbi = IntPtr.Zero; try { FILE_BASIC_INFORMATION fbi = new FILE_BASIC_INFORMATION(); IO_STATUS_BLOCK iosb = new IO_STATUS_BLOCK(); using (FileStream fs = new FileStream(path2file, FileMode.Open, FileAccess.Read, FileShare.Read)) { p_fbi = Marshal.AllocHGlobal(Marshal.SizeOf(fbi)); IntPtr iprc = NtQueryInformationFile(fs.SafeFileHandle, ref iosb, p_fbi, (uint)Marshal.SizeOf(fbi), FILE_INFORMATION_CLASS.FileBasicInformation); brc = iprc == IntPtr.Zero && iosb.status == 0; if (brc) { brc = false; fbi = (FILE_BASIC_INFORMATION)Marshal.PtrToStructure(p_fbi, typeof(FILE_BASIC_INFORMATION)); creationTime = DateTime.FromFileTime(fbi.CreationTime); lastAccessTime = DateTime.FromFileTime(fbi.LastAccessTime); lastWriteTime = DateTime.FromFileTime(fbi.LastWriteTime); changeTime = DateTime.FromFileTime(fbi.ChangeTime); brc = true; } } } catch (Exception ex) { brc = false; errMsg = ex.Message; } finally { if (p_fbi != IntPtr.Zero) { Marshal.FreeHGlobal(p_fbi); } } return brc; }
Pay no attention to the GetFourFileTimes() method as it will not be brought over into my PowerShell code. As you can see, there are a few things happening here that I need to migrate but nothing that is incredibly complex.
$fbi = New-Object "FileBasicInformation" $iosb = New-Object "IOStatusBlock" $FileStream = [System.IO.File]::Open("C:\Users\proxb\desktop\desktop.ini",'Open','Read','ReadWrite') $p_fbi = [System.Runtime.InteropServices.Marshal]::AllocHGlobal([System.Runtime.InteropServices.Marshal]::SizeOf($fbi))
First I create new objects using my newly created Structs. I then open up a file stream to a file of my choosing and making sure that others could access it if needed by supply values for the parameters of the Open() method of the System.Io.File type that ensure the file is not being locked.
I then get the physical location in memory ($p_fbi using AllocHGlobal()) of the FileBasicInformation struct type by first getting its size in bytes using the SizeOf() method (in this case, the size is 64 bytes).
$iprc = [ntdll]::NtQueryInformationFile($FileStream.SafeFileHandle, $iosb, $p_fbi, [System.Runtime.InteropServices.Marshal]::SizeOf($fbi), [FileInformationClass]::FileBasicInformation )
Here is where I call my NtQueryInformationFile() method with the required parameter values. I supply my SafeFileHandle from the file stream, my IO_STATUS_BLOCK struct that will be updated for review to see if something went wrong, the physical location of the FILE_BASIC_INFORMATION struct type that will also be updated by the method so I can view the timestamps, the size in bytes of the FILE_BASIC_INFORMATION type and finally the FILE_INFORMATION_CLASS type with FileBasicInformation. The returned IntPtr value will be used later on to determine if this was successful.
If ($IsOK) { # Pull data from unmanaged memory block into a usable object $fbi = [System.Runtime.InteropServices.Marshal]::PtrToStructure($p_fbi, [FileBasicInformation]) [pscustomobject]@{ FullName = $FileStream.Name CreationTime = [datetime]::FromFileTime($fbi.CreationTime) LastAccessTime = [datetime]::FromFileTime($fbi.LastAccessTime) LastWriteTime = [datetime]::FromFileTime($fbi.LastWriteTime) ChangeTime = [datetime]::FromFileTime($fbi.ChangeTime) } } Else { Write-Warning "$($Item): $(New-Object ComponentModel.Win32Exception)" }
After running the method, I now determine if everything went according to plan. If not an error will be thrown, otherwise I will continue on my path by marshaling the data ($p_fbi using PtrToStructure()) from the unmanaged memory over to a managed object that I can then look at (my $fbi type I created earlier). Now it is just a matter of converting the timestamps to a more human readable value using the FromFileTime() method and then displaying the results.
So there you go! I have tossed out my C# here string that was compiled using Add-Type and instead took the dive into Reflection to accomplish the same thing. While this is something that a very small percentage of people might ever use, it was absolutely a fun time learning more about this!
By the way, I did update my Get-FileTimeStamp function to use Reflection instead of compiled C#. You can download the updated function from the link below.
http://gallery.technet.microsoft.com/scriptcenter/Get-MFT-Timestamp-of-a-file-9227f399
Filed under: powershell Tagged: c#, changetime, mft, pinvoke, Powershell, reflection, timestamp
