9

In Windows, is there a way to check for the existence of an environment variable for another process? Just need to check existence, not necessarily get value.

I need to do this from code.

Sean Bright
  • 118,630
  • 17
  • 138
  • 146
jalex
  • 93
  • 1
  • 3

3 Answers3

11

If you know the virtual address at which the environment is stored, you can use OpenProcess and ReadProcessMemory to read the environment out of the other process. However, to find the virtual address, you'll need to poke around in the Thread Information Block of one of the process' threads.

To get that, you'll need to call GetThreadContext() after calling SuspendThread(). But in order to call those, you need a thread handle, which you can get by calling CreateToolhelp32Snapshot with the TH32CS_SNAPTHREAD flag to create a snapshot of the process, Thread32First to get the thread ID of the first thread in the process, and OpenThread to get a handle to the thread.

Adam Rosenfield
  • 390,455
  • 97
  • 512
  • 589
  • +1 great answer. You'll still need to dig around a bit to find a definition of the thread context structure. The NT DDK and Wine are good places for that kind of thing. Looks like maybe it's the 8th pointer from the beginning of the structuree? – Marsh Ray Jul 30 '09 at 00:30
8

Here is a working example which the printed output can be used to check existence as well as read the value, (build it as the same architecture as the executable's process identifier you must target):

getenv.cpp

#include <string>
#include <vector>
#include <cwchar>

#include <windows.h>
#include <winternl.h>

using std::string;
using std::wstring;
using std::vector;
using std::size_t;

// define process_t type
typedef DWORD process_t;

// #define instead of typedef to override
#define RTL_DRIVE_LETTER_CURDIR struct {\
  WORD Flags;\
  WORD Length;\
  ULONG TimeStamp;\
  STRING DosPath;\
}\

// #define instead of typedef to override
#define RTL_USER_PROCESS_PARAMETERS struct {\
  ULONG MaximumLength;\
  ULONG Length;\
  ULONG Flags;\
  ULONG DebugFlags;\
  PVOID ConsoleHandle;\
  ULONG ConsoleFlags;\
  PVOID StdInputHandle;\
  PVOID StdOutputHandle;\
  PVOID StdErrorHandle;\
  UNICODE_STRING CurrentDirectoryPath;\
  PVOID CurrentDirectoryHandle;\
  UNICODE_STRING DllPath;\
  UNICODE_STRING ImagePathName;\
  UNICODE_STRING CommandLine;\
  PVOID Environment;\
  ULONG StartingPositionLeft;\
  ULONG StartingPositionTop;\
  ULONG Width;\
  ULONG Height;\
  ULONG CharWidth;\
  ULONG CharHeight;\
  ULONG ConsoleTextAttributes;\
  ULONG WindowFlags;\
  ULONG ShowWindowFlags;\
  UNICODE_STRING WindowTitle;\
  UNICODE_STRING DesktopName;\
  UNICODE_STRING ShellInfo;\
  UNICODE_STRING RuntimeData;\
  RTL_DRIVE_LETTER_CURDIR DLCurrentDirectory[32];\
  ULONG EnvironmentSize;\
}\

// shortens a wide string to a narrow string
static inline string shorten(wstring wstr) {
  int nbytes = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int)wstr.length(), NULL, 0, NULL, NULL);
  vector<char> buf(nbytes);
  return string { buf.data(), (size_t)WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int)wstr.length(), buf.data(), nbytes, NULL, NULL) };
}

// replace all occurrences of substring found in string with specified new string
static inline string string_replace_all(string str, string substr, string nstr) {
  size_t pos = 0;
  while ((pos = str.find(substr, pos)) != string::npos) {
    str.replace(pos, substr.length(), nstr);
    pos += nstr.length();
  }
  return str;
}

// func that splits string by first occurrence of equals sign
vector<string> string_split_by_first_equalssign(string str) {
  size_t pos = 0;
  vector<string> vec;
  if ((pos = str.find_first_of("=")) != string::npos) {
    vec.push_back(str.substr(0, pos));
    vec.push_back(str.substr(pos + 1));
  }
  return vec;
}

// checks whether process handle is 32-bit or not
static inline bool IsX86Process(HANDLE process) {
  BOOL isWow = true;
  SYSTEM_INFO systemInfo = { 0 };
  GetNativeSystemInfo(&systemInfo);
  if (systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_INTEL)
    return isWow;
  IsWow64Process(process, &isWow);
  return isWow;
}

// helper to open processes based on pid with full debug privileges
static inline HANDLE OpenProcessWithDebugPrivilege(process_t pid) {
  HANDLE hToken;
  LUID luid;
  TOKEN_PRIVILEGES tkp;
  OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
  LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);
  tkp.PrivilegeCount = 1;
  tkp.Privileges[0].Luid = luid;
  tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
  AdjustTokenPrivileges(hToken, false, &tkp, sizeof(tkp), NULL, NULL);
  CloseHandle(hToken);
  return OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);
}

