Reading the Process Environment Block in C#/Powershell
I was doing some research to figure out how to extract open file handles for a given process because I wanted to extract them in C#/Powershell.
If you’re somewhat familiar with process memory and debugging, you know that every process has a Process Environment Block (PEB https://en.wikipedia.org/wiki/Process_Environment_Block ), which is a structure that holds information about the process. My initial assumption was that somewhere in the processes user-land memory, there would be a pointer to open file handles. So I started digging around the PEB looking for information. Turns out I was wrong. Apparently, it’s a tougher task than I thought because the filename’s associated with the handles are maintained in kernel space memory. Either way, I did learn something: how to interact with a remote process’s PEB with C#.
First, here’s a quick look at CreateProcessA:
BOOL CreateProcessA(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
Looking at a diagram of the PEB, you’ll notice RTL_USER_PROCESS_PARAMETERS
If you de-reference the pointer held in 0x020 of the PEB, you’re led to the RTL_USER_PROCESS_PARAMETERS struct. In x64dbg, you can do the following: Go To Expression—> Type ‘peb()’ —> In the dump window, right-click the address at the 0x20 offset—>’Follow QWORD in Dump’—> Choose the panel you want to dump the address contents in. The struct is defined as below below:
typedef struct _RTL_USER_PROCESS_PARAMETERS
{
ULONG MaximumLength;
ULONG Length;
ULONG Flags;
ULONG DebugFlags;
PVOID ConsoleHandle;
ULONG ConsoleFlags;
PVOID StandardInput;
PVOID StandardOutput;
PVOID StandardError;
CURDIR CurrentDirectory;
UNICODE_STRING DllPath;
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine;
PVOID Environment;
ULONG StartingX;
ULONG StartingY;
ULONG CountX;
ULONG CountY;
ULONG CountCharsX;
ULONG CountCharsY;
ULONG FillAttribute;
ULONG WindowFlags;
ULONG ShowWindowFlags;
UNICODE_STRING WindowTitle;
UNICODE_STRING DesktopInfo;
UNICODE_STRING ShellInfo;
UNICODE_STRING RuntimeData;
RTL_DRIVE_LETTER_CURDIR CurrentDirectores[32];
ULONG EnvironmentSize;
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
You’ll notice that the arguments from CreateProcessA are the same/similar to a few for the fields in the struct. Well, no pointers to open file handles; neither here or anywhere in the PEB. As far as this struct, it makes sense since this is all established at process start-up and a user may open many files during the life of a process.
Bummer. But then I got interested in how to extract these so I went for the obvious ones CmdLine and CurrentDirectory.
In order to extract those two as a base for extracting the other strings, a little research showed that you can use the ReadProcessMemory and NtQueryInformationProcess Windows API functions. So, I gave it a shot in Powershell using a custom C# type:
$WinAPI = @'
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.ComponentModel;
namespace PEB
{
public static class ProcessUtilities
{
public static string GetCurrentDirectory(int processId)
{
return GetProcessParametersString(processId, Environment.Is64BitOperatingSystem ? 0x38 : 0x24);
}
public static string GetCurrentDirectory(this Process process)
{
if (process == null)
throw new ArgumentNullException("process");
return GetCurrentDirectory(process.Id);
}
public static string GetCommandLine(int processId)
{
return GetProcessParametersString(processId, Environment.Is64BitOperatingSystem ? 0x70 : 0x40);
}
public static string GetCommandLine(this Process process)
{
if (process == null)
throw new ArgumentNullException("process");
return GetCommandLine(process.Id);
}
private static string GetProcessParametersString(int processId, int offset)
{
IntPtr handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, processId);
if (handle == IntPtr.Zero)
throw new Win32Exception(Marshal.GetLastWin32Error());
int processParametersOffset = Environment.Is64BitOperatingSystem ? 0x20 : 0x10;
try
{
if (Environment.Is64BitOperatingSystem && !Environment.Is64BitProcess) // are we running in WOW?
{
PROCESS_BASIC_INFORMATION_WOW64 pbi = new PROCESS_BASIC_INFORMATION_WOW64();
int hr = NtWow64QueryInformationProcess64(handle, 0, ref pbi, Marshal.SizeOf(pbi), IntPtr.Zero);
if (hr != 0)
throw new Win32Exception(hr);
long pp = 0;
hr = NtWow64ReadVirtualMemory64(handle, pbi.PebBaseAddress + processParametersOffset, ref pp, Marshal.SizeOf(pp), IntPtr.Zero);
if (hr != 0)
throw new Win32Exception(hr);
UNICODE_STRING_WOW64 us = new UNICODE_STRING_WOW64();
hr = NtWow64ReadVirtualMemory64(handle, pp + offset, ref us, Marshal.SizeOf(us), IntPtr.Zero);
if (hr != 0)
throw new Win32Exception(hr);
if ((us.Buffer == 0) || (us.Length == 0))
return null;
string s = new string('\0', us.Length / 2);
hr = NtWow64ReadVirtualMemory64(handle, us.Buffer, s, us.Length, IntPtr.Zero);
if (hr != 0)
throw new Win32Exception(hr);
return s;
}
else // we are running with the same bitness as the OS, 32 or 64
{
PROCESS_BASIC_INFORMATION pbi = new PROCESS_BASIC_INFORMATION();
int hr = NtQueryInformationProcess(handle, 0, ref pbi, Marshal.SizeOf(pbi), IntPtr.Zero);
if (hr != 0)
throw new Win32Exception(hr);
IntPtr pp = new IntPtr();
if (!ReadProcessMemory(handle, pbi.PebBaseAddress + processParametersOffset, ref pp, new IntPtr(Marshal.SizeOf(pp)), IntPtr.Zero))
throw new Win32Exception(Marshal.GetLastWin32Error());
UNICODE_STRING us = new UNICODE_STRING();
if (!ReadProcessMemory(handle, pp + offset, ref us, new IntPtr(Marshal.SizeOf(us)), IntPtr.Zero))
throw new Win32Exception(Marshal.GetLastWin32Error());
if ((us.Buffer == IntPtr.Zero) || (us.Length == 0))
return null;
string s = new string('\0', us.Length / 2);
if (!ReadProcessMemory(handle, us.Buffer, s, new IntPtr(us.Length), IntPtr.Zero))
throw new Win32Exception(Marshal.GetLastWin32Error());
return s;
}
}
finally
{
CloseHandle(handle);
}
}
private const int PROCESS_QUERY_INFORMATION = 0x400;
private const int PROCESS_VM_READ = 0x10;
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_BASIC_INFORMATION
{
public IntPtr Reserved1;
public IntPtr PebBaseAddress;
public IntPtr Reserved2_0;
public IntPtr Reserved2_1;
public IntPtr UniqueProcessId;
public IntPtr Reserved3;
}
[StructLayout(LayoutKind.Sequential)]
private struct UNICODE_STRING
{
public short Length;
public short MaximumLength;
public IntPtr Buffer;
}
// for 32-bit process in a 64-bit OS only
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_BASIC_INFORMATION_WOW64
{
public long Reserved1;
public long PebBaseAddress;
public long Reserved2_0;
public long Reserved2_1;
public long UniqueProcessId;
public long Reserved3;
}
// for 32-bit process in a 64-bit OS only
[StructLayout(LayoutKind.Sequential)]
private struct UNICODE_STRING_WOW64
{
public short Length;
public short MaximumLength;
public long Buffer;
}
[DllImport("ntdll.dll")]
private static extern int NtQueryInformationProcess(IntPtr ProcessHandle, int ProcessInformationClass, ref PROCESS_BASIC_INFORMATION ProcessInformation, int ProcessInformationLength, IntPtr ReturnLength);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, ref IntPtr lpBuffer, IntPtr dwSize, IntPtr lpNumberOfBytesRead);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, ref UNICODE_STRING lpBuffer, IntPtr dwSize, IntPtr lpNumberOfBytesRead);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, [MarshalAs(UnmanagedType.LPWStr)] string lpBuffer, IntPtr dwSize, IntPtr lpNumberOfBytesRead);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
[DllImport("kernel32.dll")]
private static extern bool CloseHandle(IntPtr hObject);
// for 32-bit process in a 64-bit OS only
[DllImport("ntdll.dll")]
private static extern int NtWow64QueryInformationProcess64(IntPtr ProcessHandle, int ProcessInformationClass, ref PROCESS_BASIC_INFORMATION_WOW64 ProcessInformation, int ProcessInformationLength, IntPtr ReturnLength);
[DllImport("ntdll.dll")]
private static extern int NtWow64ReadVirtualMemory64(IntPtr hProcess, long lpBaseAddress, ref long lpBuffer, long dwSize, IntPtr lpNumberOfBytesRead);
[DllImport("ntdll.dll")]
private static extern int NtWow64ReadVirtualMemory64(IntPtr hProcess, long lpBaseAddress, ref UNICODE_STRING_WOW64 lpBuffer, long dwSize, IntPtr lpNumberOfBytesRead);
[DllImport("ntdll.dll")]
private static extern int NtWow64ReadVirtualMemory64(IntPtr hProcess, long lpBaseAddress, [MarshalAs(UnmanagedType.LPWStr)] string lpBuffer, long dwSize, IntPtr lpNumberOfBytesRead);
}
}
'@
$refAssemblies = ("System","System.Diagnostics.Process","System.Runtime.InteropServices","System.ComponentModel")
Add-Type -TypeDefinition $WinAPI -Language CSharp -ReferencedAssemblies $refAssemblies
And a quick demo of grabbing the CommandLine arg from a PowerPoint process:
Pass in the Process ID to the assembly
As a side note, command line args are already exposed through the Get-WMIObject cmdlet so as far as extracting this information, this particular example isn’t earth-shattering information, but this is meant to show that you can interact with and extract PEB information from a live process in Powershell. You’ll have to modify the C# signature to extract whatever you want.