吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 401|回复: 2
上一主题 下一主题
收起左侧

[Web逆向] 【算法分析】某 PHP 混淆器 V3.1.0 静态解混淆与防护强度评测

[复制链接]
跳转到指定楼层
楼主
imgjx 发表于 2026-10-10 20:07 回帖奖励
本帖最后由 imgjx 于 2026-10-13 12:32 编辑

某 PHP 混淆器 V3.1.0 及变种加密强度分析

声明

本文目的不是破解,而是分析某 PHP 混淆器 V3.1.0 及其变种的加密强度与防逆向能力。文中相关商业名称均已消去,仅讨论技术原理。本文不提供任何形式的成品工具或一键解码脚本,仅分享分析思路与伪代码。

样本

一份由该工具 V3.1.0 加密的 PHP 文件,原始代码仅为一行 echo 1;,用于测试加密强度。加密后的文件结构如下:

  • 开头:版权注释,包含工具名、版本号、创建时间、联系方式
  • 主体:if(!defined(...))define(...); $GLOBALS[...]=explode('|,|2|2','...');
  • 后续:大量 goto 语句构成的跳转网络
  • 结尾:echo $________;

第一层:字符串表

加密代码的核心入口是一句 explode:

$GLOBALS[A__AA_AAA]=explode('|,|2|2','H*|,|2|262324A...|,|2|...');

用一条正则即可提取完整字符串表:

