3

To get it out the way: I understand there are different ways to go about this such as Get-ChildItem.

So, I have a custom class I'm defining to get rid of some of the overhead PowerShell cmdlets have, as well as some .Net classes. The function will run just fine locally, but as soon as I try using it as scriptblock definition with Invoke-Command against a remote computer, it will just hang; even if I invoke it against my own computer. There is a process that's created for a WinRM Plugin that is shown in Task Manager but, that's it. Here are some working examples:

PS C:\Users\Abraham> Get-FolderSize -Path C:\Users\Abraham
143.98GB

PS C:\Users\Abraham> Invoke-Command -ScriptBlock ${Function:Get-FolderSize} -ArgumentList C:\Users\Abraham
143.98GB

As shown above, this will work just fine and return the sum of all files. Then, when I pass a computer name to Invoke-Command for remote execution - it just hangs:

Invoke-Command -ScriptBlock ${Function:Get-FolderSize} -ArgumentList C:\Users\Abraham -ComputerName $env:COMPUTERNAME

It goes without saying that a PSSession doesn't work either; this will be the primary method of running the function - passing it to an open PSSession.

My question is, what the hell is wrong? lol. Is there something going on behind the scenes that won't allow the use of P/Invoke remotely?

Here is the actual function:

Function Get-FolderSize ([parameter(Mandatory)][string]$Path) {
    Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;

namespace ProfileMethods
{
    public class DirectorySum
    {
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        private struct WIN32_FIND_DATA
        {
            public uint dwFileAttributes;
            public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
            public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
            public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
            public uint nFileSizeHigh;
            public uint nFileSizeLow;
            public uint dwReserved0;
            public uint dwReserved1;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
            public string cFileName;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
            public string cAlternateFileName;
        }

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private static extern IntPtr FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA lpFindFileData);

        [DllImport("kernel32.dll")]
        private static extern bool FindClose(IntPtr hFindFile);

        public long GetFolderSize(string path)
        {
            long size = 0;
            List<string> dirList = new List<string>();
            WIN32_FIND_DATA fileData;
            IntPtr hFile = FindFirstFile(path + @"\*.*", out fileData);
            if (hFile != IntPtr.Zero)
            {
                do
                {
                    if (fileData.cFileName == "." || fileData.cFileName == "..")
                    {
                        continue;
                    }
                    string fullPath = path + @"\" + fileData.cFileName;
                    if ((fileData.dwFileAttributes & 0x10) == 0x10)
                    {
                        dirList.Add(fullPath);
                    }
                    else
                    {
                        size += ((long)fileData.nFileSizeHigh * (long)uint.MaxValue + (long)fileData.nFileSizeLow);
                    }
                } while (FindNextFile(hFile, out fileData));
                FindClose(hFile);
                foreach (string dir in dirList)
                {
                    size += GetFolderSize(dir);
                }
            }
            return size;
        }
    }
}
"@
    $program = [ProfileMethods.DirectorySum]::new()
    switch ($program.GetFolderSize($Path))
    {
        {$_ -lt 1GB} { '{0}MB' -f [math]::Round($_/1MB,2); Continue }
        {$_ -gt 1GB -and $_ -lt 1TB} { '{0}GB' -f [math]::Round($_/1GB,2); Continue }
        {$_ -gt 1TB} { '{0}TB' -f  [math]::Round($_/1TB,2); Continue }
    }
}

EDIT: Update - So, it works on subfolders, but not the root folder. Example:

$path = 'C:\Users\Abraham\Desktop' #works
Invoke-Command -ScriptBlock ${Function:Get-FolderSize} -ArgumentList $path -ComputerName $env:COMPUTERNAME

...works, but the root folder C:\Users\Abraham doesn't.


Note: Passing the UNC path to the function/method will work.

