Monday, June 6, 2011

Reading Environment Variables from Another Running Process

Recently I was working on some changes aimed to achieve better usability experiences for Eazfuscator.NET.

One of the common pitfalls is the requirement to restart Microsoft Visual Studio after the first Eazfuscator.NET installation. This requirement is commonly addressed by a message box during installation asking user to restart the running instances of Visual Studio. While it is a technically simple solution, it adds one additional screen (a message box) to the user interface and user should react to that message box by pressing OK button. This can worsen the user experience as this stands out of the expected installation workflow.

So I decided to implement another solution. Whenever user tries to protect a project the following screen appears:


As you can see, there is a large amount of space where the desired message can reside. And it computes pretty well to the user workflow, unobtrusively suggesting to restart Visual Studio to complete the installation of Eazfucator.NET.  Of course, there is nothing to complete; the restart of Visual Studio is required just to apply an updated set of environment variables Eazfuscator.NET integration relies on.

The next thing to decide is when to show that message.
And here comes the most interesting part, actually directly reflecting the topic.

The only reliable way to detect the necessity of Visual Studio restart is to analyze its current environment variables. Visual Studio generally runs as devenv.exe process, so we have to retrieve its environment variable values.

The first implementation, I naively thought it had to work out:

    class Program
    {
        static void Main(string[] args)
        {
            var processes = Process.GetProcessesByName("devenv");
            foreach (var process in processes)
            {
                var env = process.StartInfo.EnvironmentVariables;
 
                string path = env["PATH"];
                Console.WriteLine(path);
            }
        }
    }

While it runs without errors, the retrieved value of PATH environment variable is wrong. It corresponds to the current running process, not that remote devenv.exe process it supposed to be. How does this happen? Well, this is a flaw in System.Diagnostics.Process class from .NET Framework. While it seems like it has to work out, actually it does not, giving you the wrong values without raising exceptions.

After investigation, it became evident that Windows has no API to retrieve the environment variables of another running process in the system.

But there is a good workaround which allows to retrieve the desired values with the help of ntdll.dll API and ReadProcessMemory function.

The technical details and sample C++ code can be found here. Unfortunately, I did not find a decent implementation for .NET which would handle both 32 bit and 64 bit scenarios. That's why I created the component for reading the environment variables of a running process from scratch.

So, let me introduce the interface part of the component. It is very simple and everything is implemented inside one class:

    using System.Collections.Specialized;
    using System.Diagnostics;
 
    static class ProcessEnvironment
    {
        public static StringDictionary GetEnvironmentVariables(this Process process)
        {
            // The implementation goes here.
        }
 
        public static StringDictionary TryGetEnvironmentVariables(this Process process)
        {
            // The implementation goes here.
        }
    }

As you can see, ProcessEnvironment is a static class with two extension methods.

And this is how it can be used:

    class Program
    {
        static void Main(string[] args)
        {
            var processes = Process.GetProcessesByName("devenv");
            foreach (var process in processes)
            {
                var env = process.GetEnvironmentVariables();
 
                string path = env["PATH"];
                Console.WriteLine(path);
            }
        }
    }

Neatly done, isn't it?

The full sources of the component and sample usage can be found in the sample project.

Download sample project

13 comments:

  1. Hello Oleksiy,

    Congratulations on a job well done.

    Thank you very much.

    Chikelue

    ReplyDelete
  2. Excellent.. Well done.

    Do you have a twitter account ?

    I for one would be interested in following, reading your comments, progress etc.

    ReplyDelete
  3. Thank you, CastleSoft! I have no Twitter account. This blog is the only source for now.

    ReplyDelete
  4. Hi Oleksiy, thank you for your contribution. Do you think that there are significative differences with other ofuscator that need to be paid to use? I would check you software and if I use it, I will donate. Thank you.

    Jorge

    ReplyDelete
  5. Hi Oleksiy,

    Do you think your obfuscator prevents 95% of attacks almost? Best regards,

    Jorge

    ReplyDelete
  6. Oleksiy, thanks for the obfuscator!
    Before I always saw the original project file path inside the compiled .exe file. I could change it by hand, but with your obfuscator it's much more smooth ;-)

    ReplyDelete
  7. It is good to see Eazfuscator still in development!

    Please, give us a new stable version soon!

    Many thanks,
    Thiago

    ReplyDelete
  8. Hi,
    I previously used another implementation (gone through the pain of defining the PEB for both x86 and x64) and had problems to read the environment of 32-bit processes from a 64-bit program. I was really puzzled Turned out I used the wrong parameter in NtQueryInformationProcess :-)
    So I'm really thankful for helping me spot the obvious.

    However, I found an issue with your implementation. When getting the contents of the environment using ReadProcessMemory, it is required to specify the exact length of the buffer, and here this is not the case.
    The part preceding the environment needs to be deducted from the regionsize, otherwise the call will fail. (Note that this doesn't occur on every occasion though).

    so:

    instead of
    if (!HasReadAccess(hProcess, penv, out dataSize))
    throw new Exception("Unable to read environment block.");

    MEMORY_BASIC_INFORMATION mbi = new MEMORY_BASIC_INFORMATION();
    if (!GetProcessMemoryInfo(hProcess, pEnvironment, out dataSize ,out mbi ))
    throw new Exception("Unable to read environment block.");

    -> EnvDataLength = (uint)Decrement(mbi.RegionSize /*(dataSize)*/, Decrement(pEnvironment, mbi.BaseAddress)).ToInt32();

    const int MaxEnvSize = 32767;
    if (EnvDataLength > MaxEnvSize)
    EnvDataLength = MaxEnvSize

    ReplyDelete
    Replies
    1. Thank you for pointing this out. The project was correspondingly updated.

      Delete
  9. Brilliant ! You saved my day ;)

    ReplyDelete
  10. Grrrippp : you're right, I had the same issue as you.

    To correct, simply replace in the function HasReadAccess() the line :

    size = memInfo.RegionSize.ToInt32();

    by this one :

    size = memInfo.RegionSize.ToInt32() - Convert.ToInt32((address.ToInt64() - memInfo.BaseAddress.ToInt64()));

    Hope it will help,

    Cheers

    Olivier

    ReplyDelete
    Replies
    1. Thank you for pointing this out. The project was correspondingly updated.

      Delete