标题: 王一航对《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的反馈, 请勿直接在迷题解析中反馈最终答案,这也是保护他人乐趣,算是一种公共礼貌。 话说我设计的这个迷题反馈方式还可以吧。