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

Dynamically Add/Edit Environment Variables of Remote Process

4.96/5 (19 votes)
30 Dec 2008CPOL4 min read 69.4K   2.1K  
Dynamically Add/Edit Environment variables of Remote process
DynamicEnvVar_src

Introduction

In many situations, it’s necessary to set application specific environment variables for testing and debugging purposes. These kind of variables are exclusive to the application and will be dead when the application exits. For example, suppose if you've a rendering application in which the rendering path is specified as environment variable. Sometimes it might be necessary to change these variables for testing or in some situations you may need to add or delete the existing one. In most cases, the application specific variables are controlled launching through script files after setting up necessary variables or by a launcher application (Environment variables are inherited from Parent Process). Still dynamically updating variables is out of our reach.

In this article, I'm introducing a simple tool to dynamically set environment strings to any process in the system. This tool won't modify any settings of the system or user environment variables. Instead it adds/updates a particular process’s environment variables.

This tool supports the following operations:

  1. Add a new variable
  2. Replace existing variable
  3. Append to existing variable (separated by “;”)
  4. Deleting a variable

How Do I?

To Add/Replace a variable, just select a process or enter a valid process ID in the corresponding text boxes. Then press “Set” button. If it’s required to add to an existing variable, please check “Add to existing”. To delete a variable, enter a valid variable name and leave the value text box as empty. Make sure that you've unchecked the “Add to existing” check box. Under the hood OK let’s see how I accomplished this. It’s simple as follows:

  1. Create a shared memory with user entered content and options
  2. Inject our own DLL to the target process
  3. In the DLL main, open the share memory and read the required data and finally set the variable
  4. Return from DLL
  5. Now the DLL is not necessary to remain in the process, eject it.

Why Injecting?

Windows provided APIs will work with the current process. These APIs can't work with a foreign process. If a DLL is injected to a particular process, further code will be executed in the context of that particular process.

How to Inject a DLL?

There are several methods to inject a DLL. Please see Windows via C/C++ (or its previous version) to know more about different DLL injection methods. Here in this program, I've used Remote threads. I hope the following code can explain better than words. Anyway the basic technique is to set LoadLibrary as thread routine to load the DLL in remote process. The name of the DLL file must be accessible from the target process itself. For that, we've to use virtual memory APIs. See the code.

C++
// Function to inject the library
void CDLLInjector::InjectLibW( DWORD dwProcessId )
{
	HANDLE hProcess= NULL; // Process handle
	PWSTR pszLibFileRemote = NULL;
	HANDLE hRemoteThread = NULL; 
	__try
	{
		hProcess = OpenProcess(
			PROCESS_QUERY_INFORMATION |   
			PROCESS_CREATE_THREAD     | 
			PROCESS_VM_OPERATION |
			PROCESS_VM_WRITE,  // For CreateRemoteThread
			FALSE, dwProcessId);

		if( !hProcess )
		{
			AfxMessageBox( _T("Failed to update selected process") );
			__leave;
		}

		WCHAR szFilePath[MAX_PATH];
		GetModuleFileNameW( NULL, szFilePath, MAX_PATH );

		// Remove file name of the string
		PathRemoveFileSpecW( szFilePath );

		// Append the DLL file which is there in the same directory of exe
		LPCWSTR pszLib = L"\\SetEnvLib.dll";

		// Append the string
		wcscat_s( szFilePath, MAX_PATH, pszLib ); 

		int cch = 1 + lstrlenW(szFilePath);
		int cb  = cch * sizeof(WCHAR);

		// Allocate space in the remote process for the pathname
		pszLibFileRemote = (PWSTR) VirtualAllocEx( hProcess, 
			NULL, cb, 
			MEM_COMMIT, PAGE_READWRITE);
		if ( pszLibFileRemote == NULL) 
		{
			AfxMessageBox( _T("Unable to allocate memory") );
			return;
		}

		// Copy the DLL's pathname to the remote process' address space
		if (!WriteProcessMemory(hProcess, pszLibFileRemote,
			(PVOID) szFilePath, cb, NULL)) 
		{
			AfxMessageBox( _T( "Failed to write" ));
			return;
		};

		// Create remote thread and inject the library
		hRemoteThread = CreateRemoteThread( hProcess, NULL, 0, 
			(LPTHREAD_START_ROUTINE)LoadLibraryW, 
			pszLibFileRemote, NULL,NULL );

		if( !hRemoteThread )
		{
			AfxMessageBox( _T("Failed to update selected process") );
			__leave;
		}

		WaitForSingleObject( hRemoteThread, INFINITE );

		AfxMessageBox( _T("Successfully Set values"));
	}
	__finally // Do the cleanup
	{
		// Free the remote memory that contained the DLL's pathname
		if (pszLibFileRemote != NULL) 
			VirtualFreeEx(hProcess, pszLibFileRemote, 0, MEM_RELEASE);

		if ( hRemoteThread != NULL) 
			CloseHandle(hRemoteThread);

		if ( hProcess != NULL) 
			CloseHandle(hProcess);
	}
}

How to Eject an Injected DLL?

Once you have completed the task, it’s not good to make our DLL as a burden to the target process. Again we unload the library in the same way we loaded it. Create Remote thread with FreeLibrary API and pass the module base address of SetEnvVarLIb.dll. For this, we can use Toolhelp library APIs. If we create the module handle snapshot of the target process, it’d be able to get the base of address of our injected DLL. See the code below for more details:

C++
// Eject the library loaded to remote process
BOOL CDLLInjector::EjectLibW( DWORD dwProcessID )
{
	BOOL bOk		   = FALSE; // Assume that the function fails
	HANDLE hthSnapshot = NULL;
	HANDLE hProcess = NULL, hThread = NULL;

	__try
	{
		// Grab a new snapshot of the process
		hthSnapshot = CreateToolhelp32Snapshot
				(TH32CS_SNAPMODULE, dwProcessID );
		if (hthSnapshot == INVALID_HANDLE_VALUE) __leave;

		// Get the HMODULE of the desired library
		MODULEENTRY32W me = { sizeof(me) };
		BOOL bFound = FALSE;
		BOOL bMoreMods = Module32FirstW(hthSnapshot, &me);

		// Iterate through all the loaded modules
		for (; bMoreMods; bMoreMods = Module32NextW(hthSnapshot, &me))
		{
			bFound = (_wcsicmp(me.szModule,  L"SetEnvLib.dll" ) == 0) || 
				(_wcsicmp(me.szExePath, L"SetEnvLib.dll" ) == 0);
			if (bFound) break;
		}
		if (!bFound) __leave;

		// Get a handle for the target process.
		hProcess = OpenProcess(
			PROCESS_QUERY_INFORMATION |   
			PROCESS_CREATE_THREAD     | 
			PROCESS_VM_OPERATION,  // For CreateRemoteThread
			FALSE, dwProcessID);
		if (hProcess == NULL) __leave;

		// Get the address of FreeLibrary in Kernel32.dll
		PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
			GetProcAddress
			(GetModuleHandle(TEXT("Kernel32")), "FreeLibrary");
		if (pfnThreadRtn == NULL) __leave;

		// Create a remote thread that calls FreeLibrary()
		hThread = CreateRemoteThread(hProcess, NULL, 0, 
			pfnThreadRtn, me.modBaseAddr, 0, NULL);
		if (hThread == NULL) __leave;

		// Wait for the remote thread to terminate
		WaitForSingleObject(hThread, INFINITE);

		bOk = TRUE; // Everything executed successfully
	}
	__finally 
	{
		if (hthSnapshot != NULL) 
			CloseHandle(hthSnapshot);

		if (hThread     != NULL) 
			CloseHandle(hThread);

		if (hProcess    != NULL) 
			CloseHandle(hProcess);
	}

	return bOk;
}

Preparing the Shared Memory before Inject

OK now you learned about the basics of Injecting and Ejecting. Before injecting the code, we have to prepare the data in a shared memory. In our case, we’ve 3 things to share to the DLL.

  1. Variable
  2. Value
  3. Flag to specify add to existing or not

The following structure will help us to hold the required information:

C++
// Structure to share data

#pragma pack( push )
#pragma pack( 4 )
struct SharedData
{
	BOOL bAddToExisting;
	WCHAR strVariable[1024];
	WCHAR strValue[1024];

	SharedData()
	{
		ZeroMemory( strVariable, sizeof( strVariable ));
		ZeroMemory( strValue, sizeof( strValue ));
		bAddToExisting = TRUE;
	}
};

To keep the size of structure the same in both DLL and our injecting application, we’ve set the packing values explicitly. Otherwise it might be affected with the project settings or pragma pack pre-processor. Now create and write the data to shared memory. To keep it uniform, we’re always dealing with UNICODE character set. The CDLLInjector::SetEnvironmentVariable function accepts CString values which define the character set according to the project settings. Internally the function converts ANSI string to UNICODE if the project’s settings is not UNICODE and calls the UNICODE functions which does the actual task.