// get wide character string of pids environ based on handle
static inline wchar_t *GetEnvironmentStringsW(HANDLE proc) {
  PEB peb;
  SIZE_T nRead;
  ULONG res_len = 0;
  PROCESS_BASIC_INFORMATION pbi;
  RTL_USER_PROCESS_PARAMETERS upp;
  HMODULE p_ntdll = GetModuleHandleW(L"ntdll.dll");
  typedef NTSTATUS (__stdcall *tfn_qip)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG);
  tfn_qip pfn_qip = tfn_qip(GetProcAddress(p_ntdll, "NtQueryInformationProcess"));
  NTSTATUS status = pfn_qip(proc, ProcessBasicInformation, &pbi, sizeof(pbi), &res_len);
  if (status) { return NULL; } 
  ReadProcessMemory(proc, pbi.PebBaseAddress, &peb, sizeof(peb), &nRead);
  if (!nRead) { return NULL; }
  ReadProcessMemory(proc, peb.ProcessParameters, &upp, sizeof(upp), &nRead);
  if (!nRead) { return NULL; }
  PVOID buffer = upp.Environment;
  ULONG length = upp.EnvironmentSize;
  wchar_t *res = new wchar_t[length / 2 + 1];
  ReadProcessMemory(proc, buffer, res, length, &nRead);
  if (!nRead) { return NULL; }
  res[length / 2] = 0;
  return res;
}

// get env of pid as a narrow string
string env_from_pid(process_t pid) {
  string envs;
  HANDLE proc = OpenProcessWithDebugPrivilege(pid);
  wchar_t *wenvs = NULL;
  if (IsX86Process(GetCurrentProcess())) {
    if (IsX86Process(proc)) {
      wenvs = GetEnvironmentStringsW(proc);
    }
  } else {
    if (!IsX86Process(proc)) {
      wenvs = GetEnvironmentStringsW(proc);
    }
  }
  string arg;
  if (wenvs == NULL) { 
    return ""; 
  } else {
    arg = shorten(wenvs);
  }
  size_t i = 0;
  do {
    size_t j = 0;
    vector<string> envVec = string_split_by_first_equalssign(arg);
    for (const string &env : envVec) {
      if (j == 0) { 
        if (env.find_first_of("%<>^&|:") != string::npos) { continue; }
        if (env.empty()) { continue; }
        envs += env; 
      } else { envs += "=\"" + string_replace_all(env, "\"", "\\\"") + "\"\n"; }
      j++;
    }
    i += wcslen(wenvs + i) + 1;
    arg = shorten(wenvs + i);
  } while (wenvs[i] != L'\0');
  if (envs.back() == '\n') { envs.pop_back(); }
  if (wenvs != NULL) { delete[] wenvs; } 
  CloseHandle(proc);
  return envs;
}

// test function (can be omitted)
int main(int argc, char **argv) {
  if (argc == 2) {
    printf("%s", env_from_pid(stoul(string(argv[1]), nullptr, 10)).c_str());
    printf("%s", "\r\n");
  } else {
    printf("%s", env_from_pid(GetCurrentProcessId()).c_str());
    printf("%s", "\r\n");
  }
  return 0;
}

buildx86.sh

g++ getenv.cpp -o getenv.exe -std=c++17 -static-libgcc -static-libstdc++ -static -m32

buildx64.sh

g++ getenv.cpp -o getenv.exe -std=c++17 -static-libgcc -static-libstdc++ -static -m64

Quotes are added around the printed value for clarity, and escaping is applied to inner quotes.

  • 1
    This was an extremely helpful answer. Do you have any documentation for your special characters ("%<>^&|:") possibly found in the first env string? – Daniel Widdis Jun 28 '21 at 00:52
  • 1
    Rather than requesting `PROCESS_ALL_ACCESS` you can just get the access you need: `PROCESS_QUERY_INFORMATION | PROCESS_VM_READ`. – Daniel Widdis Jun 28 '21 at 04:25
  • @DanielWiddis thanks for the helpful info, I replaced it with the proper flags you requested. I do not know anything about the first environment string with the odd characters unfortunately, however I am able to reproduce something similar which I believe is what you are talking about. –  Jun 30 '21 at 14:38
  • 1
    I see `=::=::\ ` as the first "environment" on both Win7 and Win10. So the code works, I'm just baffled as to why such an entry exists and if it is consistent and if the characters have any meaning and if it's overkill to look for all special characters vs. just that one String. In any case, thanks again, I've seen almost all the bits of the puzzle in many other answers but your code tied it all together. – Daniel Widdis Jul 01 '21 at 05:05
  • @DanielWiddis It's a variable named `=::` with value ``::\``, the rules is to find the first equal sign from the second char to the end as the delimiter, and the variable name can contain `=` only at the first place. – Tuff Contender Feb 12 '22 at 12:25
  • It seem directly `OpenProcess` works, is `AdjustTokenPrivileges` needed? – Tuff Contender Feb 12 '22 at 12:44
  • @TuffContender Right, I knew the variable/value part. My question is "what does it mean" and is it always consistent like that. Also `AdjustTokenPrivileges` is necessary for processes you don't own but works fine on your own processes without that elevation. – Daniel Widdis Feb 12 '22 at 16:42
  • @DanielWiddis ``=::=::\`` seems set by explorer.exe, each time user types some letter in the address bar, after suggestions given. – Tuff Contender Feb 15 '22 at 19:27
1

With a utility:

You can use Process Explorer.

Right click on the process, go to Properties... and there is an Environment tab which lists the environment variables for that process.

With code:

There doesn't appear to be a Win32 API call to do this directly, but apparently you get fiddle with the results of GetProcessStrings to get access to this information. This CodeProject article has some code to get you started.

Sean Bright
  • 118,630
  • 17
  • 138
  • 146