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);
Это ни в коем случае не инструмент для оптимизации программ в такой технике, а набросок, позволяющий ускорить подбор букв.
Для более глубоких оптимизаций нужно научить эту программу дробить выражения, которые уже вычислялись, на куски и эффективно использовать их, а так же подбирать частоиспользуемым кускам имена переменных меньшей длины.
У меня дробления нет, я использую предыдущие выражения целиком, а имена переменным моя программа вообще не подбирает.
Вот, если бы онлайн-сервис...
Вставляешь свой PHP-код, а на выходе получаешь подобие первого листинга из статьи.
Произвольный код не получится в такое перегнать — ключевые слова преобразовать не во что, разве что через eval, а это уже как-то не очень интересно — можно просто сделать небольшой стартовый код в phpfuck, а остальное тупо кодировать в заданный алфавит.