近来看到有网友咨询 PHP 源码保护、防破解的问题, 我也很久没有了解了. 借机更新一下自己的认识, 了解了解市场现状.
PHP 源码保护方案有多种,本文说的是对 opcode 进行加密混淆的方案.一般认为,这种方案的加密强度较强,保护程度也较高.
本文调研了两款 PHP 源码加密产品.调研过程中关注两个重点:
为了不对产品本身造成不好的影响, 我们称这两款产品分别为 AAA 和 BBB.
AAA 是国内产品,号称 "最佳 PHP 源代码加密编译器".
BBB 是国外产品,号称 "the most widely trusted PHP protection tool".
先来看 AAA.
首先,我们需要一段 PHP 代码作为被保护对象.这里选取一个对 PDO
类进行简易封装的 Db 类. 完整源码见: Db.php
然后,使用 AAA 试用版 对Db.php
加密, 加密时选择 PHP 版本 8.0,加密完成后下载回来,然后将对应的 AAA_loader_80_nts.so
也下载回来.
php 的 opcache 扩展有个方便的功能,可以把 php 代码的 opcode dump 出来.
$ ~/tmp/php-8.0.30/bin/php -d 'opcache.enable_cli=1' -d 'opcache.opt_debug_level=0x10000' ../Db.php
$_main:
; (lines=1, args=0, vars=0, tmps=0)
; (before optimizer)
; /home/hgy/Downloads/php-opcode-test/Db.php:1-97
; return [] RANGE[0..0]
0000 RETURN int(1)
OurBlog_Db::__construct:
; (lines=36, args=0, vars=0, tmps=18)
; (before optimizer)
; /home/hgy/Downloads/php-opcode-test/Db.php:9-17
; return [] RANGE[0..0]
0000 V1 = NEW 3 string("PDO")
0001 INIT_FCALL 1 96 string("getenv")
0002 SEND_VAL string("DB_HOST") 1
0003 V2 = DO_ICALL
0004 T3 = CONCAT string("mysql:host=") V2
0005 T4 = CONCAT T3 string(";port=")
0006 INIT_FCALL 1 96 string("getenv")
0007 SEND_VAL string("DB_PORT") 1
0008 V5 = DO_ICALL
0009 T6 = CONCAT T4 V5
0010 T7 = CONCAT T6 string(";dbname=")
0011 INIT_FCALL 1 96 string("getenv")
0012 SEND_VAL string("DB_DATABASE") 1
0013 V8 = DO_ICALL
0014 T9 = CONCAT T7 V8
0015 T10 = CONCAT T9 string(";charset=utf8")
0016 SEND_VAL_EX T10 1
// 由于 V2EX 限制主题内容不能超过 20000 个字符,这里删除了余下的 opcode
现在我们拿到了 Db.php
未加密混淆的 opcode.
再来看看 AAA 加密混淆过的 Db-AAA.php
的 opcode 长什么样.
将 AAA_loader_80_nts.so
加到 php.ini
里并配置好.
~/tmp/php-8.0.30/bin/php -d 'opcache.enable_cli=1' -d 'opcache.opt_debug_level=0x1000' Db-AAA.php
什么输出都没有.
可以理解,应该是 AAA_loader_80_nts.so
来接管处理 Db-AAA.php
, opcache 扩展不起作用了.
那还有什么办法能拿到 opcode 吗?可以用 phpdbg
.
$ ~/tmp/php-8.0.30/bin/phpdbg -p* Db-AAA.php
function name: (null)
L1-97 {main}() /home/hgy/Downloads/php-opcode-test/AAA/Db-AAA.php - 0x719eb1e09cb0 + 1 ops
L97 #0 FETCH_DIM_W<-1> 1 NEXT
user class: OurBlog_Db
10 methods: __construct, __clone, getInstance, fetchOne, fetchRow, fetchAll, fetchCol, insert, update, __call
function name: __construct
L9-17 OurBlog_Db::__construct() /home/hgy/Downloads/php-opcode-test/AAA/Db-AAA.php - 0x719eb1e065a0 + 36 ops
L11 #0 NEW<3> "PDO" @0
L2147483647 #1 YIELD<1> "VTM]\\V"
L1073741823 #2 PRE_INC "ssipe "
L12 #3 DO_FCALL @1
L12 #4 FETCH_DIM_W "mysql:host=" @1 ~2
L12 #5 FETCH_DIM_W ~2 ";port=" ~1
L2147483647 #6 MATCH_ERROR<1> "VTM]\\V"
L1073741823 #7 JMPZ_EX "ssihe "
L12 #8 DO_FCALL @3
L12 #9 FETCH_DIM_W ~1 @3 ~2
L12 #10 FETCH_DIM_W ~2 ";dbname=" ~1
L2147483647 #11 MATCH_ERROR<1> "VTM]\\V"
L1073741823 #12 JMPZ_EX "usmru~ eyu"
L12 #13 DO_FCALL @3
L12 #14 FETCH_DIM_W ~1 @3 ~2
L12 #15 FETCH_DIM_W ~2 ";charset=utf8" ~1
// 由于 V2EX 限制主题内容不能超过 20000 个字符,这里删除了余下的 opcode
[Script ended normally]
不过 phpdbg
输出的 opcode 没有 opcache 输出的易读,比如最后一个函数OurBlog_Db::__call()
里的call_user_func_array()
没显示完整,只显示了个call_user_func_ar
.
有没有办法让 php-8.0.30 的 phpdbg 输出像 opcache 那种样式的 opcode 呢?
这里只所以要强调 php-8.0.30 的 phpdbg , 是因为 php-8.3 的 phpdbg 输出的 opcode 已经和 opcache 风格统一了.
我们可以对 phpdbg 稍做修改,把 opcache 输出 opcode 的代码用在 phpdbg 里,这样就可以了.
给 phpdbg 新加一个参数-p**
,来调用 opcache 里的 dump 相关代码.
$ ~/tmp/php-8.0.30/bin/phpdbg -p** Db-AAA.php
$_main:
; (lines=1, args=0, vars=0, tmps=0)
; /home/hgy/Downloads/php-opcode-test/AAA/Db-AAA.php:1-97
; return []
0000 FETCH_DIM_W int(1) NEXT
OurBlog_Db::__construct:
; (lines=36, args=0, vars=0, tmps=4, dynamic, irreducable, extended_stmt, extended_fcall)
; /home/hgy/Downloads/php-opcode-test/AAA/Db-AAA.php:9-17
; return [class] RANGE[--..136834057266072]
0000 V0 = NEW 3 string("PDO")
0001 YIELD (function) string("VTM]\V")
0002 PRE_INC string("ssipe")
0003 V1 = DO_FCALL
0004 T2 = FETCH_DIM_W string("mysql:host=") V1
0005 T1 = FETCH_DIM_W T2 string(";port=")
0006 MATCH_ERROR string("VTM]\V")
0007 JMPZ_EX string("ssihe")
0008 V3 = DO_FCALL
0009 T2 = FETCH_DIM_W T1 V3
0010 T1 = FETCH_DIM_W T2 string(";dbname=")
0011 MATCH_ERROR string("VTM]\V")
0012 JMPZ_EX string("usmru~eyu")
0013 V3 = DO_FCALL
0014 T2 = FETCH_DIM_W T1 V3
0015 T1 = FETCH_DIM_W T2 string(";charset=utf8")
0016 OP_242 T1
0017 BOOL_XOR string("VTM]\V")
0018 FE_RESET_RW string("ssimy ")
0019 V1 = DO_FCALL
0020 SEND_USER V1 2
0021 MATCH_ERROR string("VTM]\V")
0022 OP_244 string("usmfuy
kxt")
0023 V1 = DO_FCALL
0024 SEND_USER V1 3
0025 DO_FCALL
0026 FETCH_DIM_W string("pdo")
0027 FETCH_DIM_W V0 NEXT
0028 EXT_STMT T0 string("FTY")
0029 CASE T0 string("AVMw@TS)FGLU")
0030 T0 = FETCH_DIM_W string("PDO") string("ATTR_ERRMODE")
0031 SR T0
0032 T0 = FETCH_DIM_W string("PDO") string("ERRMODE_EXCEPTION")
0033 GET_CLASS T0
0034 DO_FCALL
0035 FETCH_DIM_W null NEXT
OurBlog_Db::__clone:
; (lines=1, args=0, vars=0, tmps=0)
; /home/hgy/Downloads/php-opcode-test/AAA/Db-AAA.php:19-20
; return [undef, ref, class] RANGE[--..136834057268056]
0000 FETCH_DIM_W null NEXT
OurBlog_Db::getInstance:
; (lines=9, args=0, vars=0, tmps=2, dynamic, irreducable, extended_stmt, extended_fcall)
; /home/hgy/Downloads/php-opcode-test/AAA/Db-AAA.php:22-28
; return [] RANGE[--..2207613190024]
0000 T1 = FETCH_DIM_W string("instance") NEXT
0001 T0 = FETCH_DIM_W T1 null
0002 JMPZ T0 0007
0003 V0 = NEW 0 (self) (exception)
0004 DO_FCALL
0005 FETCH_DIM_W string("instance") NEXT
0006 FETCH_DIM_W V0 NEXT
0007 T0 = FETCH_DIM_W string("instance") NEXT
0008 FETCH_DIM_W T0 NEXT
OurBlog_Db::fetchOne:
; (lines=13, args=2, vars=3, tmps=1, dynamic, irreducable, extended_stmt, extended_fcall)
; /home/hgy/Downloads/php-opcode-test/AAA/Db-AAA.php:30-35
; return [ref, class] RANGE[--..6262542]
0000 CV0($Sh40) = RECV 1
0001 CV1($Sh41) = RECV_INIT 2 array(...)
0002 OP_204 T3 string("FTY")
0003 FETCH_DIM_W T3 string("AA\GXRD")
0004 SEND_USER CV0($Sh40) 1
0005 V3 = DO_FCALL
0006 CV2($Sh42) = FETCH_DIM_W V3 NEXT
0007 BIND_LEXICAL (ref) CV2($Sh42) string("TK\TLTD")
0008 SEND_USER CV1($Sh41) 1
0009 DO_FCALL
0010 OP_216 CV2($Sh42) string("TTMU_cN,Q_V")
0011 V3 = DO_FCALL
0012 FETCH_DIM_W V3 NEXT
OurBlog_Db::fetchRow:
; (lines=15, args=3, vars=4, tmps=1, dynamic, irreducable, extended_stmt, extended_fcall)
; /home/hgy/Downloads/php-opcode-test/AAA/Db-AAA.php:37-42
; return [class] RANGE[--..136834057270096]
0000 CV0($Sh43) = RECV 1
0001 CV1($Sh44) = RECV_INIT 2 array(...)
0002 CV2($Sh45) = RECV_INIT 3 zval(type=11)
0003 DECLARE_LAMBDA_FUNCTION T4 string("FTY")
0004 OP_216 T4 string("AA\GXRD")
0005 SEND_USER CV0($Sh43) 1
0006 V4 = DO_FCALL
0007 CV3($Sh46) = FETCH_DIM_W V4 NEXT
0008 SWITCH_STRING CV3($Sh46) 0008 string("TK\TLTD")
0009 SEND_USER CV1($Sh44) 1
0010 DO_FCALL
0011 BW_XOR CV3($Sh46) string("_\LVH")
0012 SEND_USER CV2($Sh45) 1
0013 V4 = DO_FCALL
0014 FETCH_DIM_W V4 NEXT
OurBlog_Db::fetchAll:
; (lines=15, args=3, vars=4, tmps=1, dynamic, irreducable, extended_stmt, extended_fcall)
; /home/hgy/Downloads/php-opcode-test/AAA/Db-AAA.php:44-49
; return [class] RANGE[--..136834057271416]
0000 CV0($Sh47) = RECV 1
0001 CV1($Sh48) = RECV_INIT 2 array(...)
0002 CV2($Sh49) = RECV_INIT 3 zval(type=11)
0003 OP_204 T4 string("FTY")
0004 OP_246 T4 string("AA\GXRD")
0005 SEND_USER CV0($Sh47) 1
0006 V4 = DO_FCALL
0007 CV3($Sh410) = FETCH_DIM_W V4 NEXT
0008 OP_220 CV3($Sh410) string("TK\TLTD")
0009 SEND_USER CV1($Sh48) 1
0010 DO_FCALL
0011 YIELD_FROM CV3($Sh410) string("WPMT^aM,")
0012 SEND_USER CV2($Sh49) 1
0013 V4 = DO_FCALL
0014 FETCH_DIM_W V4 NEXT
OurBlog_Db::fetchCol:
; (lines=15, args=2, vars=3, tmps=1, dynamic, irreducable, extended_stmt, extended_fcall)
; /home/hgy/Downloads/php-opcode-test/AAA/Db-AAA.php:51-56
; return [ref, class] RANGE[--..136834057272704]
0000 CV0($Sh411) = RECV 1
0001 CV1($Sh412) = RECV_INIT 2 array(...)
0002 OP_222 T3 string("FTY")
0003 FETCH_DIM_W T3 string("AA\GXRD")
0004 SEND_USER CV0($Sh411) 1
0005 V3 = DO_FCALL
0006 CV2($Sh413) = FETCH_DIM_W V3 NEXT
0007 CASE CV2($Sh413) string("TK\TLTD")
0008 SEND_USER CV1($Sh412) 1
0009 DO_FCALL
0010 BIND_LEXICAL (ref) CV2($Sh413) string("WPMT^aM,")
0011 T3 = FETCH_DIM_W string("PDO") string("FETCH_COLUMN")
0012 OP_231 T3
0013 V3 = DO_FCALL
0014 FETCH_DIM_W V3 NEXT
OurBlog_Db::insert:
; (lines=55, args=2, vars=7, tmps=5, dynamic, irreducable, extended_stmt, extended_fcall)
; /home/hgy/Downloads/php-opcode-test/AAA/Db-AAA.php:58-74
; return [class] RANGE[--..136834057274120]
0000 CV0($Sh414) = RECV 1
0001 CV1($Sh415) = RECV 2
0002 JMPNZ CV1($Sh415) 0011
0003 CONCAT T7 string("FTY")
0004 SWITCH_STRING T7 0004 string("RA][")
0005 T8 = DECLARE_ANON_CLASS string("INSERT INTO `")
0006 T8 = DECLARE_ANON_CLASS T8 CV0($Sh414)
0007 T7 = FETCH_DIM_W T8 string("` VALUES (NULL)")
0008 OP_228 T7
0009 DO_FCALL
0010 FETCH_DIM_W null NEXT
0011 CV2($Sh416) = FETCH_DIM_W array(...) NEXT
0012 DEFINED string("PKKVIJ%]A")
0013 SEND_USER CV1($Sh415) 1
0014 V8 = DO_FCALL
0015 V7 = FETCH_DIM_W V8 NEXT
0016 FETCH_DIM_W V7 CV3($Sh417)
0017 T9 = DECLARE_ANON_CLASS string("`")
0018 T9 = DECLARE_ANON_CLASS T9 CV3($Sh417)
0019 T8 = FETCH_DIM_W T9 string("`")
0020 FETCH_DIM_W CV2($Sh416) NEXT
0021 FETCH_DIM_W T8 NEXT
0022 FETCH_DIM_W NEXT
0023 FE_FREE V7
0024 BW_NOT string("X^I[VDD")
0025 FETCH_OBJ_IS THIS string("")
0026 SEND_USER CV2($Sh416) 2
0027 V7 = DO_FCALL
0028 FETCH_DIM_W CV2($Sh416) V7
0029 MUL string("BMKhBEQ%EF")
")30 FE_RESET_RW string("
0031 T8 = FETCH_DIM_W CV1($Sh415) NEXT
0032 T7 = FETCH_DIM_W T8 int(1)
0033 SEND_USER T7 2
0034 V8 = DO_FCALL
0035 CV4($Sh418) = FETCH_DIM_W V8 string("?")
0036 T8 = DECLARE_ANON_CLASS string("INSERT INTO `")
0037 T8 = DECLARE_ANON_CLASS T8 CV0($Sh414)
0038 T8 = DECLARE_ANON_CLASS T8 string("` (")
0039 T8 = DECLARE_ANON_CLASS T8 CV2($Sh416)
0040 T8 = DECLARE_ANON_CLASS T8 string(") VALUES (")
0041 T8 = DECLARE_ANON_CLASS T8 CV4($Sh418)
0042 CV5($Sh419) = FETCH_DIM_W T8 string(")")
0043 CONCAT T7 string("FTY")
0044 BIND_LEXICAL (ref) T7 string("AA\GXRD")
0045 SEND_USER CV5($Sh419) 1
0046 V7 = DO_FCALL
0047 CV6($Sh420) = FETCH_DIM_W V7 NEXT
0048 OP_216 CV6($Sh420) string("TK\TLTD")
0049 MATCH_ERROR string("SAKWMW!HG]C")
0050 SEND_USER CV1($Sh415) 1
0051 V7 = DO_FCALL
0052 SEND_USER V7 1
0053 DO_FCALL
0054 FETCH_DIM_W null NEXT
OurBlog_Db::update:
; (lines=44, args=3, vars=7, tmps=4, dynamic, irreducable, extended_stmt, extended_fcall)
; /home/hgy/Downloads/php-opcode-test/AAA/Db-AAA.php:76-90
; return [class] RANGE[--..136834057277472]
0000 CV0($Sh421) = RECV 1
0001 CV1($Sh422) = RECV 2
0002 CV2($Sh423) = RECV_INIT 3 string("1")
0003 JMPNZ CV1($Sh422) 0008
0004 V7 = NEW 1 string("Exception")
0005 OP_226 string("update with empty row is not allowed!")
0006 DO_FCALL
0007 FETCH_DIM_W V7 NEXT
0008 CV3($Sh424) = FETCH_DIM_W array(...) NEXT
0009 MUL string("PKKVIJ%]A")
0010 SEND_USER CV1($Sh422) 1
0011 V8 = DO_FCALL
0012 V7 = FETCH_DIM_W V8 NEXT
0013 FETCH_DIM_W V7 CV4($Sh425)
0014 T9 = DECLARE_ANON_CLASS string("`")
0015 T9 = DECLARE_ANON_CLASS T9 CV4($Sh425)
0016 T8 = FETCH_DIM_W T9 string("` = ?")
0017 FETCH_DIM_W CV3($Sh424) NEXT
0018 FETCH_DIM_W T8 NEXT
0019 FETCH_DIM_W NEXT
0020 FE_FREE V7
0021 FETCH_FUNC_ARG (global) string("X^I[VDD")
0022 OP_205 string("")
0023 SEND_USER CV3($Sh424) 2
0024 V7 = DO_FCALL
0025 FETCH_DIM_W CV3($Sh424) V7
0026 T8 = DECLARE_ANON_CLASS string("UPDATE `")
0027 T8 = DECLARE_ANON_CLASS T8 CV0($Sh421)
0028 T8 = DECLARE_ANON_CLASS T8 string("` SET ")
0029 T8 = DECLARE_ANON_CLASS T8 CV3($Sh424)
0030 T8 = DECLARE_ANON_CLASS T8 string(" WHERE ")
0031 CV5($Sh426) = FETCH_DIM_W T8 CV2($Sh423)
0032 POST_INC T7 string("FTY")
0033 BW_XOR T7 string("AA\GXRD")
0034 SEND_USER CV5($Sh426) 1
0035 V7 = DO_FCALL
0036 CV6($Sh427) = FETCH_DIM_W V7 NEXT
0037 OP_220 CV6($Sh427) string("TK\TLTD")
0038 FETCH_FUNC_ARG string("SAKWMW!HG]C")
0039 SEND_USER CV1($Sh422) 1
0040 V7 = DO_FCALL
0041 SEND_USER V7 1
0042 DO_FCALL
0043 FETCH_DIM_W null NEXT
OurBlog_Db::__call:
; (lines=10, args=2, vars=2, tmps=2, dynamic, irreducable, extended_stmt, extended_fcall)
; /home/hgy/Downloads/php-opcode-test/AAA/Db-AAA.php:92-95
; return [!ref, class] RANGE[--..109071675059458]
0000 CV0($Sh428) = RECV 1
0001 CV1($Sh429) = RECV 2
0002 DECLARE_LAMBDA_FUNCTION T3 string("FTY")
0003 T2 = FETCH_DIM_W T3 NEXT
0004 T2 = DECLARE_ANON_CLASS CV0($Sh428)
0005 NEW 0 string("call_user_func_array") T2
0006 SEND_UNPACK CV1($Sh429)
0007 FETCH_DIM_W NEXT
0008 V2 = DO_FCALL
0009 FETCH_DIM_W V2 NEXT
[Script ended normally]
好,现在我们拿到了 AAA 加密混淆过的 opcode.
接下来就要把这些 opcode 给反编译成 PHP 代码. 这可不好弄. 不过好在有 AI 大模型,是时候展现 AI 真正的实力了!
以下是 Google Gemini 反编译的结果:
@see https://g.co/gemini/share/148782890130
大家可以自行对比一下,反正我是被震惊到了!
也有可能 Db.php
的代码较为常见,被 AI 蒙对了.
腾讯元宝 DeepSeek-R1 反编译的结果如下:
@see https://yuanbao.tencent.com/bot/app/share/chat/KUiqoTNjZalJ
AAA 的调研我们先到这里.
AAA 留给我们的问题是, opcode 到底能不能程序化地反编译成 PHP 代码.
去 BBB 的网站上把 BBB 的 encoder 试用版 和 loader 都下载回来.
BBB 的 encoder 没有 php-8.0 版本的, 那我们就选最高可用版本 php-8.3 的.
同样对 Db.php
进行加密, 得到加密后的文件 Db-BBB.php
./BBB_encoder_evaluation/BBB_encoder.sh -C -x86-64 -83 ../Db.php -o Db-BBB.php
接下来我们用同样的办法尝试拿到 Db-BBB.php
的 opcode.
将 BBB_loader_lin_8.3.so
加到 php.ini 里配置好.
先用 opcache 尝试一下:
$ ~/tmp/php-8.3.21/bin/php -d 'opcache.enable_cli=1' -d 'opcache.opt_debug_level=0x1000' Db-BBB.php
没有任何输出.
再用 phpdbg 尝试一下:
$ ~/tmp/php-8.3.21/bin/phpdbg -p* Db-BBB.php
Segmentation fault (core dumped)
直接 segfault 了.
我们使用 gdb 来调试一下.
$ gdb ~/tmp/php-8.3.21/bin/phpdbg
(gdb) b phpdbg_compile_file
(gdb) r -p* ../Db.php
(gdb) n
(gdb)
250 ret = PHPDBG_G(compile_file)(file, type);
(gdb)
251 if (ret == NULL) {
(gdb) set print pretty on
(gdb) p *ret
$1 = {
type = 2 '\002',
arg_flags = "\000\000",
fn_flags = 100663296,
function_name = 0x0,
scope = 0x0,
prototype = 0x0,
num_args = 0,
required_num_args = 0,
arg_info = 0x0,
attributes = 0x0,
run_time_cache__ptr = 0x0,
T = 0,
cache_size = 0,
last_var = 0,
last = 1,
opcodes = 0x7ffff5002450,
static_variables_ptr__ptr = 0x0,
static_variables = 0x0,
vars = 0x0,
refcount = 0x7ffff5004008,
last_live_range = 0,
last_try_catch = 0,
live_range = 0x0,
try_catch_array = 0x0,
filename = 0x7ffff505e3c0,
line_start = 1,
line_end = 97,
doc_comment = 0x0,
last_literal = 1,
num_dynamic_func_defs = 0,
literals = 0x7ffff5002470,
dynamic_func_defs = 0x0,
reserved = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
}
(gdb) c
Continuing.
...
[Script ended normally]
[Inferior 1 (process 187677) exited normally]
(gdb) r -p* Db-BBB.php
(gdb) n
(gdb)
250 ret = PHPDBG_G(compile_file)(file, type);
(gdb)
251 if (ret == NULL) {
(gdb) p *ret
$2 = {
type = 2 '\002',
arg_flags = "\000\000",
fn_flags = 100663296,
function_name = 0x0,
scope = 0x0,
prototype = 0x0,
num_args = 0,
required_num_args = 0,
arg_info = 0x0,
attributes = 0x0,
run_time_cache__ptr = 0x7ffff5004030,
T = 1,
cache_size = 0,
last_var = 0,
last = 0,
opcodes = 0x1,
static_variables_ptr__ptr = 0x0,
static_variables = 0x0,
vars = 0x0,
refcount = 0x7ffff5004020,
last_live_range = 0,
last_try_catch = 0,
live_range = 0x0,
try_catch_array = 0x0,
filename = 0x0,
line_start = 1,
line_end = 0,
doc_comment = 0x0,
last_literal = 0,
num_dynamic_func_defs = 0,
literals = 0x0,
dynamic_func_defs = 0x0,
reserved = {0x0, 0x0, 0x0, 0x7ffff5081460, 0x0, 0x0}
}
(gdb) quit
对比两次 p *ret
不难发现, 未加密的 Db.php
:
opcodes = 0x7ffff5002450, reserved = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
已加密的 Db-BBB.php
:
opcodes = 0x1, reserved = {0x0, 0x0, 0x0, 0x7ffff5081460, 0x0, 0x0}
0x1
显然不是一个有效的内存地址,现在 opcode 在哪儿,不好找了.
再次 gdb 关注一下 zend_compile_file
和 zend_execute_ex
:
$ gdb ~/tmp/php-8.3.21/bin/php
(gdb) watch zend_compile_file
(gdb) watch zend_execute_ex
(gdb) r Db-BBB.php
...
(gdb)
Continuing.
Hardware watchpoint 1: zend_compile_file
Old value = (zend_op_array *(*)(zend_file_handle *, int)) 0x555555786ae0 <phar_compile_file>
New value = (zend_op_array *(*)(zend_file_handle *, int)) 0x7ffff40e5041
0x00007ffff40571d4 in ?? () from /home/hgy/Downloads/php-opcode-test/BBB/BBB/BBB_loader_lin_8.3.so
(gdb)
Continuing.
Hardware watchpoint 2: zend_execute_ex
Old value = (void (*)(zend_execute_data *)) 0x55555596adf0 <execute_ex>
New value = (void (*)(zend_execute_data *)) 0x7ffff40f2784
0x00007ffff40571de in ?? () from /home/hgy/Downloads/php-opcode-test/BBB/BBB/BBB_loader_lin_8.3.so
可以看到 BBB_loader_lin_8.3.so
既接管了 zend_compile_file
, 又接管了 zend_execute_ex
.
这样 opcodes 就成了个黑盒子, 我们既不知道在哪儿,也不知道内容.
这怎么办呢? 是不是说 BBB 这个产品加密强度非常强,值得信赖呢?
别急,网上搜索一下. 很快就找到了这个 https://dezender.xyz/
在 DECODERS 菜单里,就有 BBB PHP 8.3, 可以在线试用,只不过只能 decode 10 行,我们试一下.
成功解密.
<?php
/*
* @ https://dezender.xyz - BBB Decoder Online
* @ Decoder version: 3.0.0
* @ Release: 2025/04/09
*/
class OurBlog_Db {
protected static $instance = null;
protected $pdo = null;
protected function __construct(){
$this->pdo = new PDO("mysql:host=" . getenv("DB_HOST") . ";port=" . getenv("DB_PORT") . ";dbname=" . getenv("DB_DATABASE") . ";charset=utf8", getenv("DB_USER"), getenv("DB_PASSWORD"));
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
private function __clone(){
}
// This is the demo version. Demo version decode 10 lines only.
简直太强了!!!
1
prodcd 11 天前
我司买的估计就是你说的 BBB ,这玩意能反编译?
|
![]() |
2
heguangyu5 OP @prodcd 到 dezender.xyz 上试一下看看?毕竟我用的是试用版,也许正式版本更强一些?
|
3
prodcd 11 天前
@heguangyu5 我这个是 SG 16
|
![]() |
4
kk2syc 11 天前
不论是代码层面混淆加密还是扩展形式的,最后都能从改造编译过的 php 内存里重新 dump 出来,只不过是代码不易读而已,跑起来完全没问题。脚本语言的弱势。
|
![]() |
5
heguangyu5 OP @prodcd BBB 不是 SG,但我印象里 SG 还不如 BBB,所以就没看 SG.稍后我再看下.
|
6
prodcd 11 天前
@heguangyu5 我这边更担心的是,客户给的是个虚拟机,我把加密后的代码部署到虚拟机,假设虚拟机在纯内网运行,客户的技术人员完全有能力复制该虚拟机,使用同样的内网 IP 和 MAC 地址,让代码加密失去意义。
|
![]() |
7
heguangyu5 OP @prodcd 这确实是没办法.如果是一次性部署的软件,组件又简单,那确实防不住.考虑一下商务手段吧.
|
![]() |
8
heguangyu5 OP @prodcd 试用了下 SG 16 Pro, 可以比较容易拿到 opcodes,并且没做太多混淆.几乎可以理解成把 php opcodes 用 sg_load() 封装了一下. 看官网的 Features 介绍确实也没说有混淆 opcodes 的特性.
|
![]() |
9
BeforeTooLate 11 天前
这类加密对代码执行效率会打多少折扣,有性能损失吗?
|
![]() |
10
heguangyu5 OP @BeforeTooLate 一般来说,应该能提高性能.因为 php 代码已经预先编译成 opcode 了,省了一个步骤.
不过 php 的 opcache 扩展就是干这个的. 这类加密扩展比 opcache 多做了一些步骤,比不上 opcache. |
11
tangknox1 3 天前
|
![]() |
12
heguangyu5 OP @tangknox1 我并不是为了寻找加密工具而做的调研,只是为了了解下现状.Z5 的早在几年前就是一篇长文详述了逆向过程,你可以搜索下"PHP 解密:反汇编某虚拟机加密(不进行反编译)".当然 Z5 的加密强度也还是不错的.
|
13
tangknox1 3 天前
@heguangyu5 现在 PHP 加密,有没有性价比和破解难度较高的商家推荐?
|
![]() |
14
heguangyu5 OP @tangknox1 PHP 加密还是以下四种:
1. 玩障眼法的. 就是不改变源代码,对源代码做各种封装,运行时解密出来.这种最容易破解. 2. 在 PHP 源代码层面做混淆的. Z5 加密就属于这一类.没有调研过,应该没有成熟的解密工具. 3. 基于 opcode 加密混淆的 本文的 AAA 和 BBB 就是. BBB 充分证明了 opcode 加密混淆根本就不顶用. 4. 第三方实现的转译器/编译器 前 3 种都是基于 PHP 解释器.加密和解密双方比的是斗智斗勇. 而第三方实现的转译器/编译器可以 100%保护源码,无需斗智斗勇.如果想省心,推荐这种,当然会有各种限制. 实现原理: PHP ---> 转译成另一种语言 ---> 编译成机器码. 当然机器码,比如汇编,也是可以反汇编的,但反汇编得到的源码和 PHP 源码就差的太远了. 目前有 3 个选择: 1) PeachPie (开源) 将 PHP 编译到.NET, 是.NET Foundation 支持的项目,如果有.NET 相关技术背景可考虑. 除加密外,还可提升性能,但和 PHP 兼容性待验证,看 issue 列表就知道了. 2) KPHP (开源) 将 PHP 转译成 C++.俄罗斯 vk.com 的项目.同样除加密外,可提升性能.但它只实现了**a limited subset of PHP**,可能需要大幅调整 PHP 源代码才能编译通过. 3) BPC (闭源,本人作品) 将 PHP 转译成 scheme,再转译成 C. 源码保护没问题,还有授权机制,但性能不高.与 PHP7.2 高度兼容,跑通了 PHP7.2 的 phpt 测试用例.通常 PHP 代码稍做调整就能编译成功. |