Escaping ionCube with a PHP Extension + AI
This is the story of how I built a C extension that connects to the Zend engine while it runs. It extracts the hidden instructions (opcode metadata) from encrypted PHP files, and then uses AI to turn them back into readable source code.
The Problem
IonCube encrypts PHP files into unreadable binary code. Its loader extension runs this code inside its own virtual machine. You cannot see the original source code or the instructions (opcodes), it just looks like random data.
However, there is a catch: ionCube still needs the Zend engine to work. To run a function,
it must fill out a standard structure called zend_op_array. This structure contains class names, method names, argument names, local variable names, and
all the exact text and numbers (literal values) the function uses.
These details are stored in plain text within Zend. This is our way in.
How the Hook Works
I created a PHP C extension called ARM_decoder. When a request starts, it replaces two important functions in the Zend engine:
zend_compile_file catches the file before it compiles and saves the file name.zend_execute_ex runs every time a function is called. It reads the zend_op_array right after ionCube prepares it, saves all the data to a .txt file, and then lets ionCube continue normally./* PHP_RINIT hook install */
orig_compile_file_ptr = zend_compile_file;
zend_compile_file = arm_compile_file;
orig_execute_ex_ptr = zend_execute_ex;
zend_execute_ex = arm_execute_ex;
What Gets Extracted
Every time a function runs, the tool writes down a structured file.
For example, from the function RDCore::__construct, it found 147 text strings and numbers, all easy to read:
| idx | type | value |
|---|---|---|
| [0] | STRING | "instance" |
| [1] | STRING | "ErrorHandler" |
| [2] | STRING | "init" |
| [3] | STRING | "/var/www/html/core" |
| [4] | STRING | "loadConfig" |
| [5] | STRING | "#^(.+)\\:[0-9]+$#" |
| [6] | LONG | 80 |
| [7] | TRUE | true |
| [8] | STRING | "Location: " |
| [9] | LONG | 302 |
When we combine these values with variable names (like $baseDir, $request, $core_dir_name)
and the list of arguments, we can mostly rebuild the function's code without ever seeing the actual instructions (opcodes).
Tutorial
Requirements
php8.2-devbuild-essential, autoconf1. Build the extension
cd /path/to/ARM_decoder_source
phpize8.2
./configure --enable-ARM_decoder \
--with-php-config=/usr/bin/php-config8.2
make -j$(nproc)
# output: modules/ARM_decoder.so
2. Decode a single file
php8.2 -n \
-d "zend_extension=.../ioncube_loader_lin_8.2.so" \
-d "extension=.../ARM_decoder.so" \
target_file.php
3. Decode all IonCube files in a web root
grep -rl --include="*.php" \
--exclude-dir=vendor \
"if(extension_loaded('ionCube Loader'))" /var/www/html \
| while read -r target_file; do
echo "Decoding: $target_file"
php8.2 -n \
-d "zend_extension=.../ioncube_loader_lin_8.2.so" \
-d "extension=.../ARM_decoder.so" \
"$target_file"
done
4. Combine and organize dumps
python3 combine_dumps.py \
/path/to/arm_decoder_output/opcodes \
/path/to/arm_decoder_output/combined
The Python script groups every dump by class and rebuilds a proper folder structure:
If ionCube removes the original file name (showing "unknown" as the source), the script uses the namespace to create the folder path. For example, Profis\MyWebSite\builder\ai\AiClientMessage
becomes Profis/MyWebSite/builder/ai.txt.
AI Reconstruction
Next, we give these combined text files to an AI, along with instructions explaining how the files are formatted.
The AI then matches the text and numbers to the function code, uses fn_flags to figure out if the function is public or private,
and uses argument and variable names to build the exact function structure.
[LITERALS]
[0] STRING "instance"
[1] STRING "ErrorHandler"
[2] STRING "init"
[3] STRING "/var/www/html/core"
[VARS]
CV0 = $baseDir
CV1 = $mpublic function __construct(
$baseDir = null
) {
$m = ErrorHandler
::getInstance();
$m->init();
define('DIR_RD_BASE',
'/var/www/html/core');
}
Github Repo: escaping-ioncube
Gemini Output: Gemini COOKING
ionCube is built to stop regular people from looking at the code, but it cannot stop someone who controls the PHP engine itself. As soon as it loads a function to run, the Zend engine needs real, unencrypted data. There is no way to avoid this. Using AI makes it easy to process hundreds of these text files. The AI uses the same logic a human expert would use, but it never gets tired.