使用PHP opcache运行bin文件,利用vld扩展获取字节码,猜解程序逻辑,最后成功得到答案。

题目背景

题目链接为PHP逆向工程趣味迷题
具体要求为运行PHP opcache生成的bin文件,并解决题目给出的问题,具体为加密一个字符串。

环境准备

Linux 安装多版本PHP

由于我使用的操作系统为Kali Linux,系统默认的库只有部分PHP,老版本PHP无法安装。因此需要准备多版本PHP。
实现方法参考了这篇文章在Debian 10上安装和切换PHP 8、7.4和5.6版本

相关代码为:

sudo apt-get update
sudo apt-get install lsb-release apt-transport-https ca-certificates
sudo wget https://packages.sury.org/php/apt.gpg -O /etc/apt/trusted.gpg.d/php.gpg
echo "deb https://packages.sury.org/php/ bullseye main" | sudo tee /etc/apt/sources.list.d/php.list
sudo apt-get update

其他资料及相关文章学习

相关链接及文章里提到的链接都值得观看。

  1. GoSecure Binary Webshell Through OPcache in PHP 7 - Ian Bouchard
  2. GoSecure Detecting Hidden Backdoors in PHP OPcache - Ian Bouchard
  3. GoSecure php7-opcache-override
  4. scz 直接调用OPcache生成之some.php.bin中的函数
  5. scz 《围观0CTF2018之ezDoor》
  6. 010 Editor
  7. zsx 0CTF2018之ezDoor的全盘非预期解法
  8. zsx 从PHP源码与扩展开发谈PHP任意代码执行与防御
  9. PHP vld 扩展

看完上面这些文章,你应该知道:

  • PHP opcache的工作原理
  • PHP opcache 后门的原理
  • 使用 vld 分析PHP opcache生成的字节码
  • 使用 010 Editor 对二进制文件的简单修改
  • 在没有源代码情况下调用PHP opcache生成bin文件的函数

题目分析

根据题目的bin文件,需要确定PHP版本。通过linux下的strings命令,查看源文件路径为/home/scz/src/php73/scz_puzzles.php,可以判断版本为PHP 7.3,具体小版本不知道、也不影响后续解题。
同时命令输出结尾也会有一些奇怪的信息。

$ strings scz_puzzles.php.bin.bak 
OPCACHE
a9ef565f37f8a07aab1bd494d026e597
scz_puzzles.php:2293176:2199960
/home/scz/src/php73/scz_puzzles.php
oooo00o
...
ooooooo
Right! But you need to guess another puzzles ...
What's this?
fd81682965a6a8c1289ed6478ad2740647509a847d7483c3008b647cd7e1270e2a40d618719696983c6ad9550e2ea81e71d322a5cf51b16629551db8c3ef2f499262ec558bb7ca6d
Wrong!
ooo oooo00o 0000ooo ooo oooo00o 0000ooo ooo oooo00o 0000ooo
ooo oooo00o 000 ooo oooo00o 000 ooo oooo00o 000 ooo oooo00o 000
ooo0

安装php7.3 : sudo apt-get install -y php7.3 php7.3-dev php7.3-opcache

运行程序

因为没有源文件,所以必须引导PHP访问opcache生成的二进制文件,根据zsx 和 scz的博客,我成功运行了文件。

错误的system id

理论上的步骤为首先php -S 运行一个phpinfo页面,使用 system_id_scraper.py 计算出system id,然后修改题目的二进制文件。
实际上system_id_scraper.py算出来的system id 是错的。

正确的system id

为了得到正确的system id,只需要开启opchche功能即可,然后访问本地的某个页面,在opcache 文件缓存路径上查看即可。
(注:我Linux本地用户名也同样为3个字母,所以我修改了opcache文件里路径中的用户名,并与opcahce文件建立相同的路径,没有测试这里不一致的影响。)

