Recently, I made the mistake of volunteering to undertake the creation of a process environment block parsing tool in PowerShell. Several painstaking days of work later, Get-PEB was created. Get-PEB is a self-contained script that will retrieve and parse the PEB of an arbitrary process, independent of Windows OS version (well, XP and above) and architecture – i.e. it will retrieve the PEB of 32-bit, 64-bit, and Wow64 processes.
What is the process environment block? It is a structure that is formed during process initialization that contains data pertinent to the execution of a process and is closely associated with the EPROCESS data structure in the kernel. The structure isn’t fully documented by Microsoft but fortunately, the symbols for the PEB and its embedded structures are made available via `dt nt!_PEB` in Windbg. Additionally, there is a wealth of open source documentation.
The process environment block is also heavily used by shellcode to resolve dll function addresses without needing to call GetProcAddress.
Running Get-PEB is pretty simple. You can either give it a process ID via the ‘-Id’ parameter or you can just pipe the output of Get-Process (ps) to it via the pipeline. For example, let’s say I’m interested in retrieving the PEB from a notepad.exe process:
One of the techniques used by shellcode to get the addresses of loaded modules in memory is to walk the InLoadOrderModuleList doubly linked list to obtain the base address of kernel32.dll and ntdll.dll. Ntdll and kernel32 are almost always the second and third entries in this list. I didn’t bother to parse every possible substructure contained within the PEB in Get-PEB but I did make sure to parse the InLoadOrderModuleList, InMemoryOrderModuleList, and InInitializationOrderModuleList linked lists which point to a series of LDR_DATA_TABLE_ENTRY structures.
So, if I wanted to view the InLoadOrderModuleList field in notepad.exe, I simply type the following:
This script was not trivial to produce. I faced a number of challenges during its creation:
Problem: The definition of the PEB structure has evolved over time since Windows XP and I wanted to reflect these changes dynamically based upon the version of Windows running.
Solution: Reflection was suited perfectly for this task since its intended use is the creation of assemblies, modules, and types (structures are a subset of types in .NET) on the fly. I wrote some logic that retrieves the NTDDI representation of the Windows version and built up the structure of the PEB dynamically based upon the well documented difference in ReactOS.
Problem: Parsing memory structure pointed to by memory addresses in the virtual address space of another process proved problematic.
Solution: I developed a helper function - Get-StructFromMemory to address this issue. It is basically calls ReadProcessMemory in kernel32, copies data from the other process into local virtual memory and calls [Runtime.InteropServices.Marshal]::PtrToStructure.
Problem: Making it all look pretty.
Solution: ps1xml files are designed to format the output of objects. The challenge is that since the PEB structure is created dynamically, I have to account for every possible type that it might emit. The included Get-PEB.format.ps1xml accounts for all these possibilities.
I hope you enjoy Get-PEB. As usual, I encourage you to ask questions, report bugs, propose improvements. Hopefully, time permitting, I’ll parse additional substructures in the PEB. The next one on the list for me is the _RTL_USER_PROCESS_PARAMETERS structure pointed to by the ProcessParameters field.
When and if you do add the _RTL_USER_PROCESS_PARAMETERS structure, that will be of great value and significance because, at last, one can obtain another process' current working directory. And having looked all over the web for a way to do so in PS has convinced me that you are closer to accomplishing that in PS than anyone else.
ReplyDeleteSigh. I was holding off on parsing that until someone asked. ;D You're right though. It would be one of the more valuable substructures to parse.
DeleteI'll try get that done soon.
Thanks for your interest in Get-PEB and PowerSploit. :D
Hey Paul,
DeleteI just pushed the changes. Get-PEB will now parse _RTL_USER_PROCESS_PARAMETERS. :D
Enjoy,
Matt
This is an amazing script, I needed it primarily for grabbing CurrentDirectory. However, it doesn't seem to work on Windows Server 2008 R2.
ReplyDeleteThanks. :D
DeleteEven without you telling me what the issue was, I assume you were getting the following error message: 'Cannot get the PEB of a 64-bit process from a Wow64 process. Use 64-bit PowerShell and try again.' Right?
Anyway, I fixed that issue and it works for me now on Server 2008 R2.
Enjoy!
~Matt
Matt, thanks so much. Truly invaluable!
ReplyDeleteBut one thing I noticed is that it doesn't give the correct CurrentDirectory.
For now, I'm relying on a Python module, psutil:
import psutil
p = psutil.Process()
p.getcwd()
If a Python module can do it, so can PS, no? LOL
Thanks but you already requested that feature and I already implemented it. See my comment to you on May 24. Anyway, just download the latest version of Get-PEB. Here's an example of its use:
Delete$PEB = Get-Process -Id $PID | Get-PEB
$PEB.ProcessParameters.CurrentDirectory
Hi Matt. Thanks for your reply.
DeleteYes, you're right that I did request that feature and I sincerely thank you for taking the time to implement it.
Indeed, that's exactly how I'm using the new feature (as you showed above). But the string value I get is just "C:\Windows\" and that differs from what ProcessExplorer or psutil shows me.
I'm not saying that it's a bug in your code - don't know for sure.
I don't understand why it .ProcessParameters.CurrentDirectory shows that string for ANY PID\process.
I think I should clarify what I'm doing.
ReplyDeleteUsing ProcessExplorer or Task Manager I get the PID of process I'm in interested in, say, 11796.
Then, in PS I do:
$PEB = Get-PEB -Id 11796
$PEB.ProcessParameters.CurrentDirectory
gives me "C:\Windows\" for any process.
I've gotten pretty good at PS but, am I doing something incorrectly? :)
Hmm. Considering I'm getting conflicting information in Get-PEB vs. WinDbg, it would appear as though my script has a bug. I'll dig into it and hopefully have a solution shortly. Thanks for bringing that to my attention.
DeleteHey Paul. I figured it out. Before reading ahead, run Get-PEB from 32-bit PowerShell and observe the difference.
DeleteThe misrepresented CurrentDirectory is caused by the fact that Get-PEB, if run from 64-bit PowerShell retrieves the 64-bit PEB of a 32-bit process. To fix this peculiarity, I'll have to determine the bitness of the process and get its corresponding PEB. I can't explain why C:\Windows is the most common CurrentDirectory in this case, but I can tell you with certainty that the issue is dual PEBs in a wow64 process.
Hi Mark.
DeleteYou're right! I just finished trying it inside a 32- and 64-bit PS and, in the 32-bit PS, I obtained the correct CurrentDirectory. Good find!
In determining the bitness of the process, here's a small bit of Python code from the psutil module that may help you:
// get the address of ProcessParameters
#ifdef _WIN64
if (!ReadProcessMemory(processHandle, (PCHAR)pebAddress + 32,
&rtlUserProcParamsAddress, sizeof(PVOID), NULL))
#else
if (!ReadProcessMemory(processHandle, (PCHAR)pebAddress + 0x10,
&rtlUserProcParamsAddress, sizeof(PVOID), NULL))
#endif
{
CloseHandle(processHandle);
if (GetLastError() == ERROR_PARTIAL_COPY) {
// this occurs quite often with system processes
return AccessDenied();
}
else {
return PyErr_SetFromWindowsErr(0);
}
}
// Read the currentDirectory UNICODE_STRING structure.
// 0x24 refers to "CurrentDirectoryPath" of RTL_USER_PROCESS_PARAMETERS
// structure, see:
// http://wj32.wordpress.com/2009/01/24/howto-get-the-command-line-of-processes/
#ifdef _WIN64
if (!ReadProcessMemory(processHandle, (PCHAR)rtlUserProcParamsAddress + 56,
¤tDirectory, sizeof(currentDirectory), NULL))
#else
if (!ReadProcessMemory(processHandle, (PCHAR)rtlUserProcParamsAddress + 0x24,
¤tDirectory, sizeof(currentDirectory), NULL))
#endif
{
CloseHandle(processHandle);
if (GetLastError() == ERROR_PARTIAL_COPY) {
// this occurs quite often with system processes
return AccessDenied();
}
else {
return PyErr_SetFromWindowsErr(0);
}
}
Thank you for this script!
ReplyDeleteIt helped me a lot.