
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