In the previous chapter, we explored the parts of a user’s identity and how to manage and store it. Now let’s talk about how to manage users while they are active on our systems and networks.
Typical user activities fall into four domains:
- Processes
Users run processes that can be spawned, killed, paused, and resumed on the machines we manage. These processes compete for a computer’s finite processing power, adding resource issues to the list of problems a system administrator needs to mediate.
- File operations
Most of the time, operations like writing, reading, creating, deleting, and so on take place when a specific user process interacts with files and directories in a filesystem. But under Unix, there’s more to this picture. Unix uses the filesystem as a gateway to more than just file storage. Device control, input/output, and even some process control and network access operations are file operations. We dealt with filesystem administration in Chapter 2, but in this chapter we’ll approach this topic from a user administration perspective.
- Network usage
Users can send and receive data over network interfaces on our machines. There is material elsewhere in this book on networking, but we’ll address this issue here from a different perspective.
- OS-specific activities
This last domain is a catchall for the OS-specific features that users can access via different APIs. Included in this list are things like GUI element controls, shared memory usage, file-sharing APIs, sound, and so on. This category is so diverse that it would be impossible to do it justice in this book. I recommend that you track down the OS-specific web sites for information on these topics.
We’ll begin by looking at ways to deal with the first three of these domains using Perl. Because we’re interested in user administration, the focus here will be on dealing with processes that other users have started.
We’re going to briefly look at four different ways to deal with process control on Windows, because each of these approaches opens up a door to interesting functionality outside the scope of our discussion that is likely to be helpful to you at some point. We’re primarily going to concentrate on two tasks: finding all of the running processes and killing select processes.
There are a number of programs available to us that display and manipulate processes. The first edition of this book used the programs pulist.exe and kill.exe from the Windows 2000 Resource Kit. Both are still available for download from Microsoft as of this writing and seem to work fine on later versions of the operating system. Another excellent set of process manipulation tools comes from the Sysinternals utility collection, which Mark Russinovich and Bryce Cogswell formerly provided on their Sysinternals web site and which is now available through Microsoft (see the references section at the end of this chapter). This collection includes a suite of utilities called PsTools that can do things the standard Microsoft-supplied tools can’t handle.
For our first example, we’re going to use two programs Microsoft ships with the operating system. The programs tasklist.exe and taskkill.exe work fine for many tasks and are a good choice for scripting in cases where you won’t want to or can’t download other programs to a machine.
By default tasklist
produces output in
a very wide table that can sometimes be difficult to read. Adding /FO list
provides output like this:
Image Name: System Idle Process PID: 0 Session Name: Console Session#: 0 Mem Usage: 16 K Status: Running User Name: NT AUTHORITY\SYSTEM CPU Time: 1:09:06 Window Title: N/A Image Name: System PID: 4 Session Name: Console Session#: 0 Mem Usage: 212 K Status: Running User Name: NT AUTHORITY\SYSTEM CPU Time: 0:00:44 Window Title: N/A Image Name: smss.exe PID: 432 Session Name: Console Session#: 0 Mem Usage: 372 K Status: Running User Name: NT AUTHORITY\SYSTEM CPU Time: 0:00:00 Window Title: N/A Image Name: csrss.exe PID: 488 Session Name: Console Session#: 0 Mem Usage: 3,984 K Status: Running User Name: NT AUTHORITY\SYSTEM CPU Time: 0:00:08 Window Title: N/A Image Name: winlogon.exe PID: 512 Session Name: Console Session#: 0 Mem Usage: 2,120 K Status: Running User Name: NT AUTHORITY\SYSTEM CPU Time: 0:00:08 Window Title: N/A
Another format option for tasklist
makes using it from Perl pretty trivial:
CSV
(Comma/Character Separated Values).
We’ll talk more about dealing with CSV files in Chapter 5, but here’s a small
example that demonstrates how to parse that data:
use Text::CSV_XS; my $tasklist = "$ENV{'SystemRoot'}\\SYSTEM32\\TASKLIST.EXE"; my $csv = Text::CSV_XS->new(); # /v = verbose (includes User Name), /FO CSV = CSV format, /NH - no header open my $TASKPIPE, '-|', "$tasklist /v /FO CSV /NH" or die "Can't run $tasklist: $!\n"; my @columns; while (<$TASKPIPE>) { next if /^$/; # skip blank lines in the input $csv->parse($_) or die "Could not parse this line: $_\n"; @columns = ( $csv->fields() )[ 0, 1, 6 ]; # grab name, PID, and User Name print join( ':', @columns ), "\n"; } close $TASKPIPE;
tasklist
can also provide some other interesting information, such as the
dynamic link libraries (DLLs) used by a particular process. Be sure to run
it with the /?
switch to see its usage
information.
The other program I mentioned, taskkill.exe, is equally easy to use. It takes as an argument a task name (called the “image name”), a process ID, or a more complex filter to determine which processes to kill. I recommend the process ID format to stay on the safe side, since it is very easy to kill the wrong process if you use task names.
taskkill
offers two different ways to shoot down processes. The first is
the polite death: taskkill.exe /PID
<process id>
will ask the specified
process to shut itself down. However, if we add /F
to the command line, it forces the issue: taskkill.exe /F /PID
<process id>
works more like the
native Perl kill()
function and
kills the process with extreme prejudice.
The second approach[19] uses the
Win32::Process::Info
module, by Thomas R.
Wyant. Win32::Process::Info
is very easy
to use. First, create a process info object, like so:
use Win32::Process::Info; use strict; # the user running this script must be able to use DEBUG level privs my $pi = Win32::Process::Info->new( { assert_debug_priv => 1 } );
The new()
method can optionally take a
reference to a hash containing configuration information. In this case we
set the config variable assert_debug_priv
to true
because we want our program to
use debug-level privileges when requesting information. This is necessary if
getting a list of all of the process owners is important to you. If you
leave this out, you’ll find that the module (due to the Windows security
system) will not be able to fetch the owner of some of the processes. There
are some pretty scary warnings in the module’s documentation regarding this
setting; I haven’t had any problems with it to date, but you should be sure
to read the documentation before you follow my lead.
Next, we retrieve the process information for the machine:
my @processinfo = $pi->GetProcInfo();
@processinfo
is now an array of
references to anonymous hashes. Each anonymous hash has a number of keys
(such as Name
, ProcessId
, CreationDate
,
and ExecutablePath
),
each with its expected value. To display our process info in the same
fashion as the example from the last section, we could use the following
code:
use Win32::Process::Info; my $pi = Win32::Process::Info->new( { assert_debug_priv => 1 } ); my @processinfo = $pi->GetProcInfo(); foreach my $process (@processinfo) { print join( ':', $process->{'Name'}, $process->{'ProcessId'}, $process->{'Owner'} ), "\n"; }
Once again, we get output like this:
System Idle Process:0: System:4: smss.exe:432:NT AUTHORITY\SYSTEM csrss.exe:488:NT AUTHORITY\SYSTEM winlogon.exe:512:NT AUTHORITY\SYSTEM services.exe:556:NT AUTHORITY\SYSTEM lsass.exe:568:NT AUTHORITY\SYSTEM svchost.exe:736:NT AUTHORITY\SYSTEM svchost.exe:816:NT AUTHORITY\NETWORK SERVICE svchost.exe:884:NT AUTHORITY\SYSTEM svchost.exe:960:NT AUTHORITY\SYSTEM svchost.exe:1044:NT AUTHORITY\NETWORK SERVICE svchost.exe:1104:NT AUTHORITY\LOCAL SERVICE ccSetMgr.exe:1172:NT AUTHORITY\SYSTEM ccEvtMgr.exe:1200:NT AUTHORITY\SYSTEM spoolsv.exe:1324:NT AUTHORITY\SYSTEM ...
Win32::Process::Info
provides more info
about a process than just these fields (perhaps more than you will ever
need). It also has one more helpful feature: it can show you the process
tree for all processes or just a particular process. This allows you to
display the subprocesses for each process (i.e., the list of processes that
process spawned) and the subprocesses for those subprocesses, and so
on.
So, for example, if we wanted to see all of the processes spawned by one of the processes just listed, we could write the following:
use Win32::Process::Info; use Data::Dumper; my $pi = Win32::Process::Info->new( { assert_debug_priv => 1 } ); # PID 884 picked for this example because it has a small number of children my %sp = $pi->Subprocesses(884); print Dumper (\%sp);
This yields:
$VAR1 = { '3320' => [], '884' => [ 3320 ] };
which shows that this instance of svchost.exe (PID
884
) has one child, the process with
PID 3320
. That process does not have any
children.
Of the approaches we’ll consider, this third approach is probably the most
fun. In this section we’ll look at a module by Jens Helberg called
Win32::Setupsup
and a module by
Ernesto Guisado, Jarek Jurasz, and Dennis K. Paulsen
called
Win32::GuiTest
. They have similar
functionality but achieve the same goals a little differently. We’ll look
primarily at Win32::Setupsup
, with a few
choice examples from Win32::GuiTest
.
Note
In the interest of full disclosure, it should be mentioned that (as of
this writing) Win32::Setupsup
had not
been developed since October 2000 and is kind of hard to find (see the
references at the end of this chapter). It still works well, though, and
it has features that aren’t found in Win32::GuiTest
; hence its inclusion
here. If its orphan status bothers you, I recommend looking at Win32::GuiTest
first to see if it meets
your needs.
Win32::Setupsup
is called “Setupsup”
because it is primarily designed to supplement software installation (which
often uses a program called setup.exe).
Some installers can be run in so-called “silent mode” for totally
automated installation. In this mode they ask no questions and require no
“OK” buttons to be pushed, freeing the administrator from having to babysit
the install. Software installation mechanisms that do not offer this mode
(and there are far too many of them) make a system administrator’s life
difficult. Win32::Setupsup
helps deal
with these deficiencies: it can find information on running processes and
manipulate them (or manipulate them dead if you so choose).
Note
For instructions on getting and installing Win32::Setupsup
, refer to the section Module Information for This Chapter.
With Win32::Setupsup
, getting the list
of running processes is easy. Here’s an example:
use Win32::Setupsup; use Perl6::Form; my $machine = ''; # query the list on the current machine # define the output format for Perl6::Form my $format = '{<<<<<<<} {<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<}'; my ( @processlist, @threadlist ); Win32::Setupsup::GetProcessList( $machine, \@processlist, \@threadlist ) or die 'process list error: ' . Win32::Setupsup::GetLastError() . "\n"; pop(@processlist); # remove the bogus entry always appended to the list print <<'EOH'; Process ID Process Name ========== =============================== EOH foreach my $processlist (@processlist) { print form $format, $processlist->{pid}, $processlist->{name}; }
Killing processes is equally easy:
KillProcess($pid, $exitvalue, $systemprocessflag) or die 'Unable to kill process: ' . Win32::Setupsup::GetLastError( ) . "\n";
The last two arguments are optional. The second argument kills the process
and sets its exit value accordingly (by default, it is set to 0
). The third argument allows you to kill
system-run processes (providing you have the Debug
Programs
user right).
That’s the boring stuff. We can take process manipulation to yet another level by interacting with the windows a running process may have open. To list all of the windows available on the desktop, we use:
Win32::Setupsup::EnumWindows(\@windowlist) or die 'process list error: ' . Win32::Setupsup::GetLastError( ) . "\n";
@windowlist
now contains a list of
window handles that are converted to look like normal numbers when you print
them. To learn more about each window, you can use a few different
functions. For instance, to find the titles of each window, you can use
GetWindowText()
like so:
use Win32::Setupsup; my @windowlist; Win32::Setupsup::EnumWindows( \@windowlist ) or die 'process list error: ' . Win32::Setupsup::GetLastError() . "\n"; my $text; foreach my $whandle (@windowlist) { if ( Win32::Setupsup::GetWindowText( $whandle, \$text ) ) { print "$whandle: $text", "\n"; } else { warn "Can't get text for $whandle" . Win32::Setupsup::GetLastError() . "\n"; } }
Here’s a little bit of sample output:
66130: chapter04 - Microsoft Word 66184: Style 194905150: 66634: setupsup - WordPad 65716: Fuel 328754: DDE Server Window 66652: 66646: 66632: OleMainThreadWndName
As you can see, some windows have titles, while others do not. Observant
readers might notice something else interesting about this output. Window
66130
belongs to a Microsoft Word
session that is currently running (it is actually the one in which this
chapter was composed). Window 66184
looks
vaguely like the name of another window that might be connected to Microsoft
Word. How can we tell if they are related?
Win32::Setupsup
has an
EnumChildWindows()
function that can show
us the children of any given window. Let’s use it to write something that
will show us a basic tree of the current window hierarchy:
use Win32::Setupsup; my @windowlist; # get the list of windows Win32::Setupsup::EnumWindows( \@windowlist ) or die 'process list error: ' . Win32::Setupsup::GetLastError() . "\n"; # turn window handle list into a hash # NOTE: this conversion populates the hash with plain numbers and # not actual window handles as keys. Some functions, like # GetWindowProperties (which we'll see in a moment), can't use these # converted numbers. Caveat implementor. my %windowlist; for (@windowlist) { $windowlist{$_}++; } # check each window for children my %children; foreach my $whandle (@windowlist) { my @children; if ( Win32::Setupsup::EnumChildWindows( $whandle, \@children ) ) { # keep a sorted list of children for each window $children{$whandle} = [ sort { $a <=> $b } @children ]; # remove all children from the hash; we won't directly # iterate over them foreach my $child (@children) { delete $windowlist{$child}; } } } # iterate through the list of windows and recursively print # each window handle and its children (if any) foreach my $window ( sort { $a <=> $b } keys %windowlist ) { PrintFamily( $window, 0, %children ); } # print a given window handle number and its children (recursively) sub PrintFamily { # starting window - how deep in a tree are we? my ( $startwindow, $level, %children ) = @_; # print the window handle number at the appropriate indentation print( ( ' ' x $level ) . "$startwindow\n" ); return unless ( exists $children{$startwindow} ); # no children, done # otherwise, we have to recurse for each child $level++; foreach my $childwindow ( @{ $children{$startwindow} } ) { PrintFamily( $childwindow, $level, %children ); } }
There’s one last window property function we should look at before moving
on: GetWindowProperties()
. GetWindowProperties()
is basically a catchall for the rest of
the window properties we haven’t seen yet. For instance, using GetWindowProperties()
we can query the process
ID for the process that created a specific window. This could be combined
with some of the functionality we just saw for the Win32::Process::Info
module.
The Win32::Setupsup
documentation contains a list of the available properties that
can be queried. Let’s use one of them to write a very simple program that
will print the coordinates of a rectangular window on the desktop. GetWindowProperties()
takes three arguments: a
window handle, a reference to an array that contains the names of the
properties to query, and a reference to a hash where the query results will
be stored. Here’s the code we need for our task:
use Win32::Setupsup; # Convert window ID into a form that GetWindowProperties can cope with. # Note: 'U' is a pack template that is only available in Perl 5.6+ releases. my $whandle = unpack 'U', pack 'U', $ARGV[0]; my %info; Win32::Setupsup::GetWindowProperties( $whandle, ['rect'], \%info ); print "\t" . $info{rect}{top} . "\n"; print $info{rect}{left} . ' -' . $whandle . '- ' . $info{rect}{right} . "\n"; print "\t" . $info{rect}{bottom} . "\n";
The output is a bit cutesy. Here’s a sample showing the top, left, right,
and bottom coordinates of the window with handle 66180
:
154 272 −66180- 903 595
GetWindowProperties()
returns a special
data structure for only one property, rect
. All of the others will simply show up in the referenced
hash as normal keys and values. If you are uncertain about the properties
being returned by Perl for a specific window, the windowse utility is often helpful.
Now that we’ve seen how to determine various window properties, wouldn’t it be spiffy if we could make changes to some of these properties? For instance, it might be useful to change the title of a particular window. With this capability, we could create scripts that used the window title as a status indicator:
"Prestidigitation In Progress ... 32% complete"
Making this change to a window is as easy as a single function call:
Win32::Setupsup::SetWindowText($handle,$text);
We can also set the rect
property we
just saw. This code makes the specified window jump to the position we’ve
specified:
use Win32::Setupsup; my %info; $info{rect}{left} = 0; $info{rect}{right} = 600; $info{rect}{top} = 10; $info{rect}{bottom} = 500; my $whandle = unpack 'U', pack 'U', $ARGV[0]; Win32::Setupsup::SetWindowProperties( $whandle, \%info );
I’ve saved the most impressive function for last. With SendKeys()
, it is
possible to send arbitrary keystrokes to any window on the desktop. For
example:
use Win32::Setupsup; my $texttosend = "\\DN\\Low in the gums"; my $whandle = unpack 'U', pack 'U', $ARGV[0]; Win32::Setupsup::SendKeys( $whandle, $texttosend, 0 ,0 );
This will send a “down cursor key” followed by some text to the specified
window. The arguments to SendKeys()
are
pretty simple: window handle, text to send, a flag to determine whether a
window should be activated for each keystroke, and an optional time between
keystrokes. Special key codes like the down cursor are surrounded by
backslashes. The list of available keycodes can be found in the module’s
documentation.
Before we move on to another tremendously useful way to work with user
processes in the Windows universe, I want to briefly look at a module that
shares some functionality with Win32::Setupsup
but can do even more interesting stuff. Like
Win32::Setupsup
, Win32::GuiTest
can return information about
active windows and send keystrokes to applications. However, it offers even
more powerful functionality.
Here’s an example slightly modified from the documentation (stripped of comments and error checking, be sure to see the original) that demonstrates some of this power:
use Win32::GuiTest qw(:ALL); system("start notepad.exe"); sleep 1; MenuSelect("F&ormat|&Font"); sleep(1); my $fontdlg = GetForegroundWindow(); my ($combo) = FindWindowLike( $fontdlg, '', 'ComboBox', 0x470 ); for ( GetComboContents($combo) ) { print "'$_'" . "\n"; } SendKeys("{ESC}%{F4}");
This code starts up notepad, asks it to open its font settings by choosing the appropriate menu item, and then reads the contents of the resulting dialog box and prints what it finds. It then sends the necessary keystrokes to dismiss the dialog box and tell notepad to quit. The end result is a list of monospaced fonts available on the system that looks something like this:
'Arial' 'Arial Black' 'Comic Sans MS' 'Courier' 'Courier New' 'Estrangelo Edessa' 'Fixedsys' 'Franklin Gothic Me 'Gautami' 'Georgia' 'Impact' 'Latha' 'Lucida Console' 'Lucida Sans Unicod 'Mangal' 'Marlett' 'Microsoft Sans Ser 'Modern' 'MS Sans Serif'
Let’s look at one more example (again, adapted from the module’s documentation because it offers great example code):
use Win32::GuiTest qw(:ALL); system 'start notepad'; sleep 1; my $menu = GetMenu( GetForegroundWindow() ); menu_parse($menu); SendKeys("{ESC}%{F4}"); sub menu_parse { my ( $menu, $depth ) = @_; $depth ||= 0; foreach my $i ( 0 .. GetMenuItemCount($menu) - 1 ) { my %h = GetMenuItemInfo( $menu, $i ); print ' ' x $depth; print "$i "; print $h{text} if $h{type} and $h{type} eq 'string'; print "------" if $h{type} and $h{type} eq 'separator'; print "UNKNOWN" if not $h{type}; print "\n"; my $submenu = GetSubMenu( $menu, $i ); if ($submenu) { menu_parse( $submenu, $depth + 1 ); } } }
As in the previous example, we begin by spinning up
notepad. We can then examine the menus of the
application in the foreground window, determining the number of top-level
menu items and then iterating over each item (printing the information and
looking for submenus of each item as we go). If we find a submenu, we
recursively call menu_parse()
to examine
it. Once we’ve completed the menu walk, we send the keys to close the
notepad window and quit the application.
The output looks like this:
0 &File 0 &New Ctrl+N 1 &Open... Ctrl+O 2 &Save Ctrl+S 3 Save &As... 4 ------ 5 Page Set&up... 6 &Print... Ctrl+P 7 ------ 8 E&xit 1 &Edit 0 &Undo Ctrl+Z 1 ------ 2 Cu&t Ctrl+X 3 &Copy Ctrl+C 4 &Paste Ctrl+V 5 De&lete Del 6 ------ 7 &Find... Ctrl+F 8 Find &Next F3 9 &Replace... Ctrl+H 10 &Go To... Ctrl+G 11 ------ 12 Select &All Ctrl+A 13 Time/&Date F5 2 F&ormat 0 &Word Wrap 1 &Font... 3 &View 0 &Status Bar 4 &Help 0 &Help Topics 1 ------ 2 &About Notepad
Triggering known menu items from a script is pretty cool, but it’s even cooler to have the power to determine which menu items are available. This lets us write much more adaptable scripts.
We’ve only touched on a few of Win32::GuiTest
’s advanced features here. Some of the other
impressive features include the ability to read the text context of a window
using
WMGetText()
and the ability to select
individual tabs in a window with
SelectTabItem()
. See
the documentation and the example directory (eg) for
more details.
With the help of these two modules, we’ve taken process control to an entirely new level. Now it is possible to remotely control applications (and parts of the OS) without the explicit cooperation of those applications. We don’t need them to offer command-line support or a special API; we have the ability to essentially script a GUI, which is useful in a myriad of system administration contexts.
Let’s look at one final approach to Windows process control before we switch to another operating system. By now you’ve probably figured out that each of these approaches is not only good for process control, but also can be applied in many different ways to make Windows system administration easier. If you had to pick the approach that would yield the most reward in the long term to learn, WMI-based scripting is probably it. The first edition of this book called Windows Management Instrumentation “Futureland” because it was still new to the scene when the book was being written. In the intervening time, Microsoft, to its credit, has embraced the WMI framework as its primary interface for administration of not just its operating systems, but also its other products, such as MS SQL Server and Microsoft Exchange.
Unfortunately, WMI is one of those not-for-the-faint-of-heart technologies that gets very complex very quickly. It is based on an object-oriented model that has the power to represent not only data, but also relationships between objects. For instance, it is possible to create an association between a web server and the storage device that holds the data for that server, so that if the storage device fails, a problem for the web server will be reported as well. We don’t have the space to deal with this complexity here, so we’re just going to skim the surface of WMI by providing a small and simple introduction, followed by a few code samples.
If you want to get a deeper look at this technology, I recommend searching for WMI-related content at http://msdn.microsoft.com. You should also have a look at the information found at the Distributed Management Task Force’s website. In the meantime, here is a brief synopsis to get you started.
WMI is the Microsoft implementation and extension of an unfortunately named initiative called the Web-Based Enterprise Management initiative, or WBEM for short. Though the name conjures up visions of something that requires a browser, it has virtually nothing to do with the World Wide Web. The companies that were part of the Distributed Management Task Force (DMTF) wanted to create something that could make it easier to perform management tasks using browsers. Putting the name aside, it is clearer to say that WBEM defines a data model for management and instrumentation information. It provides specifications for organizing, accessing, and moving this data around. WBEM is also meant to offer a cohesive frontend for accessing data provided by other management protocols, such as the Simple Network Management Protocol (SNMP), discussed in Chapter 12, and the Common Management Information Protocol (CMIP).
Data in the WBEM world is organized using the Common Information Model (CIM). CIM is the source of the power and complexity in WBEM/WMI. It provides an extensible data model that contains objects and object classes for any physical or logical entity one might want to manage. For instance, there are object classes for entire networks, and objects for single slots in specific machines. There are objects for hardware settings and objects for software application settings. On top of this, CIM allows us to define object classes that describe relationships between other objects.
This data model is documented in two parts: the CIM Specification and the CIM Schema. The former describes the how of CIM (how the data will be specified, its connection to prior management standards, etc.), while the latter provides the what of CIM (the actual objects). This division may remind you of the SNMP SMI and MIB relationship (see Appendix G and Chapter 12).
In practice, you’ll be consulting the CIM Schema more than the CIM Specification once you get the hang of how the data is represented. The schema format (called MOF, for Managed Object Format) is fairly easy to read.
The CIM Schema has two layers:
The core model for objects and classes useful in all types of WBEM interaction.
The common model for generic objects that are vendor- and operating system-independent. Within the common model there are currently 15 specific areas, including Systems, Devices, Applications, Networks, and Physical.
Built on top of these two layers can be any number of extension schemas that define objects and classes for vendor- and OS-specific information. WMI is one WBEM implementation that makes heavy use of this extension mechanism.
A crucial part of WMI that distinguishes it from generic WBEM implementations is the Win32 Schema, an extension schema for Win32-specific information built on the core and common models. WMI also adds to the generic WBEM framework by providing Win32-specific access mechanisms to the CIM data.[20] Using this schema extension and set of data access methods, we can explore how to perform process control operations using WMI in Perl.
WMI offers two different approaches for getting at management data: object-oriented and query-based. With the former you specify the specific object or container of objects that contains the information you seek, while with the latter you construct a SQL-like[21] query that returns a result set of objects containing your desired data. We’ll give a simple example of each approach so you can see how they work.
The Perl code that follows does not appear to be particularly complex, so you may wonder about the earlier “gets very complex very quickly” description. The code looks simple because:
We’re only scratching the surface of WMI. We’re not even going to touch on subjects like associations (i.e., relationships between objects and object classes).
The management operations we are performing are simple. Process control in this context will consist of querying the running processes and being able to terminate them at will. These operations are easy in WMI using the Win32 Schema extension.
Our samples hide the complexity of translating WMI documentation and code samples in VBScript/JScript to Perl code. See Appendix F for some help with that task.
Our samples hide the opaqueness of the debugging process. When WMI-related Perl code fails (especially code of the object-oriented flavor), it provides very little information that would help you debug the problem. You may receive error messages, but they never say
ERROR: YOUR EXACT PROBLEM IS...
. You’re more likely to get back a message likewbemErrFailed 0x8004100
or just an empty data structure. To be fair to Perl, most of this opaqueness comes from Perl’s role in this process: it is acting as a frontend to a set of fairly complex multilayered operations that don’t concern themselves with passing back useful feedback when something fails.
I know this sounds pretty grim, so let me offer some potentially helpful advice before we actually get into the code itself:
Look at all of the
Win32::OLE
sample code you can lay your hands on. The ActiveState Win32-Users mailing list archive found at http://aspn.activestate.com/ASPN/Mail is a good source for this code. If you compare this sample code to equivalent VBScript examples, you’ll start to understand the necessary translation idioms. Appendix F and the section Active Directory Service Interfaces in Chapter 9 may also help.Make friends with the Perl debugger, and use it to try out code snippets as part of this learning process. There are also several REPL[22]-modules available on CPAN, such as
App::REPL
,Devel::REPL
, andShell::Perl
, that can make interactive prototyping easier. Other integrated development environment (IDE) tools may also offer this functionality.Keep a copy of the WMI SDK handy. The documentation and the VBScript code examples are very helpful.
Use the WMI object browser in the WMI SDK frequently. It helps you get the lay of the land.
Now let’s get to the Perl part of this section. Our initial task will be to determine what information we can retrieve about Windows processes and how we can interact with that information.
First we need to establish a connection to a WMI
namespace. A namespace is defined in the WMI SDK as “a unit for grouping
classes and instances to control their scope and visibility.” In this case,
we’re interested in connecting to the root of the standard cimv2
namespace, which contains all of the
data that is interesting to us.
We will also have to set up a connection with the appropriate security
privileges and impersonation level. Our program will need to be given the
privilege to debug a process and to impersonate us; in other words, it has
to run as the user calling the script. After we get this connection, we will
retrieve a
Win32_Process
object (as defined in the
Win32 Schema).
There is a hard way and an easy way to create this connection and get the object. We’ll look at both in the first example, so you get an idea of what the methods entail. Here’s the hard way, with its explanation to follow:
use Win32::OLE('in'); my $server = ''; # connect to local machine # get an SWbemLocator object my $lobj = Win32::OLE->new('WbemScripting.SWbemLocator') or die "can't create locator object: ".Win32::OLE->LastError()."\n"; # set the impersonation level to "impersonate" $lobj->{Security_}->{impersonationlevel} = 3; # use it to get an SWbemServices object my $sobj = $lobj->ConnectServer($server, 'root\cimv2') or die "can't create server object: ".Win32::OLE->LastError()."\n"; # get the schema object my $procschm = $sobj->Get('Win32_Process');
The hard way involves:
Getting a locator object, used to find a connection to a server object
Setting the impersonation level so our program will run with our privileges
Using the locator object to get a server connection to the
cimv2
WMI namespaceUsing this server connection to retrieve a
Win32_Process
object
Doing it this way is useful in cases where you need to operate on the intermediate objects. However, we can do this all in one step using a COM moniker’s display name. According to the WMI SDK, “in Common Object Model (COM), a moniker is the standard mechanism for encapsulating the location and binding of another COM object. The textual representation of a moniker is called a display name.” Here’s an easy way to do the same thing as the previous code snippet:
use Win32::OLE('in'); my $procschm = Win32::OLE->GetObject( 'winmgmts:{impersonationLevel=impersonate}!Win32_Process') or die "can't create server object: ".Win32::OLE->LastError()."\n";
Now that we have a
Win32_Process
object in hand, we can use
it to show us the relevant parts of the schema that represent processes
under Windows. This includes all of the available Win32_Process
properties and methods we can use. The code to
do this is fairly simple; the only magic is the use of the Win32::OLE in
operator. To explain this, we
need a quick digression.
Our $procschm
object has two special
properties, Properties_
and Methods_
. Each holds a special child object,
known as a collection object in COM parlance. A
collection object is just a parent container for other objects; in this
case, they are holding the schema’s property method description objects. The
in
operator just returns an array
with references to each child object of a container object.[23] Once we have this array, we can iterate through it, returning
the Name
property of each child object as
we go. Here’s what the code looks like:
use Win32::OLE('in'); # connect to namespace, set the impersonation level, and retrieve the # Win32_process object just by using a display name my $procschm = Win32::OLE->GetObject( 'winmgmts:{impersonationLevel=impersonate}!Win32_Process') or die "can't create server object: ".Win32::OLE->LastError()."\n"; print "--- Properties ---\n"; print join("\n",map {$_->{Name}}(in $procschm->{Properties_})); print "\n--- Methods ---\n"; print join("\n",map {$_->{Name}}(in $procschm->{Methods_}));
The output (on a Windows XP SP2 machine) looks like this:
--- Properties --- Caption CommandLine CreationClassName CreationDate CSCreationClassName CSName Description ExecutablePath ExecutionState Handle HandleCount InstallDate KernelModeTime MaximumWorkingSetSize MinimumWorkingSetSize Name OSCreationClassName OSName OtherOperationCount OtherTransferCount PageFaults PageFileUsage ParentProcessId PeakPageFileUsage PeakVirtualSize PeakWorkingSetSize Priority PrivatePageCount ProcessId QuotaNonPagedPoolUsage QuotaPagedPoolUsage QuotaPeakNonPagedPoolUsage QuotaPeakPagedPoolUsage ReadOperationCount ReadTransferCount SessionId Status TerminationDate ThreadCount UserModeTime VirtualSize WindowsVersion WorkingSetSize WriteOperationCount WriteTransferCount --- Methods --- Create Terminate GetOwner GetOwnerSid SetPriority AttachDebugger
Now let’s get down to the business at hand. To retrieve a list of running
processes, we need to ask for all instances of Win32_Process
objects:
use Win32::OLE('in'); # perform all of the initial steps in one swell foop my $sobj = Win32::OLE->GetObject( 'winmgmts:{impersonationLevel=impersonate}') or die "can't create server object: ".Win32::OLE->LastError()."\n"; foreach my $process (in $sobj->InstancesOf("Win32_Process")){ print $process->{Name}." is pid #".$process->{ProcessId},"\n"; }
Our initial display name did not include a path to a specific object
(i.e., we left off !Win32_Process
). As a result, we receive a server connection
object. When we call the InstancesOf()
method, it returns a collection object that holds all of the instances of
that particular object. Our code visits each object in turn and prints its
Name
and ProcessId
properties. This yields a list
of all the running processes.
If we wanted to be a little less beneficent when iterating over each process, we could instead use one of the methods listed earlier:
foreach $process (in $sobj->InstancesOf("Win32_Process")){
$process->Terminate
(1);
}
This will terminate every process running. I do not recommend that you run this code as is; customize it for your specific needs by making it more selective.
One last note before we move on. Earlier in this section I mentioned that
there are two ways to query information using WMI: the object-oriented and
query-based approaches. Up to now we’ve been looking at the fairly
straightforward object-oriented approach. Here’s a small sample using the
query-based approach, just to pique your interest. First, let’s recreate the
output from the preceding sample. The highlighted line is the key change
here, because it uses WQL instead of InstancesOf()
to retrieve all of the process objects:
use Win32::OLE('in');
my $sobj = Win32::OLE->GetObject('winmgmts:{impersonationLevel=impersonate}')
or die 'can't create server object: ' . Win32::OLE->LastError() . "\n";
my $query = $sobj->ExecQuery('SELECT Name, ProcessId FROM Win32_Process');
foreach my $process ( in $query ) {
print $process->{Name} . ' is pid #' . $process->{ProcessId}, "\n";
}
Now we can start throwing in SQL-like syntax in the highlighted query string. For example, if we only wanted to see the process IDs of the svchost.exe processes running on the system, we could write:
use Win32::OLE('in'); my $sobj = Win32::OLE->GetObject('winmgmts:{impersonationLevel=impersonate}') or die "can't create server object: " . Win32::OLE->LastError() . "\n"; my $query = $sobj->ExecQuery( 'SELECT ProcessId FROM Win32_Process WHERE Name = "svchost.exe"'); print "SvcHost processes: " . join( ' ', map { $_->{ProcessId} } ( in $query) ), "\n";
WQL can handle queries with other SQL-like stanzas. For example, the following is valid WQL to retrieve information on all running processes that have names that begin with “svc”:
SELECT * from Win32_Process WHERE Name LIKE "svc%"
If you are SQL-literate (even if the sum of your knowledge comes from Appendix D in this book), this may be a direction you want to explore.
Now you have the knowledge necessary to begin using WMI for process control. WMI has Win32 extensions for many other parts of the operating system, including the registry and the event log facility.
This is as far as we’re going to delve into process control on Windows. Now let’s turn our attention to another major operating system.
Strategies for Unix process control offer another multiple-choice situation. Luckily, these choices aren’t nearly as complex as those that Windows offers. When we speak of process control under Unix, we’re referring to three operations:
Enumerating the list of running processes on a machine
Changing their priorities or process groups
Terminating the processes
For the final two of these operations, there are Perl functions to do the
job: setpriority()
, setpgrp()
, and
kill()
. The first one offers us a few
options. To list running processes, you can:
Call an external program like ps.
Take a crack at deciphering /dev/kmem.
Look through the /proc filesystem (for Unix versions that have one).
Use the
Proc::ProcessTable
module.
Let’s discuss each of these approaches. For the impatient reader, I’ll reveal
right now that Proc::ProcessTable
is my
preferred technique. You may want to just skip directly to the discussion of
that module, but I recommend reading about the other techniques anyway, since
they may come in handy in the future.
Common to all modern Unix variants is a program called ps, used to list running processes. However, ps is found in different places in the filesystem on different Unix variants, and the command-line switches it takes are also not consistent across variants. Therein lies one problem with this option: it lacks portability.
An even more annoying problem is the difficulty in parsing the output (which also varies from variant to variant). Here’s a snippet of output from ps on an ancient SunOS machine:
USER PID %CPU %MEM SZ RSS TT STAT START TIME COMMAND dnb 385 0.0 0.0 268 0 p4 IW Jul 2 0:00 /bin/zsh dnb 24103 0.0 2.610504 1092 p3 S Aug 10 35:49 emacs dnb 389 0.0 2.5 3604 1044 p4 S Jul 2 60:16 emacs remy 15396 0.0 0.0 252 0 p9 IW Jul 7 0:01 -zsh (zsh) sys 393 0.0 0.0 28 0 ? IW Jul 2 0:02 in.identd dnb 29488 0.0 0.0 68 0 p5 IW 20:15 0:00 screen dnb 29544 0.0 0.4 24 148 p7 R 20:39 0:00 less dnb 5707 0.0 0.0 260 0 p6 IW Jul 24 0:00 -zsh (zsh) root 28766 0.0 0.0 244 0 ? IW 13:20 0:00 -:0 (xdm)
Notice the third line. Two of the columns have run together, making parsing this output an annoying task. It’s not impossible, just vexing. Some Unix variants are kinder than others in this regard (for example, later operating systems from Sun don’t have this problem), but it is something you may have to take into account.
The Perl code required for this option is straightforward: use open()
to run ps,
while(<FH>){...}
to
read the output, and
split()
, unpack()
, or substr()
to
parse it. You can find a recipe for this in the
Perl
Cookbook
, by Tom Christiansen and Nathan Torkington (O’Reilly).
I only mention this option for completeness’s sake. It is possible to write code that opens up a device like /dev/kmem and accesses the current running kernel’s memory structures. With this access, you can track down the current process table in memory and read it. However, given the pain involved (taking apart complex binary structures by hand), and its extreme nonportability (a version difference within the same operating system is likely to break your program), I’d strongly recommend against using this option.[24]
If you decide not to heed this advice, you should begin by memorizing the
Perl documentation for
pack()
, unpack()
, and the header files for your kernel. Open the
kernel memory file (often /dev/kmem), then
read()
and unpack()
to your heart’s content. You may find it instructive
to look at the source for programs like
top
that perform this task using a great deal of C code. Our next
option offers a slightly better version of this method.
One of the more interesting additions to Unix found in most of the current variants is the /proc filesystem. This is a magical filesystem that has nothing to do with data storage. Instead, it provides a file-based interface for the running process table of a machine. A “directory” named after the process ID appears in this filesystem for each running process. In this directory are a set of “files” that provide information about that process. One of these files can be written to, thus allowing control of the process.
It’s a really clever concept, and that’s the good news. The bad news is that each Unix vendor/developer team decided to take this clever concept and run with it in a different direction. As a result, the files found in a /proc directory are often variant-specific, both in name and format. For a description of which files are available and what they contain, you will need to consult the manual pages (usually found in sections 4, 5, or 8) for procfs or mount_ procfs on your system.
The one fairly portable use of the /proc filesystem
is the enumeration of running processes. If we want to list just the process
IDs and their owners, we can use Perl’s directory and
lstat()
operators:
opendir my $PROC, '/proc' or die "Unable to open /proc:$!\n"; # only stat the items in /proc that look like PIDs for my $process (grep /^\d+$/, readdir($PROC)){ print "$process\t". getpwuid((lstat "/proc/$process")[4])."\n"; } closedir $PROC;
If you are interested in more information about a process, you will have
to open and unpack()
the appropriate
binary file in the /proc directories. Common names for
this file are status and psinfo.
The manual pages cited a moment ago should provide details about the C
structure found in this file, or at least a pointer to a C include file that
documents this structure. Because these are operating system-specific (and
OS version-specific) formats, you’re
still going to run into the problem of program fragility mentioned in the
discussion of the previous option.
You may be feeling discouraged at this point because all of our options so far look like they require code with lots of special cases (one for each version of each operating system we wish to support). Luckily, we have one more option up our sleeve that may help in this regard.
Daniel J. Urist (with the help of some volunteers) has been kind enough to write
a module called Proc::ProcessTable
that offers a consistent interface to the process table for the
major Unix variants. It hides the vagaries of the different
/proc or kmem implementations
for you, allowing you to write relatively portable code.
Simply load the module, create a Proc::ProcessTable::Process
object, and run methods from that
object:
use Proc::ProcessTable; my $tobj = new Proc::ProcessTable;
This object uses Perl’s tied variable functionality to present a real-time view of the system. You do not need to call a special function to refresh the object; each time you access it, it re-reads the process table.
To get at this information, you call the object method
table()
:
my $proctable = $tobj->table( );
table()
returns a reference to an array
with members that are references to individual process objects. Each of
these objects has its own set of methods that returns information about that
process. For instance, here’s how you would get a listing of the process IDs
and owners:
use Proc::ProcessTable; my $tobj = new Proc::ProcessTable; my $proctable = $tobj->table(); foreach my $process (@$proctable) { print $process->pid . "\t" . getpwuid( $process->uid ) . "\n"; }
If you want to know which process methods are available on your Unix
variant, the fields()
method of your
Proc::ProcessTable
object ($tobj
in the preceding code) will return a
list for you.
Proc::ProcessTable
also adds three
other methods to each process object—kill()
, priority()
, and
pgrp()
—which are just frontends to
the built-in Perl function we mentioned at the beginning of this
section.
To bring us back to the big picture, let’s look at some of the uses of
these process control techniques. We started to examine process control in
the context of user actions, so let’s look at a few teeny scripts that focus
on these actions. We will use the Proc::ProcessTable
module on Unix for
these examples, but these ideas are not operating system-specific.
The first example is slightly modified from the documentation for Proc::ProcessTable
:
use Proc::ProcessTable; my $t = new Proc::ProcessTable; foreach my $p (@{$t->table}){ if ($p->pctmem > 95){ $p->kill(9); } }
When run on the Unix variants that provide the
pctmem()
method (most do), this code will
shoot down any process consuming 95% of the machine’s memory. As it stands,
it’s probably too ruthless to be used in real life. It would be much more
reasonable to add something like this before the kill()
command:
print 'about to nuke '.$p->pid."\t". getpwuid($p->uid)."\n"; print 'proceed? (yes/no) '; chomp($ans = <>); next unless ($ans eq 'yes');
There’s a bit of a race condition here: it is possible that the system state will change during the delay induced by prompting the user. Given that we are only prompting for huge processes, though, and huge processes are those least likely to change state in a short amount of time, we’re probably fine coding this way. If you wanted to be pedantic, you would probably collect the list of processes to be killed first, prompt for input, and then recheck the state of the process table before actually killing the desired processes. This doesn’t remove the race condition, but it does make it much less likely to occur.
There are times when death is too good for a process. Sometimes it is important to notice that a process is running while it is running so that some real-life action (like “user attitude correction”) can be taken. For example, at our site we have a policy against the use of Internet Relay Chat (IRC) bots. Bots are daemon processes that connect to an IRC network of chat servers and perform automated actions. Though bots can be used for constructive purposes, these days they play a mostly antisocial role on IRC. We’ve also had security breaches come to our attention because the first (and often only) thing the intruder has done is put up an IRC bot of some sort. As a result, noting their presence on our system without killing them is important to us.
The most common bot by far is called eggdrop. If we wanted to look for this process name being run on our system, we could use code like this:
use Proc::ProcessTable; my $logfile = 'eggdrops'; open my $LOG, '>>', $logfile or die "Can't open logfile for append:$!\n"; my $t = new Proc::ProcessTable; foreach my $p ( @{ $t->table } ) { if ( $p->fname() =~ /eggdrop/i ) { print $LOG time . "\t" . getpwuid( $p->uid ) . "\t" . $p->fname() . "\n"; } } close $LOG;
If you’re thinking, “This code isn’t good enough! All someone has to do is rename the eggdrop executable to evade its check,” you’re absolutely right. We’ll take a stab at writing some less naïve bot-check code in the very last section of this chapter.
In the meantime, let’s take a look at one more example where Perl assists us in managing user processes. So far all of our examples have been fairly negative, focusing on dealing with resource-hungry and naughty processes. Let’s look at something with a sunnier disposition.
There are times when a system administrator needs to know which (legitimate) programs users on a system are using. Sometimes this is necessary in the context of software metering, where there are legal concerns about the number of users running a program concurrently. In those cases there is usually a licensing mechanism in place to handle the bean counting. Another situation where this knowledge comes in handy is that of machine migration. If you are migrating a user population from one architecture to another, you’ll want to make sure all the programs used on the previous architecture are available on the new one.
One approach to solving this problem involves replacing every non-OS binary available to users with a wrapper that first records that a particular binary has been run and then actually runs it. This can be difficult to implement if there are a large number of binaries. It also has the unpleasant side effect of slowing down every program invocation.
If precision is not important and a rough estimate of which binaries are
in use will suffice, we can use Proc::ProcessTable
to solve this problem. Here’s some code
that wakes up every five minutes and surveys the current process landscape.
It keeps a simple count of all of the process names it finds, and it’s smart
enough not to count processes it saw during its last period of wakefulness.
Every hour it prints its findings and starts collecting again. We wait five
minutes between each run because walking the process table is usually a
resource-intensive operation, and we’d prefer this program to add as little
load to the system as possible:
use Proc::ProcessTable; my $interval = 300; # sleep interval of 5 minutes my $partofhour = 0; # keep track of where in the hour we are my $tobj = new Proc::ProcessTable; # create new process object my %last; # to keep track of info from the previous run my %current; # to keep track of data from the current run my %collection; # to keep track of info over the entire hour # forever loop, collecting stats every $interval secs # and dumping them once an hour while (1) { foreach my $process ( @{ $tobj->table } ) { # we should ignore ourselves next if ( $process->pid() == $$ ); # save this process info for our next run # (note: this assumes that your PIDs won't recycle between runs, # but on a very busy system that may not be the case) $current{ $process->pid() } = $process->fname(); # ignore this process if we saw it during the last iteration next if ( $last{ $process->pid() } eq $process->fname() ); # else, remember it $collection{ $process->fname() }++; } $partofhour += $interval; %last = %current; %current = (); if ( $partofhour >= 3600 ) { print scalar localtime(time) . ( '-' x 50 ) . "\n"; print "Name\t\tCount\n"; print "--------------\t\t-----\n"; foreach my $name ( sort reverse_value_sort keys %collection ) { print "$name\t\t$collection{$name}\n"; } %collection = (); $partofhour = 0; } sleep($interval); } # (reverse) sort by values in %collection and by key name sub reverse_value_sort { return $collection{$b} <=> $collection{$a} || $a cmp $b; }
There are many ways this program could be enhanced. It could track processes on a per-user basis (i.e., only recording one instance of a program launch per user), collect daily stats, present its information as a nice bar graph, and so on. It’s up to you where you might want to take it.
For the last section of this chapter, we’re going to lump two of the user action domains together. The processes we’ve just spent so much time controlling do more than just suck up CPU and memory resources; they also perform operations on filesystems and communicate on a network on behalf of users. User administration requires that we deal with these second-order effects as well.
Our focus in this section will be fairly narrow. We’re only interested in looking at file and network operations that other users are performing on a system. We’re also only going to focus on those operations that we can track back to a specific user (or a specific process run by a specific user). With these blinders in mind, let’s go forth.
If we want to track other users’ open files, the closest we can come involves using a former third-party command-line program called handle, written by Mark Russinovich (formerly of Sysinternals). See the references section at the end of this chapter for information on where to get it. handle can show us all of the open handles on a particular system. Here’s an excerpt from some sample output:
System pid: 4 NT AUTHORITY\SYSTEM 7C: File (-W-) C:\pagefile.sys 5DC: File (---) C:\Documents and Settings\LocalService\Local Settings\ Application Data\Microsoft\Windows\UsrClass.dat 5E0: File (---) C:\WINDOWS\system32\config\SAM.LOG 5E4: File (---) C:\Documents and Settings\LocalService\NTUSER.DAT 5E8: File (---) C:\WINDOWS\system32\config\system 5EC: File (---) C:\WINDOWS\system32\config\software.LOG 5F0: File (---) C:\WINDOWS\system32\config\software 5F8: File (---) C:\WINDOWS\system32\config\SECURITY 5FC: File (---) C:\WINDOWS\system32\config\default 600: File (---) C:\WINDOWS\system32\config\SECURITY.LOG 604: File (---) C:\WINDOWS\system32\config\default.LOG 60C: File (---) C:\WINDOWS\system32\config\SAM 610: File (---) C:\WINDOWS\system32\config\system.LOG 614: File (---) C:\Documents and Settings\NetworkService\NTUSER.DAT 8E0: File (---) C:\Documents and Settings\dNb\Local Settings\Application Data\Microsoft\Windows\UsrClass.dat.LOG 8E4: File (---) C:\Documents and Settings\dNb\Local Settings\Application Data\Microsoft\Windows\UsrClass.dat 8E8: File (---) C:\Documents and Settings\dNb\NTUSER.DAT.LOG 8EC: File (---) C:\Documents and Settings\dNb\NTUSER.DAT B08: File (RW-) C:\Program Files\Symantec AntiVirus\SAVRT B3C: File (R--) C:\System Volume Information\_restore{96B84597-8A49-41EE- 8303-02D3AD2B3BA4}\RP80\change.log B78: File (R--) C:\Program Files\Symantec AntiVirus\SAVRT\0608NAV~.TMP ------------------------------------------------------------------------------ smss.exe pid: 436 NT AUTHORITY\SYSTEM 8: File (RW-) C:\WINDOWS 1C: File (RW-) C:\WINDOWS\system32
You can also request information on specific files or directories:
> handle.exe c:\WINDOWS\system32\config
Handle v3.3
Copyright (C) 1997-2007 Mark Russinovich
Sysinternals - www.sysinternals.com
System pid: 4 5E0: C:\WINDOWS\system32\config\SAM.LOG
System pid: 4 5E8: C:\WINDOWS\system32\config\system
System pid: 4 5EC: C:\WINDOWS\system32\config\software.LOG
System pid: 4 5F0: C:\WINDOWS\system32\config\software
System pid: 4 5F8: C:\WINDOWS\system32\config\SECURITY
System pid: 4 5FC: C:\WINDOWS\system32\config\default
System pid: 4 600: C:\WINDOWS\system32\config\SECURITY.LOG
System pid: 4 604: C:\WINDOWS\system32\config\default.LOG
System pid: 4 60C: C:\WINDOWS\system32\config\SAM
System pid: 4 610: C:\WINDOWS\system32\config\system.LOG
services.exe pid: 552 2A4: C:\WINDOWS\system32\config\AppEvent.Evt
services.exe pid: 552 2B4: C:\WINDOWS\system32\config\Internet.evt
services.exe pid: 552 2C4: C:\WINDOWS\system32\config\SecEvent.Evt
services.exe pid: 552 2D4: C:\WINDOWS\system32\config\SysEvent.Evt
svchost.exe pid: 848 17DC: C:\WINDOWS\system32\config\systemprofile\
Application Data\Microsoft\SystemCertificates\My
ccSetMgr.exe pid: 1172 2EC: C:\WINDOWS\system32\config\systemprofile\
Application Data\Microsoft\SystemCertificates\My
ccEvtMgr.exe pid: 1200 23C: C:\WINDOWS\system32\config\systemprofile\
Application Data\Microsoft\SystemCertificates\My
Rtvscan.exe pid: 1560 454: C:\WINDOWS\system32\config\systemprofile\
Application Data\Microsoft\SystemCertificates\My
handle can provide this information for a specific
process name using the -p
switch.
Using this executable from Perl is straightforward, so we won’t provide any sample code. Instead, let’s look at a related and more interesting operation: auditing.
Windows allows us to efficiently watch a file, directory, or hierarchy of
directories for changes. You could imagine repeatedly performing stat()
s on the desired object or objects, but that
would be highly CPU-intensive. Under Windows, we can ask the operating system to
keep watch for us.
There is a specialized Perl module that makes this job relatively painless for
us:
Win32::ChangeNotify
by Christopher J. Madsen.
There is also a related helper module:
Win32::FileNotify
by Renee Baecker.
Win32::ChangeNotify
is pretty easy to use,
but it does have one gotcha. The module uses the Win32 APIs to ask the OS to let
you know if something changes in a directory. You can even specify what kind of
change to look for (last write time, file or directory names/sizes, etc.). The
problem is that if you ask it to watch a directory for changes, it can tell you
when something changes, but not what has changed. It’s up
to the program author to determine that with some separate code. That’s where
Win32::FileNotify
comes in. If you just
need to watch a single file, Win32::FileNotify
will go the extra step of double-checking
whether the change the OS reported is in the file being audited.
Because they’re so small, we’ll look at examples of both modules. We’ll start with the specific case of watching to see if a file has changed:
use Win32::FileNotify; my $file = 'c:\windows\temp\importantfile'; my $fnot = Win32::FileNotify->new($file); $fnot->wait(); # at this point, our program blocks until $file changes ... # go do something about the file change
And here’s some code to look for changes in a directory (specifically, files coming and going):
use Win32::ChangeNotify; my $dir = 'c:\importantdir'; # watch this directory (second argument says don't watch for changes # to subdirectories) for changes in the filenames found there my $cnot = Win32::ChangeNotify->new( $dir, 0, 'FILE_NAME' ); while (1) { # blocks for 10 secs (10,000 milliseconds) or until a change takes place my $waitresult = $cnot->wait(10000); if ( $waitresult == 1 ) { ... # call or include some other code here to figure out what changed # reset the ChangeNotification object so we can continue monitoring $cnot->reset; } elsif ( $waitresult == 0 ) { print "no changes to $dir in the last 10 seconds\n"; } elsif ( $waitresult == −1 ) { print "something went blooey in the monitoring\n"; last; } }
That was filesystem monitoring. What about network access monitoring? There
are two fairly easy ways to track network operations under Windows. Ideally, as
an administrator you’d like to know which process (and therefore which user) has
opened a network port. While I know of no Perl module that can perform this
task, there are at least two command-line tools that provide the information in
a way that could be consumed by a Perl program. The first,
netstat
, actually ships with the system, but
very few people know it can do this (I certainly didn’t for a long time). Here’s
some sample output:
> netstat -ano
Active Connections
Proto Local Address Foreign Address State PID
TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 932
TCP 0.0.0.0:445 0.0.0.0:0 LISTENING 4
TCP 127.0.0.1:1028 0.0.0.0:0 LISTENING 1216
TCP 192.168.16.129:139 0.0.0.0:0 LISTENING 4
UDP 0.0.0.0:445 *:* 4
UDP 0.0.0.0:500 *:* 680
UDP 0.0.0.0:1036 *:* 1068
UDP 0.0.0.0:1263 *:* 1068
UDP 0.0.0.0:4500 *:* 680
UDP 127.0.0.1:123 *:* 1024
UDP 127.0.0.1:1900 *:* 1108
UDP 192.168.16.129:123 *:* 1024
UDP 192.168.16.129:137 *:* 4
UDP 192.168.16.129:138 *:* 4
UDP 192.168.16.129:1900 *:* 1108
The second is another tool from Mark Russinovich, formerly of Sysinternals: TcpView (or more precisely, the tcpvcon utility that comes in that package). It has the nice property of being able to output the information in CSV form, like so:
> tcpvcon -anc
TCPView v2.51 - TCP/UDP endpoint viewer
Copyright (C) 1998-2007 Mark Russinovich
Sysinternals - www.sysinternals.com
TCP,alg.exe,1216,LISTENING,127.0.0.1:1028,0.0.0.0:0
TCP,System,4,LISTENING,0.0.0.0:445,0.0.0.0:0
TCP,svchost.exe,932,LISTENING,0.0.0.0:135,0.0.0.0:0
TCP,System,4,LISTENING,192.168.16.129:139,0.0.0.0:0
UDP,svchost.exe,1024,*,192.168.16.129:123,*:*
UDP,lsass.exe,680,*,0.0.0.0:500,*:*
UDP,svchost.exe,1068,*,0.0.0.0:1036,*:*
UDP,svchost.exe,1108,*,192.168.16.129:1900,*:*
UDP,svchost.exe,1024,*,127.0.0.1:123,*:*
UDP,System,4,*,192.168.16.129:137,*:*
UDP,svchost.exe,1108,*,127.0.0.1:1900,*:*
UDP,lsass.exe,680,*,0.0.0.0:4500,*:*
UDP,System,4,*,192.168.16.129:138,*:*
UDP,svchost.exe,1068,*,0.0.0.0:1263,*:*
UDP,System,4,*,0.0.0.0:445,*:*
This would be trivial to parse with something like
Text::CSV::Simple
or Text::CSV_XS
.
Let’s see how we’d perform the same tasks within the Unix world.
To handle the tracking of both file and network operations in Unix, we can use a single approach.[25] This is one of few times in this book where calling a separate executable is clearly the superior method. Vic Abell has given an amazing gift to the system administration world by writing and maintaining a program called lsof (LiSt Open Files) that can be found at ftp://vic.cc.purdue.edu/pub/tools/unix/lsof. lsof can show in detail all of the currently open files and network connections on a Unix machine. One of the things that makes it truly amazing is its portability. The latest version as of this writing runs on at least nine flavors of Unix (the previous version supported an even wider variety of Unix flavors) and supports several OS versions for each flavor.
Here’s a snippet of lsof’s output, showing an excerpt of the output for one of the processes I am running. lsof tends to output very long lines, so I’ve inserted a blank line between each line of output to make the distinctions clear:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME firefox-b 27189 dnb cwd VDIR 318,16168 36864 25760428 /home/dnb firefox-b 27189 dnb txt VREG 318,37181 177864 6320643 /net/csw (fileserver:/vol/systems/csw) firefox-b 27189 dnb txt VREG 136,0 56874 3680 /usr/openwin/lib/X11/fonts/Type1/outline/Helvetica-Bold.pfa firefox-b 27189 dnb txt VREG 318,37181 16524 563516 /net/csw (fileserver:/vol/systems/csw) firefox-b 27189 dnb 0u unix 105,43 0t0 3352 /devices/pseudo/tl@0:ticots->(socketpair: 0x1409) (0x300034a1010) firefox-b 27189 dnb 2u unix 105,45 0t0 3352 /devices/pseudo/tl@0:ticots->(socketpair: 0x140b) (0x300034a01d0) firefox-b 27189 dnb 4u IPv6 0x3000349cde0 0t2121076 TCP localhost:32887->localhost:6010 (ESTABLISHED) firefox-b 27189 dnb 6u FIFO 0x30003726ee8 0t0 2105883 (fifofs) ->0x30003726de0 firefox-b 27189 dnb 24r VREG 318,37181 332618 85700 /net/csw (fileserver:/vol/systems/csw) firefox-b 27189 dnb 29u unix 105,46 0t1742 3352 /devices/pseudo/tl@0:ticots->/var/tmp/orbit-dnb/linc -6a37-0-47776fee636a2 (0x30003cc1900->0x300045731f8) firefox-b 27189 dnb 31u unix 105,50 0t0 3352 /devices/pseudo/tl@0:ticots->/var/tmp/orbit-dnb/linc -6a35-0-47772fb086240 (0x300034a13a0) firefox-b 27189 dnb 43u IPv4 0x30742eb79b0 0t42210 TCP desktop.example.edu:32897->images.slashdot.org:www (ESTABLISHED)
This output demonstrates some of the power of this command. It shows the
current working directory (VDIR
), regular
files (VREG
), pipes (FIFO
), and network connections (IPv4/IPv6
) opened by this process.
The easiest way to use lsof from Perl is to invoke its
special “field” mode (-F
). In this mode, its
output is broken up into specially labeled and delimited fields, instead of the
ps-like columns just shown. This makes parsing the
output a cinch.
There is one quirk to the field mode output. It is organized into what the
author calls “process sets” and “file sets.” A process set is a set of field
entries referring to a single process, and a file set is a similar set for a
file. This all makes more sense if we turn on field mode with the 0
option. Fields are then delimited with NUL
(ASCII 0) characters, and sets with NL
(ASCII 12) characters. Here’s a similar group
of lines to those in the preceding output, this time in field mode (NUL
is represented as ^@
). I’ve added spaces between the lines again to make it easier
to read:
p27189^@g27155^@R27183^@cfirefox-bin^@u6070^@Ldnb^@ fcwd^@a ^@l ^@tVDIR^@N0x30001b7b1d8^@D0x13e00003f28^@s36864^@i25760428^@k90^@n/home/dnb^@ ftxt^@a ^@l ^@tVREG^@N0x3000224a0f0^@D0x13e0000913d^@s177864^@i6320 643^@k1^@n/net/csw (fileserver:/vol/systems/csw)^@ ftxt^@a ^@l ^@tVREG^@N0x30001714950^@D0x8800000000^@s35064^@i2800^@k1^@n/usr/lib/nss_files.so.1 ^@tVREG^@N0x300036226c0^@D0x8800000000^@s56874^@i3680^@k1^@n/usr/ openwin/lib/X11/fonts/Type1/outline/Helvetica-Bold.pfa^@ ftxt^@a ^@l ^@tunix^@F0x3000328c550^@C6^@G0x3;0x0^@N0x300034a1010^@D0x8800 000000^@o0t0^@i3352^@n/devices/pseudo/tl@0:ticots->(socketpair: 0x1409) (0x300034a1010)^@ f1^@au^@l ^@tDOOR^@F0x3000328cf98^@C1^@G0x2001;0x1^@N0x3000178b300^@D0x13 c00000000^@o0t0^@i54^@k27^@n/var/run (swap) (door to nscd[240])^@ f4^@au^@l ^@tIPv6^@F0x300037258f0^@C1^@G0x83;0x1^@N0x300034ace50^@d0x3000349 cde0^@o0t3919884^@PTCP^@nlocalhost:32887->localhost:6010^@TST= ESTABLISHED^@TQR=0^@TQS=8191^@TWR=49152^@TWW=13264^@ f5^@au^@l ^@tFIFO^@F0x30003724f50^@C1^@G0x3;0x0^@N0x30003726de0^@d0x30003726 de0^@o0t0^@i2105883^@n(fifofs) ->0x30003726ee8^@ f6^@au^@l ^@tFIFO^@F0x30003725420^@C1^@G0x3;0x0^@N0x30003726ee8^@d0x30003726 ee8^@o0t0^@i2105883^@n(fifofs) ->0x30003726de0^@ f7^@aw^@lW^@tVREG^@F0x30003724c40^@C1^@G0x302;0x0^@N0x30001eadbf8^ @D0x13e00003f28^@s0^@i1539532^@k1^@n/home/dnb (fileserver:/vol/homedirs/systems/dnb)^@ f8^@au^@l ^@tIPv4^@F0x30003724ce8^@C1^@G0x83;0x0^@N0x300034ac010^@d0x 300040604f0^@o0t4094^@PTCP^@ndesktop.example.edu:32931->web -vip.srv.jobthread.com:www^@TST=CLOSE_WAIT^@TQR=0^@TQS=0^@TWR=49640^@TWW=6960^@ f44^@au^@l ^@tVREG^@F0x3000328c5c0^@C1^@G0x2103;0x0^@N0x300051cd3f8^@ D0x13e00003f28^@s276^@i16547341^@k1^@n/home/dnb (fileserver:/vol/ homedirs/systems/dnb)^@ f45^@au^@l ^@tVREG^@F0x30003725f80^@C1^@G0x3;0x0^@N0x300026ad920^@D0x 13e00003f28^@s8468^@i21298675^@k1^@n/home/dnb (fileserver:/vol/homedirs/systems/dnb)^@ f46^@au^@l ^@tIPv4^@F0x30003724a10^@C1^@G0x83;0x0^@N0x309ab62b578^@d0x30742 eb76b0^@o0t20726^@PTCP^@ndesktop.example.edu:32934->216.66.26. 161:www^@TST=ESTABLISHED^@TQR=0^@TQS=0^@TWR=49640^@TWW=6432^@ f47^@au^@l ^@tVREG^@F0x3000328c080^@C1^@G0x2103;0x0^@N0x30002186098^@D0x 13e00003f28^@s66560^@i16547342^@k1^@n/home/dnb (fileserver:/vol/ homedirs/systems/dnb)^@ f48^@au^@l
Let’s deconstruct this output. The first line is a process set (we can tell
because it begins with the letter p
):
p27189^@g27155^@R27183^@cfirefox-bin^@u6070^@Ldnb^@ fcwd^@a ^@l
Each field begins with a letter identifying the field’s contents (p
for pid
,
c
for command
, u
for uid
, and L
for
login
) and ends with a delimiter
character. Together the fields on this line make up a process set. All of the
lines that follow, up until the next process set, describe the open
files/network connections of the process described by this process set.
Let’s put this mode to use. If we wanted to show all of the open files on a system and the PIDs that are using them, we could use code like this:[26]
use Text::Wrap; my $lsofexec = '/usr/local/bin/lsof'; # location of lsof executable # (F)ield mode, NUL (0) delim, show (L)ogin, file (t)ype and file (n)ame my $lsofflag = '-FL0tn'; open my $LSOFPIPE, '-|', "$lsofexec $lsofflag" or die "Unable to start $lsofexec: $!\n"; my $pid; # pid as returned by lsof my $pathname; # pathname as returned by lsof my $login; # login name as returned by lsof my $type; # type of open file as returned by lsof my %seen; # for a pathname cache my %paths; # collect the paths as we go while ( my $lsof = <$LSOFPIPE> ) { # deal with a process set if ( substr( $lsof, 0, 1 ) eq 'p' ) { ( $pid, $login ) = split( /\0/, $lsof ); $pid = substr( $pid, 1, length($pid) ); } # deal with a file set; note: we are only interested # in "regular" files (as per Solaris and Linux, lsof on other # systems may mark files and directories differently) if ( substr( $lsof, 0, 5 ) eq 'tVREG' or # Solaris substr( $lsof, 0, 4 ) eq 'tREG') { # Linux ( $type, $pathname ) = split( /\0/, $lsof ); # a process may have the same pathname open twice; # these two lines make sure we only record it once next if ( $seen{$pathname} eq $pid ); $seen{$pathname} = $pid; $pathname = substr( $pathname, 1, length($pathname) ); push( @{ $paths{$pathname} }, $pid ); } } close $LSOFPIPE; foreach my $path ( sort keys %paths ) { print "$path:\n"; print wrap( "\t", "\t", join( " ", @{ $paths{$path} } ) ), "\n"; }
This code instructs lsof to show only a few of its
possible fields. We iterate through its output, collecting filenames and PIDs in
a hash of lists. When we’ve received all of the output, we print the filenames
in a nicely formatted PID list (thanks to David Muir Sharnoff’s
Text::Wrap
module):
/home/dnb (fileserver:/vol/homedirs/systems/dnb): 12777 12933 27293 28223 /usr/lib/ld.so.1: 10613 12777 12933 27217 27219 27293 28147 28149 28223 28352 28353 28361 /usr/lib/libaio.so.1: 27217 28147 28352 28353 28361 /usr/lib/libc.so.1: 10613 12777 12933 27217 27219 27293 28147 28149 28223 28352 28353 28361 /usr/lib/libmd5.so.1: 10613 27217 28147 28352 28353 28361 /usr/lib/libmp.so.2: 10613 27217 27219 28147 28149 28352 28353 28361 /usr/lib/libnsl.so.1: 10613 27217 27219 28147 28149 28352 28353 28361 /usr/lib/libsocket.so.1: 10613 27217 27219 28147 28149 28352 28353 28361 /usr/lib/sparcv9/libnsl.so.1: 28362 28365 /usr/lib/sparcv9/libsocket.so.1: 28362 28365 /usr/platform/sun4u-us3/lib/libc_psr.so.1: 10613 12777 12933 27217 27219 27293 28147 28149 28223 28352 28353 28361 /usr/platform/sun4u-us3/lib/sparcv9/libc_psr.so.1: 28362 28365 ...
For our last example of tracking Unix file and network operations, let’s return to an earlier example, where we attempted to find IRC bots running on a system. There are more reliable ways to find network daemons like bots than looking at the process table. A user may be able to hide the name of a bot by renaming the executable, but he’ll have to work a lot harder to hide the open network connection. More often than not, this connection is to a server running on TCP ports 6660–7000. lsof makes looking for these processes easy:
my $lsofexec = '/usr/local/bin/lsof'; # location of lsof executable my $lsofflag = '-FL0c -iTCP:6660-7000'; # specify ports and other lsof flags # This is a hash slice being used to preload a hash table, the # existence of whose keys we'll check later. Usually this gets written # like this: # %approvedclients = ('ircII' => undef, 'xirc' => undef, ...); # (but this is a cool idiom popularized by Mark-Jason Dominus) my %approvedclients; @approvedclients{ 'ircII', 'xirc', 'pirc' } = (); open my $LSOFPIPE, "$lsofexec $lsofflag|" or die "Unable to start $lsofexec:$!\n"; my $pid; my $command; my $login; while ( my $lsof = <$LSOFPIPE> ) { ( $pid, $command, $login ) = $lsof =~ /p(\d+)\000 c(.+)\000 L(\w+)\000/x; warn "$login using an unapproved client called $command (pid $pid)!\n" unless ( exists $approvedclients{$command} ); } close $LSOFPIPE;
This is the simplest check we can make. It will catch users who rename eggdrop to something like pine or -tcsh, as well as those users who don’t even attempt to hide their bots. However, it suffers from a similar flaw to our other approach. If a user is smart enough, she may rename her bot to something on our “approved clients” list. To continue our hunt, we could take at least two more steps:
Use lsof to check that the file opened for that executable really is the file we expect it to be, and not some random binary in a user filesystem.
Use our process control methods to check that the user is running this program from an existing shell. If this is the only process running for a user (i.e., if the user has logged off but left it running), it is probably a daemon and hence a bot.
This cat-and-mouse game brings us to a point that will help wrap up the chapter. In Chapter 3, we mentioned that users are fundamentally unpredictable. They do things system administrators don’t anticipate. There is an old saying: “Nothing is foolproof because fools are so ingenious.” It is important to come to grips with this fact as you program Perl for user administration. You’ll write more robust programs as a result, and when one of your programs goes “blooey” because a user did something unexpected, you’ll be able to sit back calmly and admire the ingenuity.
Module |
CPAN ID |
Version |
---|---|---|
|
HMBRAND |
0.32 |
|
WYANT |
1.011 |
|
JHELBERG |
1.0.1.0 |
|
KARASIK |
1.54 |
|
JDB |
0.1703 |
|
DURIST |
0.41 |
|
2.121 | |
|
JDB |
1.05 |
|
RENEEB |
0.1 |
|
MUIR |
2006.1117 |
If you want to install Win32::Setupsup
, you’ll
need to get it from a different PPM repository than the default one configured
when you first installed ActiveState Perl. It can be found (as of this writing)
in the very handy supplementary repository maintained by Randy Kobes at the University of Winnipeg. I’d recommend adding this
repository even if you don’t plan to use Win32::Setupsup
. The easiest way to do this is from the command
line, like so:
$ ppm repo add uwinnipeg http://theoryx5.uwinnipeg.ca/ppms/
or, if using Perl 5.10:
$ ppm repo add uwinnipeg http://cpan.uwinnipeg.ca/PPMPackages/10xx/
You can also add it to the GUI version of PPM4 by choosing Preferences in the Edit menu and selecting the Repositories tab. More info about this repository can be found at http://theoryx5.uwinnipeg.ca/ppms/.
http://aspn.activestate.com/ASPN/Mail/ hosts the Perl-Win32-Admin and Perl-Win32-Users mailing lists. Both lists and their archives are invaluable resources for Win32 programmers.
http://www.microsoft.com/whdc/system/pnppwr/wmi/default.mspx is the current home for WMI at Microsoft.com. This address has changed a few times since the first edition, so doing a web search for “WMI” may be a better way to locate the WMI URL du jour at Microsoft.
http://technet.microsoft.com/sysinternals/ is the home (as of this writing) of the handle program and many other valuable Windows utilities that Microsoft acquired when it bought Sysinternals and hired its principals. http://sysinternals.com still exists as of this writing and redirects to the correct Microsoft URL. If you can’t find these utilities in any of Microsoft’s websites, perhaps going to that URL will point you at the current location.
http://www.dmtf.org is the home of the Distributed Management Task Force and a good source for WBEM information.
If you haven’t yet, you must download the Microsoft Scriptomatic tool (version 2 as of this writing) from http://www.microsoft.com/technet/scriptcenter/tools/scripto2.mspx. This Windows tool from “the Microsoft Scripting Guys” lets you poke around the WMI namespaces on your machine. When you find something you might be interested in using, it can write a script to use it for you. Really. But even better than that, it can write the script for you in VBScript, JScript, Perl, or Python. I’m raving about this tool both here and in the other chapters that mention WMI because I like it so much. If you want to use it under Vista, though, be sure to read the section on Vista in Chapter 1.
[19] In the first edition of this book, this section was called “Using
the Win32::IProc module.” Win32::IProc
shared the fate of the module I describe
in the sidebar The Ephemeral Nature of Modules.
[20] As much as Microsoft would like to see these data access mechanisms become ubiquitous, the likelihood of finding them in a non-Win32 environment is slight. This is why I refer to them as “Win32-specific.”
[21] Microsoft provides WQL, a scaled-down query language based on SQL syntax, for this purpose. Once upon a time it also provided ODBC-based access to the data, but that approach has been deprecated in more recent OS releases.
[22] REPL stands for Read-Eval-Print Loop, a term from the LISP (LISt Processing) world. A REPL lets you type code into a prompt, have it be executed by the language’s interpreter, and then review the results.
[23] See the section Active Directory Service Interfaces for details
on another prominent use of in
.
[24] Later, we’ll look at a module called Proc::ProcessTable
that can do this for you without
you having to write the code.
[25] This is the best approach for portability. Various OSs have their own
mechanisms (inotify, dnotify,
etc.), and frameworks like DTrace are very cool. Mac OS X 10.5+ has a
similar auditing facility to the one we saw with Windows (Mac::FSEvents
gives you easy access to
it). However, none of these options is as portable as the approach
described here.
[26] If you don’t want to parse lsof’s field mode by
hand Marc Beyer’s Unix::Lsof
will
handle the work for you.
Get Automating System Administration with Perl, 2nd Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.