[username]@[hostame] ~/s/php73> pwd
/home/[username]/src/php73
[username]@[hostame] ~/s/php73> ls /home/[username]/src/php73/ -alh
total 28K
drwxr-xr-x 3 [username] sudo 4.0K Oct 4 06:56 ./
drwxr-xr-x 4 [username] sudo 4.0K Oct 3 11:52 ../
-rw-r--r-- 1 [username] sudo 0 Oct 4 04:59 ezdoor_chall.php
-rw-r--r-- 1 [username] sudo 1.5K Oct 4 05:00 patch_chall.php
-rw-r--r-- 1 [username] sudo 2.1K Oct 4 06:48 patch_exit.php
-rw-r--r-- 1 [username] sudo 21 Oct 3 22:10 phpinfo.php
-rw-r--r-- 1 [username] sudo 0 Oct 3 11:46 scz_puzzles.php
-rw-r--r-- 1 [username] sudo 72 Oct 4 07:11 test_vld.php
drwxr-xr-x 11 [username] sudo 4.0K Oct 3 21:58 vld/
[username]@[hostame] ~/s/php73 [1]> php7.3 -S [ip_address]:9999 -d opcache.enable_cli=1 -d opcache.file_cache="/home/[username]/src/opcache" -d opcache.file_cache_only=1 -d opcache.validate_timestamps=0
PHP 7.3.31-1+0~20210923.88+debian11~1.gbpac4058 Development Server started at Mon Oct 4 20:37:38 2021
Listening on http://[ip_address]:9999
Document root is /home/[username]/src/php73
Press Ctrl-C to quit.

然后在另外终端窗口访问对应的phpinfo地址,curl http://[ip_address]:9999/phpinfo.php,访问后就会生成对应的opcache bin文件。
查看opcache.file_cache配置路径的目录结构,发现正确的system id 为 ac616b4a451981be62b807ad0bcc0769
(注:即使在同一个php环境下,相同端口的php -S服务,每次不同启动时对应的system id 也不同,具体生成system id细节需要看源码才能弄清楚。)

[username]@[hostame] ~/s/php73> tree ~/src/opcache/ -D
/home/[username]/src/opcache/
└── [Oct 3 22:05] ac616b4a451981be62b807ad0bcc0769
└── [Oct 3 22:05] home
└── [Oct 3 22:05] [username]
└── [Oct 3 22:05] src
└── [Oct 4 07:11] php73
├── [Oct 4 04:57] ezdoor_chall.php.bin
├── [Oct 4 05:00] patch_chall.php.bin
├── [Oct 4 06:48] patch_exit.php.bin
├── [Oct 3 22:10] phpinfo.php.bin <----
├── [Oct 3 22:11] scz_puzzles.php.bin
└── [Oct 4 07:11] test_vld.php.bin

5 directories, 6 files

使用 010 Editor 修改题目给出的二进制文件,将文件的system id 修改为本地的system id,然后将文件放入对应的路径中,即可通过在本地实现访问。

[username]@[hostame] ~/s/php73> curl http://[ip_address]:9999/scz_puzzles.php
Right! But you need to guess another puzzles ...
What's this?
fd81682965a6a8c1289ed6478ad2740647509a847d7483c3008b647cd7e1270e2a40d618719696983c6ad9550e2ea81e71d322a5cf51b16629551db8c3ef2f499262ec558bb7ca6d

至此,我们已经成功运行了题目给出的opcache文件,输出结果与此前通过strings命令查看结果一致,但题目给出的hash含义需要进一步分析。

程序逻辑分析

运行vld

安装vld,进行配置重新运行php -S。这里system id会变更,需要重新更改opcache文件。再次访问,即可看到vld解析出来的字节码。

[username]@[hostame] ~/s/php73> php7.3 -S [ip_address]:9999 -d opcache.enable_cli=1 -d opcache.file_cache="/home/[username]/src/opcache" -d opcache.file_cache_only=1 -d opcache.validate_timestamps=0 -d"extension=/home/[username]/src/php73/vld/modules/vld.so" -d vld.active=1 -d opcache.file_cache_consistency_checks=0
PHP 7.3.31-1+0~20210923.88+debian11~1.gbpac4058 Development Server started at Sun Oct 3 22:32:32 2021
Listening on http://[ip_address]:9999
Document root is /home/[username]/src/php73
Press Ctrl-C to quit.
Finding entry points
Branch analysis from position: 0
1 jumps found. (Code = 62) Position 1 = -2
filename: /home/[username]/src/php73/scz_puzzles.php
function name: (null)
number of ops: 3
compiled vars: none
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
261 0 E > INIT_FCALL 'ooooooo'
1 DO_UCALL
263 2 > RETURN 1

branch: # 0; line: 261- 263; sop: 0; eop: 2; out0: -2
path #1: 0,

程序逻辑分析

针对程序的分析,主要时集中在如下代码部分,参见scz的文章直接调用OPcache生成之some.php.bin中的函数,可以调用opcache文件中的函数、进行分析。

filename:       /home/[username]/src/php73/scz_puzzles.php
function name: ooooooo
number of ops: 20
compiled vars: !0 = $o0000, !1 = $oooo0, !2 = $ooo0
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
240 0 E > INIT_STATIC_METHOD_CALL 'oooo00o', 'o00o00o'
1 SEND_VAL 'ooo+oooo00o+000+ooo+oooo00o+000+ooo+oooo00o+000+ooo+oooo00o+000'
2 DO_UCALL $3
3 QM_ASSIGN !0 $3
241 4 INIT_STATIC_METHOD_CALL 'oooo00o', 'o0o0o0o'
5 SEND_VAR !0
6 SEND_VAL 'ooo+oooo00o+0000ooo+ooo+oooo00o+0000ooo+ooo+oooo00o+0000ooo'
7 DO_UCALL $3
8 QM_ASSIGN !1 $3
244 9 INIT_STATIC_METHOD_CALL 'oooo00o', 'o00000o'
10 SEND_VAR !0
11 SEND_VAR !1
12 DO_UCALL $3
13 QM_ASSIGN !2 $3
247 14 IS_IDENTICAL ~3 !2, 'ooo+oooo00o+0000ooo+ooo+oooo00o+0000ooo+ooo+oooo00o+0000ooo'
15 > JMPZ ~3, ->18
251 16 > ECHO 'Right%21+But+you+need+to+guess+another+puzzles+...%0AWhat%27s+this%3F%0Afd81682965a6a8c1289ed6478ad2740647509a847d7483c3008b647cd7e1270e2a40d618719696983c6ad9550e2ea81e71d322a5cf51b16629551db8c3ef2f499262ec558bb7ca6d%0A'
258 17 > EXIT
255 18 > ECHO 'Wrong%21%0A'
258 19 > EXIT

分析解密

为了不影响解密体验,这里不给出分析解密过程。将上述字节码还原成源代码,并运行即可。
采用题目本身给出的密钥,将hash转换成字符串(pack),即可得到结果。

vld 的坑

此外又一个坑是vld在输出参数时会进行urlencode,所以需要进行解码。
比如下面代码:

<?php
$a = "touch /tmp/111 && chmod +x /tmp/111";
echo($a);
?>

在vld中输出结果为:

Finding entry points
Branch analysis from position: 0
1 jumps found. (Code = 62) Position 1 = -2
filename: /home/[username]/src/php73/test_vld.php
function name: (null)
number of ops: 3
compiled vars: !0 = $a
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > ASSIGN !0, 'touch+%2Ftmp%2F111+%26%26+chmod+%2Bx+%2Ftmp%2F111'
3 1 ECHO !0
5 2 > RETURN 1

branch: # 0; line: 2- 5; sop: 0; eop: 2; out0: -2
path #1: 0,
[Mon Oct 4 21:47:42 2021] [ip_address]:60908 [200]: /test_vld.php