Weekend Scripter: Using PowerShell to Aid in Security Forensics

Summary: Guest blogger, Will Steele, discusses using Windows PowerShell to aid with security forensics.

Microsoft Scripting Guy, Ed Wilson, is here. I have had many interesting email threads with Will Steele, and I have even spoken at the Dallas Fort Worth PowerShell User Group via Live Meeting. Therefore, it is with great pride that I introduce Will Steele.

Photo of Will Steele

Will Steele live near Dallas, Texas with his wife and three kids. He works as a senior network analyst at a financial services provider and he manages a document imaging system with a heavy investment in Microsoft enterprise technologies. Last year Will started the Dallas-Fort Worth PowerShell users group, and he contributes to the Windows PowerShell community in forums and through his blog. 

Blog: Another computer blog

Take it away Will…

Here’s a hypothetical thought about how Windows PowerShell can help in forensics registry analysis. The layout: You are a systems admin for a large IT corporation. You learn that a spreadsheet containing highly sensitive information was accessed without permission by a server in your group the previous day. Your task? Verify who opened it with one condition: you can’t use any non-Microsoft tools. You start by coming up with two simple questions:

  • Who accessed the server within the past 24 hours?
  • How, when, and where was the file accessed?

Some logs indicated which machine accessed the file, but didn’t indicate the user. Only a handful of people are now possible candidates. The number of folks with full administrative rights and access to the servers is small. Conferring with your manager about who was working yesterday, you come up with a list of four possible.

Getting down to work, you launch Windows PowerShell and plan to keep an audit trail of what you do. A log will serve perfectly as documentation of your research, so, you run this command:

md C:\research

Start-Transcript –Path C:\research\analysis.log

You then decide to see if any of these people were out of the office during the time of the incident. Remoting was enabled on your domain for all of your administrators, so, their workstations would allow you to query the workstation logs. You need an event log query to develop a timeline of log on/log off events and run it against the server. Because all the workstations run Windows 7, you use this to check for logon/logoff event IDs:

get-winevent -FilterHashTable @{LogName=’Security'; StartTime=’6/27/2012 12:00:00am'; ID=@(4624,4625,4634,4647,4648)} |

select timecreated,id

To identify which people you may need to look at more closely, you remotely query each machine to build a cross reference based on logon/logoffs. To save typing, you store the hash table from your server search as a $eventhashtable variable and pass it to the Get-WinEvent cmdlet inside a loop to check the four workstations.

$eventhashtable = @{LogName=’Security'; StartTime=’6/27/2012 12:00:00am'; ID=@(4624,4625,4634,4647,4648)};

