V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
heguangyu5
V2EX  ›  PHP

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

  •  
  •   heguangyu5 · 11 天前 · 1497 次点击

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

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

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

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

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

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

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