Skip to content

标题: 王一航对《PHP逆向工程趣味迷题》的解迷

创建: 2021-10-07 13:09 更新: 2021-10-08 15:58 链接: https://scz.617.cn/web/202110071309.txt

原题见

《PHP逆向工程趣味迷题》 https://scz.617.cn/web/202110012159.txt

截至2021.10.7,有两位同学反馈了解迷,第一位先成功运行scz_puzzles.php.bin, 继而通过VLD猜出相关代码逻辑,然后解迷成功,算是传统解迷思路。第二位没有动 用VLD,他用了其他技巧,我觉得第二种解法挺有趣,分享一下王一航的反馈。

由于即将分享他的解迷细节,致使该题趣味性丧失殆尽。若看到本文的同学仍有兴趣 挑战一下自己的逆向工程技能,建议不要往下看了,这就不是简单剧透的问题了,而 是让你完全失去乐趣。当然,出这道题的目的本来也是激励大家在技术道路上不断勇 攀高峰,即使被人彻底剧透,能有所收获,仍然值得。


王一航在微信中简述了大致思路

既然scz_puzzles.php.bin可以正常运行,只需要将op_array在运行时dump出来即可。 浏览了一下opcache扩展的源码,发现其提供了zend_dump_op_array()的功能。因此 只需要在加载opcache文件之后,将其中的op_array通过该函数dump出来即可。

对php-7.3.30进行patch

https://paste.ubuntu.com/p/P7dTWhhq6h/

然后运行该opcache文件,即可在stderr看到dump出的op_array

https://paste.ubuntu.com/p/SN8NNv63yb/

是一种类似汇编的文本。

然后手工将op_array汇编文本逆成PHP伪代码

https://paste.ubuntu.com/p/7nmGYBRsc4/

再看PHP伪代码的逻辑进行解迷。

他还简述了未来展望

1.

通过对opcache扩展dump出的op_array汇编文本进行解析实现反编译器,感觉PHP文件 在被opcache扩展编译成opcache文件时丢失的信息并不多,大概率可以恢复成和源码 非常类似的版本

2.

修改opcache扩展,添加反编译功能,使其可以直接dump PHP源码,或者将该功能作 为Zend Extension提供

3.

直接解析opcache文件,但是与PHP版本强相关,是一个苦差事,不如直接用 zend_dump_op_array()

4.

逆向该加密算法,需要时间和耐心

坦率地说,王一航的解法我事先没有想到过,他这个脑洞开得相当不错,我很受启发。

他的Patch如下


diff --git a/ext/opcache/zend_file_cache.c b/ext/opcache/zend_file_cache.c index 8b6ebd9fe7..78ffb88a9e 100644 --- a/ext/opcache/zend_file_cache.c +++ b/ext/opcache/zend_file_cache.c @@ -21,6 +21,7 @@ #include "zend_compile.h" #include "zend_vm.h" #include "zend_interfaces.h" +#include "Optimizer/zend_dump.h"

#include "php.h" #ifdef ZEND_WIN32 @@ -1036,6 +1037,7 @@ static void zend_file_cache_unserialize_op_array(zend_op_array *op_arr UNSERIALIZE_STR(op_array->doc_comment); UNSERIALIZE_PTR(op_array->try_catch_array); UNSERIALIZE_PTR(op_array->prototype); + zend_dump_op_array(op_array, ZEND_DUMP_RT_CONSTANTS, "scz_puzzles", NULL); return; }

@@ -1160,6 +1162,7 @@ static void zend_file_cache_unserialize_op_array(zend_op_array *op_arr UNSERIALIZE_PTR(op_array->try_catch_array); UNSERIALIZE_PTR(op_array->prototype); } + zend_dump_op_array(op_array, ZEND_DUMP_RT_CONSTANTS, "scz_puzzles", NULL); }

