Monday, August 18 2008, 15:41
Windows Vista and batch files: UAC changes the rules (Part 1 of 2)
Windows Vista… whenever I hear this name, the first I think about is the polemic and only then about the Operating System. Vista has been the target of many criticisms, some justified, some simple matters of personal taste, and some only created to put gasoline on the fire. As such, many IT professionals expressed a great dislike of UAC (User Account Control), one of the main new security features of Vista, especially because they felt it was getting in their ways too often and that they knew their job well enough to not be bugged by it. Even if they are of course able to disable it on the computers they use, they would still have to bear with it on the other computers of their networks or on their customer’s machines: they simply can’t disregard UAC’s existence and have to cope with it and make sure their software and products get along nicely with it.
Personally, I don’t dislike UAC as it is helps reducing the risks of malware infection and of an user doing something dangerous rather than having it all the time running as admin (to be honest, in corporate environment, I even prefer to have users run as real users rather than as pseudo-administrators behind the UAC safeguard, which has been possible way before Vista but that’s another topic…)
For various reasons, I’m still running on Windows XP but have a Windows Vista virtual machine that I fire up whenever I need to check if one of my programs or scripts run properly on it. Today, I wrote a batch file as a test/prototype to include it in a bigger script written in the AutoIT scripting language (good scripting language that I’m going to talk about someday). Batch files may be outdated, lack features (try to do some nested conditions/branching without the script becoming a mess…) but they have the advantage to work relatively the same way on all the NT-based operating systems (or so I thought), do not need any compilation or runtime to install on the machines and provided you do not try to do anything overly complex, they have the most straightforward syntax of any scripting language you may find, all of which makes it great to quickly test ideas involving more externals programs than complex language features.
So my little batch file had several independent executables residing in the same directory as its own’s (very classic approach), which means that they were referenced in the script by base file name, without path. Everything worked fine under XP so I decided to unpack a pristine Vista virtual machine and do the test on this operating system. This script was adding a network adapter using an external executable, which is obviously an action requiring administrative privileges. In Vista, I got into the directory containing my script, right clicked on it and selected the “Run as Administrator” option… and then the problems started…
Several message boxes were complaining that the executables files referenced by my script could not be found. A bit surprised, I looked again and they were all there. Suspecting a Path issue, I then added a “Echo %CD%” at the start of my script, which quickly confirmed the problem.
When running the script in elevated mode, the current directory was changed to C:\Windows\System32 directory (since it is where the cmd.exe command interpreter lies) and as a result, the executables that were residing in the script’s directory could not be found. It is the difference between the current directory and the application’s directory. I knew I wouldn’t have this problem with the AutoIt version as it provides easy ways to retrieve the path name the script resides in, but it is more complex with batch files and I was curious on how to fix the problem.
The surprising thing is that launching the script from the directory by double-clicking on it always worked in previous versions of the operating system, even the 16-bits ones as far as I can remember, so it isn’t really an expected change. While this feature was maybe not documented anywhere, it had become a given among scripters and I wonder how many batch files this change broke (although the problem only impacts those launched by clicking on them from the shell). I decided to check the differences between the ways the script was launched in non-elevated and elevated mode using Regedit and debuggers/monitors.
OllyDBG reveals the API call corresponding to a double click on the batch file from Explorer in non-elevated mode
As we can see, the non-elevated command simply references the script’s file name as an argument (%1), which gets executed by CreateProcess (with the lpCurrentDirectory parameter correctly set to the batch file’s directory), while the command used by the “Run As Administrator” option also includes the full path to the command-line interpreter (%SystemRoot%\System32\cmd.exe). The API function launching the process in this case seems to be CreateProcessAsUser. Unfortunately, in the latter case, I didn’t manage to view the parameters passed to the function as I didn’t manage to hook the function: the API Spy I use needs to run elevated to successfully register its hooks and then start the program I want to inspect, but which would in this case run elevated as well because being a child of the API Spy application, which is itself elevated. Unfortunately, even clicking on “Run as Administrator” ends up calling CreateProcess instead of CreateProcessAsUser in this case (because UAC doesn’t trigger since Explorer is running elevated because the API Spy itself was running elevated… sorry, still following me?). Nevertheless, my theory, even though I couldn’t confirm it is that the problem lies in the lpCurrentDirectory parameter being set to %SystemRoot%\System32 instead than the batch’s file directory.
Now, even if we try modifying the batch’s runas registry entry to work around the issue (which doesn’t work anyway), it would only impact our machine and not the other ones, so, beyond any academic curiosity, what is actually needed is to find a fix to this behaviour that can be implemented at the beginning of the batch file, by running a command changing the current directory from whatever its current value is to the directory of our script. Here is a command sequence that did the trick for me:
CD /D "%~dp0"
REM Insert your code here
pushd pushes the current directory value to the stack (similar to the push assembly instruction). In other words, it backups the current directory’s path (%SystemRoot%\System32\cmd.exe in our case of the elevated script) that we will restore a bit later. It is always nice to leave things as we found them in the first place to avoid further compatibility problems. Besides, other scripts calling this particular script may not expect this behaviour of changing the current directory and may break as the result, so it is better be safe than sorry and clean up after we are done.
CD /D "%~dp0" is going to change the current directory to the directory containing the running batch file. The /D switch is required in case our script resides in a different drive than %systemdrive% (aka C: in most cases). %~dp0 is a rather weird syntax and I have no idea of what it stands for, nor did I find a reference to any other ~dpX commands (%~dp1 doesn’t do anything for example) and even my favorite book on Command-Line scripting has nothing to say about it. As I was saying earlier, batch files have a very straightforward syntax for simple things but an incredibly obscure and strict one when you try to do something a little bit advanced (I’m thinking about this very %~dp0 variable and the for constructs).
Finally, popd restores the current directory that we previously backed up using pushd. Note that popd doesn’t require any argument and will automatically restore the path at the top of its stack.
With this, our batch file behaves the same way on Vista in elevated mode than it does on XP, but the risk to forget to add these 3 lines every time is real so in the next article I will show a little trick to make sure they get added every time automatically so they are never accidentally forgotten and to save the trouble and frustration of adding them manually every time.