Forum Discussion
Issue with date modified for NTUSER.DAT
- Feb 23, 2018
Here is the code from the script:
#Purpose: Used to set the ntuser.dat last modified date to that of the last modified date on the user profile folder.
#This is needed because windows cumulative updates are altering the ntuser.dat last modified date which then defeats
#the ability for GPO to delete profiles based on date and USMT migrations based on date.$ErrorActionPreference = "SilentlyContinue"
$Report = $Null
$Path = "C:\Users"
$UserFolders = $Path | GCI -DirectoryForEach ($UserFolder in $UserFolders)
{
$UserName = $UserFolder.Name
If (Test-Path "$Path\$UserName\NTUSer.dat")
{
$Dat = Get-Item "$Path\$UserName\NTUSer.dat" -force
$DatTime = $Dat.LastWriteTime
If ($UserFolder.Name -ne "default"){
$Dat.LastWriteTime = $UserFolder.LastWriteTime
}
Write-Host $UserName $DatTime
Write-Host (Get-item $Path\$UserName -Force).LastWriteTime
$Report = $Report + "$UserName`t$DatTime`r`n"
$Dat = $Null
}
}
Joe_Friedel, Hilde Claeys, Deleted, Ryan Pertusio, Ryan Pertusio, AHDoug, SSUtech, millermike - Tagging folks who've contributed for visibility.
I don't believe this issue is as cut and dry as everyone here is presenting. As you've likely found out, sometimes these files you are checking for don't exist, and in some cases, they have unexpected timestamps. These values get updated for various reasons (I think mostly during OS updates). How do we know which ones to trust? Based on my experience, the LastWriteTime of the user profile directory itself and the IconCache.db file seem to be the most accurate. What do you guys think? Has anyone made any progress on this? I've created a script to gather data on all of the profiles on the device. I have this problem on thousands of computers. I've attached the data for just one of those computers. The data in the linked file represents data from one of my high traffic workstations where I've used the value of 30 for the $Delete_if_Older variable. You can see by the data, many of the profiles should be flagged for deletion, but one or more of the write times are newer, so I've set the Delete flag to False. Like I stated earlier, from what I can tell, the LastWriteTime of the user profile directory itself and the IconCache.db file seem to be the most accurate. So is it best to use the LastWriteTime from IconCache.db if it exists, and if it doesn't use the LastWriteTime of the user profile directory? I found Win32_UserProfile LastUseTime to be the least accurate out of the bunch so I just excluded it from being considered for the LastUpdated variable. What do you guys think?
Here's the data from my high traffic workstation. https://1drv.ms/x/s!AoQvLmouCXZPgaApOK6SGY_bQShhGw?e=rPCjFm
Green indicates it should have been flagged for deletion, red would indicate it should not be flagged for deletion, yellow indicates the value did not exist. All-in-all, out of 173 profiles, none of them qualified to be deleted based on a 30 day threshold. I have a feeling a Windows update ran recently on this device causing the timestamps to update.
Anxious to know everyone's thoughts on this as it seems to be an issue Microsoft has swept under the rug. What about the LocalProfileUnloadTime values - did anyone figure out how to get those values with PowerShell and/or determine a timestamp from this data? Maybe we can get a Microsoft rep to reply here with a good alternative.
$Delete_if_older = '30'
$DeleteProfile = (Get-Date).AddDays(-$Delete_if_older)
$ErrorActionPreference = "SilentlyContinue"
$Path = "C:\Users"
$ExcludedUsers = @('Administrator','Public','Default')
$UserFolders = $Path | Get-ChildItem -Directory -Exclude $ExcludedUsers
$CimProfiles = Get-CimInstance -Class Win32_UserProfile
$NA = [datetime] "11/11/1111 11:11:11 AM"
$ProfileData = ForEach ($UserFolder in $UserFolders) {
$UserName = $UserFolder.Name
$UserProfileTime = $UserFolder.LastWriteTime
$CimProfile = $CimProfiles | Where-Object { $_.localpath -eq $UserFolder.FullName }
if ($CimProfile) { $CimProfileTime = $CimProfile.LastUseTime } else { $CimProfileTime = $NA }
$NTUsr = Get-Item "$UserFolder\NTUSer.dat" -Force
if ($NTUsr) { $NTUsrTime = $NTUsr.LastWriteTime } else { $NTUsrTime = $NA }
$UsrClass = Get-Item "$UserFolder\AppData\Local\Microsoft\Windows\UsrClass.dat" -force
if ($UsrClass) { $UsrClassTime = $UsrClass.LastWriteTime } else { $UsrClassTime = $NA }
$IconCache = Get-Item "$UserFolder\AppData\Local\IconCache.db" -Force
if ($IconCache) { $IconCacheTime = $IconCache.LastWriteTime } else { $IconCacheTime = $NA}
$LastUpdated = $UserProfileTime,$NTUsrTime,$UsrClassTime,$IconCacheTime | Sort-Object | Select-Object -Last 1
if ($LastUpdated -lt $DeleteProfile) { $Delete = $True } else { $Delete = $False }
# $NTUsr.LastWriteTime = $UsrClassTime # Update NTUser.dat LastWriteTime to that of the UsrClass.dat File
[PSCustomObject]@{
UserName = $UserName
ProfileFullname = $UserFolder.FullName
UserProfileTime = $UserProfileTime
CimProfileTime = $CimProfileTime
NTUsrTime = $NTUsrTime
UsrClassTime = $UsrClassTime
IconCacheTime = $IconCacheTime
LastUpdated = $LastUpdated
Delete = $Delete
}
'UserName','UserProfileTime','CimProfile','CimProfileTime','NTUsr','NTUsrTime','UsrClass','UsrClassTime','IconCache','IconCacheTime','LastUpdated','Delete' | ForEach-Object { Remove-Variable $_ }
}
I threw together some code you can use & abuse. (It has not been tested in Production, so use at your own risk. Make improvements and share if you'd like!)
To convert the LocalProfileLoadTimeHigh and LocalProfileLoadTimeLow, you combine them (text strings) and then convert to DateTime. It's convoluted. I'm guessing Microsoft could have used a single QWORD but instead chose 2 DWORDs registry entries.
# Get list of profiles
$profiles = Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\ProfileList\*"
# Loop through each profile
Foreach ($profile in $profiles) {
# Get the SID
$SID = New-Object System.Security.Principal.SecurityIdentifier($profile.PSChildName)
# Convert SID to Friendly name
$FriendlyName = $SID.Translate([System.Security.Principal.NTAccount])
# Trim and store variables
$SidTrimmed = $SID.Value
$FriendlyNameTrimmed = $FriendlyName.Value
# Store the Profile Load time (in Decimal)
# Example: ProfileLoadHighDec = 30997847 / ProfileLoadLowDec = 2259805116
$ProfileLoadHighDec = $profile.LocalProfileLoadTimeHigh
$ProfileLoadLowDec = $profile.LocalProfileLoadTimeLow
# Convert Decimal to Hex string
# Example: ProfileLoadHighHex = 01d8fd57 / ProfileLoadLowHex = 86b1d3bc
$ProfileLoadHighHex = [System.Convert]::ToString($ProfileLoadHighDec,16)
$ProfileLoadLowHex = [System.Convert]::ToString($ProfileLoadLowDec,16)
# Concatenate hex strings
# Example: 01d8fd5786b1d3bc
$ProfileHexJoined = -join ($ProfileLoadHighHex,$ProfileLoadLowHex)
# Convert to DateTime format
# Example: 11/21/2022 03:15:37
$TimestampInt = [Convert]::ToInt64($ProfileHexJoined,16)
$ProfileLoadDate = [DateTime]::FromFileTimeutc($TimestampInt)
# Optional output for example purposes
Write-Output "SID: $SidTrimmed"
Write-Output "Friendly Name: $FriendlyNameTrimmed"
Write-Output "Profile Load Date: $ProfileLoadDate"
Write-Output "`n"
}
Running this script results in the following output:
SID: S-1-5-18
SID: S-1-5-19
SID: S-1-5-20
SID: S-1-5-21-1234567890-1234567890-1234567890-1234 |
Good luck!
-rp
- ianjhart2718May 18, 2024Copper Contributor
noob alert! Late to the party I know, but I got here searching for a solution and others will too.
I believe there's a logic error in this which results in an incorrect conversion for some values of the input.
For it to work the string representation of the low DWORD value has to represent all 32 bits. In other words, some values have leading zeros. If those zeros are missing this effectively bit shifts right the high DWORD value, resulting in the wrong datetime.
The only non-contemporary date you should get is midnight 1/1/1601, the date represented by int zero. I was seeing year 1627 as an example.
Here's an alternate implementation.
Caveat: I am not a PowerShell expert, had to look up how to make a function
. I am, however, confident in the logic having been weaned on K&R.
# https://techcommunity.microsoft.com/t5/windows-deployment/issue-with-date-modified-for-ntuser-dat/m-p/102438/page/3 # made a function. Added UnloadDate # changed the logic of the function function Get-ProfileTimestamp { param ( $ProfileLoadHigh,$ProfileLoadLow ) $a = ([int64]$ProfileLoadHigh -shl 32) -bor ([int64]$ProfileLoadLow) return [DateTime]::FromFileTimeutc($a) } # Get list of profiles $profiles = Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\ProfileList\*" # Loop through each profile Foreach ($profile in $profiles) { # Get the SID $SID = New-Object System.Security.Principal.SecurityIdentifier($profile.PSChildName) # Convert SID to Friendly name $FriendlyName = $SID.Translate([System.Security.Principal.NTAccount]) # Trim and store variables $SidTrimmed = $SID.Value $FriendlyNameTrimmed = $FriendlyName.Value $ProfileLoadDate = Get-ProfileTimestamp -ProfileLoadHigh $profile.LocalProfileLoadTimeHigh -ProfileLoadLow $profile.LocalProfileLoadTimeLow $ProfileUnLoadDate = Get-ProfileTimestamp -ProfileLoadHigh $profile.LocalProfileUnLoadTimeHigh -ProfileLoadLow $profile.LocalProfileUnLoadTimeLow # Optional output for example purposes Write-Output "SID: $SidTrimmed" Write-Output "Friendly Name: $FriendlyNameTrimmed" Write-Output "Profile Load Date: $ProfileLoadDate" Write-Output "Profile UnLoad Date: $ProfileUnLoadDate" Write-Output "`n" }
For extra credit, how many bits is the data shifted for each missing 0x0?
- MrMattiPantsJan 06, 2025Copper Contributor
I too came to this realization, as I've been seeing odd dates from the 1600's, as well.
After some research & testing, I was able to determine that if the "LocalProfileLoadTimeHigh" and "LocalProfileLoadTimeLow" Registry Keys/Values are missing entirely, the Date & Time should always come out to "Monday, January 1, 1601 12:00:00 AM". However, I came across some other dates, in the 1600s, that confirmed the function is Incorrect.
Fortunately, I was able to capture an example, in case anyone would like to confirm for themselves.
Unfortunately, the Script below spits out the Date/Time as "Saturday, July 3, 1627 7:00:49 AM". However, the Correct Date/Time should be "Monday, January 6, 2025 4:07:15 PM".$ProfileLoadHighDec = 31154261 $ProfileLoadLowDec = 232160855 $ProfileLoadHighHex = [System.Convert]::ToString($ProfileLoadHighDec,16) $ProfileLoadLowHex = [System.Convert]::ToString($ProfileLoadLowDec,16) $ProfileHexJoined = -join ($ProfileLoadHighHex,$ProfileLoadLowHex) $TimestampInt = [Convert]::ToInt64($ProfileHexJoined,16) $ProfileLoadDate = [DateTime]::fromFileTimeUtc($TimestampInt) $ProfileLoadDate
On the other hand, the following example spits out the correct Date/Time, without issue.$ProfileLoadHighDec = 31154261 $ProfileLoadLowDec = 232160855 $ProfileLoadTime = [datetime]::fromFileTimeUtc([uint64]$ProfileLoadHighDec -shl 32 -bor$ProfileLoadLowDec) $ProfileLoadTime
- ianjhart2718Mar 11, 2025Copper Contributor
Joe_Friedel midnight51 Ryan Pertusio
MrMattiPantsThanks for validating the code.
I turned it into a tool. It's somewhat off topic as it's now more like delete profile (theshonkproject) or Delprof2 (Helge Klein). It's also huge, so I've decided to put it on GitHub.
https://github.com/ianjhart2718/profiletimes
I've retained a small amount of the original code for nostalgic reasons. I hope that's okay. It read like it was intended to be public domain.
I'm new to GitHub also but I believe it's functional.
- midnight51Nov 29, 2022Copper Contributor
Ryan Pertusio
First off, thank you for taking the time to look at this and reply. What exactly is this value supposed to represent? I would assume the last time the profile was loaded?
I tested but all profiles are returning the value of "01/01/1601 00:00:00".EDIT: After closer review, the workstation I tested on, NONE of the profiles had LocalProfileLoadTime values... strange. I tested on another workstation and it provided useful data. About 20 out of the 180 profiles did have erroneous values though.
So here's the next question: How accurate is this data? If it follows the same patterns as the other data, there can be some inconsistency, I could say, as I've demonstrated by pulling 180 profiles where 20 of them were invalid. Also, what about my other workstation that simply does not have LocalProfileLoadTime values at all?
When should this value be used over other values? If this value returns erroneously or not at all, what's nest? IconCache.db, then user profile directory?
- Ryan PertusioNov 29, 2022Brass Contributormidnight51
Unfortunately, all I can say is that MS told me it's the value they use for the Group Policy to cleanup old profiles. Maybe there's more to their secret sauce that they're not sharing, OR, they're using inconsistent data.- MB_99Dec 01, 2022Copper ContributorHad a thought, why not create a logon script that runs each time a user logs in, that appends their username and logon date to a CSV file. Then you can use a PowerShell script to go through the each record to determine which profiles can be removed or not based on the date. This would take any of the guess work out, and would take the reliance of modify dates to determine last logon. I don't have time right now but if I do in the near future I will write something and post back.
- Ryan PertusioNov 29, 2022Brass Contributor
Yes, it represents the last time the profile was loaded. (It matches my experience at least)
I tried it on 3 different machines, and while I did get some 01/01/1601 profiles (never logged on), the script properly returns dates on other valid logons.
I successfully tried it on 3 PCs:
* Win10 / Domain Joined
* Win11 / Azure AD Joined
* Win11 / Workgroup (home PC)
There could be something wrong with my script. I didn't test it widely beyond my 3 systems. I would check your registry and do the conversion 'manually':
- Grab the "LocalProfileLoadTimeLow" and "LocalProfileLoadTimeHigh" from 1 of the profiles:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\ - Concatenate the Low/High hex into a single hex string
- Convert the single hex string to decimal (Google a hex2dec converter)
- Take decimal (NT epoch time) and convert to DateTime at command prompt:
w32tm /ntte <nt epoch time>
My powershell script should report the same date/time as w32tm.
-Ryan
- Grab the "LocalProfileLoadTimeLow" and "LocalProfileLoadTimeHigh" from 1 of the profiles: