这次RCTF2019中,我出了一题SourceGuardian解密。和Hook zend_compile_string就能解决php_screwphp-beast等扩展一样,没有对PHP总体的执行流程做出较大更改的扩展,依然有通用的(或是较为通用的)破解方案。这其中,SourceGuardian就是一个例子。这篇文章将从Zend虚拟机的角度来谈这一类加密的破解方案。

这一题的题目和Writeup见:https://github.com/zsxsoft/my-ctf-challenges/tree/master/rctf2019/sourceguardian

我们首先需要熟悉PHP代码执行的流程──即,PHP到底是如何加载文件并执行的。PHP代码由于历史原因较为散乱,因此入手点很多。经过分析后,我认为从php-cli入手是一个不错的选择。

https://github.com/php/php-src/blob/php-7.3.5/sapi/cli/php_cli.c#L937

让我们来整理一遍吧。第一步:打开文件句柄,CLI在这里顺便处理以#!/bin/php开头的可执行文件,防止该头被输出。

if (cli_seek_file_begin(&file_handle, script_file, &lineno) != SUCCESS) {

第二步:调用php_execute_script。这个函数同时负责把auto_prepend引入。

php_execute_script(&file_handle);

往下看php_execute_script的代码:

https://github.com/php/php-src/blob/7208826fdeb2244136c11a2e3b31948dfac549a3/main/main.c#L2650

它调用了

retval = (zend_execute_scripts(ZEND_REQUIRE, NULL, 3, prepend_file_p, primary_file, append_file_p) == SUCCESS);

往下看zend_execute_scripts,其代码如下:

op_array = zend_compile_file(file_handle, type);
        if (file_handle->opened_path) {
            zend_hash_add_empty_element(&EG(included_files), file_handle->opened_path);
        }
        zend_destroy_file_handle(file_handle);
        if (op_array) {
            zend_execute(op_array, retval);

可以看到,有两个关键函数。一个是zend_compile_file,一个是zend_execute

第三步就由这两个函数负责,我们可以看一下这两个函数的定义

ZEND_API zend_op_array *(*zend_compile_file)(zend_file_handle *file_handle, int type);
ZEND_API zend_op_array *(*zend_compile_string)(zval *source_string, char *filename);

如果没有开启任何扩展(包括phar、opcache等)的话,它们指向compile_filecompile_string函数。可以看到,这两个函数的输入一个是文件句柄,一个是代码字符串;而返回则是zend_op_array类型的一个东西。它们的用途是把PHP代码编译成对应的PHP OPCode,是PHP编译代码的入口。绝大多数所谓的PHP加密,包括php-beast等扩展,均没有处理和变形PHP代码的能力,它们做的事情只是把PHP代码本身原样进行加密,之后运行时通过各种方式解密后再调用原始的compile_file函数。因此,在此处输出source_string,就可以拿到原始代码。关于如何Hook这两个函数拿到代码,网上对应的文章实在是汗牛充栋(然而大部分文章还是互相抄,根本不懂别的文章在写些什么),就不再赘述了。

我们已经知道,PHP本身执行的是OPCode而不是PHP代码。如果把这个OPCode缓存起来,不就可以提高效率了吗?这条思路引出了Zend Optimizer,也就是PHP 5.5+的OPCache的前身。它们的实现原理是,Hook这两个函数之后,直接返回它们的编译结果,以避免对PHP文件的重新编译。同样地, vld 扩展也是Hook了这两个地方。这两个地方不仅能取到代码,当然也能自己调用compile_file来得到相应的op_array。这也是我在0CTF 2018使用VLD解出了ezDoor的关键性原因,VLD能够从这个地方取得它需要的OPCode。不过,SourceGuardian并不是那种无聊的扩展,它当然没有调用到这两个函数,VLD等自然也不能用了。

点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