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:
- Add a new variable
- Replace existing variable
- Append to existing variable (separated by “;”)
- 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:
- Create a shared memory with user entered content and options
- Inject our own DLL to the target process
- In the DLL main, open the share memory and read the required data and finally set the variable
- Return from DLL
- 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.
void CDLLInjector::InjectLibW( DWORD dwProcessId )
{
HANDLE hProcess= NULL; PWSTR pszLibFileRemote = NULL;
HANDLE hRemoteThread = NULL;
__try
{
hProcess = OpenProcess(
PROCESS_QUERY_INFORMATION |
PROCESS_CREATE_THREAD |
PROCESS_VM_OPERATION |
PROCESS_VM_WRITE, FALSE, dwProcessId);
if( !hProcess )
{
AfxMessageBox( _T("Failed to update selected process") );
__leave;
}
WCHAR szFilePath[MAX_PATH];
GetModuleFileNameW( NULL, szFilePath, MAX_PATH );
PathRemoveFileSpecW( szFilePath );
LPCWSTR pszLib = L"\\SetEnvLib.dll";
wcscat_s( szFilePath, MAX_PATH, pszLib );
int cch = 1 + lstrlenW(szFilePath);
int cb = cch * sizeof(WCHAR);
pszLibFileRemote = (PWSTR) VirtualAllocEx( hProcess,
NULL, cb,
MEM_COMMIT, PAGE_READWRITE);
if ( pszLibFileRemote == NULL)
{
AfxMessageBox( _T("Unable to allocate memory") );
return;
}
if (!WriteProcessMemory(hProcess, pszLibFileRemote,
(PVOID) szFilePath, cb, NULL))
{
AfxMessageBox( _T( "Failed to write" ));
return;
};
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 {
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:
BOOL CDLLInjector::EjectLibW( DWORD dwProcessID )
{
BOOL bOk = FALSE; HANDLE hthSnapshot = NULL;
HANDLE hProcess = NULL, hThread = NULL;
__try
{
hthSnapshot = CreateToolhelp32Snapshot
(TH32CS_SNAPMODULE, dwProcessID );
if (hthSnapshot == INVALID_HANDLE_VALUE) __leave;
MODULEENTRY32W me = { sizeof(me) };
BOOL bFound = FALSE;
BOOL bMoreMods = Module32FirstW(hthSnapshot, &me);
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;
hProcess = OpenProcess(
PROCESS_QUERY_INFORMATION |
PROCESS_CREATE_THREAD |
PROCESS_VM_OPERATION, FALSE, dwProcessID);
if (hProcess == NULL) __leave;
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)
GetProcAddress
(GetModuleHandle(TEXT("Kernel32")), "FreeLibrary");
if (pfnThreadRtn == NULL) __leave;
hThread = CreateRemoteThread(hProcess, NULL, 0,
pfnThreadRtn, me.modBaseAddr, 0, NULL);
if (hThread == NULL) __leave;
WaitForSingleObject(hThread, INFINITE);
bOk = TRUE; }
__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.
- Variable
- Value
- Flag to specify add to existing or not
The following structure will help us to hold the required information:
#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.
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;
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;
}
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.
BOOL UpdateEnvVar()
{
HANDLE hMapFile = 0;
SharedData* pShared = 0;
hMapFile = OpenFileMapping( FILE_MAP_READ,
FALSE, SHAREMEM_NAME );
if (hMapFile == NULL)
{
OutputDebugString(TEXT("Could not open file mapping object"));
return FALSE;
}
pShared = (SharedData*) MapViewOfFile(hMapFile, 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
{
SetEnvironmentVariableW( pShared->strVariable, NULL );
}
}
else
{
const DWORD dwReturn = GetEnvironmentVariable
( pShared->strVariable, 0, 0 );
const DWORD dwErr = GetLastError();
if( 0 == dwReturn &&
ERROR_ENVVAR_NOT_FOUND == dwErr ) {
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?
- Use process explorer
- My Other tool - Read Process Environment Variables
History
- 30-Dec-2008 - Initial version