In this post I will show you how you can get the list of the environment variables of an external x86 process just like is done by tools like Process explorer or Process hacker.
To locate the Environment Variables of a process you must access the PEB (Process Enviroment Block) of the application and follow this secuence to resolve the address of this buffer.
PEB -> ProcessParameters(RTL_USER_PROCESS_PARAMETERS) -> Environment (Pointer)
This is the definition of the PEB structure
typedef struct _PEB { BYTE Reserved1[2]; BYTE BeingDebugged; BYTE Reserved2[1]; PVOID Reserved3[2]; PPEB_LDR_DATA Ldr; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; BYTE Reserved4[104]; PVOID Reserved5[52]; PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine; BYTE Reserved6[128]; PVOID Reserved7[1]; ULONG SessionId; } PEB, *PPEB;
And this is the definition of the RTL_USER_PROCESS_PARAMETERS.
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]; } RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
NtQueryInformationProcess and VirtualQueryEx
After of translate the above structures to delphi records, we need to get a pointer to the PEB of the process, this task must be done with the NtQueryInformationProcess function, passing the ProcessBasicInformation value in the ProcessInformationClass parameter, this will return a PROCESS_BASIC_INFORMATION structure having the following layout:
typedef struct _PROCESS_BASIC_INFORMATION { PVOID Reserved1; PPEB PebBaseAddress; PVOID Reserved2[2]; ULONG_PTR UniqueProcessId; PVOID Reserved3; } PROCESS_BASIC_INFORMATION;
Now using the ReadProcessMemory method you can read the PEB and the ProcessParameters (RTL_USER_PROCESS_PARAMETERS) of the application, to finally get the Pointer to the environment variables.
// get the PROCESS_BASIC_INFORMATION to access to the PEB Address if (NtQueryInformationProcess(ProcessHandle,0{=>ProcessBasicInformation},@ProcessBasicInfo, SizeOf(ProcessBasicInfo), @ReturnLength)=STATUS_SUCCESS) and (ReturnLength=SizeOf(ProcessBasicInfo)) then begin //read the PEB struture if not ReadProcessMemory(ProcessHandle, ProcessBasicInfo.PEBBaseAddress, @Peb, sizeof(Peb), lpNumberOfBytesRead) then RaiseLastOSError else begin //read the RTL_USER_PROCESS_PARAMETERS structure if not ReadProcessMemory(ProcessHandle, Peb.ProcessParameters, @Rtl, SizeOf(Rtl), lpNumberOfBytesRead) then RaiseLastOSError
After of that we need calculate the size of the Env. variables buffer to read. This can be done using the VirtualQueryEx function which retieve the range of memory pages of the queried memory block and then using the ReadProcessMemory function again you can get the environment variables into a buffer.
Try this sample
//get the size of the Env. variables block if VirtualQueryEx(ProcessHandle, Rtl.Environment, Mbi, SizeOf(Mbi))=0 then RaiseLastOSError else EnvStrLength :=(mbi.RegionSize -(ULONG_PTR(Rtl.Environment) - ULONG_PTR(mbi.BaseAddress))); SetLength(EnvStrBlock, EnvStrLength); //read the content of the env. variables block if not ReadProcessMemory(ProcessHandle, Rtl.Environment, @EnvStrBlock[0], EnvStrLength, lpNumberOfBytesRead) then RaiseLastOSError else {$IFDEF UNICODE} Result:=TEncoding.Unicode.GetString(EnvStrBlock); {$ELSE} SetString(WS, PWideChar(@EnvStrBlock[0]), Length(EnvStrBlock) div 2); Result:=WS; {$ENDIF}
The source code
Finally this is the full source code to read the environment variables of an external x86 process, the code was tested from Delphi 2007 to XE2 under WinXP and Windows 7.
program NtQueryInformationProcess_EnvVars; //Author Rodrigo Ruz (RRUZ) //2012-05-26 {$APPTYPE CONSOLE} {$IFDEF CPUX64} Sorry only 32 bits support{$ENDIF} {$R *.res} uses Classes, SysUtils, Windows; type _UNICODE_STRING = record Length: Word; MaximumLength: Word; Buffer: LPWSTR; end; UNICODE_STRING = _UNICODE_STRING; //http://msdn.microsoft.com/en-us/library/windows/desktop/ms684280%28v=vs.85%29.aspx PROCESS_BASIC_INFORMATION = record Reserved1 : Pointer; PebBaseAddress: Pointer; Reserved2: array [0..1] of Pointer; UniqueProcessId: ULONG_PTR; Reserved3: Pointer; end; //http://undocumented.ntinternals.net/UserMode/Structures/RTL_DRIVE_LETTER_CURDIR.html _RTL_DRIVE_LETTER_CURDIR = record Flags: Word; Length: Word; TimeStamp: ULONG; DosPath: UNICODE_STRING; end; RTL_DRIVE_LETTER_CURDIR = _RTL_DRIVE_LETTER_CURDIR; _CURDIR = record DosPath: UNICODE_STRING; Handle: THANDLE; end; CURDIR = _CURDIR; //http://undocumented.ntinternals.net/UserMode/Structures/RTL_USER_PROCESS_PARAMETERS.html _RTL_USER_PROCESS_PARAMETERS = record MaximumLength: ULONG; Length: ULONG; Flags: ULONG; DebugFlags: ULONG; ConsoleHandle: THANDLE; ConsoleFlags: ULONG; StandardInput: THANDLE; StandardOutput: THANDLE; StandardError: THANDLE; CurrentDirectory: CURDIR; DllPath: UNICODE_STRING; ImagePathName: UNICODE_STRING; CommandLine: UNICODE_STRING; Environment: Pointer; StartingX: ULONG; StartingY: ULONG; CountX: ULONG; CountY: ULONG; CountCharsX: ULONG; CountCharsY: ULONG; FillAttribute: ULONG; WindowFlags: ULONG; ShowWindowFlags: ULONG; WindowTitle: UNICODE_STRING; DesktopInfo: UNICODE_STRING; ShellInfo: UNICODE_STRING; RuntimeData: UNICODE_STRING; CurrentDirectories: array[0..31] of RTL_DRIVE_LETTER_CURDIR; end; RTL_USER_PROCESS_PARAMETERS = _RTL_USER_PROCESS_PARAMETERS; PRTL_USER_PROCESS_PARAMETERS = ^RTL_USER_PROCESS_PARAMETERS; _PEB = record Reserved1 : array [0..1] of Byte; BeingDebugged : Byte; Reserved2 : Byte; Reserved3 : array [0..1] of Pointer; Ldr : Pointer; ProcessParameters : PRTL_USER_PROCESS_PARAMETERS; Reserved4 : array [0..102] of Byte; Reserved5 : array [0..51] of Pointer; PostProcessInitRoutine : Pointer; Reserved6 : array [0..127] of byte; Reserved7 : Pointer; SessionId : ULONG; end; PEB=_PEB; function NtQueryInformationProcess(ProcessHandle : THandle; ProcessInformationClass : DWORD; ProcessInformation : Pointer; ProcessInformationLength : ULONG; ReturnLength : PULONG ): LongInt; stdcall; external 'ntdll.dll'; type TIsWow64Process = function(Handle:THandle; var IsWow64 : BOOL) : BOOL; stdcall; var _IsWow64Process : TIsWow64Process; procedure Init_IsWow64Process; var hKernel32 : Integer; begin hKernel32 := LoadLibrary(kernel32); if (hKernel32 = 0) then RaiseLastOSError; try _IsWow64Process := GetProcAddress(hkernel32, 'IsWow64Process'); finally FreeLibrary(hKernel32); end; end; function ProcessIsX64(hProcess: DWORD): Boolean; var IsWow64 : BOOL; PidHandle : THandle; begin Result := False; if not Assigned(_IsWow64Process) then Init_IsWow64Process; if Assigned(_IsWow64Process) then begin //check if the current app is running under WOW if _IsWow64Process(GetCurrentProcess(), IsWow64) then Result := IsWow64 else RaiseLastOSError; {$IFNDEF CPUX64} //the current delphi App is not running under wow64, so the current Window OS is 32 bit //and obviously all the apps are 32 bits. if not Result then Exit; {$ENDIF} if (_IsWow64Process(hProcess, IsWow64)) then Result := not IsWow64 else RaiseLastOSError; end; end; function GetEnvVarsPid(dwProcessId : DWORD): string; const STATUS_SUCCESS = $00000000; SE_DEBUG_NAME = 'SeDebugPrivilege'; OffsetProcessParametersx32 = $10; var ProcessHandle : THandle; ProcessBasicInfo : PROCESS_BASIC_INFORMATION; ReturnLength : DWORD; lpNumberOfBytesRead : ULONG_PTR; TokenHandle : THandle; lpLuid : TOKEN_PRIVILEGES; OldlpLuid : TOKEN_PRIVILEGES; Rtl : RTL_USER_PROCESS_PARAMETERS; Mbi : TMemoryBasicInformation; Peb : _PEB; EnvStrBlock : TBytes; EnvStrLength : ULONG; {$IFNDEF UNICODE} WS : WideString; {$ENDIF} begin Result:=''; if OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY, TokenHandle) then begin try if not LookupPrivilegeValue(nil, SE_DEBUG_NAME, lpLuid.Privileges[0].Luid) then RaiseLastOSError else begin lpLuid.PrivilegeCount := 1; lpLuid.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED; ReturnLength := 0; OldlpLuid := lpLuid; //Set the SeDebugPrivilege privilege if not AdjustTokenPrivileges(TokenHandle, False, lpLuid, SizeOf(OldlpLuid), OldlpLuid, ReturnLength) then RaiseLastOSError; end; ProcessHandle := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, false, dwProcessId); if ProcessHandle=0 then RaiseLastOSError else try if ProcessIsX64(ProcessHandle) then raise Exception.Create('Only 32 bits processes are supported'); // get the PROCESS_BASIC_INFORMATION to access to the PEB Address if (NtQueryInformationProcess(ProcessHandle,0{=>ProcessBasicInformation},@ProcessBasicInfo, SizeOf(ProcessBasicInfo), @ReturnLength)=STATUS_SUCCESS) and (ReturnLength=SizeOf(ProcessBasicInfo)) then begin //read the PEB struture if not ReadProcessMemory(ProcessHandle, ProcessBasicInfo.PEBBaseAddress, @Peb, sizeof(Peb), lpNumberOfBytesRead) then RaiseLastOSError else begin //read the RTL_USER_PROCESS_PARAMETERS structure if not ReadProcessMemory(ProcessHandle, Peb.ProcessParameters, @Rtl, SizeOf(Rtl), lpNumberOfBytesRead) then RaiseLastOSError else begin //get the size of the Env. variables block if VirtualQueryEx(ProcessHandle, Rtl.Environment, Mbi, SizeOf(Mbi))=0 then RaiseLastOSError else EnvStrLength :=(mbi.RegionSize -(ULONG_PTR(Rtl.Environment) - ULONG_PTR(mbi.BaseAddress))); SetLength(EnvStrBlock, EnvStrLength); //read the content of the env. variables block if not ReadProcessMemory(ProcessHandle, Rtl.Environment, @EnvStrBlock[0], EnvStrLength, lpNumberOfBytesRead) then RaiseLastOSError else {$IFDEF UNICODE} Result:=TEncoding.Unicode.GetString(EnvStrBlock); {$ELSE} { SetLength(Result, Length(EnvStrBlock) div 2); WideCharToMultiByte( CP_ACP , 0, PWideChar(@EnvStrBlock[0]), -1, @Result[1], Rtl.EnvironmentSize, nil, nil ); } SetString(WS, PWideChar(@EnvStrBlock[0]), Length(EnvStrBlock) div 2); Result:=WS; {$ENDIF} end; end; end else RaiseLastOSError; finally CloseHandle(ProcessHandle); end; finally CloseHandle(TokenHandle); end; end else RaiseLastOSError; end; function GetEnvVarsPidList(dwProcessId : DWORD): TStringList; var PEnvVars: PChar; PEnvEntry: PChar; begin Result:=TStringList.Create; PEnvVars := PChar(GetEnvVarsPid(dwProcessId)); PEnvEntry := PEnvVars; while PEnvEntry^ <> #0 do begin Result.Add(PEnvEntry); Inc(PEnvEntry, StrLen(PEnvEntry) + 1); end; end; Var EnvVars : TStringList; begin ReportMemoryLeaksOnShutdown:=True; try EnvVars:=GetEnvVarsPidList(4724); try Writeln(EnvVars.Text); finally EnvVars.Free; end; except on E:Exception do Writeln(E.Classname, ':', E.Message); end; Readln; end.
May 26, 2012 at 7:34 pm
why the first line of the unit is “program NtQueryInformationProcess_EnvVars;” ? not “Unit”?
May 26, 2012 at 7:43 pm
This is Demo Console application not a unit, but of course you can create a unit with this code.
May 26, 2012 at 8:01 pm
raised exception class eoserror with message ‘system error. code:87’
May 26, 2012 at 8:04 pm
Which version of Delphi and Windows are you using? and in which line the exception is raised?
Pingback: Getting the environment variables of an external x86 and x64 process « The Road to Delphi – a Blog about programming