static void zend_file_cache_unserialize_func(zval *zv,

zend_dump_op_array()的输出形如


ooooooo: ; (lines=20, args=0, vars=3, tmps=1) ; (scz_puzzles) ; /home/scz/src/php73/scz_puzzles.php:235-259 L0 (240): INIT_STATIC_METHOD_CALL 1 string("oooo00o") string("o00o00o") L1 (240): SEND_VAL string("ooo oooo00o 000 ooo oooo00o 000 ooo oooo00o 000 ooo oooo00o 000") 1 L2 (240): V3 = DO_UCALL L3 (240): CV0($o0000) = QM_ASSIGN V3 L4 (241): INIT_STATIC_METHOD_CALL 2 string("oooo00o") string("o0o0o0o") L5 (241): SEND_VAR CV0($o0000) 1 L6 (241): SEND_VAL string("ooo oooo00o 0000ooo ooo oooo00o 0000ooo ooo oooo00o 0000ooo") 2 L7 (241): V3 = DO_UCALL L8 (241): CV1($oooo0) = QM_ASSIGN V3 L9 (244): INIT_STATIC_METHOD_CALL 2 string("oooo00o") string("o00000o") L10 (244): SEND_VAR CV0($o0000) 1 L11 (244): SEND_VAR CV1($oooo0) 2 L12 (244): V3 = DO_UCALL L13 (244): CV2($ooo0) = QM_ASSIGN V3 L14 (247): T3 = IS_IDENTICAL CV2($ooo0) string("ooo oooo00o 0000ooo ooo oooo00o 0000ooo ooo oooo00o 0000ooo") L15 (247): JMPZ T3 L18 L16 (251): ECHO string("Right! But you need to guess another puzzles ... What's this? fd81682965a6a8c1289ed6478ad2740647509a847d7483c3008b647cd7e1270e2a40d618719696983c6ad9550e2ea81e71d322a5cf51b16629551db8c3ef2f499262ec558bb7ca6d ") L17 (258): EXIT L18 (255): ECHO string("Wrong! ") L19 (258): EXIT

$_main: ; (lines=3, args=0, vars=0, tmps=0) ; (scz_puzzles) ; /home/scz/src/php73/scz_puzzles.php:1-263 L0 (261): INIT_FCALL 0 144 string("ooooooo") L1 (261): DO_UCALL L2 (263): RETURN int(1)


他的手工逆向结果展示


class oooo00o { public static $oo = 0xFFFFFFFF;

public static function o0($oooo) {
    $oo00000 = unpack("N*", $oooo);
    $ooo0000 = array();
    $oooo00 = 0;
    foreach ($oo00000 as $oooo000) {
        $ooo0000[$oooo00++] = $oooo000;
    }
    return $ooo0000;
}

public static function o0o($oo0000) {
    return pack("N", $oo0000);
}

public static function o00o($oo0, $o00) {
    if (SELF::$debug) {var_dump(debug_backtrace());}
    $oo0 = SELF::o0($oo0);
    $ooooo00 = $oo0[0];
    $oooooo0 = $oo0[1];
    $o00 = SELF::o0($o00);
    $ooooo0o = $o00[0];
    $oooo0oo = $o00[1];
    return SELF::o0o(($ooooo00 ^ $ooooo0o) & SELF::$oo).SELF::o0o(($oooooo0 ^ $oooo0oo) & SELF::$oo);
}

public static function o000o($oooo0, $o0000) {
    if (SELF::$debug) {var_dump(debug_backtrace());}
    $ooo000 = 0;
    $o0000 = SELF::o0($o0000);
    $oo0oooo = $o0000[1];
    $o0ooooo = $o0000[0];
    $ooooo0 = 0;
    while ($ooooo0 < 16) {
        $ooo000 += 0x6e36b677;
        $o0ooooo += (((SELF::$oo & ($oo0oooo << 4)) + $oooo0[0]) ^ ($oo0oooo + $ooo000)) ^ ((SELF::$oo & ($oo0oooo >> 7)) - $oooo0[1]);
        $o0ooooo &= SELF::$oo;
        $oo0oooo += (((SELF::$oo & ($o0ooooo << 4)) - $oooo0[2]) ^ ($o0ooooo + $ooo000)) ^ ((SELF::$oo & ($o0ooooo >> 7)) + $oooo0[3]);
        $oo0oooo &= SELF::$oo;
        ++$ooooo0;
    }
    return SELF::o0o($o0ooooo).SELF::o0o($oo0oooo);
}

public static function oo0oo($oooo0, $o0000) {
    if (SELF::$debug) {var_dump(debug_backtrace());}
    $ooo000 = SELF::$oo & 0x6e36b6770;
    $o0000 = SELF::o0($o0000);
    $o0ooooo = $o0000[0];
    $oo0oooo = $o0000[1];
    $oo0 = $oooo0[0];
    $o00 = $oooo0[1];
    $oo00 = $oooo0[2];
    $o000 = $oooo0[3];
    $ooooo0 = 0;
    while ($ooooo0 < 16) {
        $oo0oooo -= ((($o0ooooo << 4) - $oo00) ^ ($o0ooooo + $ooo000)) ^ (($o0ooooo >> 7) + $o000);
        $oo0oooo &= SELF::$oo;
        $o0ooooo -= ((($oo0oooo << 4) + $oo0) ^ ($oo0oooo + $ooo000)) ^ (($oo0oooo >> 7) - $o00);
        $o0ooooo &= SELF::$oo;
        $ooo000 -= 0x6e36b677;
        $ooo000 &= SELF::$oo;
        ++$ooooo0;
    }
    return SELF::o0o($o0ooooo).SELF::o0o($oo0oooo);
}

public static function o0o0o0o($oooo0, $o0000) {
    if (SELF::$debug) {var_dump(debug_backtrace());}
    $o00oooo = strlen($o0000);
    $o0o0ooo = ((8 - ($o00oooo + 2)) % 8) + 2;

    if ( ($o0o0ooo < 2) || $o0o0ooo >= 9) {
        $o0o0ooo = $o0o0ooo + 8;
    }

    $o0oo0oo = "";
    $ooooo0 = 0;

    while ($ooooo0 < $o0o0ooo) {
        $o0oo0oo .= chr(rand(0, 255));
        ++$ooooo0;
    }

    $o0000 = chr(($o0o0ooo - 2) | 0xf8).$o0oo0oo.$o0000;
    $o0ooo0o = strlen($o0000) + 7;
    $o0000 = pack("a".$o0ooo0o, $o0000);
    $o0oooo0 = pack("a8", "");
    $oo0ooo0 = pack("a8", "");
    $o00000 = "";
    pack("a8", "");
    $ooooo0 = 0;

    while ($ooooo0 < strlen($o0000)) {
        $o = SELF::o00o(substr($o0000, $ooooo0, 8), $o0oooo0);
        $o0oooo0 = SELF::o00o(SELF::o000o($oooo0, $o), $oo0ooo0);
        $oo0ooo0 = $o;
        $o00000.=$o0oooo0;
        $ooooo0 = $ooooo0 + 8;
    }
    return $o00000;
}

public static function o00000o($oooo0, $o0000) {
    if (SELF::$debug) {var_dump(debug_backtrace());}
    $oo0000 = strlen($o0000);
    $ooo0oo0 = SELF::oo0oo($oooo0, $o0000);
    $oooo0o0 = (ord($ooo0oo0[0]) & 7) + 2;
    $o00000 = $ooo0oo0;
    $ooooo00 = substr($o0000, 0, 8);
    $ooooo0 = 8;
    while ($ooooo0 < $oo0000) {
        $oo00ooo = SELF::o00o(SELF::oo0oo($oooo0, SELF::o00o(substr($o0000, $ooooo0, $ooooo0+8), $ooo0oo0)), $ooooo00);
        $ooo0oo0 = SELF::o00o($oo00ooo, $ooooo00);
        $ooooo00 = substr($o0000, $ooooo0, $ooooo0+8);
        $o00000 .= $oo00ooo;
        $ooooo0 = $ooooo0 + 8;
    }
    if (substr($o00000, -7) != pack("a7", "")) {
        return "";
    } else {
        return substr($o00000, $oooo0o0 + 1, -7);
    }
}

public static function o00o00o($oooo0) {
    if (SELF::$debug) {var_dump(debug_backtrace());}
    $ooo000 = array();
    $ooooo0 = 0;

    while ($ooooo0 < 256) {
        $ooo000[$ooooo0] = $ooooo0;
        ++$ooooo0;
    }

    $oooo00 = 0;
    $ooooo0 = 0;
    while ($ooooo0 < 256) {
        $oooo00 = (($oooo00 + $ooo000[$ooooo0]) + ord($oooo0[$ooooo0 % strlen($oooo0)])) % 0x100;
        $oo00ooo = $ooo000[$ooooo0];
        $ooo000[$ooooo0] = $ooo000[$oooo00];
        $ooo000[$oooo00] = $oo00ooo;
        ++$ooooo0;
    }

    $oo00ooo = array();
    $ooooo0 = 0;

    while ($ooooo0 < 64) {
        $oo00ooo[$ooooo0] = $ooo000[$ooooo0 * 4];
        $oooo00 = 1;
        while ($oooo00 < 4) {
            $oo00ooo[$ooooo0] = $oo00ooo[$ooooo0] | ($ooo000[$oooo00 + ($ooooo0 * 4)] << ($oooo00 * 8));
            ++$oooo00;
        }
        ++$ooooo0;
    }

    return $oo00ooo;
}

}

function ooooooo() { $o0000 = oooo00o::o00o00o("ooo oooo00o 000 ooo oooo00o 000 ooo oooo00o 000 ooo oooo00o 000"); $oooo0 = oooo00o::o0o0o0o($o0000, "ooo oooo00o 0000ooo ooo oooo00o 0000ooo ooo oooo00o 0000ooo"); $ooo0 = oooo00o::o00000o($o0000, $oooo0); if ($ooo0 == "ooo oooo00o 0000ooo ooo oooo00o 0000ooo ooo oooo00o 0000ooo") { echo "Right! But you need to guess another puzzles ...\nWhat's this?\nfd81682965a6a8c1289ed6478ad2740647509a847d7483c3008b647cd7e1270e2a40d618719696983c6ad9550e2ea81e71d322a5cf51b16629551db8c3ef2f499262ec558bb7ca6d\n"; exit; } else { echo "Wrong!\n"; exit; } }

ooooooo();

王一航的这个纯手工逆向结果很赞,换我是没这个纯手工耐心的,还原度相当高,有 一些小问题,但做源码审计足矣。

我做些技术补充。为了充分利用zend_dump_op_array(),还可以考虑直接在gdb中调 用它。

ls -l scz_puzzles.php

确认scz_puzzles.php为空

php73 \ -d opcache.enable_cli=1 -d opcache.file_cache="/home/scz/src/opcache" \ -d opcache.validate_timestamps=0 -d opcache.file_cache_consistency_checks=0 \ -f scz_puzzles.php

确认scz_puzzles.php.bin可以正常执行

gdb -q -nx -x gdbinit_x64.txt -x gdbhelper.py -ex 'display/5i $pc' php73

catch load opcache commands $bpnum silent b zend_file_cache.c:1162 commands $bpnum silent call (void)zend_dump_op_array($rbx,0x80000000,"any",0) c end c end r -d opcache.enable_cli=1 -d opcache.file_cache="/home/scz/src/opcache" -d opcache.validate_timestamps=0 -d opcache.file_cache_consistency_checks=0 -f scz_puzzles.php

catch load opcache commands $bpnum silent b *(zend_file_cache_unserialize_op_array+412) commands $bpnum silent call (void)zend_dump_op_array($rbx,0x80000000,"any",0) c end c end r -d opcache.enable_cli=1 -d opcache.file_cache="/home/scz/src/opcache" -d opcache.validate_timestamps=0 -d opcache.file_cache_consistency_checks=0 -f scz_puzzles.php

在这个断点处$rbx对应"zend_op_array *",0x80000000即ZEND_DUMP_RT_CONSTANTS。

上述方案的好处是不用Patch并编译源码,在现有环境中迅速得到汇编代码。这种断 点都是环境相关的,理解原理后根据自己的环境自行修改以适配不同PHP版本。

迷题中的加密算法不值得逆向分析,但王一航已经给出了手工逆向分析后的伪PHP代 码,眼尖的同学可能看出了端倪。我挖了一些与最终答案不相关的坑,当时是准备对 付我这类人的,有不少跟我逆向工程经验相近的人,TA们在面临加密算法时会取巧, 所挖的坑就是让这种取巧不成立。有兴趣、有闲情的同学或可对之解析一二,不说破 了,本来就是迷题么,讲究解迷的乐趣。

若看到此处,仍有兴趣解迷,可以继续按原题中的约定进行时间戳及SHA256的反馈, 请勿直接在迷题解析中反馈最终答案,这也是保护他人乐趣,算是一种公共礼貌。

话说我设计的这个迷题反馈方式还可以吧。