Perl and Windows UAC
Windows security is an attempt to balance two needs - on the one hand, Windows is a user-centric operating system that is intended to be operated by your grandmother without the need for dedicated support staff, while on the other, there are roughly as many Windows machines connected to the Internet as there are stars in the Orion Arm, and if they don't have some kind of security, the botnets on them will bootstrap to sapience and we will all be dead.

The result, for now, is UAC. UAC (User Account Control) is Microsoft's way of letting Windows machines run in user mode and still permit "dangerous" things like installers to enter Administrator mode. When they do this, the entire screen goes dark and a prominent dialog asks the user if they really want the program to change their system. The key is that the user can't miss this behavior. (Here's an interesting article exploring one quasi-vulnerability malware authors use to social-engineer their way around UAC - but note that there is no way to make admin changes to the system with UAC enabled, without that popup. I call that a pretty effective engineering solution.)

So what requires a program to run as Administrator? To start, any write access to the Registry. And there's just one little problem with that when you're using Perl: Win32::TieRegistry (best practice for Registry access) requests write access by default. So when I was building my little utility script for TRADOS Registry entries, working out just how Win32::TieRegistry was supposed to work took a disproportionate amount of time (leading, in fact, to this very article). When you're just wanting to read the Registry, pro tip: just request read access.

But let's assume you do want to modify the Registry. To run a program as Administrator, you have to request a UAC pop up. And the recommended way to do that is to make a manifest (which is a simple XML file) and embed it into the program's executable, or at least place it in the directory next to that executable. Well, but wait a minute. The Perl executable is not your program, is it? And the vast majority of Perl programs don't need admin mode, obviously, so popping up UAC for every Perl call would be ridiculous - not to mention that calling this from the command mode starts the program in a separate console and process anyway.

All in all, a mess.

But there are a couple of ways to do this for scripting languages. One is to create a shortcut to call the script for you, then click "Advanced" and check the "Run as administrator" button. You can even run that from the command line with "start my_shortcut.link". Your script will run in a separate console, but it will pop up a UAC box and work.

It's easy enough to create a shortcut from Perl using Win32::Shortcut, but that only wraps the IShellLink interface, which doesn't expose that admin flag (because it was designed before UAC was invented). To gain access to that flag, you need the IShellLinkDataList interface (but of course), and set the SDLF_RUN_AS_USER flag (but of course). And of course there's the whole sticky wicket of having to put another file next to your script, which is just messy and prone to error

I wanted a technique I could automate quickly and easily, so that wasn't it. Some more poking turned up the Elevate Command Power Toy. There's a download link at the top of that article that installs a bunch of little apps for running different things with elevated privileges - the ones you want are "elevated.cmd" and "elevated.vbs". Toss those into a directory in your path, and you can now just say:

c:\projects\trados_languages> elevate perl trados_languages.pl

This was already much better! It can be used for anything at all, so if I want to edit a system file or something, I can just call it on my editor. Very flexible!

Looking a little closer, though, it turns out that the key to that power toy is a single call: the Shell.Application interface exposes ShellExecute, which can be called with the application (perl), a string containing arguments (the script and any arguments it wants), the local directory (which can be left blank to use the current directory), and a verb.

It's not documented (thanks, Microsoft), but that verb can be "runas" to pop up the UAC dialog before running the program identified. Essentially, this is what the shortcut does for you.

At any rate, I've wrapped up this solution into a convenient module called Win32::RunAsAdmin, called like this:

use Win32::RunAsAdmin qw(force);

... everything else ...

Seriously. Just put that at the top and your script will pop up a UAC box and run in a separate process as Administrator. Here's what it does:

  • Call Win32::RunAsAdmin::check to see if we're in admin mode already. If so, we're done.
  • Otherwise, determine the Perl executable, calling script, and arguments.
  • Call ShellExecute to restart the calling script as Administrator.

It's still not perfect. The elevated privilege is attached to the Perl executable, not the script, and so a separate console is started, which makes it incompatible with shell-style usage. But for simple utility scripts, it's a convenient way to bootstrap into Administrator.

(For the record, I believe the only way to run an elevated script as a shell-style script would be to open an elevated console to start with, then just call the script.)

For more details about the API this module exposes (for example, if you simply want to run some other program as Administrator instead of rerunning the current script), check its CPAN documentation here.

At some later date, I'd like to look a little closer at manifests and other UAC-related topics, and those might go into Win32::RunAsAdmin, but this much gets me what I wanted to achieve today.

Detecting elevated privileges

It's pretty cheesy, but the way I learned about UAC in the first place was when Win32::TieRegistry failed because it requests write access to the Registry by default. There is probably a more standard way of detecting elevation in the running process, but this works. I just did this:

   use Win32::TieRegistry qw( ::KEY_ );

   my $key = Win32::TieRegistry->Open( "LMachine", {Access=>KEY_READ()|KEY_WRITE()} );

If $key is undefined, you're not running with elevated privileges. I've exported that as Win32::RunAsAdmin::check.

Update 2014-05-05: Thanks to Alex at blogs.perl.org, I learned that there's a perfectly good Win32 API call, "IsAdminUser()", that handles this "the right way". So v0.02 of the module uses that.

Running using Win32::OLE

Since this technique only works through the Shell object, you've got to invoke it using Win32::OLE. Fortunately, that can be done in two lines:

   use Win32::OLE;

   my $shell = Win32::OLE->new("Shell.Application");
   $shell->ShellExecute ($executable, $args, $directory, 'runas');  # undef is fine for the directory

The value of the Perl community

After pushing Win32::RunAsAdmin to CPAN, I posted a blog post about it and also blabbed it to Perlmonks - each netted me one suggestion for improvement, which I incorporated into the second version (and in the case of the Perlmonks suggestion, that saved me a huge hassle when I went to use Win32::RunAsAdmin in a new case I hadn't considered!)

Perl rocks.

Anyway, that's really just about all that's of any interest in this module. As always, drop me a line if you have any questions or suggestions.






Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.