从Zend虚拟机分析PHP加密扩展

栏目: PHP · 发布时间: 4年前

内容简介:这次RCTF2019中,我出了一题SourceGuardian解密。和Hook这一题的题目和Writeup见:我们首先需要熟悉PHP代码执行的流程──即,PHP到底是如何加载文件并执行的。PHP代码由于历史原因较为散乱,因此入手点很多。经过分析后,我认为从

这次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. zend_compile 里调用了 zend_compile_top_stmt

https://github.com/php/php-src/blob/79f41944babaa4d4ae4f1928fcb999feaf9a48b9/Zend/zend_language_scanner.l#L603

  1. zend_compile_top_stmt 里,当检测到有一个函数存在时,就调用 zend_compile_func_decl

https://github.com/php/php-src/blob/79f41944babaa4d4ae4f1928fcb999feaf9a48b9/Zend/zend_compile.c#L8146

  1. zend_compile_func_decl 里,先调用 zend_begin_func_decl ,把函数名和对应的 op_array 的地址插入到 CG(function_table) ,再然后编译这个函数内部的内容。

https://github.com/php/php-src/blob/79f41944babaa4d4ae4f1928fcb999feaf9a48b9/Zend/zend_compile.c#L5754

──因此,只要执行函数,要执行的函数对应的OPCode一定早就在内存里了。那我们要破解这一类的加密,就有两种方案了。下面,我将先介绍第一种方案,即获取正在运行的函数的OPCode。

第四步:调用 zend_execute ,准备执行OPCode。 zend_compile_file 返回的 op_array 即是外部,无任何函数包裹的代码的起始点,从这个点开始执行代码。这个文件因为是由代码生成器生成的,比较大,我直接贴出完整代码,并附上注释: https://raw.githubusercontent.com/php/php-src/php-7.3.5/Zend/zend_vm_execute.h

ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)
{
  zend_execute_data *execute_data;

  if (EG(exception) != NULL) {
    return;
  }

  // 压栈,生成execute_data
  execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE,
    (zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data)));

  // 设置符号表
  if (EG(current_execute_data)) {
    execute_data->symbol_table = zend_rebuild_symbol_table();
  } else {
    execute_data->symbol_table = &EG(symbol_table);
  }

  EX(prev_execute_data) = EG(current_execute_data);
  // 初始化execute_data
  i_init_code_execute_data(execute_data, op_array, return_value);

  // 执行
  zend_execute_ex(execute_data);
  zend_vm_stack_free_call_frame(execute_data);
}

此处的栈,即是PHP函数的调用栈。每次执行一个新函数,PHP都会为其把参数等压到这个栈中。如果我们需要跟踪函数运行轨迹,从 zend_vm_stack_push_call_frame 监控是再方便不过的选择。关于这一点,请看我给 CISCN 2019写的Writeup

而这个 zend_execute 会在什么时候被调用呢?搜搜代码,会发现 DO_FCALLINCLUDE_OR_EVAL 这两个指令会触发 zend_execute_ex

https://github.com/php/php-src/blob/79f41944babaa4d4ae4f1928fcb999feaf9a48b9/Zend/zend_vm_def.h#L4050

INCLUDE_OR_EVAL 这个指令对我们意义不大,因为SourceGuardian并没有用到 eval ,读文件也只能读到 sg_load 函数。但是,如果在没加密的代码里调用了函数,PHP就会将其编译为 DO_FCALL 指令。如果我们调用了加密过的函数, DO_FCALL 就能把这个函数的OPCode送上门来。

──这就是我在sourceguardian这题中魔改VLD的基础。对于file1.php,就只需要在 vld_execute_ex 中调用 vld_dump_oparray(&execute_data->func->op_array); ,天朗气清,惠风和畅。除了部分指令的返回值所存储的临时变量未知(但它是顺序增长的,完全可以猜测)之外,其他的一切都出来了。

从Zend虚拟机分析PHP加密扩展

──甚至包括变量名。我猜测SourceGuardian不去除变量名的原因是为了最大限度上保留PHP的动态功能,但它太弱了,变量名完全不需要保留。只有遇到 $$a 这一类PHP独有动态语法(对应指令包括 ASSIGN_DIM 等)时,才有保留变量名的必要。