Abraham Zinala
  • 195
  • 2
  • 9
  • 1
    @VomitIT-ChunkyMessStyle, it's not, luckily. Read somewhere a while ago that P/Invoke is a major security concern so that may be the reason. Just wanted to see if anyone else had an idea – Abraham Zinala Feb 04 '23 at 05:02
  • 1
    @VomitIT-ChunkyMessStyle, hmm.. what's odd is that it'll do it for the subfolders just not the root folder. I dont know why. So, it does work on `C:\Users\Abraham\Desktop`, but not `C:\Users\Abraham`. – Abraham Zinala Feb 22 '23 at 20:55
  • Does it hang when invoked against other computers? Have you tried monitoring its activity with ProcMon? Is it actually following an infinite cycle of junctions or symlinks? – u1686_grawity Feb 22 '23 at 21:59
  • @user1686, honestly, I haven't tried it. I've only attempted my own PC using different variants of names like: `localhost`. – Abraham Zinala Feb 22 '23 at 22:12
  • You should perhaps [enable WinRM](https://www.domstamand.com/connecting-to-windows-server-2019-core-through-winrm-and-windows-admin-center/) on the target computer. – harrymc Feb 23 '23 at 11:50
  • @harrymc, it is in fact already enabled. It works remotely for the subfolders, just not the root folder. – Abraham Zinala Feb 23 '23 at 13:36
  • Have you tried to debug your script, for example to examine the list of files? – harrymc Feb 23 '23 at 13:53
  • I tested against remote machines and it works just fine as-is in my environment. However, for profiles that are large, it does NOT calculate those correctly. I ran it against a 20 GB profile for example, and it stated that is was 321 GB for some reason so you have something off in the code somewhere or something needs adjusted for sizes when they get larger from what I'm seeing. Otherwise, invoking remotely works just fine for me. It is likely you need to use `-credential` parameter and put one that has local admin access to the remote machine you run it against. – Vomit IT - Chunky Mess Style Feb 23 '23 at 14:12
  • @harrymc, yes I have. – Abraham Zinala Feb 23 '23 at 14:24
  • @VomitIT-ChunkyMessStyle, im querying profiles that are over 200GBs. On my own profile it takes about a second to get the size and it matches with the size property in explorer. I will setup a domain in my hyper v environment and give it another shot – Abraham Zinala Feb 23 '23 at 14:26
  • It might just be a matter of permissions of your user account. You can have permissions for a subfolder but not to the folder, and `C:\Users` is a good example. – harrymc Feb 23 '23 at 15:45
  • I think the answer provided is onto something with that infinite loop if logic in the C or C# or whatever it is does not handle the [reparse points](https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-points) correctly. From [File Attribute Constants](https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants) and [WIN32_FIND_DATAA structure](https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-win32_find_dataa) similar logic as provided there or some `if ((fileData.dwFileAttributes & FileAttributes.ReparsePoint) == 0)` variation perhaps – Vomit IT - Chunky Mess Style Feb 23 '23 at 18:55

1 Answers1

4

This is a guess because you haven't tried to investigate anything so there's not nearly enough information to provide an accurate answer.

but the root folder C:\Users\Abraham doesn't.

if ((fileData.dwFileAttributes & 0x10) == 0x10)

Modern Windows systems might need additional checks here – you need to pay attention to reparse points, which come in various types (symlinks, junctions, mount points, OneDrive placeholders...) and may also have the "Directory" flag in addition to the "ReparsePoint" flag (which is 0x400).

In particular, as Vista moved the old "~\Local Settings" directory to ~\AppData\Local, for compatibility it places a directory junction at "~\AppData\Local\Application Data" which points... back to the same "~\AppData\Local".

Junctions are slightly more special than symlinks and can have their own ACLs, so normally this junction will have a Deny ACE that stops you from doing FindFirstFile(@"Application Data\*.*") (i.e. it only allows direct access to known paths). But if the ACLs on it have ever been reset, any program trying to enumerate the contents of AppData will end up following an infinite loop, descending into the same junction forever (until it runs out of path length limit).

FindFirstFile(path + @"\*.*", out fileData);

Remember that file names are not required to have an . in it. FindFirstFile() deliberately makes this work (by allowing .* to match an empty string) to help out programs still stuck in the "8.3 filename.ext" era, but that won't be the case for programs implementing their own wildcard expansion.

u1686_grawity
  • 426,297
  • 64
  • 894
  • 966
  • I have implemented this on my laptop, but I won't be able to try this on my at home environment. Since the bounty is over with, I'd rather not have the points go to waste so I just marked yours as the answer without really testing it. Also, to comment on your first comment of my "*non-attempts*", there was simply too much that I had done to have fit it all in this post. Then, it would've made the post irrelevant to the proper eyes, like yourself, with too much fluff. – Abraham Zinala Mar 01 '23 at 23:41