解密混淆的PHP代码

###0x00 前言

被混淆PHP代码见的很多了,但以前比较懒总是用扩展直接decode掉,最近就想手动操作下看看不使用扩展怎么进行解密。

0x01 原理

一般来说PHP的混淆都会通过多次eval来还原并执行php代码,所以我们可以通过hook PHP的eval函数来打印其参数来解密代码。

0x02 环境配置

OS: ubuntu 18.04

PHP: 5.6.36

首先要安装PHP,此处安装的版本为5.6.36

1.安装相关的依赖库

1
2
sudo apt-get update
sudo apt-get install libxml2-dev build-essential openssl libssl-dev make curl libcurl4-gnutls-dev libjpeg-dev libpng-dev libtool-bin bison php-fpm

2.编译安装libiconv

libiconv官网下载压缩包,放到/usr/local/src下,解压,编译安装,这里下载的是libiconv-1.15.tar.gz

1
2
3
4
5
6
sudo tar zxvf libiconv-1.15.tar.gz
cd libiconv-1.15
sudo ./configure --prefix=/usr/local
sudo make
sudo make install
sudo ldconfig // 刷新动态链接库缓存

注意,这里是将libiconv安装到了系统默认的lib目录下,安装路径/usr/local不可随意更改,否则后面会出现编译错误。另外,执行sudo make之后会有如下warning

1
warning: remember too run 'libtool --finish /usr/local/lib'

按照warning的提示执行一下

1
libtool --finish /usr/local/lib

3.编译安装PHP

PHP官网下载php5.6.36压缩包放到/usr/local/src下,解压,编译安装

1
2
3
4
5
6
sudo tar zxvf php-5.6.36.tar.gz
cd php-5.6.36
#由于我们后面要进行调试,所以要在编译时加上-g参数,加调试符号
sudo ./configure CFLAGS="-g" CXXFLAGS="-g"
sudo make ZEND_EXTRA_LIBS='-liconv' -j16
sudo make install

4.补充下扩展的编译安装

在此处以zlib的编译安装为例,由于我们的PHP是编译安装的,所以在路径php-5.6.36/ext/zlib/下就有所需要的文件。如果要安装的扩展在php源码ext目录中没有,可以从PECL上搜索你需要的扩展进行编译安装。

1.在对应的扩展目录运行phpize命令(如果没有提示autoconf记得apt安装)

1
2
3
4
5
zeroyu@ubuntu:~/Desktop/php-5.6.36/ext/zlib$ phpize 
Configuring for:
PHP Api Version: 20131106
Zend Module Api No: 20131226
Zend Extension Api No: 220131226

2.运行configure命令

1
2
3
zeroyu@ubuntu:~/Desktop/php-5.6.36/ext/zlib$ ./configure
checking for grep that handles long lines and -e... /bin/grep
checking for egrep... /bin/grep -E

3.运行make命令(之后可以运行也可以不运行make test)

4.运行make install命令

5.配置ini文件

通过运行 php --ini查找php.ini文件位置,然后在文件中添加extension=zlib.so

通过编译安装的PHP是没有php.ini文件的,但是可以通过php --ini查看配置文件的路径,例如:

1
2
3
4
5
zeroyu@ubuntu:~/Desktop/php-5.6.36/ext/zlib$ php --ini
Configuration File (php.ini) Path: /usr/local/lib
Loaded Configuration File: (none)
Scan for additional .ini files in: (none)
Additional .ini files parsed: (none)

可以看到此时Configuration File显示的是none。这种情况你只需要将之前在PHP官网下载的PHP文件中的php.ini拷贝一份到Configuration File (php.ini) Path路径下就可以了。操作完之后再执行php --ini可以看到如下显示:

1
2
3
4
5
zeroyu@ubuntu:~/Desktop/php-5.6.36/ext/zlib$ php --ini
Configuration File (php.ini) Path: /usr/local/lib
Loaded Configuration File: /usr/local/lib/php.ini
Scan for additional .ini files in: (none)
Additional .ini files parsed: (none)

0x03 开始Hook

PHP中的eval函数在Zend里需要调用zend_compile_string函数,我们先看下zend_compile_string函数的位置。

1
2
3
4
5
6
7
8
zeroyu@ubuntu:~/Desktop/php-5.6.36/Zend$ grep -rn "zend_compile_string" *
Binary file zend_alloc.o matches
Binary file zend_API.o matches
Binary file zend_ast.o matches
Binary file zend_builtin_functions.o matches
zend.c:693: zend_compile_string = compile_string;
Binary file zend_closures.o matches
zend_compile.c:98:ZEND_API zend_op_array *(*zend_compile_string)(zval *source_string, char *filename TSRMLS_DC);

我们发现zend_compile_string函数其实就是compile_string函数。所以我在这儿测试一个前几天偶然间得到的被混淆的PHP代码,如下所示可以看到在compile_string中已经获取到eval参数的值。

我们可以看到程序断下来后,compile_string的第一个参数source_string为php代码中eval函数的参数在Zend中的结构——即zval_structsource_string.value.str.val即为参数的字符串形式。所以之后修改修改compile_string函数来打印eval的参数就可以得到解密后的代码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
zeroyu@ubuntu:~/Desktop$ gdb php
gdb-peda$ set args a.php
gdb-peda$ b compile_string
Breakpoint 1 at 0x46e73f: file Zend/zend_language_scanner.l, line 716.
gdb-peda$ r
Starting program: /usr/local/bin/php a.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

