Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

Read Environment Strings of Remote Process

4.87/5 (11 votes)
27 Apr 2008CPOL4 min read 1   2.7K  
A Tool to read Environment variables of a remote process
ReadEnv1.png

Introduction

This is a simple tool to read the Environment Variables of a specified process running in the system.

Background

PEB-RTL-ENV1.png

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?

ASM
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.

C++
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.

ASM
mov eax,dword ptr fs:[00000018h]  

- FS:[0x18] points to Thread environment block of a process

ASM
mov eax,dword ptr [eax+30h]  

– An offset of 0x30 points to Process Environment Block (PEB) as documented in Winternl.h:

ASM
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…

ASM
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:

C++
BOOL CProcessEnvReader::ReadEnvironmentBlock( HANDLE hProcess, _ENVSTRING_t& stEnvData )
{
    // Buffer to hold the string read from process
    UCHAR* pchBuffEnvString = 0;
    stEnvData.Clear();
    __try
    {
        PROCESS_BASIC_INFORMATION stBasiProcInfo = { 0 };
        ULONG uReturnLength = 0;
        NTSTATUS ntStat = QueryInformationProcesss( hProcess,
            ProcessBasicInformation,
            &stBasiProcInfo,
            sizeof(stBasiProcInfo),
            &uReturnLength );
        // Read the process environment block
        PEB peb = { 0 };
        SIZE_T nReturnNumBytes = 0;
        // Check read access of specified location in the processs 
        // and get the size of block
        ReadProcessMemory( hProcess,(LPCVOID)stBasiProcInfo.PebBaseAddress, 
				&peb, sizeof(peb) , &nReturnNumBytes );
        // Get the address of RTL_USER_PROCESS_PARAMETERS structure
        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;
        }
        // Get the first 0x64 bytes of RTL_USER_PROCESS_PARAMETERS strcuture
        char cBuffRTLUserInfo[0x64] = {0};
        ReadProcessMemory( hProcess,(LPCVOID)pRTLUserInfo, 
		cBuffRTLUserInfo, 0x64 , &nReturnNumBytes );
        // Validate the read operation
        if( !nReturnNumBytes )
        {

            SHOW_ERR_DLG(_T("Error Reading Process Memory"));
            return FALSE;
        }
        // Get the value at offset 0x48 to get the pointer to environment string block
        UCHAR* pAddrEnvStrBlock = (UCHAR*) *((long*)(&cBuffRTLUserInfo[0]+0x48));
        if( !HasReadAccess( hProcess, pAddrEnvStrBlock, nReadbleSize ))
        {
            SHOW_ERR_DLG(_T("Error Reading Process Memory"));
            return FALSE;
        }
        // Allocate buffer for to copy the block
        pchBuffEnvString = new UCHAR[nReadbleSize];
        memset( pchBuffEnvString, 0, sizeof( UCHAR )* nReadbleSize);
        // Read the environment block
        ReadProcessMemory( hProcess,(LPCVOID)pAddrEnvStrBlock,
            pchBuffEnvString, nReadbleSize , &nReturnNumBytes );
        // Cleanup existing data if any

        if( nReturnNumBytes )
        {
            // Set the values in the return pointer
            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:

C++
 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)