/explode\s*\(\s*['"]\|,\|2\|2['"]\s*,\s*['"]([\s\S]*?)['"]\s*\)/

提取后按 |,|2|2 分割,得到 14 段数据。每段数据对应一个字符串常量或一段 payload。

第二层:逐段解码

对每一段数据,解码流程是固定的:

hex 解码 → 判断是否为 base64 → 若是则 base64 解码 → 尝试 gzip/deflate 解压

伪代码:

FUNCTION decode_piece(piece):
    FOR skip FROM 0 TO 3:
        candidate = piece[skip:]
        # 跳过前几个字节,尝试匹配纯十六进制串
        IF candidate IS NOT hex string:
            CONTINUE

        # 第一步:hex 解码
        bytes = hex_decode(candidate)

        # 第二步:如果结果是短可读文本,直接返回
        IF bytes IS printable AND length(bytes) < 300:
            RETURN bytes

        # 第三步:如果是 base64,先 base64 解码
        IF bytes IS valid base64:
            decoded = base64_decode(bytes)

            # 第四步:尝试解压
            decompressed = try_decompress(decoded)
            IF decompressed EXISTS:
                RETURN decompressed
            ELSE:
                RETURN decoded

        # 第五步:不是 base64,直接尝试解压
        decompressed = try_decompress(bytes)
        IF decompressed EXISTS:
            RETURN decompressed

        RETURN bytes

    RETURN decode_failed

其中 try_decompress 的逻辑:

FUNCTION try_decompress(bytes):
    # 依次尝试三种压缩格式,哪个成功用哪个
    FOR format IN [deflate-raw, deflate, gzip]:
        TRY:
            stream = create_decompression_stream(format)
            write(stream, bytes)
            close(stream)
            result = read_all(stream)
            IF result IS NOT empty:
                RETURN result
        CATCH exception:
            CONTINUE
    RETURN null

逐段处理结果:

段索引 解码后内容 长度
0 (失败) —
1 ob_end_clean 12 字节
2 return eval($_); 18 字节
3 ob_start 8 字节
4 gzuncompress 12 字节
5 $_ 2 字节
6 base64_decode 13 字节
7 __BFeVWX 8 字节
8 主 payload 1140 字节
9 printf 6 字节
10 ob_get_contents 15 字节
11 Loading DeckPHP Encode 22 字节
12 function_exists 15 字节
13 __BFeVWX 8 字节

第 8 段是主 payload,解码过程为:

hex 解码 → 952 字节
base64 解码 → 714 字节
deflate 解压 → 1140 字节

第三层:payload 分析

第 8 段解码后的内容是一段 PHP 代码,核心结构如下:

$____ = 'printf';
$___________ = 'DeckPHP Encode Class...';
/* [DECKPHP] */
$__... = '...'; // 一个超长 base64 字符串

在这个 payload 中,存在一个长度超过 400 字符的 base64 串。提取并解码:

伪代码:

FUNCTION extract_final_layer(source):
    # 在源码中找所有 $变量 = '超长base64字符串';
    regex = /\$\w+\s*=\s*'([A-Za-z0-9+/=]{200,})'/g
    longest = null

    FOR EACH match IN regex.execute(source):
        current = match[1]
        # 保留最长的那一个
        IF longest IS null OR length(current) > length(longest):
            longest = current

    IF longest IS null:
        RETURN source  # 没找到,返回原样

    # base64 解码
    decoded = base64_decode(longest)

    # 尝试解压
    decompressed = try_decompress(decoded)
    IF decompressed EXISTS:
        RETURN decompressed

    # 没有压缩,直接当文本返回
    RETURN decoded

对该 base64 串解码:

base64 解码 → 331 字节

解码结果直接就是可读文本,即最终源码。

最终源码

/*
本代码由  - Version V3 创建
创建时间 2026-10-09 12:02:44
技术支持 去个敏<>
严禁反编译、逆向等任何形式的侵权行为,违者将追究法律责任
*/
if(!defined('_AAA____A'))define('_AAA____A', 'AA__AA___');
$GLOBALS[_AAA____A]=explode('|W|S|I','H*');
unset($�);$�;
echo 1;

原始代码 echo 1; 被完整还原。加密代码中大量 goto 跳转、变量混淆、字符串表索引,在静态解码面前全部失效——因为解码过程根本没有执行加密代码,而是直接从数据层把源码捞了出来。

完整流程伪代码

把上面各层串起来,整体分析流程如下:

FUNCTION analyze_encrypted_file(file_content):
    # ===== 第一步:提取字符串表 =====
    match = regex_extract(file_content, /explode\(...\)/)
    IF match FAILED:
        OUTPUT "未找到字符串表"
        RETURN

    raw_table = match.group[1]
    pieces = split(raw_table, '|,|2|2')

    # ===== 第二步:逐段解码 =====
    decoded_table = []
    FOR EACH piece IN pieces:
        result = decode_piece(piece)
        decoded_table.append(result)

    # ===== 第三步:找主 payload =====
    # 解码后最长的那段就是主 payload
    candidates = filter(decoded_table, length > 100)
    IF candidates IS empty:
        OUTPUT "未找到主 payload"
        RETURN

    main_payload = candidates.sort_by_length_desc().first()

    # ===== 第四步:提取最后一层 =====
    final_source = extract_final_layer(main_payload)

    # ===== 第五步:输出 =====
    OUTPUT final_source

整个流程不需要执行加密代码,不需要调试器,不需要理解 goto 跳转逻辑。

变种分析

闲来无事到这个混淆器官网生成了一个变种玩。该变种与原版共用相同的顶层结构(版权注释 + explode 字符串表 + goto 网络),但在分隔符、段索引、payload 位置上有所差异。以下分析该变种。

变种 A:分隔符变化 + 超长 payload 段

特征:

$GLOBALS[__A____A_]=explode('|3|B|,','H*|3|B|,7072696E7466|3|B|,...|3|B|,654E7139...');

差异:

  • 分隔符从 |,|2|2 变为 |3|B|,
  • 常量名从 A__AA_AAA 变为 __A____A_
  • 主 payload 段从第 8 段变为第 3 段
  • 主 payload 解码过程从 hex + base64 + deflate 变为 hex + base64 + gzinflate
  • goto 网络规模从 184 个变为 500 个

分析:

变种 A 的差异体现在参数层面,而非结构层面。分隔符、常量名、段索引都是可变参数,不影响解码流程。

主 payload 的定位方式需要从“固定取第 8 段”改为“取解码后最长的段”。这是因为变种 A 把主 payload 放在了第 3 段,而原版在第 8 段。通过“取最长”这一策略,可以自动适配不同变种的段索引。

伪代码:

FUNCTION find_main_payload(decoded_table):
    # 找出解码后长度超过 100 的所有段
    candidates = []
    FOR EACH item IN decoded_table:
        IF item.decoded EXISTS AND length(item.decoded) > 100:
            candidates.append(item)

    IF candidates IS empty:
        RETURN null

    # 按长度降序排列,取最长
    candidates.sort_by_length_desc()
    RETURN candidates.first()

goto 网络规模的变化不改变防护本质。无论 184 个还是 500 个 goto,它们都只影响动态执行时的控制流。在静态解码路径下,我们不执行这些 goto。当然goto最后可以解:参考 https://www.52pojie.cn/thread-1760466-1-1.html

变种 A 完整解码链

第 1 层:explode('|3|B|,', ...)  → 14 段
第 2 层:逐段 hex 解码
第 3 层:第 3 段 hex 解码 → 2784 字节
第 4 层:base64 解码 → 2088 字节
第 5 层:gzinflate 解压 → 3776 字节(中间代码)
第 6 层:从中间代码里提取 $___________ 的 base64 → 3080 字符
第 7 层:base64 解码 → 2206 字节(最终 payload)
第 8 层:最终 payload 里 explode('|#|X|9', ...) → 7 段
第 9 层:逐段 hex 解码 → 业务字符串

变种对比总表

特性 原版 V3 变种 A
分隔符 \|,\|2\|2 \|3\|B\|,
常量名 A__AA_AAA __A____A_
段数 14 14
主 payload 段 第 8 段 第 3 段
主 payload 解码 hex + base64 + deflate hex + base64 + gzinflate
中间代码 无 有(3776 字节)
最终 payload 内嵌 explode 无 有(\|#\|X\|9)
goto 数 184 500
静态解码耗时 约 20 分钟 约 35 分钟

通用解码流程伪代码

针对原版和变种 A,通用解码流程如下:

FUNCTION analyze_encrypted_file_v3(file_content):
    # ===== 第一步:提取字符串表(通用)=====
    match = regex_extract(file_content, /explode\s*\(\s*'([^']+)'\s*,\s*'([^']+)'\s*\)/)
    IF match FAILED:
        OUTPUT "未找到字符串表"
        RETURN

    separator = match.group[1]   # 自动读取分隔符,适配 |,|2|2 和 |3|B|,
    raw_table = match.group[2]
    pieces = split(raw_table, separator)

    # ===== 第二步:逐段解码(通用)=====
    decoded_table = []
    FOR EACH piece IN pieces:
        result = decode_piece(piece)
        decoded_table.append(result)

    # ===== 第三步:找主 payload(通用)=====
    # 不依赖固定索引,而是取解码后最长的段
    main_payload = find_main_payload(decoded_table)
    IF main_payload IS null:
        OUTPUT "未找到主 payload"
        RETURN

    # ===== 第四步:提取最终源码(通用)=====
    # 主 payload 可能直接是源码,也可能内部还有一层
    final_source = extract_final_layer(main_payload)

    # ===== 第五步:如果最终源码里还有 explode,递归解一层(变种 A)=====
    IF final_source CONTAINS "explode(":
        inner_match = regex_extract(final_source, /explode\s*\(\s*'([^']+)'\s*,\s*'([^']+)'\s*\)/)
        IF inner_match FOUND:
            inner_sep = inner_match.group[1]
            inner_table = inner_match.group[2]
            inner_pieces = split(inner_table, inner_sep)
            inner_decoded = []
            FOR EACH p IN inner_pieces:
                inner_decoded.append(decode_piece(p))
            final_source = replace_in_source(final_source, inner_decoded)

    # ===== 第六步:输出 =====
    OUTPUT final_source

其中 extract_final_layer 的逻辑如下:

FUNCTION extract_final_layer(source):
    # 在源码中找所有 $变量 = '超长base64字符串';
    # 取最长的那一个
    regex = /\$\w+\s*=\s*'([A-Za-z0-9+/=]{200,})'/g
    matches = regex_findall(source, regex)

    IF matches IS empty:
        RETURN source

    longest = matches.sort_by_length_desc().first()

    # base64 解码
    decoded = base64_decode(longest)

    # 尝试解压
    decompressed = try_decompress(decoded)
    IF decompressed EXISTS:
        RETURN decompressed

    RETURN decoded

该流程的核心设计原则是:

  1. 不依赖固定分隔符:动态读取 explode 的第一个参数
  2. 不依赖固定段索引:取解码后最长的段作为主 payload
  3. 不依赖固定解码链:每段都尝试 hex → base64 → 解压
  4. 不执行加密代码:纯数据层解码,goto 网络完全绕过

因此,原版和变种 A 共用同一套分析逻辑,只需在参数层面自动适配即可。

技术分析

该混淆器 V3.1.0 及变种 A 的防护层次如下:

1. 字符串表分割

用 explode 将多个字符串常量存为一张表,按索引访问。防护意图是让分析者难以直接定位关键字符串。

实际效果:一条正则即可提取完整表,逐段解码即可还原所有字符串。分隔符变化不构成防护,动态读取即可适配。

2. hex + base64 + gzip 多层编码

数据经过 hex 编码、base64 编码、gzip/deflate 压缩三层处理。

实际效果:解码流程是标准算法,按固定顺序逐层剥离即可。压缩格式变化(deflate vs gzinflate)不构成防护,try_decompress 依次尝试即可适配。

3. 超长 base64 隐藏源码

主 payload 中嵌入超长 base64 串,解码后得到最终源码。

实际效果:正则匹配长 base64 串,一次解码即得源码。变种 A 在中间代码里再次嵌入超长 base64,但仍然是同一策略。

4. goto 跳转网络

大量 goto 语句构成控制流混淆,变量名用不可读字符替换。

实际效果:静态解码完全绕过这一层。不执行代码,goto 网络就毫无意义。无论 184 个还是 500 个 goto,静态解码路径完全不关心。

5. 嵌套动态函数

使用 __BFeVWX 或 __QApZFI 动态创建函数,增加动态分析难度。

实际效果:静态解码完全绕过这一层。不需要执行 __BFeVWX 或 __QApZFI,直接从数据层提取最终 payload。

评测报告

防护层 设计意图 实际破解成本 变种适配成本
字符串表分割 隐藏关键字符串 一条正则,约 5 分钟 +2 分钟
hex + base64 + gzip 多层编码增加解码难度 标准解码流程,约 10 分钟 0 分钟
超长 base64 隐藏源码 隐藏最终源码 正则匹配长串,约 5 分钟 0 分钟
goto 跳转网络 控制流混淆 静态解码完全绕过,0 分钟 0 分钟
变量名混淆 增加阅读难度 静态解码完全绕过,0 分钟 0 分钟
嵌套动态函数 增加动态分析难度 静态解码完全绕过,0 分钟 0 分钟
版权声明 法律威慑 无技术防护效果 0 分钟

原版静态解码耗时:约 20 分钟。

变种 A 静态解码耗时:约 35 分钟。

结论

该混淆器 V3.1.0 及变种 A 的核心防护思路是“多层编码 + 控制流混淆”,但存在一个根本性问题:所有编码都是可逆的,且解码逻辑完全暴露在文件自身中。

explode 字符串表是入口,正则即可提取。hex、base64、gzip 三层编码是标准算法,按固定顺序逐层剥离即可。超长 base64 串是最后一层,正则匹配即可定位。

变种 A 增加了嵌套层,但嵌套的每一层仍然是标准算法的组合。分隔符、常量名、段索引、压缩格式的变化,都只是参数层面的差异,不改变解码流程的本质。

整个解码过程不需要执行加密代码,不需要调试,不需要理解 goto 跳转逻辑,纯粹是数据层的逆向。因此,goto 混淆、变量名混淆、嵌套动态函数这些针对“动态分析”的防护手段,在“静态解码”面前完全失效。

变种 A 的差异只体现在参数和层数上,不体现在解码难度上。每增加一层标准编码,解码时间线性增加,但解码方法不变。这意味着变种的防护效果与层数成正比,而与“新颖性”无关。

这不是工程上的胜利,更不是混淆和技术上的胜利,而是营销上的胜利,毕竟官网吹的有多牛,实际摔地上就有多惨。当然这玩意还是有点难度(提交的样例echo 1因为本身简单分析也easy)

免责声明

本文仅用于技术研究与学习,分析对象为自行加密的测试样本。文中不提供任何成品解码工具,不针对任何具体商业产品进行破解。文中所涉商业名称均已消去,如无意中侵犯了您的权益,请联系删除。

样本.zip

7.13 KB, 下载次数: 2, 下载积分: 吾爱币 -1 CB

第一个样本

发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
576239926 发表于 2026-10-13 11:46
本帖最后由 576239926 于 2026-10-13 11:59 编辑

/*
本代码由 DeckPHP - Version V3 创建
创建时间 2026-10-09 12:02:44
技术支持 Moyu <2763994904@qq.com>
严禁反编译、逆向等任何形式的侵权行为,违者将追究法律责任
*/
if(!defined('_AAA____A'))define('_AAA____A', 'AA__AA___');$GLOBALS[_AAA____A]=explode('|W|S|I','H*');unset($&#65533;);$&#65533;; echo 1;

好像不完美,有乱码变量残留
3#
 楼主| imgjx 发表于 2026-10-13 12:06 |楼主
576239926 发表于 2026-10-13 11:46
/*
本代码由 DeckPHP - Version V3 创建
创建时间 2026-10-09 12:02:44

手动删掉即可,反正D**kPHP V3十分简单
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-10-15 07:51

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表