Windows Environment Variable Expansion: Admin vs Non-Admin - c#

I have a C# application that when run sets some environment variables.
These environment variables need to be set system wide.
I use this code to actually do the set.
public static void SetEnvironmentVariable(string _keyName, string _value, RegistryValueKind _type)
{
using (RegistryKey reg = Registry.LocalMachine.OpenSubKey(#"SYSTEM\CurrentControlSet\Control\Session Manager\Environment", true))
{
if (reg != null)
{
reg.SetValue(_keyName, _value, _type);
}
else
{
string x =
string.Format(
"Could find registry key that hosts Environment Variables: SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment");
App.AppLogger.Error(x);
throw new Exception(x);
}
}
}
This code works, but I notice some strange behavior.
Immediately after running my app and this code, I run cmd as a regular user and take a look a the environment using the "set" command.
It shows no changes.
I then run a cmd prompt as admin and run set. It shows the changes. Not only that, it shows fully expanded variables. Where ALLFOO=Foo, and PATH=C:\Windows\System32;%ALLFOO%;, the set command shows PATH=C:\Windows\System32;Foo;.
I then log off and back on. I then run cmd as a normal user.
I type set and it shows the new environment variables, but not expanded.
It shows PATH=C:\Windows\System32;%ALLFOO%; ( It seems to have no trouble expanding %SYSTEMROOT% for some reason.)
I get that the log-off and back on causes the new Explorer.exe that's launched to get the new env vars for the regular user's cmd.exe, but I don't understand why they are not expanded.
Why would running cmd and set as administrator show fully expanded environment variables and running cmd and set as a normal user would not?
When setting the env vars in the C# program i use the RegistryValueKind.ExpandString enumeration.
Edit:
I am aware of the order of declaration time to expansion time issue with system variables and have edited the question example to reflect this.

Changes to the environment variables are not inmediatly visible. You need to notify the running processes of changes to the registry.
And, you should remember that the environment you see in a process (your cmd windows) is not readed from the registry, it is inherited from its parent process. You can send the notification, but some processes will handle it and some not.
The rest of the problems should be explained by the environment load order:
Some values are retrieved from the registry before processing the environment itself. %systemroot% is defined in
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRoot
System environment is loaded
REG_SZ variables, in alphabetic order, are readed first
REG_EXPAND_SZ, in alphabetic order, are readed. As the REG_SZ values have been readed, they can be expanded. If a REG_EXPAND_SZ variable makes reference to another REG_EXPAND_SZ variable that still has not been expanded (alphabetic order), this reference can not be resolved and is not expanded.
When the user logs in, the user environment is processed.
Same REG_SZ, then REG_EXPAND_SZ variable are processed.
Variables defined at user level overwrite the values of the system environment, except for some variables (ex. PATH) where the values are concatenated.
All that means that
Different users can and probably will see different environments
Same user can see different environments depending on what the parent process is. It is not the same to use Win+R and execute cmd than to use Shift+right click in a folder and select "Open command here". Parent processes are different.

Related

C# Winform Registry set and get functions appear to work, but don't actually change the registry

I'm trying to use this registry hack I found online:
;Disables F1 key - Help and Support - in Windows 10
;Ramesh Srinivasan, Winhelponline.com
[HKEY_CURRENT_USER\SOFTWARE\Classes\Typelib\{8cec5860-07a1-11d9-b15e-000d56bfe6ee}\1.0\0\win32]
#=""
[HKEY_CURRENT_USER\SOFTWARE\Classes\Typelib\{8cec5860-07a1-11d9-b15e-000d56bfe6ee}\1.0\0\win64]
#=""
When I run it as a .reg command via Windows Explorer and watch the registry with regedit, it works as intended. Removing it is another registry file that simply removes the \0 subkey (and win32 and 64 with it). I'm trying to emulate this function with C# in a Winform using .net CORE:
private void CheckF1()
{
// Registry data from ;Ramesh Srinivasan, Winhelponline.com
RegistryKey F1key = Registry.CurrentUser.OpenSubKey(#"SOFTWARE\Classes\TypeLib\{8cec5860-07a1-11d9-b15e-000d56bfe6ee}\1.0\0");
// EGADS! It's active!
if (F1key == null)
{
fckF1RestoreBtn.Enabled = false;
fckF1KillBtn.Enabled = true;
fckF1Status.Text = "That creepy bugger is waiting and watching.";
}
else
{
fckF1RestoreBtn.Enabled = true;
fckF1KillBtn.Enabled = false;
fckF1Status.Text = "The F1-Help function had been put in it's place.";
}
}
private void fckF1KillBtn_Click(object sender, EventArgs e)
{
Registry.CurrentUser.CreateSubKey(#"SOFTWARE\Classes\Typelib\{8cec5860-07a1-11d9-b15e-000d56bfe6ee}\1.0\0\win32");
Registry.CurrentUser.CreateSubKey(#"SOFTWARE\Classes\Typelib\{8cec5860-07a1-11d9-b15e-000d56bfe6ee}\1.0\0\win64");
CheckF1();
}
private void fckF1RestoreBtn_Click(object sender, EventArgs e)
{
Registry.CurrentUser.DeleteSubKeyTree(#"SOFTWARE\Classes\Typelib\{8cec5860-07a1-11d9-b15e-000d56bfe6ee}\1.0\0");
CheckF1();
}
Weirdly the code "sees" a setting and responds like it should. Even to the point that whichever toggle position it was in on close it remembers when I load the file again. It's almost like it's playing along to screw with me. Regardless, when I watch the registry, none of my code has any actual effect though by all appearances it seems to work otherwise (it doesn't actually of course because the registry change isn't happening).
NOTE: I have already updated my manifest file for the project to include elevated permissions:
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
Here's the breakpoint on the test statement to see if the subkeys are there that shows they are:
Meanwhile the registry location for the exact path shown in the debug doesn't have the 1.0\0 path at all:
I don't know how the code is reading phantom values. Someone closed my previous question pointing to another answer that didn't have any effect (Registry key deleted but still getting value from registry c#):
"Prefer 32 bit" was never checked for my project in the first place
Modifying my code as recommended had no effect
var key = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64)
.OpenSubKey(#"SOFTWARE\WOW6432Node\Classes\TypeLib\{8cec5860-07a1-11d9-b15e-000d56bfe6ee}\1.0\0");
Adding "Wow64Node" to the path makes no difference in execution.
EXPECTED BEHAVIOR
Checking to see if the HKEY_CURRENT_USER\SOFTWARE\Classes\Typelib{8cec5860-07a1-11d9-b15e-000d56bfe6ee}\1.0\0 path is present should return null if the \0 path is not present
Adding and removing the subkeys should show in the registry
So apparently HKEY_CURRENT_USERS is an alias. When the above code runs, it updates in HKEY_USERS under the specific logged in user. There's a question that talks about this behavior here: write registry to hkey_current_user instead of hkey_users
That said, the code appears to work, it's just that the registry doesn't update HKCU like when you run .reg commands. To verify it was working, I'd run the toggle that kills the keys then click them in Regedit and it would say they didn't exist. When I toggled back, I could click on them. So basically, it works (not sure if it required the "using" blocks as others suggested, but I see no reason to take them out).
Now my problem is that it points to the admin user and NOT the regular user because I'm running it and regedit as admin. It took forever to determine this based on running regedit as user in one case and admin in another. Bottom line, this won't work and I'll probably end up running .reg files in the command line instead.

C# - Load existing system environment variables when the current process don't have them loaded

On Windows, I have a C# assembly that is COM visible. It references other assemblies to control an application in the machine. It works fine.
However, under Apache Web Server and using CGI, it doesn't work. After doing some debuging, I found out that the problem is that, while running under Apache's CGI, the environment variables SYSTEMROOT and SYSTEMDRIVE, which aparently are needed by the referenced assemblies, are not loaded.
I can configure Apache to pass those environemtn variables too, but before doing so, I'd really like to know if there's some command I can put on my C# COM visible assembly to make it load environment variables as if it was, let's say, the SYSTEM user or something like that, so it doesn't have to relay on the environment passed by the starting application.
How do you force loading an existent system environment variable in C#, when IT IS NOT SET in the current process (or it was process-deleted by the launching process)?
Thanks in advance for any suggestions!
EDIT 1 - ADDED INFO: Just to make it more clear (as I see in the current answers it's not so clear): Apache intendedly deletes a lot of environment variables for CGI processes. It's not that Apache cannot see them, it can, but it won't pass them to CGI processes.
This should do the trick:
Environment.GetEnvironmentVariable("variable", EnvironmentVariableTarget.Machine);
I did a small test and it is working:
//has the value
string a = Environment.GetEnvironmentVariable("TMP");
Environment.SetEnvironmentVariable("TMP", null);
//does not have has the value
a = Environment.GetEnvironmentVariable("TMP");
//has the value
a = Environment.GetEnvironmentVariable("TMP", EnvironmentVariableTarget.Machine);
SOLUTION: Marco's answer was great and technically answered my question - except that I found out that the environment variables SYSTEMROOT and SYSTEMDRIVE are not really set in the registry where all environment variables are set, so, the chosen answer works for all variables except those two, which I specified in the OP.
SYSTEMROOT is defined on the registry in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRoot, and apparently (after more research), SYSTEMDRIVE is generated as a substring of SYSTEMDRIVE.
So, to get SYSTEMDRIVE and SYSTEMROOT from registry and load them into the environment:
using Microsoft.Win32;
namespace MySpace
{
public class Setup
{
public Setup()
{
SetUpEnvironment();
}
private void SetUpEnvironment()
{
string test_a = Environment.GetEnvironmentVariable("SYSTEMDRIVE", EnvironmentVariableTarget.Process);
string test_b = Environment.GetEnvironmentVariable("SYSTEMROOT", EnvironmentVariableTarget.Process);
if (test_a == null || test_a.Length == 0 || test_b == null || test_b.Length == 0)
{
string RegistryPath = "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
string SYSTEMROOT = (string) Registry.GetValue(RegistryPath, "SystemRoot", null);
if (SYSTEMROOT == null)
{
throw new System.ApplicationException("Cannot access registry key " + RegistryPath);
}
string SYSTEMDRIVE = SYSTEMROOT.Substring(0, SYSTEMROOT.IndexOf(':') + 1);
Environment.SetEnvironmentVariable("SYSTEMROOT", SYSTEMROOT, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("SYSTEMDRIVE", SYSTEMDRIVE, EnvironmentVariableTarget.Process);
}
}
}
}
Then you can just call Setup setup = new Setup(); from other classes. And that's it. :-)
Environment.GetEnvironmentVariable
see reference here.
e.g.
Environment.CurrentDirectory = Environment.GetEnvironmentVariable("windir");
DirectoryInfo info = new DirectoryInfo(".");
lock(info)
{
Console.WriteLine("Directory Info: "+info.FullName);
}
Are the variables set as system wide?
If they are not, that is what you need to do, otherwise create user variables for the user the COM is running under.
Thank you. I cannot state with any certainty that this has once and for all driven a stake through the heart of the vampire, but amazingly enough, the error has disappeared (for now). The odd thing is that access to the statement
Environment.GetEnvironmentVariable("variable", EnvironmentVariableTarget.Machine);
is a real oddity in the debugger. It does not show up in Intellisense and does not even appear to fire, which leads me to suspect, which you all knew already, that this is some sort of magic runtime object Environment that has no instantiation in the debugger but also can be benignly jumped over. Oh well.
Oh and I should mention that after you see that error, you will note oddities in your Windows OS, which is worrisome. In particular, you will see, if you try to use the Control Panel /System/Advanced Properties (whatever) that it cannot load the dialog for the environment variables any more, indicating that %windir% has been seriously hosed (compromised) across all applications. Bad bad bad....

gettext(i18n) in process running as root/system process

It's not possible to setlocale of a process running as root, how can I use gettext to obtain localized strings (the locale is not auto detected, nor can I force it to a particular value using setlocale) in such a process. Is there any workaround?
Edit -> Adding the code, I used for testing the issue. I force changed the environment variables - LC_ALL, LANGUAGE, LANG to fr_FR.UTF8. I set the locale to fr_FR.UTF8 explicitly too. All the putenv calls returned 0 and the setlocale call returned "C".
int err = putenv("LC_ALL=fr_FR.UTF8");
err = putenv("LANG=fr_FR.UTF8");
err = putenv("LANGUAGE=fr_FR.UTF8");
char *loc = setlocale(LC_ALL, "fr_FR.UTF8");
bindtextdomain("default", "locale");
textdomain("default");
char *text = gettext("hello");
The same code snippet works if I try it in a user process. The exe of both the processes are in same directory, which contains directory locale\fr\LC_MESSAGES\default.mo
Can we change the locale of a process which is running as system process and not user?

How to get the (.lnk) shortcut filepath in a program which started by the shortcut?

I have a c# program which open *.postfix file.
If a user runs a (.lnk)shortcut which points to my type of file, my program will open the target.
So, how could my program know it is started by a (.lnk)shortcut (and get it's file path)?
In some circumstances,i need to replace the .lnk file.
Thanks!
Edited
First, thanks to guys who answered my question.
By following #Anders answer, i find out my problem lays here.
I made some changes to windows registry, so browser knows to throw customized protocol string to certain program.
some thing like this..
[InternetShortcut]
URL=myProtocol://abcdefg.....
That's maybe why i lost lpTitle. :(
I'm going to try this way:
Whenever my program invoked, of course fed with %1, program checks current opened explorer(Window), and try to get it's current path with IWebBrowserApp. With that path and desktop of course, scan and analyze *.lnk to determine which one to replace.
I think this will probably work, but not be sure. I will try.
continued
In native code you can call GetStartupInfo, if the STARTF_TITLEISLINKNAME bit is set in STARTUPINFO.dwFlags then the path to the .lnk is in STARTUPINFO.lpTitle. I don't know if there is a .NET way to get this info, you probably have to P/Invoke...
You don't. There's no way to do it. End of story.
So this has been brought to my attention due to a recent downvote. There's an accepted answer showing an idea that gets the path to the launching shortcut most of the time. However my answer is to the whole. OP wants the link to the shortcut so he can change it. That is what can't be done most of the time.
Most likely case is the shortcut file exists in the start menu but is unwritable. However other cases involve the shortcut coming from another launching application that didn't even read it from a disk but from a database (I've seen a lot of corporate level restricted application launch tools). I also have a program that launches programs from shortcuts not via IShellLink but by parsing the .lnk file (because it must not start COM for reasons) and launching the program contained. It doesn't pass STARTF_TITLEISLINKNAME because it's passing an actual title.
If you're using Visual Studio Setup Project to build an installer and do the file type association, you should follow these instructions http://www.dreamincode.net/forums/topic/58005-file-associations-in-visual-studio/
Open up your solution in Visual studio.
Add a Setup Project to your solution by file , add project,New project, Setup & Deployment projects,Setup project
Right-click on your setup project in the "Solution Explorer" window,Select view,then select file types.
you'll see the "file types" window displayed in Visual studio.At the top of the window will be "File types on target machine"
Right-click on "File types on target machine".the menu will pop up with Add "file type" Click on this.
you'll see "New document Type#1" added,and "&open"underneath it.
The "new document type#1" can be anything you want - change it to something descriptive.although the user never sees this,never use something common- be as unique as possible,Because you can overlay current file associations without even realizing it.For example,you might think"pngfile" might be a useful name- but using that will now send all"*.png" files to your application,instead of to an image viewer.A good practice maybe "YourCompantName.Filetype",where your company name is your name of your company's name, and "Filetype" is a descriptive text of your file.
In the "properties" window for your new type,you will need to change a few properties.:
Command:Change to the application that you want to run.If you click on the "..." and you will proberly want to locate and use the "primary Output..." File
Description: This is the description of the file type(if it doesn't describe it's self"
Extensions:This your list of extensions for you chosen Program.Separate each one with a ","
Icon:This will associate the icon with your file type,This shows up in the window explorer.
Now we move to that "&open ".This is an action that is available if your right-click on the file.The default action("&Open" is currently set as the default) is what happens when you double click on the file.Right click on your "New document type#1" to add actions,but for the moment,lets define our "&open" action
Click on "&Open".You will see in the properties window "Name","Arguments","Verbs". Verb is hidden from the user,but is the key that is stored in the registry.Leave it same as the name,But without the "&".The default for"Arguments" is "%1",Which means to pass the full path and filename to your application.You can add other stuff here as well,if you need to pass flags to your application to do special stuff.All this infomaton is getting passed to your application on the command line,so you'll need to be familiar with the "Environment.CommandLine" object.
If you need to set a different action as your default,just right click on the action and "set as default"
Basically, you'll pass the file path as an argument to your program. Then if it's a console application or Windows Forms , you should check the arguments in Program.Main
static void Main(string[] args)
{
//if file association done with Arguments %1 as per forum post above
//you file path should be in args[0]
string filePath = null;
if(args != null && args.Length > 0)
filePath = args[0];
}
For a WPF application you'll need to handle that in the StartUp event for your Application
void App_Startup(object sender, StartupEventArgs e)
{
string filePath = null;
if ((e.Args != null) && (e.Args.Length > 0))
{
filePath = e.Args[0];
}
}

Add new environment variable to a process launched by ProcessStartInfo.LoadUserProfile

I'm logged on my pc with the user 'podosta'
This is working fine
ProcessStartInfo p = new ProcessStartInfo("c:\myapp.exe");
p.UserName = "myuser";
p.Domain = "mydomain";
p.Password = SecureString;
p.UseShellExecute = false;
p.LoadUserProfile = true;
Process.Start(p);
Environment variables of the process are the environment variable of the user 'myuser'
ex: %USERPROFILE% points to the profile of 'myuser'
This is not working
ProcessStartInfo p = new ProcessStartInfo("c:\myapp.exe");
p.UserName = "myuser";
p.Domain = "mydomain";
p.Password = SecureString;
p.UseShellExecute = false;
p.LoadUserProfile = true;
p.EnvironmentVariables.Add("MY_NEW_VARIABLE", "SOME_TEXT");
Process.Start(p);
Environment variables of the process are the environment variable of me 'podosta'
ex: %USERPROFILE% points to my profile 'podosta'
By the way, %MY_NEW_VARIABLE% is created
Why this behavior ?
I need to start an application with a RunAs, having the environment of the runned user and add some additional environment variable to the process.
Thanks
The behaviour you're seeing is basically document on MSDN for the CreateProcessWithLogon Win32 function which works in the background when starting a process for a different user (using System.Diagnostics.Process):
A pointer to an environment block for the new process. If this
parameter is NULL, the new process uses an environment created from
the profile of the user specified by lpUsername.
So as soon as you specify a single one environment variable for the new process, you need to specify them all.
The problem now is the ProcessStartInfo.EnvironmentVariables property. As soon as you access it the first time (for a given instance of ProcessStartInfo), it will be automatically populated with the complete environment for the current user (ref).
Take this line, which runs in the context of user "podosta":
p.EnvironmentVariables.Add("MY_NEW_VARIABLE", "SOME_TEXT");
It does two things:
fill the p.EnvironmentVariables with the environment of "podosta" (including "USERPROFILE=...podosta").
Add the new variable "MY_NEW_VARIABLE=SOME_TEXT" to that.
This combined set is then passed to the process launched as "myuser" and thus it gets the "wrong" (better: unexpected) value for "USERPROFILE".
If you want to make sure that you pass a NULL environment block to the underlying Win32 function, to get the behavior described in the quote above, you cannot ever "touch" the EnvironmentVariables property - not even iterate and dump it for logging purposes.
It is also not sufficient to do EnvironmentVariables.Clear(). That will not set the internal field to null and the CreateProcessWithLogon function is just passed an empty environment block, but not a null one.
If you fail to do so, you get all sorts of funny behaviours, for example the then-executing process will have a value for USERPROFILE that is that of the user that started the process, etc.
You might get around that stuff by calling CreateEnvironmentBlock via p/invoke, add your environment variables as needed, then calling CreateProcessAsUser (also via p/invoke), passing that modified environment. That is all not an easy undertaking, especially when you want all the features of the Process-class like output redirection, etc.
It is kind of unfortunate that the ProcessStartInfo.EnvironmentVariables property works as it does.

Categories

Resources