Cover V10, I02
Listing 1


A Centralized and Flexible Antivirus Solution

Dani Pardo

Viruses can represent a huge problem, specially now that information flows so fast. If your network has a reasonable number of clients, you probably don't want to spend much time regularly installing and upgrading antivirus software. Using client-side antiviruses also has the disadvantage of slowing down performance, and your users might even find them difficult to run.

Client/Server Antivirus

The solution I present here uses Linux smb support to scan viruses in your network client's hard disk. The idea is to share the client's hard disk or directory, and let the server scan it via the network. This solution saves you from installing, operating, and maintaining separate antivirus installations on each client. However, this approach also has some limitations. Performing virus scans from the server consumes a lot of network bandwith. Also, I do not recommend this approach if you are concerned about privacy and internal security. This solution also has the drawback that it won't be able to scan the MBR (the boot sector) of the hard disk and the partition table.

It uses McAffe's AVP (Antiviral Toolkit Pro), with the kernel's ability to mount NetBIOS shares. The user only requires a Web browser to tell the server which shared resource needs to be scanned, and AVP's output report will be emailed to him (or to the sys admin). So, you just need the server to run something like:

 # smbmount //novita/C /mnt/smb/novita/C
# uvscan -c -r /mnt/smb/novita/C |mail user@domain In this example, we suppose machine "novita" wants to scan share "C" (usually the hard disk). If we want to write this CGI, the first question to ask is: How do we know the client's Netbios name, and how do we get his shared resources? Here is an easy way using Perl's CGI and socket modules:

 use CGI;
 use Socket;
 use Sys::Hostname;
 $query=new CGI;
 my $hostname = gethostbyaddr(inet_aton($query->remote_host()),AF_INET);
 $hostname=~s/; # Cut the domain
 $hostname=uc($hostname); # Uppercase the name

Thus, you get the TCP/IP hostname, via a DNS server. (You'll need a DNS server in your network.) In my network, TCP/IP names and Netbios names are the same. If that's not true in your case, you might need to add some extra code to get the Netbios name having the TCP/IP name -- probably by using a database or text file. To get a client's availiable resources and store them in an array using Samba's smbclient:

 my @shares;
 open (FILE,"/usr/local/samba/bin/smbclient -L $hostname -U \
  nobody%nopass |grep Disk| cut -f 1 -d \" \" |");
 while (<FILE>)
 $_=~s/^\s+//; # Delete spaces
 if (! ($_=~/PRINTER\$/)) # We dont want the PRINTER$ share.
 {push @shares, $_;}
 close FILE;
Notice that we login to the client machine with a username/password nobody%nopass. This is because if omitted, smbclient will interactively prompt us for a password and use our login name as the netbios username. We always send user=nobody, and password=nopass in our script. This will work for Win95/98 clients, but not for NT. NT will require us to be a user of the system. Now if you're using NT clients, you'll have to adapt this section to get the available resources (e.g., adding a user "nobody" with password "nopass" in all NT clients in our network). Note that if somebody also has a shared printer, you'll find a shared directory named PRINTER$, which you don't want to take care of. With the shared resources in an array, you can present them to the users via a pop-up menu in a browser for users to select the desired directory to scan:

 print $query->start_form;
 if ($#shares==-1) # Empty array, nothing shared from the client
 print "<B>You don't have any shared resource to scan!B><P>";
 # A link to an HTML help page
 print '<a href="http://calix:85/help.html"><img SRC="/help.gif" \
  BORDER=0 height=40 width=200></a>'; }
 print "Select desired resource: " . \
  $query->popup_menu('Shares',\@shares) . "</P>";
 print "Send report (e-mail) to: " . $query->textfield(-name=>'email',
 -size=>30) . "</P>";
 print $query->checkbox(-name=>'ScanNow',-label=>'Check now');
 print '<a href="http://calix:85/help.html"><img SRC="/help.gif" \
BORDER=0 height=40 width=200></a>'; print "<P>" . \
$query->submit(-name=>'Iniciar'); } print $query->end_form; print $query->end_html;
I've also included the textfield "email", in which the user can enter his email address, and a check button that, when active, the system will start scanning immediately. If not activated, the system will scan at 23:00 p.m.

 use CGI
 $query=new CGI;
 if ($query->param())
 # Use parameters and do the scanning, or program an at job that
 # does the scanning
 # Check hostname, resources, and ask for input
 Mount and Scan
When users press the submit button, this script will begin the second part, receiving $query->param('Shares'), $query->param('e-mail'), and $query->param('ScanNow') as parameters.

First, it creates the appropiate directory in which to mount the share. To support multiple simultaneous clients, I used the convention of /mnt/smb/$hostname/$share. Also, check the given email to be a valid (allowed) address:

 if (!($email=~/[a-z-A-Z-0-1]+\ # only numeric or 
  # alfanumeric address 
  # from our domain
 print "<B>Error:</B> Invalid e-mail address: $email";
Since there are two cases (scan inmediately or scanning at night), we'll program only one at job to be run "now" or at 23:00, for example):

 if ($query->param('ScanNow') eq 'on')
 print "<B>You'll soon receive the antivirus report by e-mail.</B>";
 print "<B>Ready<P>Remember to leave the computer on \
 system ("at $time -f /tmp/atjob.$hostname.$share");
I've checked parameters, created the appropiate directory, written the commands file, and called at to execute this file at a programmed time:
 mkdir "/mnt/smb/$hostname",500;
 mkdir "/mnt/smb/$hostname/$share",500;
 open FD, ">/tmp/atjob.$hostname.$share" || die "Can't create tmp file";
 print FD "/usr/bin/smbmount //$hostname/$share \
  /mnt/smb/$hostname/$share -N > /dev/null \n";
 print FD "/usr/local/bin/uvscan -c -r \
  /mnt/smb/$hostname/$share |mail -s \"Antivirus repot\" $email \n";
 print FD "/usr/bin/smbumount /mnt/smb/$hostname/$share\n";
 # We delete in two steps, instead to do a rm -r (just in case)
 print FD "rmdir /mnt/smb/$hostname/$share\n";
 print FD "rmdir /mnt/smb/$hostname\n";
 close FD;
 chmod 500,"/tmp/atjob.$hostname.$share";
 To Do
Listing 1 provides the entire script. The script can be optimized, but it gets the job done. If you're concerned with security or privacy, you will probably find this solution too high-risk. (It's not the best CGI to run in your main server where all your users have a shell login.) Security can be enhanced by adding support for password-protected shares. This can be achieved by adding a text field where the user could enter the password of the share, so you'll receive $password=$query->param("password") as a parameter. That way you'll be able to invoke smbmount with:

 /usr/bin/smbmount //$hostname/$share /mnt/smb/$hostname/$share \
  -U nobody%$password -N >/dev/null
In my case, omitting passwords made things simpler.

Another issue with this solution is the need for a Linux kernel. At first, I wanted it to run Samba's smbsh (Samba shell) in order to run the CGI in other UNIXes. smbsh also has the advantage that you can scan all your network at one time (using something like uvscan -r /SMB). The biggest advantage is, of course, that now you only need to upgrade the server's antivirus information (the DAT files) and forget about the clients. Enjoy it.

Dani Pardo is a 27-year-old programmer and system manager in Enplater s.a., a packaging factory in Estartit (Spain). When not working, he spends his weekends going out with his friends until noon. He can be reached at: