Ok, so, ignoring the fact that code on a computer controlled by the person trying to break it is nearly impossible to hide (there is in fact a nice paper somewhere about agent platforms and how one can be developed to be cryptographically strong by making the actual operations performed on behalf of the agent non-obvious, but this requires a remote trusted server or something, I forget), lets just ponder Ways Of Making It Tricky.
The first limitation is mod_perl. The first question is whether you have access to the mod_perl code for the server, or whether its running on client-installed mod_perls in which case no modification can be made to mod_perl itself.
The second limitation is that any obscured perl source is subject to perltidy and Deparse. There isn't a lot that can be done in terms of obscuring to stop someone changing parts of the source. Making large-scale changes would be difficult without everything neat and tidy, but little changes wouldn't be too hard, the success people have in deconstructing a lot of the obscured stuff here is a good indication of that.
Lets assume we have no access to the mod_perl installation. This means that our ultimate objective is to make life as difficult as possible for someone to understand what the perl code is doing to retrieve its source for mod_perl. This also means you're gonna get a hell of a performance hit, since there are going to have to be multiple execution phases as one version of the code is decoded and then eval()'d.
Probably the most effective method without burning too much performance is to use a standard crypto algorithm like AES for the main perl source. This makes that part impractical to break, lacking the key. Of course, we need the key to decrypt the code for execution so our next task is to hide that as well as we can.
To do this, we take a short section of perl that does the decryption and eval(). We then proceed to spend as much time possible making it miserably difficult to understand. Since its a much smaller piece of code than the original, its easier to obscure in many different ways, and the performance hit from multiple eval()s of this section shouldn't be great. It is, unfortunately, also easier to figure out what a small piece of code is doing, each layer of obscure+eval could be slowly peeled back by use of perltidy, Deparse, and worst of all, the debugger which could run through until the final decoding lap and retrieve the key.
So the next thing to do is to discourage those as much as possible. At each layer, put in a different check for mod_perl, to make sure you're not being run under regular perl. When you find it, don't die, sleep. or throw a weird error, or loop, or delete yourself if you're really paranoid. You also want to check for the presence of the debugger, there are various ways of finding out whether you're running in debug mode under mod_perl, use as many different checks as you can and again, sleep or die or do random things when you find it.
Now we have a nasty, hard to break little decoder, but we have a few more tricks up our sleeve yet. The first is to detect tampering with yourself. An MD5 in a higher layer of the decode routine can run a check against the code on the filesystem and kill itself if the code doesn't match. More importantly, the md5 should attempt to notify a pre-determined email address if such tampering is taking place. This gives you plenty of warning if someone is trying to modify your code.
Next we attempt to make life even more miserable by doing things like converting the resulting perl to bytecode (I'm not sure if mod_perl will run bytecode automatically but I think it will), potentially adding checks against the hardware to see if it matches the system you installed on, possibly call out to a key server controlled by your company to gain part of the decryption key on initial startup (thus preventing the script from being run on unauthorised platforms/by unauthorised people) (sign the transaction usign public key crypto and a random session key so they can't fake it, SSL with a cert would work). If possibly, include any pure-perl modules you're going to use within the code itself rather than loading from the path, since those modules could be compromised with print()s and other thigns to attempt to access the plain, decoded source.
Our remaining problem is very simply that somewhere, in memory, is a copy of the operable bytecode. Its not nearly as useful as source but its still there. Your remaining option here is to do progressive decrypts. Implement a function call wrapper that decrypts certain sections of the code as they are required and destroys old code as it becoems defunct, so that no one memory snapshot will give them all the souce.
At this point, it'll be easier for them to write their own than break yours, which is essentially the point of the entire operation.
I note for completion that I would never, ever accept code like this running on a system I admined. I think the entire thing is a pointless exercise in futility and firmly believe that open source software is by far the preferable form of distribution, even if I have to pay for the application in the first place.
Still, it was fun to think about :)
The key to the entire thing is misdirection. The more time you waste of the person trying to break in, the more likely it is that they'll give up. Thus, making things "appear" to work is more effective than giving a clear indication that it failed. The first attempt to break through the evals, say by adding a print() that is picked up by the md5 challenge could, for example, result in the activation of a sub that re-writes the perl file that was executed with a similar but subtly different perl file that will never ever work by corrupting the encrypted section of the code and buggering up the higher layers of the decoder routine. They'll spend hours trying to figure out what the hell went on, unless they're a professional (the AV guys always start with a fresh copy and keep it in a sandbox so they can see what file ops its doing).
It is also possible under C (but not necessarily under perl) to subvert the ptrace mechanism. I'm not sure if its still possible but two years ago I wrote some code that, if you ran strace on it, managed to make a fork of itself the parent of strace and nuke the process. Such things are very platform specific however.