heguangyu5
V2EX  ›  PHP

202505 最新调研: PHP Opcode 加密混淆哪家强?

  •  1
     
  •   heguangyu5 · May 26, 2025 · 5464 views
    This topic created in 488 days ago, the information mentioned may be changed or developed.

    近来看到有网友咨询 PHP 源码保护、防破解的问题, 我也很久没有了解了. 借机更新一下自己的认识, 了解了解市场现状.

    PHP 源码保护方案有多种,本文说的是对 opcode 进行加密混淆的方案.一般认为,这种方案的加密强度较强,保护程度也较高.

    本文调研了两款 PHP 源码加密产品.调研过程中关注两个重点:

    1. 如何拿到加密混淆后的 opcode.
    2. opcode 能反编译回 PHP 代码吗?

    为了不对产品本身造成不好的影响, 我们称这两款产品分别为 AAA 和 BBB.

    AAA 是国内产品,号称 "最佳 PHP 源代码加密编译器".

    BBB 是国外产品,号称 "the most widely trusted PHP protection tool".

    先来看 AAA.

    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 的网站上把 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. AAA 可以比较容易拿到加密混淆过的 opcodes, 但没有成熟可用的反编译工具.
    2. BBB 不容易拿到加密混淆过的 opcodes, 但有成熟的反编译工具.
    14 replies  •  2025-06-04 08:51:51 +08:00
    prodcd
        1
    prodcd  
       May 26, 2025
    我司买的估计就是你说的 BBB ,这玩意能反编译?
    heguangyu5
        2
    heguangyu5  
    OP
       May 26, 2025
    @prodcd 到 dezender.xyz 上试一下看看?毕竟我用的是试用版,也许正式版本更强一些?
    prodcd
        3
    prodcd  
       May 26, 2025
    @heguangyu5 我这个是 SG 16
    v1
        4
    v1  
       May 26, 2025
    不论是代码层面混淆加密还是扩展形式的,最后都能从改造编译过的 php 内存里重新 dump 出来,只不过是代码不易读而已,跑起来完全没问题。脚本语言的弱势。
    heguangyu5
        5
    heguangyu5  
    OP
       May 26, 2025
    @prodcd BBB 不是 SG,但我印象里 SG 还不如 BBB,所以就没看 SG.稍后我再看下.
    prodcd
        6
    prodcd  
       May 26, 2025
    @heguangyu5 我这边更担心的是,客户给的是个虚拟机,我把加密后的代码部署到虚拟机,假设虚拟机在纯内网运行,客户的技术人员完全有能力复制该虚拟机,使用同样的内网 IP 和 MAC 地址,让代码加密失去意义。
    heguangyu5
        7
    heguangyu5  
    OP
       May 26, 2025
    @prodcd 这确实是没办法.如果是一次性部署的软件,组件又简单,那确实防不住.考虑一下商务手段吧.
    heguangyu5
        8
    heguangyu5  
    OP
       May 26, 2025
    @prodcd 试用了下 SG 16 Pro, 可以比较容易拿到 opcodes,并且没做太多混淆.几乎可以理解成把 php opcodes 用 sg_load() 封装了一下. 看官网的 Features 介绍确实也没说有混淆 opcodes 的特性.
    BeforeTooLate
        9
    BeforeTooLate  
       May 26, 2025
    这类加密对代码执行效率会打多少折扣,有性能损失吗?
    heguangyu5
        10
    heguangyu5  
    OP
       May 26, 2025
    @BeforeTooLate 一般来说,应该能提高性能.因为 php 代码已经预先编译成 opcode 了,省了一个步骤.

    不过 php 的 opcache 扩展就是干这个的. 这类加密扩展比 opcache 多做了一些步骤,比不上 opcache.
    tangknox1
        11
    tangknox1  
       Jun 2, 2025
    楼主可以试试 Z5 加密
    zblog 团队研发的。
    https://z5encrypt.com/
    heguangyu5
        12
    heguangyu5  
    OP
       Jun 3, 2025
    @tangknox1 我并不是为了寻找加密工具而做的调研,只是为了了解下现状.Z5 的早在几年前就是一篇长文详述了逆向过程,你可以搜索下"PHP 解密:反汇编某虚拟机加密(不进行反编译)".当然 Z5 的加密强度也还是不错的.
    tangknox1
        13
    tangknox1  
       Jun 3, 2025
    @heguangyu5 现在 PHP 加密,有没有性价比和破解难度较高的商家推荐?
    heguangyu5
        14
    heguangyu5  
    OP
       Jun 4, 2025
    @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 代码稍做调整就能编译成功.
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Privacy   ·   Solana   ·   2771 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 42ms · UTC 13:25 · PVG 21:25 · LAX 06:25 · JFK 09:25
    ♥ Do have faith in what you're doing.