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.

.php (encrypted)
→
ARM_decoder.so
→
opcode dumps
→
Python combiner
→
AI → PHP source

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;
ionCube cannot hide class names, variable names, or text strings from Zend. Our tool sits right between ionCube and Zend, reading everything ionCube just prepared before its own virtual machine takes control.

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:

idxtypevalue
[0]STRING"instance"
[1]STRING"ErrorHandler"
[2]STRING"init"
[3]STRING"/var/www/html/core"
[4]STRING"loadConfig"
[5]STRING"#^(.+)\\:[0-9]+$#"
[6]LONG80
[7]TRUEtrue
[8]STRING"Location: "
[9]LONG302

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

PHP 8.2 with php8.2-dev
ionCube Loader v13 for PHP 8.2 (ARM64)
build-essential, autoconf

1. 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:

combined/
  _var_www_html_core/
    RDCore.class.php.txt
    ErrorHandler.class.php.txt
  Profis/MyWebSite/builder/
    ai.txt   # class AiClientMessage

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.

input: dump
[LITERALS]
  [0] STRING "instance"
  [1] STRING "ErrorHandler"
  [2] STRING "init"
  [3] STRING "/var/www/html/core"
[VARS]
  CV0 = $baseDir
  CV1 = $m
output: PHP
public 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.

For authorized security research and penetration testing only. Run it on systems you own or have explicit written permission to test.

References & Further Reading