5,276,156 members and growing! (19,923 online)
Email Password   helpLost your password?
General Reading » Book Chapters » New Riders     Intermediate

Win32 Perl Programming: The Administrator's Handbook. Chapter 3 - Tools

By New Riders

This chapter covers an assortment of scripts simplify and lessen the workload of an administrator.
NT4, Windows, Dev

Posted: 12 Mar 2001
Updated: 12 Mar 2001
Views: 70,517
Announcements
Want a new Job?



Search    
Advanced Search
Sitemap
5 votes for this Article.
Popularity: 2.66 Rating: 3.80 out of 5
1 vote, 50.0%
1
0 votes, 0.0%
2
0 votes, 0.0%
3
0 votes, 0.0%
4
1 vote, 50.0%
5
Sample Image
Title Windows NT Win32 Perl Programming: The Administrator's Handbook
Authors Dave Roth
PublisherNew Riders
PublishedOct 2000
ISBN 1578702151
Price US 35.00
Pages 416

Chapter 3 - Tools

Every good administrator assembles a collection of tools that she simply cannot do without. These tools perform specific tasks that enable her to quickly perform her job. A perfect example of such a utility is the popular XCACLS.EXE program that comes with the Windows NT Resource Kit.

This utility (and its little brother CACLS.EXE) dumps any permissions that are applied to a file or directory. Without such a tool, you are doomed to open an Explorer window, select the object in question, right-click, select the Properties context menu, and then go to the Security tab on the resulting property page. If this procedure doesn't sound like very much work, you have not administered a network of thousands of machines with various security permissions applied to various directories and files.

This chapter covers an assortment of scripts that were created for my own toolkit. To be sure, I wrote each of these scripts to simplify and lessen my workload as an administrator. I included these scripts only for the sake of demonstrating how you can leverage Win32 Perl to solve problems that administrators face every day. When you scrutinize the code, it becomes obvious that there is almost no Win32 task that cannot be managed with a bit of Perl coding.

These scripts were not written to be simple or to be tutorials but instead to get into the nitty-gritty of task solving. I recommend that you use a good Win32 API reference while looking at the code that makes up these scripts; in particular, check out Microsoft's MSDN library (http://msdn.microsoft.com/) and, of course, my other book, Win32 Perl Programming: The Standard Extensions (New Riders Publishing).

The File System

The Win32 operating system makes copious use of its file system. As an administrator, you have the burden of managing this important resource. Your job covers the entire range of administrative tasks, ranging from securing files and directories to creating directories and updating outdated files. Many different tools and utilities available from various companies can help you administer your machine's file system. However, you will not be able to easily find some tools.

Many of the existing tools are confusing because they reflect aspects of the file system that are not commonly known, such as alternate data streams. Some tools are not used simply because administrators have no idea that they exist, such as tools used to discover hidden directory shares on remote machines. The following sections focus on file system-related tools that can save you a great deal of work.

Discovering Shared Directories

Ever since Windows for Workgroups was released, sharing directories for others to connect to has been quite simple. From a Windows NT/2000 command line, you can easily share a directory by using the net share command. The following line shares a temporary directory with the name TempDir:

net share TempDir=c:\temp

Alternatively, you can share directories from the File Manager, the Explorer, and the Service Manager GUI applications.

The Win32 world permits you to hide shared directories-that is, share a directory that nobody can see when browsing the network. The only way to access the directory is to know it has been shared and explicitly access the hidden share. You hide directories by appending a dollar sign ($) to the end of the share name. For example, the following command line creates a hidden shared directory called MyHiddenShare$:

net share MyHiddenShare$=c:\temp

Now, only users who know about this hidden share can access it.

You can see which directories your machine is sharing by using this command line:

net share

This line displays all the shared directories on the local machine. However, there is no easy way to discover shared directories on a remote machine. You can always open an Explorer window for a particular machine, but this solution is bulky and displays only nonhidden directories.

Let's say that you need to see what shares are available on some remote machine. Explorer won't help you in this case. You could walk over to the machine, log on, and issue the net share command, but this method is not very convenient, especially if the machine is across town or across the country. And assuming that you don't have some type of Telnet daemon installed, this problem is quite serious.

At this point, you can turn to the DumpShares.pl script (shown in Example 3.1). The actual core of the script revolves around line 11, where a call is made into the Win32::Lanman extension to get a list of shared directory names.

As long as you can physically connect to the remote machine, you can see all its shared directories, regardless of whether or not you are granted administrative privileges on the remote machine. If you lack admin privileges on the remote machine, the shared directory's paths are not visible.

The script takes in any number of remote machine names and tries to resolve each passed-in machine name. If nothing is passed in, the local machine's shared directories are displayed.

Example 3.1 Displaying All Shared Directories on a Remote Machine

01. use Win32;
02. use Win32::Lanman;
03.
04. push( @ARGV, Win32::NodeName() ) if( ! scalar @ARGV );
05. foreach my $Machine ( @ARGV )
06. {
07.   my @List;
08.   $Machine =~ s#^[\\/]*#\\\\#;
09.  
10.   print "\nShare list for ‘$Machine'\n";
11.   if( Win32::Lanman::NetShareEnum( $Machine, \@List ) )
12.   {
13.     foreach my $Share ( @List )
14.     {
15.       my( $Remark, $Path, $NetName );
16.      
17.       $NetName = $Share->{netname};
18.       if( "" ne $Share->{remark} )
19.       {
20.         $Remark = "($Share->{remark})";
21.       }
22.       if( "" ne $Share->{path} )
23.       {
24.         $Path = $Share->{path};
25.       }
26.       else
27.       {
28.         $Path = "No permission to display";
29.       }
30.       push( @ShareList, { name => $NetName,
31.                           remark => $Remark,
32.                           path => $Path } );
33.     }
34.     $~ = Share_Header;
35.     write;
36.
37.     $~ = Share_Info;   
38.     $iCount = 0;
39.
40.     foreach $Share ( sort( {
41.                             lc $a->{name} cmp lc $b->{name}
42.                            } @ShareList ) )
43.     {
44.         $iCount++;
45.         write;
46.     }
47.   }
48.   else
49.   {
50.       print "...not available : ";
51.       print Win32::FormatMessage( Win32::Lanman::GetLastError() );
52.   }
53.  
54.   print "\n";
55. }
56.
57. format Share_Info =
58. @>>> @<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<
59. $iCount, $Share->{name}, $Share->{remark}, $Share->{path} 
60. ~                       ^<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<
61.                         $Share->{remark}, $Share->{path} 
62. ~                       ^<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<
63.                         $Share->{remark}, $Share->{path} 
64. .
65.      
66. format Share_Header =
67. @||| @<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<
68. "Num", "Share Name", "Comment", "Path"
69. ---- ------------------ ----------------------- ------------------------
70. .