但对file2.php,我们要Hook的点就不是在这里了。

PHP中,除了 DO_FCALL 之外,还有不少调用函数的指令。从这里可以看出PHP在什么情况下会调用哪些指令:

https://github.com/php/php-src/blob/3619cc78a8a0a65edaa2a39a7b73ddb84aeb1ebe/Zend/zend_compile.c#L3015

  • DO_ICALL: 调用PHP内部函数
  • DO_UCALL: 调用已定义的函数
  • CALL_TRAMPOLINE: 当调用 __call()__callStatic() 时使用,避免栈内多一个函数
  • DO_FCALL_BY_NAME / DO_FCALL: 其他情况

DO_ICALL暂且不论,我们再看看 DO_UCALLDO_FCALL_BY_NAME ,它们都会调用 i_init_func_execute_data(&fbc->op_array, ret, 0 EXECUTE_DATA_CC); 函数。所以说,我们直接在这个函数入手,在这里加上 vld_dump_oparray(op_array); ,就能直接按执行顺序导出调用的所有函数的OPCode。

──这是基于运行时的破解方案,还是需要运行代码。有什么非运行时的方案呢?

那当然有了。让我们回到php-cli的入口。在 zend_compile_file 之后, zend_execute 之前,把 EG(function_table) 里面的所有函数的 op_array 全部打出来,再把每个类的函数还原,之后还原类的定义即可。

──有什么能把OPCode还原成PHP代码的方法吗?

我又不是专业搞PHP破解的,也就比赛时破解一下而已,人工转就够了.jpg

从VLD的输出转换成PHP代码不算太难,期待有人写开源工具.jpg (其实我觉得可以从Zend Guard的破解 工具 魔改的)

──那其他的扩展呢?

ionCube 10的原理不同,不知道为什么一定会segfault,懒得看了.jpg

Swoole Compiler的强度还是比SourceGuardian强很多的。尽管如此,在一定程度上可以用这种方式来dump出某些明文字符串,只是,好像哪儿不对。

从Zend虚拟机分析PHP加密扩展

根据Swoole作者所说: https://www.zhihu.com/question/20142620

第二步:opcode 加密混淆处理。这一步才是关键,最终生成的指令连 vld、phpdbg 这些工具都无法识别。

Swoole Compiler 实际上已经修改了 Zend VM,已经无法用 PHP 内核的知识来理解了,甚至可以说它是一个全新的 VM

我们从这里的OPCode可以看出,它实际上改了 INIT_FCALL 里调用的函数名,顺便再把一些类似 IS_EQUAL 之类的指令全部转成 ASSIGN_ADD 。不过,改变的幅度有限。

先试试给已知被调用的函数打断点,例如我的file2就可以打 zif_file_get_contents 。很快就能定位到,它的 function_name 真的被改了(废话(。

从Zend虚拟机分析PHP加密扩展

不过想还原出函数名,即使不正面搞扩展,仍然有办法。其中一个方案是, INIT_FCALL 里调用了 zend_hash_find ,盯着 EG(function_table) ,拿到Key和对应的函数指针,比较函数名与指针地址即可。

从Zend虚拟机分析PHP加密扩展

至于混淆的OPCode,同理。没做过多的验证,只看了其中少数指令。这部份指令,即使看起来像是 ASSIGN_ADD ,实际上handler还在。针对handler建表就ok了。

从Zend虚拟机分析PHP加密扩展

文末:有没有大佬把剩下的坑填上啊(((


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Imperfect C++中文版

Imperfect C++中文版

威尔逊 / 荣耀、刘未鹏 / 人民邮电出版社 / 2006-1 / 75.0

汇集实用的C++编程解决方案,C++虽然是一门非凡的语言,但并不完美。Matthew Wilson使用C++十年有余,其间发现C++存在一些固有的限制,需要一些颇具技术性的工作进行弥补。本书不仅指出了C++的缺失,更为你编写健壮、灵活、高效、可维护的代码提供了实用的技术和工具。Wilson向你展示了如何克服C++的复杂性,穿越C++庞大的范式阵列。夺回对代码的控制权,从而获得更理想的结果。一起来看看 《Imperfect C++中文版》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具