Escaping ionCube with a PHP Extension + AI

How I built a C extension that hooks into the Zend engine at runtime, extract the opcode metadata from encrypted PHP files, and feeds it to an AI to reconstruct readable source code.

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

The Problem

IonCube encrypt PHP files into binary bytecode. The loader extension runs them inside its own VM. You get no source, no readable opcodes - just encrypted garbage.

The catch: ionCube still runs on top of the Zend engine. To execute a function, it has to populate the standard zend_op_array structure class names, method names, argument names, local variable names, and all the literal values the function uses. Those live in plain Zend structures. That's the attack surface.

How the Hook Works

ARM_decoder is a PHP C extension. At request startup it replaces two Zend engine function pointers:

zend_compile_file intercepts compilation, captures the target filename.
zend_execute_ex fires on every function call. Reads the zend_op_array after ionCube prepares it, dumps everything to a .txt file, then lets IonCube run normally.
/* PHP_RINIT - install both hooks */
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 can't hide class names, variable names, or string literals from Zend. The hook sits between IonCube and Zend reading everything ionCube just prepared before its VM takes over.

What Gets Extracted

For every function that executes, a structured dump is written. Example from RDCore::__construct - 147 literals, all readable:

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

Combined with variable names ($baseDir, $request, $core_dir_name) and the argument list - the function body is mostly reconstructible without ever seeing an opcode.

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

When IonCube strips the filename (source = "unknown"), the namespace itself becomes the folder path. Profis\MyWebSite\builder\ai\AiClientMessage becomes Profis/MyWebSite/builder/ai.txt.

AI Reconstruction

Feed the combined dumps into an AI with a system prompt explaining the format. The AI maps literals -> function body, fn_flags -> visibility, arg/var names -> exact signatures.

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 designed to stop casual inspection not someone who controls the PHP runtime. The moment it loads a function for execution, Zend needs real metadata. There's no way around that. The AI is what makes processing hundreds of dumps practical same reasoning a human reverse engineer would use, just without the fatigue.

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