‘workstation01′, ‘workstation02′, ‘workstation03′, ‘workstation04′ | % {

            Write “Retrieving logs for $_ at $(Get-Date)”;

            get-winevent –FilterHashTable $eventhashtable | select timecreated,id;


Moving on to the server, you learn that it hasn’t been rebooted since last night. This increases the likelihood that the registry still contains pertinent information. You now turn to the machine to get more details. First things first: getting to the machine without arousing suspicion. Thankfully, in Windows PowerShell, this is a trivial task.

New-PSSession -ComputerName server

To determine which hives to look at, you check the IDs for verification. This command will list all users on the machine by name and SID:

if(-not(Test-Path HKU:\))


            New-PSDrive HKU Registry HKEY_USERS



dir HKU:\ |

Where {($_.Name -match ‘S-1-5-[0-2][0-2]-‘) -and ($_.Name -notmatch ‘_Classes’)} |

Select PSChildName |

% {

            (([ADSI] (“LDAP://<SID=” + $_.PSChildName + “>”)).userPrincipalName -split ‘@’)[0] + ” – ” + $_.PSChildName


This loads the HKEY_USERS hive as a PSDrive and passes the SID values to the domain controller via an ADSI LDAP call, which returns the UserPrincipalName. You know that there’s a good chance the UserPrincipalName will match the name of the C:\Users\<profile> on the server. The command returns the following information.

admin01 – S-1-5-21-123456789-1234567890-1234567890-8901

admin02 – S-1-5-21-123456789-1234567890-1234567890-8902

admin03 – S-1-5-21-123456789-1234567890-1234567890-8903

admin04 – S-1-5-21-123456789-1234567890-1234567890-8904

superadminjrich – S-1-5-21-123456789-1234567890-1234567890-1472

superadminjmiller – S-1-5-21-123456789-1234567890-1234567890-1567

superadminmcruz – S-1-5-21-123456789-1234567890-1234567890-3245

To double-check these users, you run a Get-WmiObject cmdlet as a sanity check.

Get-WmiObject –Class Win32_NetworkLoginProfile | select caption,lastlogon

The following WMI result set verifies that your list is valid.

caption                           lastlogon

——-                           ———




Admin01                           20120620095738.000000-480

Admin02                           20120226122356.000000-480

Admin03                           20120627144745.000000-480

Admin04                           20120627150336.000000-480

superadminjrich                       20120313150319.000000-480

superadminjmiller                      20120627145121.000000-480

superadminmcruz                       20120417020307.000000-480

The four accounts in question do have local profile data on the server, so, you move into phase 2: find out when and where the file was accessed on the server. Noticing that Admin01 and Admin02 hadn’t logged on to the server recently eliminates them. Rather than work directly against the hives on the live server, you copy NTUSER.DAT files and disconnect from the server:

‘admin03′,’admin04′ |

% {

            md “C:\research\$_”

            copy “\\server\c$\users\$_\ntuser.dat” “C:\research\$_”



To start exploring the files, you need to load them into your current session. This old reg.exe command does the trick:

reg load HKLM\admin03 C:\research\admin03\ntuser.dat

Admin03’s hive now can be accessed via your PSSession under HKLM:\admin03. This way, you can explore their profile as if it were yours. To be sure this worked as expected, you check with regedit.

Image of menu

Switching over to Windows PowerShell, you start by examining most recently used (MRU) lists for this user. You recall that your manager mentioned a spreadsheet, so you look at several keys without finding the file. Finally, you find a key that piques your curiosity:

HKLM:\admin03\Software\Microsoft\Office\14.0\Excel\File MRU

Exploring the contents of the key is as simple as running this command:

PS HKLM:\admin03\Software\Microsoft\Office\14.0\Excel\File MRU > Get-ItemProperty .

When run, it produces this:

  Hive: HKEY_LOCAL_MACHINE\admin03\Software\Microsoft\Office\14.0\Excel

Name              Property

—-              ——–

File MRU            Max Display : 25

                Item 1   : [F00000000][T01CD5496156B3EF0][O00000000]*C:\Data\Documents\Powershell\Projects\Encoding\FormatTable.xlsx

You notice some weird values prefixing the file paths. Apparently the bracketed values are metadata for Excel. Interestingly, [T01CD5496156B3EF0] is a non-standard 64-bit Windows date and time stamp that is stored as hexadecimal. To convert it from the registry value to a [DateTime] object you use the following:

PS HKLM:\admin03\Software\Microsoft\Office\14.0\Excel\File MRU> Get-ItemProperty . | `

select ‘item *’ | `

% {$_ -split ‘\[T’} | % {$_ -split ‘\]\[‘} | Where {$_ -notmatch ‘\\’} | `

% {([Datetime][Convert]::ToInt64($_,16)).AddHours(-8)}

A list of times ordered according to how they appear in the key is produced, but you notice that there is something weird. All the time stamps are exactly 1600 years (and a few hours) off:

Wednesday, June 27, 0412 12:53:04 PM

You recall .NET DateTime objects presume January 1, 1600 as a start date. You accommodate for this with this change:

% {[DateTime]::FromFileTime([Convert]::ToInt64($_,16))}

There is proof that the file was opened when Admin03 was on call:

Wednesday, June 27, 2012 12:53:04 PM

To validate your research, some C# gets LastWriteTimes directly from the registry:

$signature = @”

using Microsoft.Win32.SafeHandles;

using System;

using System.Runtime.InteropServices;

using System.Text;


namespace Forensics


  public class Registry


    private static readonly IntPtr HKEY_DYN_DATA = new IntPtr(-2147483642);

    private static readonly IntPtr HKEY_CURRENT_CONFIG = new IntPtr(-2147483643);

    private static readonly IntPtr HKEY_PERFORMANCE_DATA = new IntPtr(-2147483644);

    private static readonly IntPtr HKEY_USERS = new IntPtr(-2147483645);

    private static readonly IntPtr HKEY_LOCAL_MACHINE = new IntPtr(-2147483646);

    private static readonly IntPtr HKEY_CURRENT_USER = new IntPtr(-2147483647);

    private static readonly IntPtr HKEY_CLASSES_ROOT = new IntPtr(-2147483648);


    private const int KEY_QUERY_VALUE = 1;

    private const int KEY_SET_VALUE = 2;

    private const int KEY_CREATE_SUB_KEY = 4;

    private const int KEY_ENUMERATE_SUB_KEYS = 8;

    private const int KEY_NOTIFY = 16;

    private const int KEY_CREATE_LINK = 32;

    private const int KEY_WRITE = 0x20006;

    private const int KEY_READ = 0x20019;

    private const int KEY_ALL_ACCESS = 0xF003F;

    public DateTime last;


    [DllImport(“advapi32.dll”, CharSet = CharSet.Auto)]

    private static extern int RegOpenKeyEx(

                SafeRegistryHandle hKey,

                string lpSubKey,

                uint ulOptions,

                uint samDesired,

                out SafeRegistryHandle hkResult



    [DllImport(“advapi32.dll”, CharSet = CharSet.Auto)]

    private static extern int RegQueryInfoKey(

                SafeRegistryHandle hKey,

                StringBuilder lpClass,

                uint[] lpcbClass,

                IntPtr lpReserved_MustBeZero,

                ref uint lpcSubKeys,

                uint[] lpcbMaxSubKeyLen,

                uint[] lpcbMaxClassLen,

                ref uint lpcValues,

                uint[] lpcbMaxValueNameLen,

                uint[] lpcbMaxValueLen,

                uint[] lpcbSecurityDescriptor,

                uint[] lpftLastWriteTime



    public static DateTime GetRegKeyLastWriteTime(string regkeyname)


      string[] parts = regkeyname.Split(‘\\’);

      string sHive = parts[0];

      string[] SubkeyParts = new string[parts.Length – 1];

      Array.Copy(parts, 1, SubkeyParts, 0, SubkeyParts.Length);

      string sSubKey = string.Join(“\\”, SubkeyParts);

      SafeRegistryHandle hRootKey = null;

      switch (sHive)


        case “HKEY_CLASSES_ROOT”: hRootKey = new SafeRegistryHandle(HKEY_CLASSES_ROOT, true); break;

        case “HKEY_CURRENT_USER”: hRootKey = new SafeRegistryHandle(HKEY_CURRENT_USER, true); break;

        case “HKEY_LOCAL_MACHINE”: hRootKey = new SafeRegistryHandle(HKEY_LOCAL_MACHINE, true); break;

        case “HKEY_USERS”: hRootKey = new SafeRegistryHandle(HKEY_USERS, true); break;

        case “HKEY_PERFORMANCE_DATA”: hRootKey = new SafeRegistryHandle(HKEY_PERFORMANCE_DATA, true); break;

        case “HKEY_CURRENT_CONFIG”: hRootKey = new SafeRegistryHandle(HKEY_CURRENT_CONFIG, true); break;

        case “HKEY_DYN_DATA”: hRootKey = new SafeRegistryHandle(HKEY_DYN_DATA, true); break;




        SafeRegistryHandle hSubKey = null;

        int iErrorCode = RegOpenKeyEx(hRootKey, sSubKey, 0, KEY_READ, out hSubKey);

        uint lpcSubKeys = 0;

        uint lpcValues = 0;

        uint[] lpftLastWriteTime = new uint[2];

        iErrorCode = Registry.RegQueryInfoKey(hSubKey, null, null, IntPtr.Zero,

        ref lpcSubKeys, null, null, ref lpcValues, null, null, null, lpftLastWriteTime);

        long LastWriteTime = (((long)lpftLastWriteTime[1]) << 32) + lpftLastWriteTime[0];

        DateTime lastWrite = DateTime.FromFileTime(LastWriteTime);

        return lastWrite;




        if (hRootKey != null && !hRootKey.IsClosed)








  public sealed class SafeRegistryHandle : SafeHandleZeroOrMinusOneIsInvalid


    public SafeRegistryHandle() : base(true) { }

    public SafeRegistryHandle(IntPtr preexistingHandle, bool ownsHandle)

      : base(ownsHandle)






    private static extern int RegCloseKey(IntPtr hKey);

    protected override bool ReleaseHandle()


      return (RegCloseKey(base.handle) == 0);





Searching against the registry key in question to validate your findings, you add the new type to your session:

Add-Type -TypeDefinition $signature -Language CSharp -PassThru | Out-Null;

And you search for LastWriteTime values:

 dir ‘HKLM:\admin03\Software\Microsoft\Office\14.0\Excel’ |

% { ($_.PSPath -split ‘:’)[2] } |

Where {[Forensics.Registry]::GetRegKeyLastWriteTime($_) -gt (Get-Date).AddDays(-2)} |

% { “$($_): $([Forensics.Registry]::GetRegKeyLastWriteTime($_))”};

This outputs the following:

HKEY_LOCAL_MACHINE\admin03\Software\Microsoft\Office\14.0\Excel\File MRU: 06/27/2012 13:53:04

HKEY_LOCAL_MACHINE\admin03\Software\Microsoft\Office\14.0\Excel\Options: 06/27/2012 08:47:50

HKEY_LOCAL_MACHINE\admin03\Software\Microsoft\Office\14.0\Excel\Place MRU: 06/27/2012 13:53:04

HKEY_LOCAL_MACHINE\admin03\Software\Microsoft\Office\14.0\Excel\Resiliency: 06/27/2012 08:47:50

The MRU time stamp confirms overlaps with your findings, supports your conclusion, and gives you evidence that you can hand over to your manager about exactly when and what had happened.


Thank you, Will, for sharing your time and knowledge. It is a great blog post.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy