ITKarma picture
A web developer knows that scripts created for commercial purposes can go for a walk on the network with overwritten copyrights; it is possible that the script will start to resell on behalf of someone else. To hide the source code of the script and prevent its change, obfuscators, minifiers, etc. are used. One of the oldest and most famous tools for encrypting scripts in PHP is ionCube. Introduced in 2002, it continues to monitor the development of PHP and declares support for the latest versions of the platform. As I will show in this article, ionCube is far from ok with PHP 7 support.

The model for using PHP encryptors is such that the programmer sells an encrypted script, and the buyer of the script on his server must install an extension module that will allow encrypted scripts to be executed. The script encrypted by ionCube looks something like this:

<?php//0059b//10.2 72////IONCUBE ONLINE ENCODER EVALUATION//THIS FILE IS LICENSED TO BE USED FOR ENCODER TESTING//PURPOSES ONLY AND SHOULD NOT BE DISTRIBUTED//if(!extension_loaded('ionCube Loader')){$__oc=strtolower(substr(php_uname(),//skipped ?> HR+cPn5yR+EksbFLjyZwm7EQh7Q0Y6YO6pLddgsuLRlBWUC5JWhAm3KcPBcRdP9D0zkMmdPNk5VG rMP1GxIwsA5NinHkQjWqG2pHL5nIZUvatUW+XMas3Knjf4wz9+DJoq47N1qZLDXwVzpOOupqa+Y4 k8PPXt8WNYXL4gbJnVu6NrqBqqwOrtlHUE9Sc30fMfAAEDTAVfa7ADHT2egTb5xxy9RGlDCjGlma RxoL1LvxvYcfe48f44x/H+GVTM7dPaYyy9DozcJjt3l8EDxcD73d67cWOtDgQGixQEmBlYJO7Cvh IAfeCBywIrDMgWfCC80uEIX+WtSmt/PuI7OXMgsNG3yVZu2HXJvXFRmXvc6748uxr+Zh0uZnAqeL pkJB5K9H5qbMr4YM/Aig+7MhwVG3KJ0kQCEhKxJe7+7Un/jSGcwQ8HKa/90ePzH2EXazm3T87pf2 hXL/exl4L7hutt/MfDGjculaEOCaoDLlUJjJqeXJL3kFDUsiPFfEL/BwAYUqe2pJAMjWXn7YIUt7 Y1DdTUD4ob/5fwE9wQwfG6PfDLPFkrGVKFpkBa95sRuA7qgtXATacXAVzsfYMxZgbwF3RcI5IxQo HTgnCg57vWmM/u6swJrgkz+747DWZRS1TfJZnKbdbmWIHAW11HG2FloKdWWSIronfqnuXTI/j2/R 9hX1Uim6mQowBwjS5zHZY8WFU9xE1KgETkCTsaDZODg9NYTICKs5aAdujzAtzxLWSicHZCfmpgzd FRhqYTYE1B9wktZsItkssDaq+xlyTZ+0LGnXAC6eaH7npS7w3NRBRj9ySVTRYPXBraVuJViMIX+U 4IzHJDFSNiT818GtS7erlLKcbGn4OZ40Ee3XEiicFzVrOfOvH0rJT3LZgVqY+KMtjqaQike2P4Dd A0SOuqOlFgQitYoo 

The users of noticed a long time ago that when the ionCube Loader module is loaded, two global functions appear in PHP with very strange by names: CDMY0CDMY and CDMY1CDMY. If you call one of these functions, then PHP will print one of the two Chinese aphorisms, and complete the execution:


It can be seen that the output line does not depend on the called function, and is selected randomly.

The question arises: why are these functions needed, and what do they do?

Simple experiments

First, let's see what parameters CDMY3CDMY accepts:


It looks like it takes two numbers, but whatever the numbers are, it prints the same aphorisms.

Search for use

On some muddy Indonesian forum, it’s possible to find an example example - most likely, obtained by then the PHP bytecode disassembler:

function tconnect( ) { $__tmp=_dyuweyrj4( 21711392, 920173696 ); return $__tmp[0]; return 1; } function tvariable( ) { $__tmp=_dyuweyrj4( 21720496, 920165136 ); return $__tmp[0]; return 1; } 

What is interesting in pairs of numbers (21711392, 920173696) and (21720496, 920165136)? An attentive researcher will notice that the XOR of numbers in each pair gives 932443808. Let's try to call CDMY5CDMY ourselves with a pair of numbers that give the result of XOR 932443808:


Nothing printed!


ITKarma picture

Diving into the debugger

ITKarma picture

We see that an attempt is being made to read at the address CDMY8CDMY, and CDMY9CDMY is equal to CDMY10CDMY — the number we passed to the function. This means that the function expects to receive the address value in the PHP process memory as a parameter, and it will add one to the dword at address CDMY11CDMY (the CDMY12CDMY command is visible just below the crash point). Let's try to pass such an address: for this, we note that the address at which the main module php.exe is loaded does not change until Windows reboots. In my case, it is CDMY13CDMY. Opening php.exe in the IDA, see what data is available for rewriting:

ITKarma picture

As an experiment, we will try to rewrite the first pointer in the CDMY14CDMY structure. There is a link to it from CDMY15CDMY; when you download php.exe at CDMY16CDMY, this link will be at CDMY17CDMY. So, we need to pass a value lower on CDMY18CDMY to the CDMY19CDMY function, i.e. CDMY20CDMY:


ITKarma picture

Crash again; but in another function inside ioncube_loader_win_7.3.dll. Interestingly, the address at which the null pointer was read - CDMY22CDMY - has nothing to do with the value passed to the function.(It is ironic that checking for a null pointer - CDMY23CDMY - is carried out immediately after accessing this pointer.) Having looked at the address CDMY24CDMY in memory, we find the CDMY25CDMY structure there, i.e. function definition. It is most convenient to look inside it through the Immediate Window:


As you can see, the null pointer that caused the crash is the CDMY27CDMY field in the CDMY28CDMY function definition. Apparently, this field is used by ionCube Loader for some of its internal purposes. To check, run the file from one line

<?php _dyuweyrj4(0x00982d23, 0x00982d23 ^ 0x3793f6a0); 
- through their Online PHP Encoder . (The result of encryption is given at the very beginning of the post.) Unfortunately, this does not help: CDMY29CDMY remains zero. But we make sure that the CDMY30CDMY function that is being executed (anonymous) is now populated:


Bug kicked out with a bug

As we can see, CDMY32CDMY is only populated with encrypted functions. (This is not their only distinguishing feature: in the printout above you can also see CDMY33CDMY instead of a plausible line number.) On the other hand, the pointer to CDMY34CDMY, which comes in a crashed function, is taken from CDMY35CDMY passed to CDMY36CDMY implicit first parameter - so this CDMY37CDMY always corresponds to the called function, namely CDMY38CDMY itself. This function is not encrypted, and therefore it will always have CDMY39CDMY null. We conclude from this: a call to CDMY40CDMY with the "correct" parameters inevitably leads to a crash (and with incorrect parameters, as we saw, to print Chinese aphorisms). The great thing about this is that the resulting crash is not intentional, but rather called using a null pointer before checking it. Such a bug in the code would catch any static analysis tool; but apparently, they don’t use anything like this in ionCube.

What happens if I fix a bug in ioncube_loader_win_7.3.dll by setting a pointer check before using it? The easiest way to do this is to use x64dbg :

ITKarma picture

We start CDMY41CDMY with the patched ionCube Loader, and... we get the crash already in a new place - again when using the null pointer from CDMY42CDMY:

ITKarma picture

That means that there was no sense from the (incorrect) check for a null pointer: in the next function called, the same pointer is used without checking. We conclude that a potential vulnerability that would allow us to change the memory of the PHP process at an arbitrary address and thereby escape from the sandbox — for example, call functions that are prohibited by the server administrator — is closed in ionCube Loader by a sequence of bugs that lead to the unintended php.exe cache.

What did the author mean?

I believe that initially CDMY43CDMY appeared in order to bind some formally correct array of "CDMY44CDMY cover" to encrypted functions - because ionCube Loader is far from the only extension module that will creep into these arrays. (One of the most common cases when extensions get into CDMY45CDMY's is caching these CDMY46CDMYs between runs of the same script; the other is PHP disassemblers like the one whose output is provided in the Indonesian forum.) CDMY47CDMY received as an argument a pointer to the encrypted function CDMY48CDMY, and passed control to the decryptor, by which ionCube Loader replaces the standard CDMY49CDMY function. The only scenario where the CDMY50CDMY could be called is if the extraneous extension module caches the “CDMY51CDMY-s” separately from the encrypted function, and then tries to execute these CDMY52CDMY-s. In this case, a call to CDMY53CDMY with a pointer to the CDMY54CDMY encrypted function will turn into a decryption and launch of this function itself.

When switching to PHP 7, the ABI changed the extension functions: instead of the four implicit parameters CDMY55CDMY, the CDMY56CDMY structure was used.Here, ionCube programmers got confused because CDMY57CDMY now gets two pointers to CDMY58CDMY: one through the CDMY59CDMY field, the second through an explicitly passed parameter. The first corresponds to CDMY60CDMY itself, the second to the encrypted function that you want to call. And here we meet another bug: incrementing the CDMY61CDMY field of the encrypted function, CDMY62CDMY completely forgets about it , and subsequently only works with its own CDMY63CDMY. Naturally, an attempt to call an extension function, as if it were an encrypted PHP function, leads to a crash - and it’s good that it doesn’t cause infinite recursion, because CDMY64CDMY is trying to call itself!

The question arises: how did the QA in ionCube miss the function that was never able to work as intended? The fact that it didn’t fall into the test plan is also evident because in the 64-bit version of ionCube Loader the parameters of CDMY65CDMY remain 32-bit - this means that the pointer to the CDMY66CDMY encrypted function is truncated to 32 bits even before the CDMY67CDMY increment, and that crash that we caught the very first, guaranteed to happen at all with any call CDMY68CDMY.

ITKarma picture

ITKarma picture.