Volume Information

For an administrator, discovering details of hard drives on remote machines can be essential. Determining how large the volume is, as well as how much space is available, is an important part of drive management. Example 3.2 retrieves such necessary information and other relevant data, such as the volume name, file system type (NTFS, FAT, FAT32, and so on), serial number, and the volume's flags. The flags indicate what features are available for the drive. The flags and their descriptions are listed in Table 3.1.

Table 3.1 Volume Flags

              Flag                                              Description

FS_CASE_IS_PRESERVED The file system preserves the case of filenames when it places a name on disk.
FS_CASE_SENSITIVE The file system supports case-sensitive file names.
FS_UNICODE_STORED_ON_DISK The file system supports Unicode in filenames as they appear on disk.
FS_PERSISTENT_ACLS The file system preserves and enforces Access Control Lists (ACLs-also known as permissions). For example, NTFS preserves and enforces ACLs, but FAT does not.
FS_FILE_COMPRESSION The file system supports file-based compression.
FS_VOL_IS_COMPRESSED The specified volume is a compressed volume-for example, a DoubleSpace volume.
FILE_NAMED_STREAMS The file system supports named streams.
FILE_SUPPORTS_ENCRYPTION The file system supports the Encrypted File System (EFS).
FILE_SUPPORTS_OBJECT_IDS The file system supports object identifiers.
FILE_SUPPORTS_REPARSE_POINTS The file system supports reparse points. This capability allows for features such as using NTFS links and moving infrequently accessed data to long-term storage such as tape backup.
FILE_SUPPORTS_SPARSE_FILES The file system supports sparse files. Sparse files have disk storage allocated only for the parts of the file that contain data. Therefore, large blocks of 0s are not stored to disk to conserve drive storage space.
FILE_VOLUME_QUOTAS The file system supports space allocation limits on a per user basis.


The script accepts a list of drive letters or UNCs like this:

perl volume.pl c:\ \\server\share

In this case, the script displays volume information for both the local machine's C:\ drive as well as for the volume that is shared by the UNC \\server\share. Figure 3.1 illustrates the output from this code. If no parameter is passed in, information regarding the volume for the current directory is displayed.

The code uses a Win32 API function that was introduced with Windows NT 4.0 and Windows 95 OEM Service Release 2 (OSR2). It will most likely fail on platforms earlier than this.

C:\>perl volume.pl c:\ \\server\share\

c:\:
FS    Serial    Volume Name                    Flags
----- --------- ------------------------------ ----------------------------
NTFS  DCF6-AF23 This is my test volume         FS_CASE_SENSITIVE
Total Size: 4,301,788,672                      FS_CASE_IS_PRESERVED
Avail Size: 726,140,928                        FS_UNICODE_STORED_ON_DISK
                                               FILE_VOLUME_QUOTAS
                                               FS_FILE_COMPRESSION
                                               FS_PERSISTENT_ACLS
                                               FILE_SUPPORTS_OBJECT_IDS
                                               FILE_SUPPORTS_SPARSE_FILES
                                               FILE_SUPPORTS_REPARSE_POINTS
                                               FILE_SUPPORTS_ENCRYPTION

\\server\share\:
FS    Serial    Volume Name                    Flags
----- --------- ------------------------------ ----------------------------
NTFS  8063-FA56 New Volume                     FS_CASE_SENSITIVE
Total Size: 18,202,226,688                     FS_CASE_IS_PRESERVED
Avail Size: 12,755,464,192                     FS_UNICODE_STORED_ON_DISK
                                               FILE_VOLUME_QUOTAS
                                               FS_FILE_COMPRESSION
                                               FS_PERSISTENT_ACLS
                                               FILE_SUPPORTS_OBJECT_IDS
                                               FILE_SUPPORTS_SPARSE_FILES
                                               FILE_SUPPORTS_REPARSE_POINTS
                                               FILE_SUPPORTS_ENCRYPTION

Figure 3.1 Displaying volume information.


Example 3.2 Retrieving Volume Information

