Introduction
This is a simple tool to read the Environment Variables of a specified process running in the system.
Background
This article is solely based on one of the blog posts of Matt Pietrek. During one of my projects, I was needed to get the environment strings of an application. Of course Process Explorer was there to help me out. Then I was thinking of writing my own. I sadly recognized that there’s no API to get environment strings of a foreign process. On Googling, I found the above mentioned blog post of Matt Pietrek. He provided an idea and left without code as an exercise for the reader.
Under the Hood of GetEnvironmentStrings API
GetProcessStrings
API can be used to get the environment strings of the current application. So what is it actually doing inside?
76D337B8 mov eax,dword ptr fs:[00000018h]
76D337BE mov eax,dword ptr [eax+30h]
76D337C1 push ebx
76D337C2 push esi
76D337C3 mov esi,dword ptr [eax+10h]
76D337C6 push edi
76D337C7 call dword ptr ds:[76D11530h]
76D337CD mov eax,dword ptr [esi+48h]
You can decrypt this assembly code with the help of winterl.h. The structures inside this header are defined in a poor manner (Platform SDK available with VS 2008). E.g. _PEB looks as follows.
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[229];
PVOID Reserved3[59];
ULONG SessionId;
} PEB, *PPEB;
It’s hard to know what they’re really meant by the “reserved” byte arrays. ReactorOS is a place where we can know something more about the Windows internal implementation. So I just tried to dig it further with ReactorOS. You can see a well documented winternl.h at ReactorOS website.
mov eax,dword ptr fs:[00000018h]
- FS:[0x18] points to Thread environment block of a process
mov eax,dword ptr [eax+30h]
– An offset of 0x30 points to Process Environment Block (PEB) as documented in Winternl.h:
mov esi,dword ptr [eax+10h] –
The next instruction dereferences a pointer at offset 0x10.This is actually pointing to RTL_USER_PROCESS_PARAMETERS
structure. This structure contains various information including command line arguments, current directory, environment variables, etc…
mov eax,dword ptr [esi+48h]
– The pointer for environment variables block is located at an offset of 0x48 as we see in the instruction.
Here we are!
The environment variables are stored in the following format in the block allocated Var1=Value1\0 Var2=Value2\0 Var3=Value3\0 ... VarN=ValueN\0\0
The block is ending with two null characters.
The remaining part of GetEnvrionmentStrings
code allocates some memory and copies each wide character in the environment string block till it finds two null values together.
Emulating the Same Routine with Another Process
We need to emulate the above routine with another process to get the environment strings. We can approach with different methods like creating remote threads and call GetEnvironmentStrings()
in the remote process. Also we can use “ReadProcessMemory” and “WriteProcessMemory” APIs to read and write process memory.
I’m approaching the latter one because it’s easy to accomplish. Anyway I never tried the first method and also I believe there we need some mechanism to share the data among processes which I feel is very costly.
To get the PEB of the remote process, we can use “NtQueryInformationProcess” function to get the information about the process. Note that this “NtQueryInformationProcess” API may be altered or unavailable in the future version of Windows.
Let me explain further with code. The following code will work fine with Windows Vista, XP and 2000 (all 32 bit versions). Use it at your own risk. The memory layout or APIs may alter in future version of Windows. I have never tried this source code with any 64 bit Windows OS.
Using the Code
The core code is implemented as a class called CProcessEnvReader
. I will explain the core Routine first. The comments inside the code seems to be enough to know about emulating the GetEnvironmentStrings
API.
The process Handle you’re passing must have PROCESS_READ_ACCESS
. For ease of use, you can use CProcessEnvReader::OpenProcessToRead
function by passing the desired PID. It will return the handle if the process successfully opened:
BOOL CProcessEnvReader::ReadEnvironmentBlock( HANDLE hProcess, _ENVSTRING_t& stEnvData )
{
UCHAR* pchBuffEnvString = 0;
stEnvData.Clear();
__try
{
PROCESS_BASIC_INFORMATION stBasiProcInfo = { 0 };
ULONG uReturnLength = 0;
NTSTATUS ntStat = QueryInformationProcesss( hProcess,
ProcessBasicInformation,
&stBasiProcInfo,
sizeof(stBasiProcInfo),
&uReturnLength );
PEB peb = { 0 };
SIZE_T nReturnNumBytes = 0;
ReadProcessMemory( hProcess,(LPCVOID)stBasiProcInfo.PebBaseAddress,
&peb, sizeof(peb) , &nReturnNumBytes );
UCHAR* puPEB = (UCHAR*)&peb;
UCHAR* pRTLUserInfo = (UCHAR*) *((long*)(puPEB+0x10));
int nReadbleSize = 0;
if( !HasReadAccess( hProcess, pRTLUserInfo, nReadbleSize ))
{
SHOW_ERR_DLG(_T("Error Reading Process Memory"));
return FALSE;
}
char cBuffRTLUserInfo[0x64] = {0};
ReadProcessMemory( hProcess,(LPCVOID)pRTLUserInfo,
cBuffRTLUserInfo, 0x64 , &nReturnNumBytes );
if( !nReturnNumBytes )
{
SHOW_ERR_DLG(_T("Error Reading Process Memory"));
return FALSE;
}
UCHAR* pAddrEnvStrBlock = (UCHAR*) *((long*)(&cBuffRTLUserInfo[0]+0x48));
if( !HasReadAccess( hProcess, pAddrEnvStrBlock, nReadbleSize ))
{
SHOW_ERR_DLG(_T("Error Reading Process Memory"));
return FALSE;
}
pchBuffEnvString = new UCHAR[nReadbleSize];
memset( pchBuffEnvString, 0, sizeof( UCHAR )* nReadbleSize);
ReadProcessMemory( hProcess,(LPCVOID)pAddrEnvStrBlock,
pchBuffEnvString, nReadbleSize , &nReturnNumBytes );
if( nReturnNumBytes )
{
stEnvData.pData = (LPCWSTR) pchBuffEnvString;
stEnvData.nSize = nNumBytes;
return TRUE;
}
else
{
SHOW_ERR_DLG(_T("Error Reading Process Memory"));
}
}
__except( SHOW_ERR_DLG( _T("Exception occured on reading process memory")))
{
SAFE_ARRAY_CLEANUP( pchBuffEnvString );
}
return FALSE;
}
Points of Interest
Even if we tracked down the location of environment block, it’s size was a mysterious thing. I was almost unaware about managing the size of the buffer need to be allocated. Using VirtualQueryEx
, I managed to get the range of pages from the specified memory location of the remote process. You could see the implementation of it as follows:
BOOL CProcessEnvReader::HasReadAccess( HANDLE hProcess,
void* pAddress, int& nSize )
{
MEMORY_BASIC_INFORMATION memInfo;
__try
{
VirtualQueryEx( hProcess, pAddress,&memInfo,sizeof(memInfo));
if( PAGE_NOACCESS == memInfo.Protect ||
PAGE_EXECUTE == memInfo.Protect )
{
nSize = 0;
return FALSE;
}
nSize = memInfo.RegionSize;
return TRUE;
}
__except( SHOW_ERR_DLG( _T("Failed to query memory access")))
{
}
return FALSE;
}
Helper Routines inside CProcessEnvReader Class
You can see a number of functions inside CProcessEnvReader
classes. These functions can be used for processing the environment block after reading it from the remote process.
Did You Find Any Area to Improve?
If you find a better idea or some area in the source code to improve, please feel free to share with me.
History
- Ver 1.0 – Initial version of
ProcessEnvReader