近期在工作中遇到了一些使用php开发的应用,而这些应用都使用了不同的源码保护方案对自身代码进行了加密处理,某些应用的加密强度甚至比较高,导致工作还没开始可能就得宣告结束。
为了解决遇到的这个问题,针对市面上一些常见的保护方案进行了研究,并且成功将这些较为常见的保护完成了代码还原,接下来将对这几种常见的保护与还原方法进行介绍。
由于加密强度不高,所以我们可以通过强行阅读处理后的文件来还原出解码方式从而获取原始代码,下面使用两种市面上常用的加密来做例子演示如何进行逆向还原。
使用这种方式处理后的文件,只能运行在php<7的版本上,而且处理后的文件代码量较少,我们甚至可以直接通过手工的方式来对其进行还原,下面是一段代码被处理之后的情况:
/*
������������Ϣ�����DZ�php�ļ������ߣ����Ա��ļ�����������Ϣֻ���ṩ�˶Ա�php�ļ����ܡ������Ҫ��PHP�ļ����м��ܣ��밴������Ϣ��ϵ��
Warning: do not modify this file, otherwise may cause the program to run.
QQ: 1833596
Website: http://www.phpjm.net/
Copyright (c) 2012-2024 phpjm.net All Rights Reserved.
*/
if
(!defined(
"EBCCDDCDEF"
)) {
define(
"EBCCDDCDEF"
,
__FILE__
);
global
$�,$��,$���,$����,$����,$������,$�������,$��������,$���������,$����������,$�����������,$�����������,$�������������,$��������������,$���������������,$���������������;
function
��
($��,$���=
""
)
{
global
$�,$��,$���,$����,$����,$������,$�������,$��������,$���������,$����������,$�����������,$�����������,$�������������,$��������������,$���������������,$���������������;
if
(
empty
($���)) {
return
base64_decode($��);
}
else
{
return
��($�����������($��,$���,$���($���)));
}
}
$���=��(
"c3RycmV2�"
);
$�����������=��(
"c3RydHI=�"
);
$����=��(
"LXLhbA==�"
,
"ZpomPWJL"
);
$����=��(
"A3p1bmNvbXByAXNz�"
,
"ZTQA"
);
$������=��(
"nmFzZTn0X2R�ln29kZQ==�"
,
"YhJpAn"
);
$����������=��(
"SHJlZ19yZXBsYWNl�"
,
"chtS"
);
$���������������=��(
"ZzdlNWM5Yjd�iMDRhNjLlNT�JhMDm0LGVkM�zlhMjM5YjYz�Z2U=�"
,
"LomFgvZ"
);
function
����
(&$����)
{
global
$�,$��,$���,$����,$����,$������,$�������,$��������,$���������,$����������,$�����������,$�����������,$�������������,$��������������,$���������������,$���������������;
$����������������=��(
"Okll�"
,
"ZGfMkO"
);
@$����������($���������������,$����.
"(@$����($������('eNptku1P2lAYxf8V�0vDhNuuUUosj5GZb�M0GJI4AghW0hLbQM�bGcFgsNpEFaohDfp�ECvC1j91vZeXGLaP�957fc87Nea57Aic2�IDRVqWixSEZSk9Ag�KCKT42MHGkEG3Hof�AwktUhGZiDJBoipr�CaT9enq+n08tDJyc�h5V0KgZNBJyHOOnb�CUIW5tB8fO4YmEny�Xy/FkP8sGYPwDnHS�RbWcRNidOWqbzU53�iDk5FaPnji5GPx4p�5YKMiGZjaLZvHwa9�n7MWhgqq/7KQ2oMt�ZHR4XOWUAuL6t/ez�32Zz2m63zWUqx5wW�OSZeXzhgrva9UApp�CQ6hxmDe0i1dN6xe�d/CE2eBhVMnQ/mp6�XouXhPcQjtFQuMZH�g2jioTtrdRZGo9U3�5ta4a+KZcIqV07xQ�T/NncOTgwoeL0yu1�FEYDExtms8Gj44Ns�NlCUwVYkIHbBzqu3�5GewS1AOSzlt2yT5�w90zFn9siE6f6C+B�G0mpSJvLiR24cfdm�1lSfOqeXtYAlQrmI�sojCx43Hnu4wqzWB�9ZQTtUJJvOCNsF0f�WFlQrxmahHBb3CwN�bCv/d/E4Pl5yh/Dv�e/dzPokVPX5vjmXl�vEDnmbyPFd74WNqz�JxAUQ5I403V97fqn�dBugrzfq2/Dd0nn9�foBvyZffyYbVoiqB�dRl4XwoTp0U1UhN5�rt5X+MgV2nEmnghp�itPaX0ZAL+E=�')));"
,
"���
������7e5c9b7b04a66e52a084ded39a239b63������"
);
return
"�"
;
}
}
else
{
global
$�,$��,$���,$����,$����,$������,$�������,$��������,$���������,$����������,$�����������,$�����������,$�������������,$��������������,$���������������,$���������������;
$���=��(
"c3RycmV2�"
);
$�����������=��(
"c3RydHI=�"
);
$����=��(
"LXLhbA==�"
,
"ZpomPWJL"
);
$����=��(
"A3p1bmNvbXByAXNz�"
,
"ZTQA"
);
$������=��(
"nmFzZTn0X2R�ln29kZQ==�"
,
"YhJpAn"
);
$����������=��(
"SHJlZ19yZXBsYWNl�"
,
"chtS"
);
$���������������=��(
"ZzdlNWM5Yjd�iMDRhNjLlNT�JhMDm0LGVkM�zlhMjM5YjYz�Z2U=�"
,
"LomFgvZ"
);
}
$���������=��(
"JU5vAjBNNE5CJ0F�DY4JxRWOZ�"
,
"ZxqeLAgnJ"
);
$��������=����($���������);
@$����������($���������������,$����.
"(@$����($������('eNo1jkEKwjAURK8�i8hcK8QSKnsVFwY�0iVBeu2sSkMaZp7�beNaZrqVY0LNwMz�bxhmuVmvNsfdcZJ�e0lOyn03Ph+"
.$���������.$��������.
"y20/nyj86HNDnNo�CXASwLPOoqW44dA�k700j8Y7y20k/fA�I1hEYscLXUEgCN6�wF0kJVBEp7E8aMt�IgFZLXO6dB6GwjQ�rEKRd0ZfPYu1/OH�fSK0QAuOANIFxx7�l0WpmeQNe07tOpv�g3hTmkTA+VZMcqM�lTK4RuHv9hfB0F7�u�')));"
,
"��
������7e5c9b7b04a66e52a084ded39a239b63�����"
);
return
true
;
cc7f01b3f642b179356e745ca3eef0b7
function func
0
($var1, $var2=
""
) {
//
global $varxxx...
if
(empty($var2)) {
return
base64_decode($var1);
}
else
{
//func
0
($varx($var1, $var2, $vary($var2)));
func
0
(strtr($var, $var2, strrev($var2)));
}
}
$���=��(
"c3RycmV2�"
);
$�����������=��(
"c3RydHI=�"
);
$����=��(
"LXLhbA==�"
,
"ZpomPWJL"
);
$����=��(
"A3p1bmNvbXByAXNz�"
,
"ZTQA"
);
$������=��(
"nmFzZTn0X2R�ln29kZQ==�"
,
"YhJpAn"
);
$����������=��(
"SHJlZ19yZXBsYWNl�"
,
"chtS"
);
$���������������=��(
"ZzdlNWM5Yjd�iMDRhNjLlNT�JhMDm0LGVkM�zlhMjM5YjYz�Z2U=�"
,
"LomFgvZ"
);
/*
string(6)
"strrev"
string(5)
"strtr"
string(4)
"eval"
string(12)
"gzuncompress"
string(13)
"base64_decode"
string(12)
"preg_replace"
string(35)
"/7e5c9b7b04a66e52a084ded39a239b63/e"
*/
function
func0
($var1,$var2=
""
)
{
if
(
empty
($var2)) {
return
base64_decode($var1);
}
else
{
return
func0(strtr($var1, $var2, strrev($var2)));
}
}
function
func1
(&$var1)
{
eval
(gzuncompress(base64_decode(
'xxxx'
)));
return
pack(
'H*'
,
'80'
);
}
$tmp_var = func0(pack(
'H*'
,
'4A553576416A424E4E4535434A30469F4459344A7852574F5A83'
), pack(
'H*'
,
'5A7871654C41676E4A'
));
$tmp_var2 = func1($tmp_var);
@preg_replace(
'/7e5c9b7b04a66e52a084ded39a239b63/e'
,
"eval(@gzuncompress(base64_decode(str . $tmp_var . $tmp_var2 . str)));"
,
'������7e5c9b7b04a66e52a084ded39a239b63�����'
);
$var1
= gzuncompress(base64_decode(
$var1
));
经过这种方式处理后的代码,和 phpjm 没有什么本质上的区别,无非就是逻辑变得更复杂了一些,处理前后的代码对比如下所示:
class
VariableVisitor
extends
NodeVisitorAbstract
{
public
function
leaveNode
(Node $node)
{
global
$varCount, $funcCount, $maps;
if
($node
instanceof
ExprVariable) {
$varName = is_string($node->name) ? $node->name : $node->name->name;
$varName = md5($varName);
if
($varName && !array_key_exists($varName, $maps)) {
$maps[$varName] =
'var'
. $varCount++;
}
$node->name = $maps[$varName];
}
if
($node
instanceof
NodeStmtFunction_) {
$funcName = $node->name->name;
$funcName = md5($funcName);
if
($funcName && !array_key_exists($funcName, $maps)) {
$maps[$funcName] =
'func'
. $funcCount++;
}
$node->name = $maps[$funcName];
}
}
}
function
func1
(&$var1, $var2)
{
$enc = func2(pack(
'H*'
,
'A6CAE0B236F23833DEA29A2B9C30E2DCD89CEA42AE32EE2BE635C4989AD2D8C2E8EE34C4CA47C29A33A0AAC8C8DAE0B037D8364333E2C8A2C4EC3932F244E04730F4A6CEA2D2E2B238C439F4EE9EB234DCCE9C30493338E8AA474396A6969847D898CC309CE44A379AC6C4A64235D6AEAEDC3749DCF4A246C630383944DEACF2A04AB22B2F2BA2A4F0333098C6B0364632E8E0A6E62FEAC637ECD849AC4546CC30A647C2F0A039C239C6A8DEC4DC35D0CCC4E24748A241AEE02B9C46E8DADADAF444A6CC'
));
$res = str_rot13(strrev(gzuncompress(stripslashes($enc))));
$s = explode(
','
, $res);
$var1 = $s[$var2];
}
function
func0
($var1)
{
php_sapi_name() ==
'cli'
?
die
() :
''
;
// 防止在cli下运行
$content = file_get_contents(
__FILE__
);
// 同样防止在cli下运行,这里可能是怕直接hook了php_sapi_name
if
(!
isset
($_SERVER[
'HTTP_HOST'
]) && !
isset
($_SERVER[
'SEREVER_ADDR'
]) && !
isset
($_SERVER[
'REMOTE_ADDR'
])) {
die
();
}
$time = microtime(
true
) *
1000
;
// 防止eval hook,断点时间超1秒就退出
eval
(
""
);
if
(microtime(
true
) *
1000
- $time >
100
) {
die
();
}
eval
(
"if(strpos(__FILE__, 'nirpnqsz') !== 0){$exitfunc();}"
);
!strpos(func2(substr($content, func2(getHexStr(
'4841A4A2'
)), func2(pack(
'H*'
,
'4841453D'
))), md5(substr($content, func2(), func2())))) ? undefined() : undefined();
$start = func2(pack(
'H*'
,
'484146A6ACA23D3D'
));
$end = func2(pack(
'H*'
,
'4841A4A8'
));
// 还原的核心代码,其实就是取了
后那一堆乱码
$content = str_rot13(@gzuncompress(func2(substr($content, $start, $end))));
return
$content;
}
php-beast
使用这个扩展对文件进行处理后,整个文件将变成完全不可读的状态,在非默认情况下脱离了扩展本身其实很难还原,加密前后对比如下:
zend_op_array *__
fastcall
cgi_compile_file
(zend_file_handle *h,
int
type)
{
const
char
*filename;
// rdi
FILE *v4;
// rax
FILE *v5;
// rbp
int
v6;
// eax
int
v7;
// eax
zend_stream_type v8;
// eax
file_handler *v9;
// rax
int
v10;
// edx
int
v11;
// r13d
beast_free_buf_t
*
free
;
// rax
beast_ops *ops;
// [rsp+8h] [rbp-40h] BYREF
char
*buf;
// [rsp+10h] [rbp-38h] BYREF
int
free_buffer;
// [rsp+18h] [rbp-30h] BYREF
int
size[
3
];
// [rsp+1Ch] [rbp-2Ch] BYREF
filename = h->filename;
free_buffer =
0
;
ops =
0L
L;
v4 = fopen(filename,
"rb"
);
v5 = v4;
if
( !v4 )
goto
final;
v6 = fileno(v4);
v7 = decrypt_file(h->filename, v6, &buf, size, &free_buffer, &ops);
// ...
return
old_compile_file(h, type);
}
首先经由beast混淆过的文件会有一个文件头特征,默认情况下将会是:0xe8, 0x16, 0xa4, 0x0c, 0xf2, 0xb2, 0x60, 0xee ,代码中会检查这个文件头来判断是否是加密文件。用户可以在编译之前修改 header.c 来替换掉这个值,所以这个特征并不是固定的。
.
data
:
000000000000E668
public
encrypt_file_header_sign
.
data
:
000000000000E668
; char encrypt_file_header_sign[
8
]
.
data
:
000000000000E668
encrypt_file_header_sign db
0E8
h,
16
h,
0
A4h,
0
Ch,
0F
2h,
0
B2h,
60
h,
0
EEh
.
data
:
000000000000E668
; DATA XREF: LOAD:
0000000000000
EF0↑o
.
data
:
000000000000E668
; .got:encrypt_file_header_sign_ptr↑o
.
data
:
000000000000E660
public
encrypt_file_header_length
.
data
:
000000000000E660
; int encrypt_file_header_length
.
data
:
000000000000E660
encrypt_file_header_length dd
8
; DATA XREF: LOAD:
00000000000010
A0↑o
.
data
:
000000000000E660
; .got:encrypt_file_header_length_ptr↑o
if
( memcmp(header, encrypt_file_header_sign, encrypt_file_header_length) )
{
if
( log_normal_file )
{
beast_write_log(beast_log_error,
"File `%s' isn't a encrypted file"
, filename);
return
-
1
;
}
return
-
1
;
}
v16 = *(_DWORD *)&header[encrypt_file_header_length];
v17 = *(_DWORD *)&header[encrypt_file_header_length +
4
];
v18 = *(_DWORD *)&header[encrypt_file_header_length +
8
];
v42 =
4386
;
v19 = (v16 <<
24
) | ((
int
)(v16 &
0xFF0000
) >>
8
) | ((v16 &
0xFF00
) <<
8
) | HIBYTE(v16);
v2
0
= (v17 <<
24
) | ((
int
)(v17 &
0xFF0000
) >>
8
) | ((v17 &
0xFF00
) <<
8
) | HIBYTE(v17);
v21 = (v18 <<
24
) | ((
int
)(v18 &
0xFF0000
) >>
8
) | ((v18 &
0xFF00
) <<
8
) | HIBYTE(v18);
if
( beast_max_filesize >
0
&& v19 > beast_max_filesize )
{
beast_write_log(
beast_log_error,
"File size `%d' out of max size `%d'"
,
(unsigned
int
)v19,
(unsigned
int
)beast_max_filesize);
return
-
1
;
}
if
( v2
0
>
0
&& v2
0
< beast_now_time )
{
beast_write_log(beast_log_error,
"File `%s' was expired"
, filename);
return
-
2
;
}
v31 = chk;
encrypt_algo = beast_get_encrypt_algo(v21);
beast_ops *__fastcall beast_get_encrypt_algo(int type)
{
return
beast_get_encrypt_algo(type);
}
beast_ops *__fastcall beast_get_encrypt_algo(int type)
{
unsigned int v1;
// edi
v1 = type -
1
;
if
( v1 >
3
)
return
ops_handler_list[
0
];
else
return
ops_handler_list[v1];
}
.
data
:
000000000020
C620
public
ops_handler_list
.
data
:
000000000020
C620 ; beast_ops *ops_handler_list[
4
]
.
data
:
000000000020
C620 ops_handler_list dq offset des_handler_ops, offset aes_handler_ops, offset base64_handler_ops
.
data
:
000000000020
C620 ; DATA XREF: LOAD:
0000000000001
AE8↑o
.
data
:
000000000020
C620 ; .got:ops_handler_list_ptr↑o
.
data
:
000000000020
C638 dq offset dword_0
.data:
000000000000E5
A8 ; char key_0[
8
]
.data:
000000000000E5
A8 key_0 db
1
,
1
Fh,
1
,
1
Fh,
1
,
0
Eh,
1
,
0
Eh
.data:
000000000000E5
A8 ; DATA XREF: des_encrypt_handler+
89
↑o
.data:
000000000000E5
A8 ; des_decrypt_handler+
50
↑o
针对加密后文件进行解密的操作如下所示(AES与BASE64同理)。
from
Crypto.Cipher
import
DES
from
Crypto.Util.Padding
import
pad
ECB_KEY =
b'x01x1Fx01x1Fx01x0Ex01x0E'
HEAD_LENGTH =
8
ENCRYPT_TYPES = [
'DES'
,
"AES"
,
'BASE64'
]
def
des_decrypt
(ciphertext)
:
cipher = DES.new(ECB_KEY, DES.MODE_ECB)
plaintext = cipher.decrypt(ciphertext)
return
plaintext.decode(
'utf-8'
, errors=
'ignore'
)
with
open(
'./index.php'
,
'rb'
)
as
f:
content = f.read()
head_sign = content[:HEAD_LENGTH]
file_size = int.from_bytes(content[HEAD_LENGTH: HEAD_LENGTH +
4
],
'big'
)
encrypt_type = int.from_bytes(content[HEAD_LENGTH + (
4
*
2
): HEAD_LENGTH + (
4
*
3
)],
'big'
)
print(
'[+] file size: %dB'
% file_size)
print(
'[+] encrypt type: %s'
% ENCRYPT_TYPES[encrypt_type -
1
])
enc_filedata = content[HEAD_LENGTH + (
4
*
3
):]
padded_content = pad(enc_filedata, DES.block_size)
plaintext = des_decrypt(padded_content)
print(plaintext)
PHP Screw
和php-beast一样,screw同样是通过hook了 compile_file 来做的混淆操作,原理上来讲大同小异,混淆算法的区别而已,加密前后对比如下所示:
int
__
fastcall
zm_startup_php_screw
(
int
type,
int
module_number
)
{
org_compile_file = (zend_op_array *(*)(zend_file_handle *,
int
))zend_compile_file;
zend_compile_file = pm9screw_compile_file;
return
0
;
}
zend_op_array *__fastcall pm9screw_compile_file(zend_file_handle *file_handle, int
type
)
{
FILE *v3;
// rax
FILE *v4;
// rbp
zend_stream_type v5;
// eax
FILE *v6;
// rax
const
char *filename;
// rdi
const
char *active_
function
_name
;
// rax
char buf[
11
];
// [rsp+5h] [rbp-53h] BYREF
char fname[
32
];
// [rsp+10h] [rbp-48h] BYREF
memset(fname,
0
, sizeof(fname));
if
( (unsigned __int8)zend_is_executing() )
{
if
( get_active_
function
_name
(
) )
{
active_
function
_name
= (
const
char *
)
get_active_function_name
(
)
;
strncpy(fname, active_
function
_name
, 0
x1EuLL
)
;
if
( fname[
0
] )
{
if
( !strcasecmp(fname,
"show_source"
) || !strcasecmp(fname,
"highlight_file"
) )
return
0
LL;
}
}
}
v3 = fopen(file_handle->filename,
"r"
);
v4 = v3;
if
( !v3 )
return
org_compile_file(file_handle,
type
);
fread(buf,
0xA
uLL,
1
uLL, v3);
if
( !memcmp(buf,
"tPM9SCREWt"
,
0xA
uLL) )
{
v5 = file_handle->
type
;
if
( file_handle->
type
== ZEND_HANDLE_FP )
{
fclose(file_handle->handle.fp);
v5 = file_handle->
type
;
}
if
( v5 == ZEND_HANDLE_FD )
close(file_handle->handle.fd);
v6 = pm9screw_ext_fopen(v4);
filename = file_handle->filename;
file_handle->handle.fp = v6;
file_handle->
type
= ZEND_HANDLE_FP;
file_handle->opened_path = (char *)expand_filepath(filename,
0
LL);
return
org_compile_file(file_handle,
type
);
}
fclose(v4);
return
org_compile_file(file_handle,
type
);
}
FILE *__
fastcall
pm9screw_ext_fopen
(FILE *fp)
{
int
v1;
// eax
int
st_size;
// ebp
int
v3;
// r12d
char
*v4;
// rbx
char
*v5;
// rsi
int
v6;
// ecx
char
*v7;
// r12
FILE *v8;
// rbp
int
newdatalen;
// [rsp+Ch] [rbp-BCh] BYREF
stat stat_buf;
// [rsp+10h] [rbp-B8h] BYREF
v1 = fileno(fp);
__fxstat(
1
, v1, &stat_buf);
st_size = stat_buf.st_size;
v3 = LODWORD(stat_buf.st_size) -
10
;
v4 = (
char
*)
malloc
(LODWORD(stat_buf.st_size) -
10
);
fread(v4, v3,
1u
LL, fp);
fclose(fp);
if
( v3 >
0
)
{
v5 = v4;
do
{
v6 = st_size -
10
;
--st_size;
++v5;
*(v5 -
1
) = LOBYTE(pm9screw_mycryptkey[v6 %
5
]) ^ ~*(v5 -
1
);
}
while
( st_size !=
10
);
}
v7 = zdecode(v4, v3, &newdatalen);
v8 = tmpfile();
fwrite(v7, newdatalen,
1u
LL, v8);
free
(v4);
free
(v7);
rewind(v8);
return
v8;
}
.data
:0000000000202128
public
pm9screw_mycryptkey
.data
:0000000000202128
; __
int16
pm9screw_mycryptkey
[5]
.data
:0000000000202128
pm9screw_mycryptkey
dw
2
B90h
, 170
h
, 0
C0h
, 501
h
, 3
Eh
.data
:0000000000202128
;
DATA
XREF
:
LOAD
:0000000000000600
↑
o
.data
:0000000000202128
;
.got
:pm9screw_mycryptkey_ptr
↑
o
在获取了对应的key后,就可以使用python对照着写一个解密脚本,使用效果如下:
def
decrypt_file(content):
file_size
=
len(content)
compress_data
=
b''
if
file_size > 0:
tmp_data
=
bytearray(content)
for
i in range(file_size):
=
ctypes.c_ubyte(screw_key[file_size % 5] ^ ~tmp_data[i]).value
file_size
-= 1
compress_data
=
bytes(tmp_data)
return
zlib.decompress(compress_data)
int
c,
len
;
char *
copy
;
if
(Z_TYPE_P(source_string) != IS_STRING) {
return
orig_compile_string(source_string, filename TSRMLS_CC);
}
len
= Z_STRLEN_P(source_string);
copy
= estrndup(Z_STRVAL_P(source_string),
len
);
if
(
len
> strlen(
copy
)) {
for
(c=
0
; c<
len
; c++)
if
(
copy
[c] ==
0
)
copy
[c] ==
'?'
;
}
php_printf(
"----------decode----------n"
);
php_printf(
"%sn"
,
copy
);
php_printf(
"----------decode----------n"
);
typedef
struct
_
zend_file_handle
{
union
{
int
fd;
FILE *fp;
zend_stream stream;
} handle;
const
char
*filename;
zend_string *opened_path;
zend_stream_type type;
zend_bool free_filename;
} zend_file_handle;
// 我们可以使用这个函数,将内容写到buf中
ZEND_API
int
zend_stream_fixup
(zend_file_handle *file_handle,
char
**buf,
size_t
*len)
;
// 需要在原始compile_file之后调用,否则文件内容仍然是加密后的
op_array = old_compile_file(file_handle, type);
char
*buf;
size_t
size;
if
(zend_stream_fixup(file_handle, &buf, &size) == SUCCESS) {
php_printf(
"---------decode--------n"
);
php_printf(
"%sn"
, buf);
php_printf(
"---------decode--------n"
);
}
.
data
:
000000000021
A7E0 off_21A7E0 dq offset aSgLoad ; DATA XREF: .
data
:
000000000021
A988↓o
.
data
:
000000000021
A7E0 ;
"sg_load"
.
data
:
000000000021
A7E8 dq offset sub_9560
__int64 __fastcall sub_9560(__int64 a1, __int64 a2)
{
return
sub_6880(a1, a2);
}
if
( v304 && !dword_21AD24 )
{
executed_scope
=
zend_get_executed_scope();
dword_21AB98
=
1;
=
executed_scope;
a2);
destroy_op_array(v304);
return
_efree(v304);
}
# Zend/zend_vm_execute.h
ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)
{
zend_execute_data *execute_data;
void *object_or_called_scope;
uint32_t call_info;
if
(EG(
exception
) !=
NULL
) {
return
;
}
object_or_called_scope = zend_get_this_object(EG(current_execute_data));
if
(EXPECTED(!object_or_called_scope)) {
object_or_called_scope = zend_get_called_scope(EG(current_execute_data));
call_info = ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE;
}
else
{
call_info = ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE | ZEND_CALL_HAS_THIS;
}
# 分配zend_execute_data
execute_data = zend_vm_stack_push_call_frame(call_info, (zend_function*)op_array,
0
, object_or_called_scope);
# 设置符号表
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->prev_execute_data = EG(current_execute_data)
i_init_code_execute_data(execute_data, op_array, return_value);
// 初始化execute_data
ZEND_OBSERVER_FCALL_BEGIN(execute_data);
zend_execute_ex(execute_data);
// 执行opcode
/* Observer end handlers are called from ZEND_RETURN */
zend_vm_stack_free_call_frame(execute_data);
//释放execute_data,销毁所有变量
}
struct
_zend_execute_data {
const
zend_op *opline; /* executed opline */
zend_execute_data
*call; /* current call */
zval
*return_value;
zend_function
*func; /* executed function */
zval
This; /* this + call_info + num_args */
zend_execute_data
*prev_execute_data;
zend_array
*symbol_table;
void
**run_time_cache; /* cache op_array->run_time_cache */
zend_array
*extra_named_params;
};
union
_zend_function {
zend_uchar
type; /* MUST be the first element of this struct! */
uint32_t
quick_arg_flags;
struct
{
zend_uchar
type; /* never used */
zend_uchar
arg_flags[3]; /* bitset of arg_info.pass_by_reference */
uint32_t
fn_flags;
zend_string
*function_name;
zend_class_entry
*scope;
zend_function
*prototype;
uint32_t
num_args;
uint32_t
required_num_args;
zend_arg_info
*arg_info; /* index -1 represents the return value info, if any */
HashTable
*attributes;
common;
zend_op_array
op_array;
zend_internal_function
internal_function;
};
在vld中,作者本身就已经hook了两个函数 compile_file 和 compile_string,留下了一个没有代码块的空函数 vld_execute_ex 也就是我们的目标hook函数。在初始化过程中,vld开始hook三个函数,这里需要注意的是,只有当 active=1 且 execute=0 的时候,zend_execute_ex 才会被替换成 vld_execute_ex。
PHP_RINIT_FUNCTION(vld)
{
old_compile_file
=
zend_compile_file;
old_compile_string
=
zend_compile_string;
old_execute_ex
=
zend_execute_ex;
if
(VLD_G(active)) {
zend_compile_file
=
vld_compile_file;
zend_compile_string
=
vld_compile_string;
if
(!VLD_G(execute)) {
zend_execute_ex
=
vld_execute_ex;
}
}
...
但是又不能进入函数直接dump就返回,因为第一次调用到 execute_ex 一定是因为 sg_load ,这个时候dump出来仍然是加密后的不可读代码,那么可以在这里加一个标志位做一次判断,看是否第一次执行,如果是的话那么让他正常走向原始 execute_ex,如果已经执行过一次,那么这时的opcode就是代码本身,取出 execute_data->func->op_array 然后使用 vld_dump_oparray dump出来,代码如下所示:
bool
flag =
false
;
static void vld_execute_ex(zend_execute_data *execute_data)
{
// nothing to do
if
(flag) {
vld_dump_oparray(&execute_data->
func
->
op_array
TSRMLS_CC
);
zend_hash_apply_with_argument
(CG(function_table)
TSRMLS_CC
,
(apply_func_args_t)
VLD_WRAP_PHP7
(vld_dump_fe)
, 0);
zend_hash_apply
(CG(class_table)
,
(apply_func_t)
VLD_WRAP_PHP7
(vld_dump_cle)
TSRMLS_CC
);
return
;
}
flag
=
true
;
old_execute_ex
(execute_data)
;
}
filename
:
/var/www/html/index.php
function
name: (null)
number
of ops: 10
compiled
vars: none
line
#* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2
0 E > INIT_FCALL 'show_source'
1
SEND_VAL '%2Fvar%2Fwww%2Fhtml%2Findex.php'
2
DO_ICALL
3
3 ECHO '%3Chr%2F%3E'
4
4 INIT_FCALL 'system'
5
SEND_VAL 'uname+-a'
6
DO_ICALL
5
7 INIT_FCALL 'phpinfo'
8
DO_ICALL
9
> RETURN 1
branch
:
# 0; line: 2- 5; sop: 0; eop: 9; out0: -2
path
#1: 0,
目前市面上还有一些强度更高的保护方案,会对opcode做进一步的处理甚至直接是个VM,在选用保护方案时,可以尽可能考虑这类破解成本较高的方案。
如果对文中提到的三种场景感兴趣,可以参考对应docker镜像 https://github.com/sco4x0/php-decrypt-env 。
【版权说明】
本作品著作权归4uuu Nya所有
未经作者同意,不得转载
天工实验室安全研究员,Nu1L Team,Web安全与代码审计。
原文始发于微信公众号(破壳平台):常见PHP源码保护与还原
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论