Julius is a systems administrator for Ayala Systems Technology. He can be contacted at [email protected].
You can convert a Perl script into a self-contained executable in more than one way. One way is to use LibZip, which I've already discussed in Part I of this article last month. LibZip builds an external self-contained library that you distribute along with your compiled Perl script. Another approach is to use PARthe Perl Archive Toolkit.
As in LibZip, you need a working C compiler to compile Perl scripts. A sample Perl script, sample1.pl (shown in Listing 1), is provided with this article. This script will be compiled to showcase the capabilities of PAR. You may also need to install Win32::Autoglob before you can use sample1.pl.
Also, although the entire discussion here focuses on building executables for the Windows system, the steps presented here apply to other nonWindows platforms, too.
The Perl Archive Toolkit
PAR is the tool to use if you dislike having to bundle an executable with a separate library and prefer, instead, to have just a single executable. PAR, which is always available from http://par.perl.org/ index.cgi, requires the following modules: File::Temp, Compress::Zlib, Archive::Zip, Module::ScanDeps, and PAR::Dist. Apart from these prerequisite modules, the PAR maintainers also recommend the following modules: Parse::Binary and Win32::Exe, if you're on a Windows system.
Assuming you have installed all the prerequisite modules, using PAR is easy.
Compiling Scripts Using the Command Line
If you prefer using the command line, the following command will compile the sample Perl script, sample1.pl, into a Windows executable:
pp -o sample1.exe sample1.pl
The resulting executable will be named sample1.exe, as specified by the -o option to the Perl Packer, pp. pp converts the source script, sample1.pl, into Windows native code.
If you want to compress the executable, you can invoke the -z option with a mandatory integer argument, from 0 to 9. 9 will use the maximum possible compression. If -z is not specified, the compression level defaults to 6.
pp -o sample1.exe -z 9 sample1.pl
You can also eliminate more excess baggage by using the filter option (-f), which requires an argument:
pp -o sample1.exe -z 9 -f PodStrip sample1.pl
This filter strips away all POD sections from the executable.
GUI-based Script Compilation
If you hate command lines, there is tkpp, a GUI-based version of pp. When you use tkpp, you must first set up some important paths: the locations of your Perl interpreter and pp. To set these, click on "File" and then choose "Preferences."
On the "Source file" text box, type the complete path to your script, or click on the icon beside the text box. In doing so, a file browser will pop up, where you can choose your script. On the "Output file" text box, type the name of the executable you want to create. Leave all other buttons in their default states. At the bottom center of the GUI, there is a "Build" button. Click this button to proceed with the compilation of your script. You should see the message "Building..." while your script is being processed. When compilation is finally completed, you should see the message "Ready."
You can also build an executable without a console window by clicking on the "GUI" checkbox. This is ignored, however, on nonWindows systems. The command-line version of this is the -gui option:
pp -o sample1.exe -z 9 -f PodStrip -gui sample1.pl
That's it!
Obfuscation
Like LibZip, PAR can also hide your source code from casual snooping by turning your script into comment-free Perl code with mangled variable names. Of course, this is not the same as encryption and surely will not discourage a determined cracker. Obfuscation cannot offer 100-percent protection of your Perl code.
To obfuscate your code, use the -f filter with Obfuscate as argument:
pp -o sample1.exe -z 9 -f PodStrip \ -f Obfuscate sample1.pl
A harmless message will be displayed during compilation, which looks something like this:
C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\6mBQIYqCex syntax OK
Just ignore it.
But before you can use obfuscation, you must first install B::Deobfuscate, and before you can do that, you need to install B::Keywords and YAML, as well. Be sure to use Version 0.35 of YAML, and not the latest development release, or else you won't be able to install B::Deobfuscate.
Caveats
Using the diagnostics Module. Just like my experience with LibZip, I found out that having use diagnostics in my scripts will compile fine, but the resulting executables will not run properly. I often get error messages similar to this:
No diagnostics? at diagnostics.pm line 408. Compilation failed in require at xxxx/sample1.pl line 8. BEGIN failedcompilation aborted at xxxx/sample1.pl line 8.
I recommend that you remove or uncomment out diagnostics from your script prior to building its native code equivalent.
Using Obfuscation. One ugly problem with using obfuscation is that the generated executable is not always guaranteed to run. If I compile my script like this:
pp -o sample1.exe -f Obfuscate -z 9 \ -f PodStrip sample1.pl
and run the resulting executable on the current directory,
sample1.exe . recursive all
I sometimes get this error:
Undefined subroutine &main::performance called at sample1.pl line 41.
Even weirder is when I rearrange the order of arguments I feed to pp:
pp -o sample1.exe -f PodStrip -z 9 \ -f Obfuscate sample1.pl
In which case I get a very different error:
String found where operator expected at script/hash.pl line 10, near "GetOptions 'digest=s'" (Do you need to predeclare GetOptions?) syntax error at script/hash.pl line 10, near "GetOptions 'digest=s'"
But I don't get these errors if I run the original Perl script!
It looks like pp itself gets confused when obfuscation is performed. I have informed the PAR maintainers about this. Let's hope this problem goes away in the next release of PAR.
What can we learn from this? Aside from offering minimal source-code protection, use of obfuscation is also problematic. So, I discourage you from employing it, at least in its present state.
Using UPX. In the previous article where I discussed LibZip, I used UPX to compress the library and executable even more. This time, I attempted to use UPX on the executable generated by PAR. It compressed the executable all right, but when I ran the executable, I got a partial error:
IO error: reading header signature : at -e line 830 IO error: reading header signature : at -e line 830
But despite this imperfection, the executable still managed to proceed as usual.
Compiling Scripts For Linux. PAR works equally well on Linux. As a matter of fact, PAR also runs on FreeBSD, AIX, Solaris, HP-UX, NetBSD, and Mac OS. From the PAR FAQ: "The resulting executable will run on any platform that supports the binary format of the generating platform.''
The steps enumerated here also apply to nonWindows systems without the slightest modification.
TPJ
#!/usr/local/bin/perl # sample1.pl # Julius C. Duque #use diagnostics; #use strict; #use warnings; use Cwd; use Getopt::Long; use File::Find; use Win32::Autoglob; use Digest::MD5; my $VERSION = "1.0.0 (for TPJ)"; my ($showfiles, $showdigests, $recursive, $all, $quiet, $help) = (); GetOptions( "showfiles" => \$showfiles, "showdigests" => \$showdigests, "recursive" => \$recursive, "all" => \$all, "quiet" => \$quiet, "help" => \$help ); $showfiles = $showdigests = 1 if ($all); syntax() if ($help or !@ARGV); foreach my $infile (@ARGV) { chomp $infile; if (! -e $infile) { print "*** ERROR: $infile does not exist, skipping it...\n" if (!$quiet); next; } elsif (-d $infile) { if ($recursive) { find({wanted => sub { if (-f) { print make_digest($_, 'MD5'); print " $_" if ($showfiles); print " [MD5]" if ($showdigests); print "\n"; } }, no_chdir => 1}, $infile); } else { if (!$quiet) { print "*** ERROR: $infile is actually a directory, skipping it.\n"; print "*** ERROR: Use --recursive if you want to process $infile recursively\n"; } next; } } else { print make_digest($infile, 'MD5'); print " $infile" if ($showfiles); print " [MD5]" if ($showdigests); print "\n"; } } sub make_digest { my ($file, $tmd) = @_; my $digest_obj; open INFILE, $file or die "Cannot open $file: $!"; binmode INFILE; $tmd = $tmd =~ /^Digest::/ ? $tmd : "Digest::$tmd"; eval "require $tmd"; $digest_obj = new $tmd; $digest_obj->addfile(*INFILE); close INFILE; return $digest_obj->hexdigest; } sub syntax { if ($^O eq "MSWin32") { $0 =~ s/.*\\//g; # Windows } else { $0 =~ s/.*\///g; } print "$0 $VERSION\n\n"; print "Usage: $0 file1 [file2 ...]\n"; print "\n"; print "Other options:\n"; print " --showfiles print filenames\n"; print " --showdigests print digests used\n"; print " --recursive recursively descend into directory\n"; print " --all implies --showfiles and --showdigests output\n"; print " --quiet suppress error messages\n"; print " --help print this help message\n"; exit 1; }Back to article