screenshot

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 it will be died 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 add/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 adding 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 be work with current process. These APIs can’t work with a foreign process. If a DLL is injected to a particular process, the 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 target process itself. For that we’ve to use virtual memory APIs. See the code.
Β 

// 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 completed the task, it’s not good to make our DLL as a burden to the target process. Again we’re unloads the library in the same way we loaded. Creating Remote thread with FreeLibrary API and passing 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.
Β 

// 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 basics of Injecting and Ejecting. Before injecting the code, we’ve 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

Β 

// 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 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 defines the character set according to the project settings. Internally the function converting ANSI string to UNICODE if the project’s settings is not UNICODE and calling the UNICODE functions which does the actual task.Β 

// 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 read it into the shared structure which we discussed previously. See the code.

// 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 is supported 1024 and same number of characters for value.
Β 

How to verify?Β 

Use process explorer tool to very it.
Β 

History

30-Dec-2008 – Initial Version