Creative Process Enumeration
Very often in engagements, you'll want to list out processes running on a host. One thing that is beneficial is to know is if the processes is a 64-bit or 32-bit process. Why do you need to know the process architecture, you might ask? The reasons are many, but one common example is that you might find a process running as system that is vulnerable to an escalation attack (DLL hijack or similar technique). Then, you would need to craft your payload according to the architecture. You would think that you could use scripts and code to list out the process architecture pretty easily. At least I thought so when I started looking into this. Let’s take a quick look at a GUI example using process explorer to list out the architecture.
As you can see from the unelevated Process Explorer, you can only see the Image Type for process launched by the user. This is due to the fact that a non-elevated user cannot get details about system processes or processes launched by other users. It states <access denied> on the username field amongst other processes. Process Explorer cannot show it, but the native Task Manager in Windows can. All you have to do is to add the column named Platform. Using the Task Manager however you can see that you are not allowed to see the UAC Virtualization for any other users process.
If we try to list out a process using PowerShell’s get-process, you will see this output:
(Get-Process)[10] | fl * Name : explorer Id : 4364 PriorityClass : Normal FileVersion : 10.0.19041.2311 (WinBuild.160101.0800) HandleCount : 2523 WorkingSet : 156053504 PagedMemorySize : 59518976 PrivateMemorySize : 59518976 VirtualMemorySize : 613797888 TotalProcessorTime : 00:00:16.1875000 SI : 2 Handles : 2523 VM : 2203932020736 WS : 156053504 PM : 59518976 NPM : 111184 Path : C:\Windows\Explorer.EXE Company : Microsoft Corporation CPU : 16.1875 ProductVersion : 10.0.19041.2311 Description : Windows Explorer Product : Microsoft® Windows® Operating System __NounName : Process BasePriority : 8 ExitCode : HasExited : False ExitTime : Handle : 2768 SafeHandle : Microsoft.Win32.SafeHandles.SafeProcessHandle MachineName : . MainWindowHandle : 131348 MainWindowTitle : MainModule : System.Diagnostics.ProcessModule (Explorer.EXE) MaxWorkingSet : 1413120 MinWorkingSet : 204800 Modules : {System.Diagnostics.ProcessModule (Explorer.EXE), System.Diagnostics.ProcessModule (ntdll.dll), System.Diagnostics.ProcessModule (KERNEL32.DLL), System.Diagnostics.ProcessModule (KERNELBASE.dll)...} NonpagedSystemMemorySize : 111184 NonpagedSystemMemorySize64 : 111184 PagedMemorySize64 : 59518976 PagedSystemMemorySize : 1099280 PagedSystemMemorySize64 : 1099280 PeakPagedMemorySize : 71675904 PeakPagedMemorySize64 : 71675904 PeakWorkingSet : 211505152 PeakWorkingSet64 : 211505152 PeakVirtualMemorySize : 820936704 PeakVirtualMemorySize64 : 2204139159552 PriorityBoostEnabled : True PrivateMemorySize64 : 59518976 PrivilegedProcessorTime : 00:00:09.2968750 ProcessName : explorer ProcessorAffinity : 1 Responding : True SessionId : 2 StartInfo : System.Diagnostics.ProcessStartInfo StartTime : 8/25/2023 4:48:07 AM SynchronizingObject : Threads : {4368, 4628, 4684, 4692...} UserProcessorTime : 00:00:06.8906250 VirtualMemorySize64 : 2203932020736 EnableRaisingEvents : False StandardInput : StandardOutput : StandardError : WorkingSet64 : 156053504 Site : Container :
As you can see, there's nothing here that immediately sticks out. Namely, whether the process is a 64-bit process or not. Let’s also do the same using vbscript together with WMI to list out the processes. For this demonstration, I am going to use WMI Explorer to generate a sample vbscript code that includes all process properties. The script looks like this:
On Error Resume Next Const wbemFlagReturnImmediately = &h10 Const wbemFlagForwardOnly = &h20 Set wshNetwork = WScript.CreateObject("WScript.Network") strComputer = wshNetwork.ComputerName strQuery = "SELECT * FROM Win32_Process" WScript.StdOut.WriteLine "" WScript.StdOut.WriteLine "=====================================" WScript.StdOut.WriteLine "COMPUTER : " & strComputer WScript.StdOut.WriteLine "CLASS : ROOT\CIMV2:Win32_Process" WScript.StdOut.WriteLine "QUERY : " & strQuery WScript.StdOut.WriteLine "=====================================" WScript.StdOut.WriteLine "" Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\ROOT\CIMV2") Set colItems = objWMIService.ExecQuery(strQuery, "WQL", wbemFlagReturnImmediately + wbemFlagForwardOnly) For Each objItem in colItems WScript.StdOut.WriteLine "Caption: " & objItem.Caption WScript.StdOut.WriteLine "CommandLine: " & objItem.CommandLine WScript.StdOut.WriteLine "CreationClassName: " & objItem.CreationClassName WScript.StdOut.WriteLine "CreationDate: " & objItem.CreationDate WScript.StdOut.WriteLine "CSCreationClassName: " & objItem.CSCreationClassName WScript.StdOut.WriteLine "CSName: " & objItem.CSName WScript.StdOut.WriteLine "Description: " & objItem.Description WScript.StdOut.WriteLine "ExecutablePath: " & objItem.ExecutablePath WScript.StdOut.WriteLine "ExecutionState: " & objItem.ExecutionState WScript.StdOut.WriteLine "Handle: " & objItem.Handle WScript.StdOut.WriteLine "HandleCount: " & objItem.HandleCount WScript.StdOut.WriteLine "InstallDate: " & objItem.InstallDate WScript.StdOut.WriteLine "KernelModeTime: " & objItem.KernelModeTime WScript.StdOut.WriteLine "MaximumWorkingSetSize: " & objItem.MaximumWorkingSetSize WScript.StdOut.WriteLine "MinimumWorkingSetSize: " & objItem.MinimumWorkingSetSize WScript.StdOut.WriteLine "Name: " & objItem.Name WScript.StdOut.WriteLine "OSCreationClassName: " & objItem.OSCreationClassName WScript.StdOut.WriteLine "OSName: " & objItem.OSName WScript.StdOut.WriteLine "OtherOperationCount: " & objItem.OtherOperationCount WScript.StdOut.WriteLine "OtherTransferCount: " & objItem.OtherTransferCount WScript.StdOut.WriteLine "PageFaults: " & objItem.PageFaults WScript.StdOut.WriteLine "PageFileUsage: " & objItem.PageFileUsage WScript.StdOut.WriteLine "ParentProcessId: " & objItem.ParentProcessId WScript.StdOut.WriteLine "PeakPageFileUsage: " & objItem.PeakPageFileUsage WScript.StdOut.WriteLine "PeakVirtualSize: " & objItem.PeakVirtualSize WScript.StdOut.WriteLine "PeakWorkingSetSize: " & objItem.PeakWorkingSetSize WScript.StdOut.WriteLine "Priority: " & objItem.Priority WScript.StdOut.WriteLine "PrivatePageCount: " & objItem.PrivatePageCount WScript.StdOut.WriteLine "ProcessId: " & objItem.ProcessId WScript.StdOut.WriteLine "QuotaNonPagedPoolUsage: " & objItem.QuotaNonPagedPoolUsage WScript.StdOut.WriteLine "QuotaPagedPoolUsage: " & objItem.QuotaPagedPoolUsage WScript.StdOut.WriteLine "QuotaPeakNonPagedPoolUsage: " & objItem.QuotaPeakNonPagedPoolUsage WScript.StdOut.WriteLine "QuotaPeakPagedPoolUsage: " & objItem.QuotaPeakPagedPoolUsage WScript.StdOut.WriteLine "ReadOperationCount: " & objItem.ReadOperationCount WScript.StdOut.WriteLine "ReadTransferCount: " & objItem.ReadTransferCount WScript.StdOut.WriteLine "SessionId: " & objItem.SessionId WScript.StdOut.WriteLine "Status: " & objItem.Status WScript.StdOut.WriteLine "TerminationDate: " & objItem.TerminationDate WScript.StdOut.WriteLine "ThreadCount: " & objItem.ThreadCount WScript.StdOut.WriteLine "UserModeTime: " & objItem.UserModeTime WScript.StdOut.WriteLine "VirtualSize: " & objItem.VirtualSize WScript.StdOut.WriteLine "WindowsVersion: " & objItem.WindowsVersion WScript.StdOut.WriteLine "WorkingSetSize: " & objItem.WorkingSetSize WScript.StdOut.WriteLine "WriteOperationCount: " & objItem.WriteOperationCount WScript.StdOut.WriteLine "WriteTransferCount: " & objItem.WriteTransferCount WScript.StdOut.WriteLine "" Next
For example, the output from the svchost.exe process looks like this:
Caption: svchost.exe CommandLine: CreationClassName: Win32_Process CreationDate: 20230825044553.785112-420 CSCreationClassName: Win32_ComputerSystem CSName: DESKTOP-7S4VDR9 Description: svchost.exe ExecutablePath: ExecutionState: Handle: 3044 HandleCount: 204 InstallDate: KernelModeTime: 312500 MaximumWorkingSetSize: MinimumWorkingSetSize: Name: svchost.exe OSCreationClassName: Win32_OperatingSystem OSName: Microsoft Windows 10 Enterprise|C:\Windows|\Device\Harddisk0\Partition3 OtherOperationCount: 113 OtherTransferCount: 1636 PageFaults: 2378 PageFileUsage: 1936 ParentProcessId: 588 PeakPageFileUsage: 2444 PeakVirtualSize: 2203401961472 PeakWorkingSetSize: 8744 Priority: 8 PrivatePageCount: 1982464 ProcessId: 3044 QuotaNonPagedPoolUsage: 11 QuotaPagedPoolUsage: 75 QuotaPeakNonPagedPoolUsage: 13 QuotaPeakPagedPoolUsage: 77 ReadOperationCount: 0 ReadTransferCount: 0 SessionId: 0 Status: TerminationDate: ThreadCount: 6 UserModeTime: 0 VirtualSize: 2203397767168 WindowsVersion: 10.0.19045 WorkingSetSize: 7729152 WriteOperationCount: 0 WriteTransferCount: 0
As you can see, there's nothing that shows the architecture from that script output here either. If we are in a lower-level coding language like C++, we can use Kernel API calls such as IsWow64Process to figure out architecture of a process. If you are using hacking tools that have that low level access you need, this is when you need to get a bit creative. Let me show you what I mean.
I had a case where a tool I used was based on some vbscript code and I needed to figure out the architecture of some of the processes to exploit further. This meant that I had to investigate ways of achieving that. After a lot of exploring processes in a lab, I realized that the vbscript code (using wmi) outputted the VirtualSize of the process. This is where things got interesting.
You see, every time a process is created in Windows, it allocates memory for that process, and uses something named virtual memory to keep track of things. Simply put, virtual memory is way for the operating system to give processes their own isolated address space. VirtualSize is the amount of virtual address space reserved for a memory allocation within a process (Virtual memory, not actual memory). The nice thing about VirtualSize is that a 32-bit process has a virtual size allocated of 4 GB while a 64-bit process typically has 8 TB.
Trying to test the VirtualSize theory in my lab, I found that key takeaway was that I could differentiate based on the size of the virtual memory. If it was 4 GB and less, then it would be a 32-bit process, and if was more than 4 GB, it would be a 64-bit process.
The VirtualSize output from the vbscript is outputted in bytes so everything less than 4,294,967,296 should be a 32-bit process and everything above should be 64-bit, regardless of how much physical memory is present. There are a few exceptions to this that I have found: “Memory compression”, “Registry”, “System”, and “System idle” processes. From my understanding, there is nothing stopping someone from launching a 64-bit process with a smaller VirtualSize, but based on default behavior, this is rarely the case. I have not yet observed that in my lab or in the wild. That being said, this is of course not a 100% guarantee way of checking it but should be good enough for use in hacking adventures.
Now, let’s write a vbscript function that shows the process architecture. This is what I came up with:
Function list_processes() Set objLocator = CreateObject("WbemScripting.SWbemLocator") Set objWMIService = objLocator.ConnectServer(".", "root\cimv2") Set col = objWMIService.ExecQuery ("Select Name,ProcessId,ParentProcessId,VirtualSize,ExecutablePath from Win32_Process") procs = "PID" & vbTab & "PPID" & vbTab & "Arch" & vbTab & "ProcessName" & vbTab & vbTab & vbTab & "Executable Path" & vbCrLf For Each obj in col if obj.VirtualSize < 4294967296 Then procarch = "x86" if obj.processid = "0" then procarch = "x64" end if if obj.processid = "4" then procarch = "x64" end if else procarch = "x64" end if if obj.Name = "Memory Compression" Then procarch = "x64" end if if obj.Name = "Registry" Then procarch = "x64" end if procs = procs & obj.ProcessId & vbTab & obj.ParentProcessId & vbTab & procarch & vbTab & obj.Name & vbTab & vbTab & vbTab & obj.ExecutablePath & vbCrLf Next list_processes = procs End Function wscript.echo list_processes()
Upon running this code with cscript <scriptname.vbs>, you can see something like this:
I highlighted some of the x86 processes so they are easier to read. You can see on the top process that it resolves the architecture, even if it cannot read the command line since it is not allowed. This same process of differentiating on VirtualSize could be used in other scripting languages as well to achieve the same result.
What I wanted to showcase in this post was that even if you do not find a way doing something using built-in scripting methods, it does not mean that it is not possible to achieve. Hope you found this useful and learned something new.