Пишу, по большей части, про историю, свою жизнь и немного про программирование.

PHPFuck для PHP8

Немного про программирование.

Один из читателей написал, что программа PHPFuck (это способ писать программы без алфавитно-цифровых символов), которую я писал несколько лет назад, не работает с восьмой версией интерпретатора ПХП. Он попробовал починить её самостоятельно, но не преуспел.

В самом деле, восьмая версия стала куда строже к программисту, из-за чего починить написанное очень тяжело, проще написать всё заново с учётом новых ограничений.

<?=[[$_=@([].[]),$___=-!![],$$_=![]+![],$$_=$$_*$$_,$__=$$_+![],$$__=$$_+$__,
$____=$_[-![]-![]],$$___="{$$__}"^$____,$$___=($$___^"$___"). ($$___^"{$$__}"^"$___"^"{$$_}").
($$$_=$$___^"{$$_}"). $$$_.($_____="{$$__}"^"{$$_}"^++$____).("$___"&$____).
([$$___=$_[+![]],"{$$_}"|++$$___][+![]]).$_____.$_[+![]].$$$_. ("$__"^$_[-![]-![]]^"$___")
],$$___][+![]];

Выше новая версия.

Кстати, она компактнее, но при этом выводит больше текста — «hello world», вместо «Hello!» предыдущего варианта. Ниже я немного расскажу откуда взялась экономия и с чем ещё пришлось столкнуться.

Принцип формирования текста остался, в общих чертах, прежним — корнем всего является комбинация @([].[]), которая даёт слово ArrayArray из-за неявного преобразования массива в текст — это такая особенность ПХП (символ @ нужен тут, чтобы подавить предупреждение, которое выдаёт интерпретатор).

Из этой строки потом рождаются все остальные символы. Дело в том, что в ПХП над символами можно производить некоторые бинарные и математические операции, что иногда позволяет получить другие символы. Например, первая буква — h в слове hello получается из сочетания '9' ^ 'a' ^ '0' (где ^ — бинарная операция «исключающего или»).

При этом цифры я получаю довольно простым способом — 0 как +!![] (преобразованием false в число), «девятку»,  — набирая нужные цифры по единицам (из +![]), а a — как предпоследнюю букву из полученной ранее строки ArrayArray, то есть @([].[])[-2] (аналогично, -2 получается тут из двух замаскированных единиц со знаком «минус»).

По сравнению с предыдущим вариантом в новом коде сделаны следующие изменения.

Во-первых, вместо того, чтобы составлять из получаемых букв printf (функцию, которую я использовал в прошлый раз для вывода текста), тут я пользуюсь выводом через короткую конструкцию <?= — в этом основная экономия. Я выбрал этот вариант, так как конструкция <?, которую я использовал ранее, больше не работает, а заменившая её <?php содержит буквы.

В этой связи весь код теперь — большое выражение, все переменные в нём лежат внутри массива. Если давать им нормальные имена, то основной принцип выглядит как-то так: <?=[ [ $v1 = …, $v2 = …, $result = $v1 . $v2 . … ], $result ][1]; То есть они вычисляются внутри массива, последний элемент которого, содержащий вычисленное выражение, выводится.

Во-вторых, в ПХП стало больше строгости и числа, которые я использую для алхимии с буквами, теперь приходится преобразовывать в строки — раньше это происходило неявно, а сейчас интерпретатор останавливается с фатальной ошибкой. То же с не заданными константами — раньше они преобразовывались в строки (чем я пользовался, используя несуществующую константу, чтобы получить символ _ для дальнейших преобразований), теперь так делать нельзя.

Кроме того, мне надоело подбирать буквы вручную, вместо этого я написал небольшую программу, которая делает это за меня. Я не стал придумывать алгоритм, который бы анализировал какие биты нужно изменить, а решил задачу полным перебором.

Ниже приведен листинг этой программы, вдруг кому-то интересно:

<?php
declare(strict_types=1);

// Доступный алфавит
$abc = str_split(count_chars("Array0123456789", 3));

// буквы, которые надо получить
$need = str_split(count_chars("hello world", 3));

function gen(array $chrs): Generator
{
    // чем операции длиннее, тем ближе к концу
    // это нужно, чтобы более короткие конструкции выигрывали
    foreach ($chrs as $ch) {
        yield $ch => [$ch, $ch];
    }

    // возврат: из чего делаем, [как выглядит, значение]
    foreach ($chrs as $ch) {
        yield $ch => ["~$ch", ~$ch];
    }

    // для символа инкремент работает, а декремент — нет (особенность ПХП)
    foreach ($chrs as $ch) {
        $value = $ch;
        yield $ch => ["++$ch", (string)++$value];
    }
}

function eval_op(string $op, string $a, string $b): string
{
    return ([
        '|' => fn($a, $b) => $a | $b,
        '&' => fn($a, $b) => $a & $b,
        '^' => fn($a, $b) => $a ^ $b,
    ][$op])($a, $b);
}

function generate(array $set): Generator
{
    foreach (gen($set) as $src1 => [$p1, $ch1]) {
        foreach (gen($set) as $src2 => [$p2, $ch2]) {
            foreach (['|', '&', '^'] as $op) {
                $result = eval_op($op, $ch1, $ch2);

                if ($result !== $ch1 && $result !== $ch2) {
                    yield [
                        $src1,
                        $src2,
                        "$p1 $op $p2",
                        $result,
                    ];
                }
            }
        }
    }
}

function out(string $str): string
{
    return preg_replace_callback(
        '/[\x00-\x1F\x80-]/s',
        fn($m) => sprintf("\\x%02X", ord($m[0])),
        $str
    );
}

$set = $abc;

do {
    foreach (generate($set) as [$src1, $src2, $str, $res]) {
        $idx = array_search($res, $need, true);

        if (!isset($rules[$res])) {
            $rules[$res] = $str;
        }

        if ($idx !== false) {
            if (!in_array($src1, $abc, true)) {
                echo out("{$rules[$src1]} = '$src1'"), "\n";
            }

            if (!in_array($src2, $abc, true)) {
                echo out("{$rules[$src2]} = '$src2'"), "\n";
            }

            echo out("$str = '$res'"), "\n\n";

            unset($need[$idx]);
        }

        $set[] = $res;
    }

    $set = array_unique($set);
} while ($need);

Это ни в коем случае не инструмент для оптимизации программ в такой технике, а набросок, позволяющий ускорить подбор букв.

Для более глубоких оптимизаций нужно научить эту программу дробить выражения, которые уже вычислялись, на куски и эффективно использовать их, а так же подбирать частоиспользуемым кускам имена переменных меньшей длины.

У меня дробления нет, я использую предыдущие выражения целиком, а имена переменным моя программа вообще не подбирает.

1 комментарий
Vladimir Novitsky 2021

Вот, если бы онлайн-сервис...
Вставляешь свой PHP-код, а на выходе получаешь подобие первого листинга из статьи.

Евгений Степанищев 2021

Произвольный код не получится в такое перегнать — ключевые слова преобразовать не во что, разве что через eval, а это уже как-то не очень интересно — можно просто сделать небольшой стартовый код в phpfuck, а остальное тупо кодировать в заданный алфавит.