01. use Win32::API::Prototype;
02.
03. ApiLink( ‘kernel32.dll', ‘BOOL GetVolumeInformation(
04.                                   LPCTSTR lpRootPathName,
05.                                   LPTSTR lpVolumeNameBuffer,
06.                                   DWORD nVolumeNameSize,
07.                                   LPDWORD lpVolumeSerialNumber,
08.                                   LPDWORD lpMaximumComponentLength,
09.                                   LPDWORD lpFileSystemFlags,
10.                                   LPTSTR lpFileSystemNameBuffer,
11.                                   DWORD nFileSystemNameSize )' )
12.     || die "Can not link to GetVolumeInformation()";
13. ApiLink( ‘kernel32.dll', ‘BOOL GetDiskFreeSpaceEx(
14.                                   LPCTSTR lpDirectoryName,
15.                                   PVOID lpFreeBytesAvailable,
16.                                   PVOID lpTotalNumberOfBytes,
17.                                   PVOID lpTotalNumberOfFreeBytes )' )
18.     || die "Wrong version of NT";
19.
20. %FLAGS = (
21.   0x00000001  =>  ‘FS_CASE_SENSITIVE',
22.   0x00000002  =>  ‘FS_CASE_IS_PRESERVED',
23.   0x00000004  =>  ‘FS_UNICODE_STORED_ON_DISK',
24.   0x00000008  =>  ‘FS_PERSISTENT_ACLS',
25.   0x00000010  =>  ‘FS_FILE_COMPRESSION',
26.   0x00000020  =>  ‘FILE_VOLUME_QUOTAS',
27.   0x00000040  =>  ‘FILE_SUPPORTS_SPARSE_FILES',
28.   0x00000080  =>  ‘FILE_SUPPORTS_REPARSE_POINTS',
29.   0x00008000  =>  ‘FS_VOL_IS_COMPRESSED',
30.   0x00000000  =>  ‘FILE_NAMED_STREAMS',
31.   0x00020000  =>  ‘FILE_SUPPORTS_ENCRYPTION',
32.   0x00010000  =>  ‘FILE_SUPPORTS_OBJECT_IDS',
33. );
34.
35. push( @ARGV, "\\" ) if( 0 == scalar @ARGV );
36.
37. foreach my $Path ( @ARGV )
38. {
39.   my $dwVolSize = 256;
40.   my $szVolName = NewString( $dwVolSize );
41.   my $pdwSerialNum = pack( "L", 0 );
42.   my $pdwMaxLength = pack( "L", 0 );
43.   my $pdwFlags = pack( "L", 0 );
44.   my $dwFSNameSize = 256;
45.   my $szFSName = NewString( $dwFSNameSize );
46.   
47.   $Path .= "\\" unless( $Path =~ /\\$/ );
48.   next unless( -d $Path );
49.   $Path = NewString( $Path );
50.  
51.   print "\n$Path:\n";
52.   $~ = DumpVol;
53.   if( GetVolumeInformation( $Path, $szVolName,
54.                             $dwVolSize, $pdwSerialNum,
55.                             $pdwMaxLength, $pdwFlags,
56.                             $szFSName, $dwFSNameSize ) )
57.   {
58.     local %Vol = (
59.       path        =>  $Path,
60.       volume_name =>  CleanString( $szVolName ),
61.       serial_num  =>  sprintf( "\U%04x-%04x",
62.                                reverse( unpack( "S2",
63.                                                 $pdwSerialNum ) ) ),
64.       max_length  =>  unpack( "L", $pdwMaxLength ),
65.       flags       =>  DecodeFlags( unpack( "L", $pdwFlags ) ),
66.       fs_name     =>  CleanString( $szFSName )
67.     );
68.     my $pFree  = pack( "L2", 0, 0 );
69.     my $pTotal = pack( "L2", 0, 0 );
70.     my $pTotalFree = pack( "L2", 0, 0 );
71.     if( GetDiskFreeSpaceEx( $Path, $pFree, $pTotal, $pTotalFree ) )
72.     {
73.       $Vol{free} = FormatNumber( MakeLargeInt( unpack( "L2",
74.                                                        $pTotalFree ) ) );
75.       $Vol{total} = FormatNumber( MakeLargeInt( unpack( "L2",
76.                                                         $pTotal ) ) );
77.     }
78.     write;
79.   }
80. }
81.
82. sub MakeLargeInt
83. {
84.   my( $Low, $High ) = @_;
85.   return( $High * ( 1 + 0xFFFFFFFF ) + $Low );
86. }
87.
88. sub DecodeFlags
89. {
90.   my( $Flags ) = @_;
91.   my $String = "";
92.   foreach my $Key ( keys( %FLAGS ) )
93.   {
94.     if( $Flags & $Key )
95.     {
96.       $String .= sprintf( "%- 30s\n", $FLAGS{$Key} );
97.     }
98.   }
99.   return( $String );       
100. }
101.
102. sub FormatNumber
103. {
104.   my( $Num ) = @_;
105.   {} while( $Num =~ s/^(-?\d+)(\d{3})/$1,$2/ );
106.   return( $Num );
107. }
108.
109. format DumpVol =
110. FS    Serial    Volume Name                       Flags
111. ----- --------- --------------------------- ----------------------------
112. @<<<< @<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<
113. $Vol{fs_name}, $Vol{serial_num}, $Vol{volume_name}, $Vol{flags}
114. Total Size: @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<
115. $Vol{total}                                 $Vol{flags
116. Avail Size: @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<
117. $Vol{free}                                  $Vol{flags}
118. ~                                           ^<<<<<<<<<<<<<<<<<<<<<<<<<<<
119.                                             $Vol{flags}
120. ~                                           ^<<<<<<<<<<<<<<<<<<<<<<<<<<<
121.                                             $Vol{flags}
122. ~                                           ^<<<<<<<<<<<<<<<<<<<<<<<<<<<
123.                                             $Vol{flags}
124. ~                                           ^<<<<<<<<<<<<<<<<<<<<<<<<<<<
125.                                             $Vol{flags}
126. ~                                           ^<<<<<<<<<<<<<<<<<<<<<<<<<<<
127.                                             $Vol{flags}
128. ~                                           ^<<<<<<<<<<<<<<<<<<<<<<<<<<<
129.                                             $Vol{flags}
130. ~                                           ^<<<<<<<<<<<<<<<<<<<<<<<<<<<
131.                                             $Vol{flags}
132. ~                                           ^<<<<<<<<<<<<<<<<<<<<<<<<<<<
133.                                             $Vol{flags}
134. ~                                           ^<<<<<<<<<<<<<<<<<<<<<<<<<<<
135.                                             $Vol{flags}
136. .

Creating Hard File Links

UNIX users could always create hard links between files, and starting with Windows 2000, so can users of the Win32 platform. Previous versions of Windows NT were able to create links by utilizing a POSIX subsystem; however, with Windows 2000 the link functionality is built directly into the Win32 API. Unfortunately, these links are not as rich as their UNIX brethren. You can create links only for files but not directories. And like UNIX's hard links, they must be located on the same volume. This means that a link must reside on the same hard drive as the file it is linked to. Win32, however, does not have the capability to symbolically link across volumes. The closest equivalent is a shortcut, but that is interpreted by an application, not the operating system (therefore, you cannot rely on all programs to understand how to use them).

This hard-linking capability is supported only on NTFS volumes. Example 3.3 creates such hard links. The script takes in two parameters like this:

perl ln.pl c:\temp\MyExistingFile.txt c:\MyNewLink.txt

The first parameter is the existing file, and the second parameter is the path to where the new linked file will be created. This new link file will be (for all practical purposes) the same file as specified in the first parameter. Editing either of these files results in both files being modified. This makes sense because you really have only one file, just two separate file system entries for it.

After a hard link has been created on an NTFS volume, it is literally linked to the original file. Even if you move the original or linked file, they both still point to the same data. Additionally, you can modify the content of either file, and it updates the same data. This can cause concurrency problems if you are accessing multiple files all linked together. Because the file is only a link to the original file, it can be deleted without affecting the original file.

Example 3.3 Creating Hard Links

01. use Win32::API::Prototype;
02.
03. if( 2 != scalar @ARGV )
04. {
05.     print "Syntax:\n  $0 <File to link to> <New Link File>\n";
06.     exit();
07. }
08.
09. my( $NewLink, $File ) = @ARGV;
10. ApiLink( ‘kernel32.dll',
11.          ‘BOOL CreateHardLink( LPCTSTR lpFileName,
12.                                LPCTSTR lpExistingFileName,
13.                                LPSECURITY_ATTRIBUTES lpSa )' )
14.   || die "This version of Windows does not support CreateHardLink()";
15. if( CreateHardLink( $File, $NewLink, 0 ) )
16. {
17.     print "Link successfully created.\n";
18. }
19. else
20. {
21.     print "Failed to create link.\nError: ";
22.     print Win32::FormatMessage( Win32::GetLastError() ), "\n";
23. }

Creating Directories

Creating directories is not a new concept. All disk-based operating systems such as UNIX, Win32, DOS, and Macintosh have the concept of some file-based container (for example, a directory or folder). You must create a container so that you can move objects such as files and other directories into it. You usually do so by using a command such as md or mkdir. Win32 exposes this functionality through its Win32 API CreateDirectory() and CreateDirectoryEx() functions. It is also available from a DOS box when you use the mkdir or md command. These techniques, however, fail to create intermediary directories. For example, if you run

md Dir1\Dir2

and Dir1 does not already exist, then Dir2 will not be created.

Tip

On the Windows NT and 2000 platforms, if command extensions are enabled, the DOS box md and mkdir commands create any intermediary directories. For more information, run cmd.exe /h.

Even Win32 Perl fails to create any intermediary directories-most likely because it simply calls into the C: library's mkdir() function, which, in turn, calls the Win32 API's CreateDirectory() function.

Example 3.4 creates as many directories as it needs in order to create the passed-in directory. There is nothing Win32-specific about this particular script; therefore, it could run on any platform that runs Perl. This script accepts only one parameter, which must be either a full path (for example, C:\temp\Dir1\Dir2), a relative path (for example, ..\Dir1\Dir2), or a UNC (for example, \\server\share\Dir1\Dir2).

To use this script, just pass in the name of the path to create-for example,

perl makedir.pl \\server\share\dir1\dir2
Example 3.4  Creating Intermediary Directories
01. $Dir = shift @ARGV || die "Syntax: $0 <Dir>";
02. $Result = 1;
03. ( $Root, $Path ) = ( $Dir =~ /^(\w:\\?|\\\\.+?\\.+?\\|\\)?(.*)$/ );
04. print "Creating directory ‘$Dir'...\n";
05.
06. if( -d $Dir )
07. {
08.     print "Directory already exists.\n";
09.     exit;
10. }
11.
12. @DirList = split( /\\/, $Path );
13. $Path = $Root;
14.
15. while( $Result && scalar @DirList )
16. {
17.     $Path .= ( shift @DirList ) . "\\";
18.
19.     next if( -d $Path );
20.     $Result = mkdir( $Path, 0777 );
21. }
22.
23. print ( ( $Result )? "Success" : "Failure (Error: $!)" ), "\n";

NTFS Streams

One of the greatest joys of using the NTFS file system is the ability to create multiple streams in a file. However, most administrators, let alone users, are not even aware of this capability.

Just like any file system, an NTFS file contains data. However, unlike other file systems, in this one the data is stored in what is called an NTFS stream. This stream is simple data that is associated with the file by means of a name. This is very similar to the way a Perl hash works. A stream name is much like a hash key. Data is associated with a hash key, just as data can be associated with a file stream name. By default, all NTFS files have only one stream, the default stream, which is the place data is stored. However, NTFS allows additional streams to be created in a file. Because each stream has a name (with the exception of the default stream), you access the stream by name.

Thinking of multiple data segments for a single file may be mind-bending-but only because it is not common. Think of multiple streams as you would a tar file, which is basically an archive of multiple data files. You could create one file with multiple streams, each stream consisting of data from different files. This would enable you to create a log file for, say, a Web server that consists of only one file. But each day the Web server creates a new log, it creates a new stream in the log file. The end result is that you could have an entire year's worth of Web server log entries consolidated into one convenient file. Then you can easily copy all the logged data from one place to another because you are copying only one file.

A Windows NT/2000 machine can store Macintosh files on its NTFS drives this way. By using alternate data streams, the operating system can store both the Macintosh data and resource forks in one file.

To access an NTFS stream you must refer to the NTFS file as you normally would, but you use a colon followed by the stream name. The format is

path\file_name:stream_name

Here's an example:

C:\temp\MyTestFile.txt:MyFirstStream

Perl can read and write to such NTFS streams using the normal file functions. You can test this capability by running Example 3.5 and Example 3.6 on an NTFS drive. Example 3.5 creates a text file with two streams: the main data stream and an alternative stream.

Example 3.6 reads back the data from both streams in the text file. If you examine the resulting text file, you will notice that the file size reflects only the data in the default data stream. If you were unaware that the file contained multiple streams, there would normally be no way to discover this fact. As you will see, Win32 Perl can be used to discover such alternate data streams.

Example 3.5 Creating a Test File with Multiple Streams

01. $File = "StreamTest.txt";
02. $Stream = "AltDataStream";
03. if( open( STREAM1, "> $File" ) )
04. {
05.   print "Writing to the main data stream.\n";
06.   print STREAM1 "\tWelcome to the main data stream.\n";
07.   close( STREAM1 );
08.  
09.   if( open( STREAM2, "> $File:$Stream" ) )
10.   {
11.     print "Writing to alternative stream ‘$Stream'.\n";
12.     print STREAM2 "\tWelcome to the alternative data stream ‘$Stream'\n";
13.     close( STREAM2 );
14.   }
15.   else
16.   {
17.     print "Error: $!\n";
18.   }
19. }
20. else
21. {
22.   print "Error: $!\n";
23. }

Example 3.6 Reading Multiple Streams from a Test File

01. $File = "StreamTest.txt";
02. $Stream = "AltDataStream";
03. if( open( STREAM1, "< $File" ) )
04. {
05.   print  "This is the main data stream for $File.\n";
06.   while( my $Line = <STREAM1> )
07.   {
08.     print $Line;
09.   }
10.   close( STREAM1 );
11.  
12.   if( open( STREAM2, "< $File:$Stream" ) )
13.   {
14.     print "\n\nThis is the data stream $File:$Stream:\n";
15.     while( my $Line = <STREAM2> )
16.     {
17.       print $Line;
18.     }
19.     close( STREAM2 );
20.   }
21.   else
22.   {
23.     print "Error: $!\n";
24.   }
25. }
26. else
27. {
28.   print "Error: $!\n";
29. }

On Windows NT/2000, the more command can be coaxed into displaying alternate data streams. You do so by redirecting the input into the more command from a data stream like this:

more < test.txt:MyStream

Tip

For some reason, most programs do not permit you to enter valid NTFS pathnames that include alternate streams. For example, an attempt to load test.txt:MyStream into Notepad, Microsoft Word, or various other programs will fail.

Example 3.7 is a replacement for the DOS type command. This script functions the same way, except that it can display NTFS data streams-something that type cannot do.

The script accepts any number of paths to display. This includes paths with wildcards. For example,

perl streamdump.pl myfile.txt:altStream1 c:\temp\*.txt

Example 3.7 Displaying NTFS Alternate Data Stream Content

01. foreach my $Mask ( @ARGV )
02. {
03.   my @List;
04.
05.   # Must be careful since glob() does not support
06.   # paths with embedded spaces.
07.   if( @List = glob( $Mask ) )
08.   {
09.     push( @FileList, @List );
10.   }
11.   else
12.   {
13.     push( @FileList, $Mask );
14.   }
15. }
16. foreach my $Path ( @FileList )
17. {
18.   print $Path, "\n";
19.   print "-" x length( $Path ), "\n";
20.   if( open( FILE, "< $Path" ) )
21.   {
22.     my $Buffer;
23.
24.     binmode( FILE );
25.     while( read( FILE, $Buffer, 1024 ) )
26.     {
27.       print $Buffer;
28.     }
29.     close( FILE );
30.   }
31.   else
32.   {
33.     print "\tError: $!\n";
34.   }
35.   print "\n";
36. }

NTFS, Alternative Data Streams, and File Sizes

Interestingly enough, the reported size of a file does not reflect the contents of alternative data streams. For example, if you create a 32-byte file and add an alternative data stream that is 500,000 bytes in size, the file still reports only 32 bytes. This makes it even more impractical for an administrator to determine exactly how much space a file truly takes up.

Just to help complicate NTFS size reporting issues, due to the nature of NTFS, a directory listing may report unexpected available disk space values. For example, if you create this text file

C:\>echo Big Old Test > test.txt

a directory listing shows how large the file is and how much space is available on the disk:

C:\>dir test.txt
 Volume in drive C is unnamed
 Volume Serial Number is DCF6-AF23

 Directory of C:\

06/01/2000  09:14a                  15 test.txt
               1 File(s)             15 bytes
               0 Dir(s)     592,346,112 bytes free

If you rewrite the file to a different file size,

C:\>echo This is really a test > test.txt

a directory listing yields

C:\>dir test.txt
 Volume in drive C is unnamed
 Volume Serial Number is DCF6-AF23

 Directory of C:\

06/01/2000  09:14a                  24 test.txt
               1 File(s)             17 bytes
               0 Dir(s)     592,346,112 bytes free

Notice that even though the file size changes, the amount of available drive space does not because NTFS calculates two different size values for each file: how large the file is and how much disk space the file takes up. In this case, the file size is either 15 or 24 bytes. However, the file takes up (at least) one sector of disk space. And each sector (on this particular volume) is made up of 1 kilobyte (1024 bytes) of space. Therefore, it takes 1024 bytes of disk space to store only 15 bytes of data. When NTFS computes the available disk space, that amount is based on the amount of unallocated disk space. Because disk space is taken up in 1KB blocks, the available free space is always rounded down to a multiple of 1024.

Enumerating NTFS Streams

The one problem with NTFS streams is that there is no way to enumerate the streams in a file. This problem is rather tragic, considering how powerful NTFS streams can be. Basically, this means that if you forget which streams you have added to a file, there is no way to discover them. This situation is akin to removing the dir command from the DOS box: How would you possibly discover which files are in a directory? Yes, bizarre as it sounds, this is the reality of Win32's lack of proper support for NTFS streams.

The good news is that you can use a hack to enumerate streams. This solution involves reading the file as backup software does, using the Win32 API's BackupRead() function.

The Streams.pl script (see Example 3.8) displays the names and sizes of all streams in an NTFS file. The script accepts any number of parameters from the command line. Each parameter is a path to a file that is to be examined for alternate streams. The file path can contain wildcards.

Example 3.8 Enumerating Streams in an NTFS File

01. use Win32::API::Prototype;
02.
03. $OPEN_EXISTING = 3;
04. $GENERIC_READ  = 0x80000000;                                    
05. $BACKUP_DATA   = 0x00000001;
06. $BACKUP_ALTERNATE_DATA = 0x00000004;
07.
08. ApiLink( ‘kernel32.dll',
09.          ‘HANDLE CreateFile( LPCTSTR pszPath,
10.                              DWORD dwAccess,
11.                              DWORD dwShareMode,
12.                              PVOID SecurityAttributes,
13.                              DWORD dwCreationDist,
14.                              DWORD dwFlags,
15.                              HANDLE hTemplate )' )
16.     || die "Can not locate CreateFile()";
17. ApiLink( ‘kernel32.dll',
18.          ‘BOOL CloseHandle( HANDLE hFile )' )
19.     || die "Can not locate CloseHandle()";
20. ApiLink( ‘kernel32.dll',
21.          ‘BOOL BackupRead( HANDLE hFile,
22.                            LPBYTE pBuffer,
23.                            DWORD dwBytesToRead,
24.                            LPDWORD pdwBytesRead,
25.                            BOOL bAbort,
26.                            BOOL bProcessSecurity,
27.                            LPVOID *ppContext)' )
28.     || die "Can not locate BackupRead()";
29. ApiLink( ‘kernel32.dll',
30.          ‘BOOL BackupSeek( HANDLE hFile,
31.                            DWORD dwLowBytesToSeek,
32.                            DWORD dwHighBytesToSeek,
33.                            LPDWORD pdwLowByteSeeked,
34.                            LPDWORD pdwHighByteSeeked,
35.                            LPVOID *pContext )' )
36.     || die "Can not create BackupSeek()";
37.
38. # Generate a list of all files to process
39. # therefore we have to expand any wildcards
40. foreach my $Mask ( @ARGV )
41. {
42.     foreach my $Path ( glob( $Mask ) )
43.     {
44.         push( @Files, $Path ) if( -f $Path );
45.     }
46. }
47.
48. foreach my $File ( @Files )
49. {
50.   print "$File\n";
51.  
52.   $hFile = CreateFile( $File,
53.                        $GENERIC_READ,
54.                        0,
55.                        undef,
56.                        $OPEN_EXISTING,
57.                        0,
58.                        0 ) || die "Can not open the file ‘$File'\n";
59.
60.   # If CreateFile() failed $hFile is a negative value
61.   if( 0 < $hFile )
62.   {
63.     my $iStreamCount = 0;
64.     my $iTotalSize = 0;
65.     my $pBytesRead = pack( "L", 0 );
66.     my $pContext = pack( "L", 0 );
67.     my $pStreamIDStruct = pack( "L5", 0,0,0,0,0,0 );
68.    
69.     while( BackupRead( $hFile,
70.                        $pStreamIDStruct,
71.                        length( $pStreamIDStruct ),
72.                        $pBytesRead,
73.                        0,
74.                        0,
75.                        $pContext ) )
76.     {
77.       my $BytesRead = unpack( "L", $pBytesRead );
78.       my $Context = unpack( "L", $pContext );
79.       my %Stream;
80.       my( $pSeekBytesLow, $pSeekBytesHigh ) = ( pack( "L", 0 ),
81.                                                 pack( "L", 0 ) );
82.       my $StreamName = "";
83.       my $StreamSize;
84.      
85.       # No more data to read
86.       last if( 0 == $BytesRead );
87.
88.       @Stream{ id, attributes,
89.                size_low, size_high,
90.                name_size } = unpack( "L5", $pStreamIDStruct );
91.      
92.       if( $BACKUP_ALTERNATE_DATA == $Stream{id} )
93.       {
94.         $StreamName = NewString( $Stream{name_size} );
95.         if( BackupRead( $hFile,
96.                         $StreamName,
97.                         $Stream{name_size},
98.                         $pBytesRead,
99.                         0,
100.                         0,
101.                         $pContext ) )
102.         {
103.           my $String = CleanString( $StreamName, 1 );
104.           $String =~ s/^:(.*?):.*$/$1/;
105.           $StreamName = $String;
106.         }
107.       }
108.       elsif( $BACKUP_DATA == $Stream{id} )
109.       {
110.         $StreamName = "<Main Data Stream>";
111.       }
112.       $StreamSize = MakeLargeInt( $Stream{size_low}, $Stream{size_high} );
113.       $iTotalSize += $StreamSize;
114.
115.       printf( "  % 3d) %s (%s bytes)\n",
116.               ++$iStreamCount,
117.               $StreamName,
118.               FormatNumber( $StreamSize )  ) if( "" ne $StreamName );
119.      
120.       # Move to next stream...
121.       if( ! BackupSeek( $hFile,
122.                         $Stream{size_low},
123.                         $Stream{size_high},
124.                         $pSeekBytesLow,
125.                         $pSeekBytesHigh,
126.                         $pContext ) )
127.       {
128.         last;
129.       }
130.       $pBytesRead = pack( "L2", 0 );
131.       $pStreamIDStruct = pack( "L5", 0,0,0,0,0 );
132.     }
133.     printf( "       Total file size: %s bytes\n",
134.             FormatNumber( $iTotalSize ) );
135.     # Abort the backup reading. Win32 API claims we MUST do this.
136.     BackupRead( $hFile, undef, 0, 0, 1, 0, $pContext );
137.     CloseHandle( $hFile );
138.   }
139.   print "\n";
140. }
141.
142. sub FormatNumber
143. {
144.   my( $Num ) = @_;
145.   {} while( $Num =~ s/^(-?\d+)(\d{3})/$1,$2/ );
146.   return( $Num );
147. }
148.
149. sub MakeLargeInt
150. {
151.   my( $Low, $High ) = @_;
152.   return( $High * ( 1 + 0xFFFFFFFF ) + $Low );
153. }

Discovering File Versions

Many binary files, such as programs and DLLs, can have version information branded into them. Such branding is not required but is incredibly helpful. When you install software that has updated binary files, it usually checks the version information on files to make sure it is not overwriting a file with an older version.

Figure 3.2 shows all the version information on a given binary file. This particular display is useful in determining not only what version the file is, but also what platform the file was created for (NT, Win32, Win16, DOS).

C:\>perl filever.pl c:\winnt\system32\AUTPRX32.DLL
1) c:\winnt\system32\AUTPRX32.DLL
        Comments............January 15, 1997
        CompanyName.........Microsoft Corporation
        FileDescription.....Microsoft Remote Automation Proxy for Windows
                            NT(TM) Operating System
        FileVersion.........5.00.3715
        InternalName........AUTPRX32.DLL
        LangID..............0x0409
        Language............English (United States)
        LegalCopyright......Copyright ¬ Microsoft Corp. 1995.
        LegalTrademarks.....Microsoft« is a registered trademark of
                            Microsoft Corporation. Windows(TM) is a
                            trademark of Microsoft Corporation
        ProductName.........Microsoft Remote Automation
        ProductVersion......5.00.3715
        Internal Info:
            DateStamp.......0
            Debug...........0
            FileType........dll
            FileVersion.....5.0.37.15
            OS..............nt
            Patched.........0
            PreRelease......0
            ProductVersion..5.0.37.15
            SpecialBuild....0

Figure 3.2 Displaying version information on a DLL file.

Example 3.9 accepts any number of parameters, each representing a path to a binary file. The paths can include wildcards and can be UNCs to access version information residing on remote machines.

Example 3.9 Displaying File Version Information

01. use Win32::AdminMisc;
02.
03. my $iCount = 0;
04. if( 0 == scalar @ARGV )
05. {
06.   Syntax();
07.   exit;
08. }
09.
10. foreach my $File ( @ARGV )
11. {
12.   my %Info;
13.
14.   $iCount++;
15.   print "$iCount) $File  ";
16.   if( -f $File )
17.   {
18.     if( Win32::AdminMisc::GetFileInfo( $File, \%Info ) )
19.     {
20.       print "\n";
21.       foreach my $Attr ( sort( keys( %Info ) ) )
22.       {
23.         next if( ref $Info{$Attr} );
24.         Display( $Attr, $Info{$Attr} );
25.       }
26.
27.       if( $Info{FileInfo} )
28.       {
29.         Display( "Internal Info:" );
30.         foreach my $Attr ( sort( keys( %{$Info{FileInfo}} ) ) )
31.         {
32.           next if( ref $Info{FileInfo}->{$Attr} );
33.           Display( "  $Attr", $Info{FileInfo}->{$Attr} );
34.         }
35.       }
36.     }
37.     else
38.     {
39.       print "unable to get version.\n";
40.     }
41.   }
42.   else
43.   {
44.     print "unable to locate file.\n";
45.   }
46.
47. }
48.
49. sub Display
50. {
51.   my( $Attr, $Data ) = @_;
52.
53.   print "\t$Attr";
54.   if( "" ne $Data )
55.   {
56.     print "." x ( 20 - length( $Attr ) ), "$Data";
57.   }
58.   print "\n";
59. }
60.
61. sub Syntax
62. {
63.   my( $Line ) = "-" x length( $0 );
64.
65.   print <<EOT;
66.
67. $0
68. $Line
69. Display version information on a particular file.
70.
71. Syntax:
72.   perl $0 File[, File2[, ... ]]]
73.
74. EOT
75. }

Permissions

Win32 security is, in part, based on permissions. Permissions, which are placed on securable objects (such as files, directories, Registry keys, and network shares), designate which users have access to perform actions. The topic of permissions is quite involved and can easily take up an entire book by itself, so I won't go into detail on how they work. For this type of information, you can consult any number of different resources, including http://www.roth.net/perl/perms/security/.

Displaying Permissions

UNIX machines have always been capable of listing who has permissions on files and directories. You get this listing by using the ls command. Win32's equivalent to ls is dir, but it does not display any permission information.

Example 3.10 displays permission information on any path passed in on the command line. This script accepts a number of parameters representing paths to securable objects, as in this example:

perl perm.pl c:\temp\*.txt \\server\HKLM\Software\ActiveState \\server2\Temp$

This example displays the permissions for all text files in the c:\temp directory as well as the HKEY_LOCAL_MACHINE\Software\ActiveState Registry key on the machine \\server and the Temp$ share on the machine \\server2.

Example 3.10 displays the permissions in the form of an array of letters. Each letter indicates the type of permission that the specified user has been granted. Table 3.2 describes these letters.

Table 3.2 Permissions by Letter

           Letter                     Permission

R User is allowed to open the object for reading.
r User is not allowed to open the object for reading.
W User is allowed to open the file for writing.
w User is not allowed to open the file for writing.
D User is allowed to delete the object.
d User is not allowed to delete the object.
X User is allowed to execute the object. When this permission is applied to a directory or Registry key, the user is allowed to access its contents.
x User is not allowed to execute the object. When this permission is applied to a directory or Registry key, the user is not allowed to access its contents.
P User is allowed to change the object's permissions.
p User is not allowed to change the object's permissions.
O User is allowed to take ownership of the object.
o User is not allowed to take ownership of the object.
A User has been granted all permissions. This permission may be set in addition to other flags.
a User has been denied all permissions. This permission may be set in addition to other flags.

Figure 3.3 shows the permissions on a specific directory (C:\temp). The script first prints the name of the owner and group of the object (in this case, a directory). Next, it prints each user and group account that has been granted or denied access to the object. The type of object that the permission relates to follows the account name. In this example, the Administrator account is granted directory permissions, and the Perltest7 account has been granted both directory and file permissions.

The permissions are then displayed. This seven-letter string indicates which permissions are granted, denied, or not addressed. A capital letter indicates that the related permission has been explicitly granted, and a lowercase letter indicates that the permission has been explicitly denied. You can see that Perltest7 has been granted read and execute but denied write permissions on files in the C:\temp directory. Additionally, the account has been denied permission to take ownership of the directory. On the other hand, the Administrator account has been granted all permissions. The letter A means that the account has been granted all permissions. This means that there was no need to explicitly grant the read, write, execute, and delete permissions. Notice that the Joel account has been granted all access (just like the Administrator); however, the delete permission has been explicitly denied. Because explicitly denied access always overrides allowed access, the user has full control over the directory, except that he cannot delete it.

C:\>perl perm.pl c:\temp

Permissions for ‘c:\temp':
  Owner: BUILTIN\Administrators
  Group: BUILTIN\Domain Admins
         Administrator (Directory)  RWXD--A
                  Joel (Directory)  RWXd--A
             Perltest7 (Directory)  -----o-
             Perltest7 (File)       RwX----

Figure 3.3 Displaying permissions on a directory.


Notes on Files and Directories

For files and directory paths, wildcards are acceptable; however, for Registry keys, they are not.

You can specify files and directories on remote machines by using a UNC. However, if you're specifying a UNC's root directory, make sure that you terminate the path with a backslash like this:

\\server\share\

If you do not append the backslash, the permissions of the network share itself are referenced.

Notes on Shared Directories and Printers

Shared directory and printer pathnames must not end with a backslash. If you specify a shared directory path with an ending backslash, the permissions for the shared directory's root directory are displayed, not the permissions for the network share itself. An attempt to reference a printer with an ending backslash simply fails.

Also, note that shared directories have neither an owner nor a group.

Notes on Registry Keys

You specify Registry keys with a full Registry key path like this:

HKEY_LOCAL_MACHINE\Software

You can abbreviate the Registry root by shortening it like this:

HKLM\Software

You also can specify Registry keys on remote machines by prepending the machine name like this:

\\server\HKEY_LOCAL_MACHINE\Software

or

\\server\HKLM\Software

Note

When accessing remote registries, only the HKEY_USERS and HKEY_LOCAL_MACHINE hives are accessible.

Example 3.10 Displaying Permission Information

01. use Win32::Perms;
02.
03. # The %PERM hash maps a permission type to its position in the
04. # permission string array. Notice that there is no 0 position
05. # since that is reserved for unhandled permission types (and
06. # we won't print it).
07. %PERM = (
08.   R   =>  1,
09.   W   =>  2,
10.   X   =>  3,
11.   D   =>  4,
12.   P   =>  5,
13.   O   =>  6,
14.   A   =>  7,
15. );
16.
17. %MAP = (
18.   ‘FILE_READ_DATA'    =>  ‘R',
19.   ‘GENERIC_READ'      =>  ‘R',
20.   ‘KEY_READ'          =>  ‘R',
21.   ‘DIR_READ'          =>  ‘R',
22.  
23.   ‘FILE_WRITE_DATA'   =>  ‘W',
24.   ‘KEY_WRITE'         =>  ‘W',
25.   ‘GENERIC_WRITE'     =>  ‘W',
26.   ‘FILE_APPEND_DATA'  =>  ‘W',
27.   ‘DIR_ADD_SUBDIR'    =>  ‘W',
28.   ‘DIR_ADD_FILE'      =>  ‘W',
29.  
30.   ‘DELETE'            =>  ‘D',
31.   ‘FILE_DELETE_CHILD' =>  ‘D',
32.  
33.   ‘FILE_EXECUTE'      =>  ‘X',
34.   ‘FILE_TRAVERSE'     =>  ‘X',
35.   ‘GENERIC_EXECUTE'   =>  ‘X',
36.   ‘DIR_TRAVERSE'      =>  ‘X',
37.   ‘DIR_EXECUTE'       =>  ‘X',
38.  
39.   ‘CHANGE_PERMISSION' =>  ‘P',
40.  
41.   ‘TAKE_OWNERSHIP'    =>  ‘O',
42.  
43.   ‘FILE_ALL_ACCESS'   =>  ‘A',
44.   ‘GENERIC_ALL'       =>  ‘A',
45.   ‘DIR_ALL_ACCESS'    =>  ‘A',
46.   ‘STANDARD_RIGHTS_ALL' => ‘A',
47. );
48.
49. push( @ARGV, "*.*" ) unless( scalar @ARGV );
50. foreach my $Mask ( @ARGV )
51. {
52.   my( @List ) = glob( $Mask );
53.   if( ! scalar @List )
54.   {
55.     push( @List, $Mask );
56.   }
57.   foreach my $Path ( @List )
58.   {
59.     print "\nPermissions for ‘$Path':\n";
60.     ReportPerms( $Path );
61.     print "\n\n";
62.   }
63. }
64.
65. sub ReportPerms
66. {
67.   my( $Path ) = @_;
68.   my( $Acct, @List );
69.   my( $Perm ) = new Win32::Perms( $Path );
70.   my( %PermList ) = ();
71.   my( $MaxAcctLength ) = 1;
72.
73.   if( ! $Perm )
74.   {
75.       print "Can not obtain permissions for ‘$Path'\n";
76.       return;
77.   };
78.
79.   printf( "  Owner: %s\n  Group: %s\n",
80.           $Perm->Owner(),
81.           $Perm->Group() );
82.   $Perm->Dump( \@List );
83.   foreach $Acct ( @List )
84.   {
85.     my( $PermMask );
86.     my( $Mask, @M, @F );
87.     my( $DaclType );
88.     my $bAllowAccess = ( "Deny" ne $Acct->{Access} );
89.     my $String;
90.     my $Account;
91.    
92.     next if( $Acct->{Entry} ne "DACL" );
93.    
94.     if( "" eq $Acct->{Account} )
95.     {
96.       $Account = $Acct->{SID};
97.     }
98.     else
99.     {
100.       $Account = "$Acct->{Domain}\\" if( "" ne $Acct->{Domain} );
101.       $Account .= $Acct->{Account};
102.     }
103.     if( length( $Account ) > $MaxAcctLength )
104.     {
105.       $MaxAcctLength = length( $Account )
106.     }
107.     $iTotal++;
108.     DecodeMask( $Acct, \@M, \@F );
109.     foreach $Mask ( @M )
110.     {
111.       $PermMask |= 2**$PERM{ $MAP{$Mask} };
112.     }
113.     $DaclType = $Acct->{ObjectName};
114.     if( 2 == $Acct->{ObjectType} )
115.     {
116.       # We have either a file or directory. Therefore we need to
117.       # figure out if this DACL represents an object (file) or
118.       # a container (dir)...
119.       if( $Acct->{Flag} & DIR )
120.       {
121.         $DaclType = "Directory";
122.       }
123.       else
124.       {
125.         $DaclType = "File";
126.       }
127.     }
128.     if( ! defined $PermList{$Account}->{$DaclType} )
129.     {
130.       # Create the permission string array. The first element in the
131.       # array must be blank since all unhandled permissions will default
132.       # to that position (and we won't print it).
133.       my $TempHash = [
134.                        " ",
135.                        split( //, "-" x scalar( keys( %PERM ) ) )

136.                      ];
137.       $PermList{$Account}->{$DaclType} = $TempHash;
138.                                    
139.     }
140.     foreach $Mask ( keys( %PERM ) )
141.     {
142.       if( $PermMask & 2**$PERM{$Mask} )
143.       {
144.         $String = $PermList{$Account}->{$DaclType};
145.         # If we already have a denied permission then skip this step
146.         # since denied access overrides any explicitly allowed access
147.         if( $String->[$PERM{$Mask}] !~ /[a-z]/ )
148.         {
149.           my $TempMask = $Mask;
150.           $TempMask = lc $Mask if( 0 == $bAllowAccess );
151.           $String->[$PERM{$Mask}] = $TempMask ;
152.         }
153.       }
154.     }
155.   }
156.  
157.   if( ! $iTotal )
158.   {
159.     # There are no DACL entries therefore...
160.     print "\t Everyone has full permissions.\n";
161.   }
162.   else
163.   {
164.     foreach my $Permission ( sort( keys( %PermList ) ) )
165.     {
166.       foreach my $DaclType ( sort( keys( %{$PermList{$Permission}} ) ) )
167.       {           
168.         my $String = $PermList{$Permission}->{$DaclType};
169.         printf( "  % " . $MaxAcctLength . "s % -11s %s\n",
170.