[----------------------------------registers-----------------------------------]
RAX: 0x5555559c2726 (<compile_string>: push rbp)
RBX: 0x555555a68767 (<execute_ex>: push rbp)
RCX: 0x7ffff7e10e48 ("/home/zeroyu/Desktop/a.php(1) : assert code")
RDX: 0x7fffffffa030 --> 0x7ffff7fd22d0 ("return $\307\365㣋\217= eval(base64_decode($\240\202\206\345\215\331ׁ\307));;")
RSI: 0x7ffff7e10e48 ("/home/zeroyu/Desktop/a.php(1) : assert code")
RDI: 0x7fffffffa030 --> 0x7ffff7fd22d0 ("return $\307\365㣋\217= eval(base64_decode($\240\202\206\345\215\331ׁ\307));;")
RBP: 0x7fffffff9fa0 --> 0x7fffffffa120 --> 0x7fffffffa270 --> 0x7fffffffa420 --> 0x7fffffffa440 --> 0x7fffffffa560 (--> ...)
RSP: 0x7fffffff9e80 --> 0x7ffff7e10e48 ("/home/zeroyu/Desktop/a.php(1) : assert code")
RIP: 0x5555559c273f (<compile_string+25>: mov rax,QWORD PTR fs:0x28)
R8 : 0x7fffffff96c0 --> 0xb ('\x0b')
R9 : 0x555555f2cd22 ("assert code")
R10: 0x7
R11: 0xa ('\n')
R12: 0x555555623210 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdfb0 --> 0x2
R14: 0x0
R15: 0x0
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x5555559c272a <compile_string+4>: sub rsp,0x120
0x5555559c2731 <compile_string+11>: mov QWORD PTR [rbp-0x118],rdi
0x5555559c2738 <compile_string+18>: mov QWORD PTR [rbp-0x120],rsi
=> 0x5555559c273f <compile_string+25>: mov rax,QWORD PTR fs:0x28
0x5555559c2748 <compile_string+34>: mov QWORD PTR [rbp-0x8],rax
0x5555559c274c <compile_string+38>: xor eax,eax
0x5555559c274e <compile_string+40>: mov edi,0xf8
0x5555559c2753 <compile_string+45>: call 0x5555559e67f3 <_emalloc>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffff9e80 --> 0x7ffff7e10e48 ("/home/zeroyu/Desktop/a.php(1) : assert code")
0008| 0x7fffffff9e88 --> 0x7fffffffa030 --> 0x7ffff7fd22d0 ("return $\307\365㣋\217= eval(base64_decode($\240\202\206\345\215\331ׁ\307));;")
0016| 0x7fffffff9e90 --> 0x8
0024| 0x7fffffff9e98 --> 0x555556280608 --> 0x0
0032| 0x7fffffff9ea0 --> 0x7fffffff9ed0 --> 0x7fffffff9f70 --> 0x7fffffffa180 --> 0x7ffff7fd6d08 --> 0x3d8f8ba3e3f5c724
0040| 0x7fffffff9ea8 --> 0x3d559e6d1a
0048| 0x7fffffff9eb0 --> 0x7fffffff9ed0 --> 0x7fffffff9f70 --> 0x7fffffffa180 --> 0x7ffff7fd6d08 --> 0x3d8f8ba3e3f5c724
0056| 0x7fffffff9eb8 --> 0x5555559e6891 (<_efree+78>: leave)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, compile_string (source_string=0x7fffffffa030, filename=0x7ffff7e10e48 "/home/zeroyu/Desktop/a.php(1) : assert code") at Zend/zend_language_scanner.l:716
716 {
gdb-peda$ p *source_string
$1 = {
value = {
lval = 0x7ffff7fd22d0,
dval = 6.953349167324743e-310,
str = {
val = 0x7ffff7fd22d0 "return $\307\365㣋\217= eval(base64_decode($\240\202\206\345\215\331ׁ\307));;",
len = 0x31
},
ht = 0x7ffff7fd22d0,
obj = {
handle = 0xf7fd22d0,
handlers = 0x31
},
ast = 0x7ffff7fd22d0
},
refcount__gc = 0xffffa060,
type = 0x6,
is_ref__gc = 0x7f
}

0x04 小结

​ 混淆代码的解密就是类似于代码执行。最终还是要执行PHP代码,而执行PHP代码的方法很多,除了eval函数还有assertcall_user_funccall_user_func_arraycreate_function等。这些函数的底层也是调用了zend_compile_string,所以也可以利用hook eval来还原混淆后的加密代码。

​ 这篇也就是对这个解密操作了一下记了个笔记,环境搭建还是比较繁琐的事,所以写的详细了点。了解归了解,现实中为了效率还是使用扩展比较方便,使用的扩展可以参考P牛的,或者这个外国老哥的。如果扩展需要编译的话,方法在上面说过了,编译后不想加入php.ini的话,可以在每次用的时候执行php -d extension=evalhook.so a.php

0x05 参考

解密混淆的PHP程序–逢魔安全实验室

phpjiami 数种解密方法—phithon

PHPDecode 在线解密工具—Medici.Yan