C++
// Update the user entered data to sharememory
BOOL CDLLInjector::CreateAndCopyToShareMem
	( LPCWSTR lpVarName, LPCWSTR lpVarVal, BOOL bAddToExisting )
{
	SharedData stData;

	int nLenVar = wcslen( lpVarName );
	if ( 0 == nLenVar || nLenVar >= _countof( stData.strVariable ))
	{
		AfxMessageBox( _T("Variable length is too high. 
				Currently supports only 1024 chars" ));
		return FALSE;
	}

	LPWSTR pBuf;

	// prepare data for copying 
	wcscpy_s( stData.strVariable, _countof( stData.strVariable), lpVarName );
	wcscpy_s( stData.strValue, _countof( stData.strValue), lpVarVal );
	stData.bAddToExisting = bAddToExisting;

	m_hMapFile = CreateFileMapping( INVALID_HANDLE_VALUE, NULL, 
				PAGE_READWRITE, 0, sizeof(stData),
				SHAREMEM_NAME );

	if ( m_hMapFile == NULL) 
	{ 
		MessageBox(0, _T("Could not create file mapping object"), 
			_T("Error"), MB_OK | MB_ICONERROR );
		return FALSE;
	}

	pBuf = (LPWSTR) MapViewOfFile( m_hMapFile, FILE_MAP_ALL_ACCESS, 
				0, 0, sizeof( stData ));

	if ( pBuf == NULL) 
	{ 
		MessageBox(0, _T("Could not map view of file"), 
			_T( "Error" ), MB_OK | MB_ICONERROR ); 

		CloseHandle(m_hMapFile);
		m_hMapFile = 0;
		return FALSE;
	}

	// Copy the data
	CopyMemory((PVOID)pBuf, &stData, sizeof( stData ));

	UnmapViewOfFile(pBuf);
	return TRUE;
}

What's Happening After Injecting to Remote process?

Now we’re done with the main application. Now we can start analyzing the DLL being injected. It simply opens the shared memory and reads it into the shared structure which we discussed previously. See the code.

C++
// Function which reads and updates shared memory
BOOL UpdateEnvVar()
{
	HANDLE hMapFile = 0;
	SharedData* pShared = 0;
	hMapFile = OpenFileMapping( FILE_MAP_READ, 
		FALSE, SHAREMEM_NAME );   // name of mapping object 

	if (hMapFile == NULL) 
	{ 
		OutputDebugString(TEXT("Could not open file mapping object"));
		return FALSE;
	} 

	pShared = (SharedData*) MapViewOfFile(hMapFile, // handle to map object
		FILE_MAP_READ, 0, 0, sizeof( SharedData ));                   

	if (pShared == NULL) 
	{ 
		OutputDebugString(TEXT("Could not map view of file")); 
		return FALSE;
	}

	if( !pShared->bAddToExisting )
	{
		if( wcslen( pShared->strValue ))
		{
			SetEnvironmentVariableW( pShared->strVariable, 
						pShared->strValue);
		}
		else
		{
			// Delete variable
			SetEnvironmentVariableW( pShared->strVariable, NULL );
		}
	}
	else
	{
		// Get the required size
		const DWORD dwReturn = GetEnvironmentVariable
					( pShared->strVariable, 0, 0 );
		const DWORD dwErr = GetLastError();

		if( 0 ==  dwReturn && 
			ERROR_ENVVAR_NOT_FOUND == dwErr ) // Variable not found
		{
			// Set the new one
			SetEnvironmentVariableW( pShared->strVariable, 
						pShared->strValue);
		}
		else if( dwReturn > 0 )
		{
			WCHAR* pstrExisting = new WCHAR[1024];
			if( 0 == GetEnvironmentVariable( pShared->strVariable, 
				pstrExisting, dwReturn ) &&
				GetLastError() == 0 )
			{
				std::wstring strNew( pstrExisting );
				strNew += L";";
				strNew += pShared->strValue;
				SetEnvironmentVariableW
				( pShared->strVariable, strNew.c_str());
			}
		}
	}
	

	if( pShared )
		UnmapViewOfFile(pShared);
	if( hMapFile )
		CloseHandle(hMapFile);
	return TRUE;
}

BOOL APIENTRY DllMain( HMODULE hModule,
		     DWORD  ul_reason_for_call,
		     LPVOID lpReserved
					  )
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		{
			TCHAR buff[MAX_PATH] = { 0 };
			_stprintf_s( buff, _T( "Attached Process: %d" ), 
						GetCurrentProcessId());
			OutputDebugString( buff );
			UpdateEnvVar();
		}

	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}

Limitations

The maximum length of variable that is supported is 1024 and the same number of characters for value.

How To Verify?

  1. Use process explorer
  2. My Other tool - Read Process Environment Variables

History

  • 30-Dec-2008 - Initial version

License

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