Based in Sydney, Australia, Foundry is a blog by Rebecca Thao. Her posts explore modern architecture through photos and quotes by influential architects, engineers, and artists.

Reading the Process Environment Block in C#/Powershell

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

71.png

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

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.

cmdline.PNG
Powershell Browser Automation

Powershell Browser Automation

Chasing AMSI (Anti-Malware Scripting Interface)

Chasing AMSI (Anti-Malware Scripting Interface)