Forum Discussion

Joe_Friedel's avatar
Joe_Friedel
Brass Contributor
Aug 31, 2017

Issue with date modified for NTUSER.DAT

Is Microsoft aware of an issue with various Windows 10 processes causing the date modified timestamp on the NTUSER.DAT file in unused profiles to change? We have specifically noticed that installation of cumulative updates modifies this file in all profiles, but there are other processes (I'm guessing scheduled tasks but I haven't found which ones) that do this too. 

 

This is particularly a problem in Education as we use the "Delete user profiles older than a specified number of days on a system restart" GPO setting to clean up old profiles on Lab and Classroom computers and that policy relies on the timestamp on the NTUSER.DAT file to determine the age of the profile. (The USMT tool also uses that file and timestamp when you specify to only capture profiles used within the last XX days.)

 

For reference, the date modified timestamp on the profile folder in the Users directory does accurately reflect the last time the profile was used.

  • Joe_Friedel's avatar
    Joe_Friedel
    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 -Directory

    ForEach ($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
        }
    }

  • Ryan Pertusio's avatar
    Ryan Pertusio
    Brass Contributor

    Joe_Friedel 

    I spoke with Microsoft Engineering on this. I asked if I could share this update. They said it was not confidential, so I'm sharing it here.

     

    The display dates in the Adv System control panel use “ntuser.dat”. There are no current plans to change how the dates are displayed.  There are processes (like those used by Windows Update) that touch the 'ntuser.dat' file & update the date.

     

    However, the Group Policy that cleans up profiles ("Delete user profiles older than a specified number of days on a system restart") uses a different calculation (instead of ntuser.dat).  It instead uses these 2 registry values:

     

    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\SID\

    LoadProfileUnloadTimeHigh

    LoadProfileUnLoadTimeLow

     

    I hope this adds value to this conversation and explains more about what's going on!

    - rp

     

    • AHDoug's avatar
      AHDoug
      Copper Contributor

      Ryan Pertusio 

       

      I'm trying to figure out just what that "different" calculation might be - but I can't make heads or tails out of it. Here's the values from a computer I tested this on after logging out:

      LocalProfileUnloadTimeHigh: 0x1d78a6d (30902893)
      LocalProfileUnloadTimeLow: 0x14faa06d1 (1336542929)

      I also ran this from psexec on that computer to get when it says I last logged out:

      net user <myusername> /domain | findstr /B /C:"Last logon"

      which produced this: Last logon 8/5/2021 7:44:44 PM

      This translates to 1628192525 in epoch time, which I'm assuming is the format used in the registry. And I couldn't figure out any calculation from the registry entries which would even come close to the final value. Maybe it's a mystery only MS knows?

       

  • Ross Laing's avatar
    Ross Laing
    Copper Contributor

    I am currently having the same issue and have a call open with Microsoft about it. This has been ongoing now for months. I initially suspected windows updates and Store updates are changing the .dat files, which makes some sense. Looking at event logs you can see the user registry hives being modified and usually preceded by windows update client kicking in to download something.

    Microsoft have been round the houses trying to blame everything but the OS, suggesting Anti-Virus software for example, but cant be that with no third party AV on the machine.

     

    I can confirm that this problem has persisted in 1709 as well. 

     

    Currently away to use process monitor to confirm what is changing the registry.

    My current feeling is that this is expected behaviour and no one wants to admit it.

     

    Just to add, I am only seeing this behaviour since going to 1703 in the Summer as we have switched from roaming profiles to local profiles and UEV, and we were wiping all roaming profiles remnants using delprof tool on startup prior to the summer so machines were clean of profiles on every boot. This is not really an option if you are using local profiles, hence the switch to the GPO. 

     

     

    • matthew hansen's avatar
      matthew hansen
      Copper Contributor

      Thanks for the update and I look forward to seeing if you're able to get a resolution. I can also confirm this is not caused by antivirus. We have Defender completely disabled via GPO and I had McAfee VSE uninstalled at the time of my testing. 

  • midnight51's avatar
    midnight51
    Copper Contributor

    Joe_FriedelHilde Claeys, Deleted, Ryan PertusioRyan PertusioAHDougSSUtechmillermike - 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. :unamused:

    $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 $_ }
    }



    • Ryan Pertusio's avatar
      Ryan Pertusio
      Copper Contributor

      midnight51

       

      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
      Friendly Name: NT AUTHORITY\SYSTEM
      Profile Load Date: 01/01/1601 00:00:00

       

      SID: S-1-5-19
      Friendly Name: NT AUTHORITY\LOCAL SERVICE
      Profile Load Date: 01/01/1601 00:00:00

       

      SID: S-1-5-20
      Friendly Name: NT AUTHORITY\NETWORK SERVICE
      Profile Load Date: 01/01/1601 00:00:00

       

      SID: S-1-5-21-1234567890-1234567890-1234567890-1234
      Friendly Name: LAPTOP-12345\username123
      Profile Load Date: 11/21/2022 03:15:37

       

       

      Good luck!

      -rp

       

       

      • ianjhart2718's avatar
        ianjhart2718
        Copper Contributor

        Ryan Pertusio 

         

        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 :smile:. 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?

    • justinsh00's avatar
      justinsh00
      Copper Contributor

      midnight51 : I found you solution to most perfect or best resolution to whatever solution or approach mentioned over web.

       

      Great and appreciate

  • Thanks for this feedback, Joe - I can pass this along to our team. If you are able to, please submit this feedback to the Windows Feedback hub - this is the best way to get bugs like this to our engineering teams.
  • matthew hansen's avatar
    matthew hansen
    Copper Contributor

    I'm currently running into this exact issue. Was there a bug report submitted or were you able to find a fix? 

    • Joe_Friedel's avatar
      Joe_Friedel
      Brass Contributor

      I did submit this in the Feedback Hub app 5 months ago. It has 1 upvote and no comments. I have not tested to see if this was fixed in 1709. We developed a PowerShell script that loops through the user profiles and sets the ntuser.dat date modified value to the date modified value from the user profile folder. We just run this on a set schedule for any computers where we're using the profile deletion GPO setting.

      • Hilde Claeys's avatar
        Hilde Claeys
        Copper Contributor

        hi Joe

         

        We struggle with the same problem in our school.  Could you share the powershell script with us?

         

  • Keith Falato's avatar
    Keith Falato
    Copper Contributor

    I wanted to post and say that we are experiencing this same issue and wanted to see if anyone had any new information on it.

     

    We are running 1703 Enterprise and are becoming increasing frustrated with it.

    • Ralph Smith's avatar
      Ralph Smith
      Copper Contributor

      Just adding we have the same problem, Windows 10 Pro 1703 and 1709.

  • HenSolo's avatar
    HenSolo
    Copper Contributor

    We are having this issue also with server 2012R2 RDS.

    At reboot time user.dat timestamp is updated and profiles are never cleaned up

     

     

  • Neil Galbraith's avatar
    Neil Galbraith
    Copper Contributor

    Just want to say a big thanks for the powershell script.  I've been tearing my hair out with this problem for several months.

     

    We changed over some of our school estate to SSD's last year and their getting filled up quite quickly now and I couldn't work out why the normal Group Policy for removing profiles wasn't working.

     

    Changing to the date on the folder isn't perfect as something else sometimes modified it as well but it's a step in the right direction.

     

    Hopefully MS can sort it out.

    • Greg Hogan's avatar
      Greg Hogan
      Copper Contributor

      Can you tell me how you put this in place?

       

      Does it work side by side with the existing GPO or, is this a stand alone script?

       

      We are having issue you are/were having.....

      • Joe_Friedel's avatar
        Joe_Friedel
        Brass Contributor

        Yes, the script is used in combination with the GPO setting. We still have the "Delete user profiles older than a specified number of days on system restart" configured. The PowerShell script to adjust the timestamps on the NTUser.dat files is executed on a daily schedule by ConfigMgr using a package with the script as the source file and a program running the following command line: "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -file Change_NTUSER_Date.ps1

        The program is deployed to always rerun. You could also accomplish this with a scheduled task running the script on each computer as well. It seemed easiest to use ConfigMgr to handle the distribution and execution of the script which also makes it easy to change, if needed.

  • Amila_M's avatar
    Amila_M
    Copper Contributor

    Hi ,
    I recently enable "Delete user profiles older than a specified number of days on a system restart" GPO and set the value to 90 days. Apply to Windows 10 computer.
    in the PC i can see Registry value is available "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\System
    CleanupProfiles REG_DWORD to 90.
    I did few restart and wait for few days, Still old user data (Last modify date from 2018 to 2021 folders) available in C:\users\ folders.
    Domain controller is Win 2019 data center.
    Does anyone successful on with this GPO?
    Can some help?
    Below is the link which i refer.

     

    https://social.technet.microsoft.com/wiki/contents/articles/28647.group-policy-how-to-automatically-delete-user-profiles-older-than-certain-number-of-days.aspx

    • Christopher Currivan's avatar
      Christopher Currivan
      Copper Contributor
      Amila_M: Joe Friedel's script may work, and MB_99's method may be an improvement on the idea of scripting it, but we didn't have days and weeks to mess with this so we went back to our old solution, which was OS protection software that deletes all changes when the system gets rebooted. But, others in our organization have recommended Microsoft's Shared PC solution, as they claim to have had much success with it.

      In the meantime and in-between time, we are still testing Shared PC at my location, so we don't have bona fide results that I can speak to. But, I recommend trying Shared PC on some systems to see what it might do for your issue (our issue, to be inclusive). So far, it's doing a good job of controlling what goes on, but we haven't had enough time to see what it does with the profiles, yet. I realize this is the biggest issue of all of them, because if the hard drive fills up, you've got a serious problem on your hands.

Resources