文件:ZendServer-2021.4.1-multi-php-Windows_x86.exe
安装后可以试用30天,想分析下限制原理, 根据安装日志,发现了2个关键的文件:
ZendServer\gui\module\Configuration\src\Configuration\License\Wrapper.php
ZendServer\gui\module\Configuration\src\Configuration\License\License.php
Wrapper.php里面有一个关键的函数:
php
public function getSerialNumberInfo($serialNumber, $userName)
{
$method = 'zem_serial_number_info';
try {
$this->validateMethod($method);
$licenseInfo = $method($serialNumber, $userName);
if (!is_array($licenseInfo)) {
throw new ZSException('unexpected response received');
}
} catch (\Exception $e) {
Log::err("method {$method} invocation failed with the following error: ".$e->getMessage());
throw new ZSException("method {$method} invocation failed with the following error: ".$e->getMessage());
}
return new License($licenseInfo);
}
根据licenseInfo = method(serialNumber, userName);这行代码可以知道,调用了函数:zem_serial_number_info来获取许可证信息,试用期限制应该就在这个函数里面了。
知道了函数名称,我们如何找到这个函数呢?想了下PHP程序的执行流程:
1、客户端请求PHP
2、PHP服务端收到请求 php.exe or php-cgi.exe
3、PHP加载zend server扩展插件,检测是否过期了
思路很清晰了,去PHP扩展目录看下:zendserver\ZendServer\php\7.4\lib\ext
发现了可疑文件:ZendUtils.dll
使用IDA分析ZendUtils.dll的导出函数get_module,找到了函数zem_serial_number_info的地址
get_module IDA代码如下:
php
void *get_module()
{
return get_module_0();
}
void *get_module_0()
{
return &unk_10061230;
}
函数代码很简单,返回了一个地址10061230,到这里需要了解PHP扩展模块的编写方法和导出函数原理,我们在IDA跳到地址10061230:
php
data:10061230 unk_10061230 db 5Ch ; \ ; DATA XREF: sub_10006E20↑o
.data:10061230 ; get_module_0↑o
.data:10061231 db 0
.data:10061232 db 0
.data:10061233 db 0
.data:10061234 db 0B6h
.data:10061235 db 16h
.data:10061236 db 34h ; 4
.data:10061237 db 1
.data:10061238 db 0
.data:10061239 db 0
.data:1006123A db 0
.data:1006123B db 0
.data:1006123C db 0
.data:1006123D db 0
.data:1006123E db 0
.data:1006123F db 0
.data:10061240 db 0
.data:10061241 db 0
.data:10061242 db 0
.data:10061243 db 0
.data:10061244 dd offset aZendUtils ; "Zend Utils"
.data:10061248 dd offset off_100610A0 //这里就是PHP扩展模块的导出函数表了
.data:1006124C dd offset sub_10001EF6
.data:10061250 dd offset sub_1000239C
再跳到地址:off_100610A0
php
.data:100610A0 off_100610A0 dd offset aZemGetExtensio
.data:100610A0 ; DATA XREF: .data:10061248↓o
.data:100610A0 ; "zem_get_extension_info_by_id"
.data:100610A4 dd offset sub_10002973
.data:100610A8 db 0
.data:100610A9 db 0
.data:100610AA db 0
.data:100610AB db 0
.data:100610AC db 0FFh
.data:100610AD db 0FFh
.data:100610AE db 0FFh
.data:100610AF db 0FFh
.data:100610B0 db 0
.data:100610B1 db 0
.data:100610B2 db 0
.data:100610B3 db 0
.data:100610B4 dd offset aZemGetExtensio_0 ; "zem_get_extension_info_by_name"
.data:100610B8 dd offset sub_100013D9
.data:100610BC align 10h
.data:100610C0 db 0FFh
.data:100610C1 db 0FFh
.data:100610C2 db 0FFh
.data:100610C3 db 0FFh
.data:100610C4 db 0
.data:100610C5 db 0
.data:100610C6 db 0
.data:100610C7 db 0
.data:100610C8 dd offset aZemGetExtensio_1 ; "zem_get_extensions_info"
.data:100610CC dd offset sub_10002432
.data:100610D0 db 0
.data:100610D1 db 0
.data:100610D2 db 0
.data:100610D3 db 0
.data:100610D4 db 0FFh
.data:100610D5 db 0FFh
.data:100610D6 db 0FFh
.data:100610D7 db 0FFh
.data:100610D8 db 0
.data:100610D9 db 0
.data:100610DA db 0
.data:100610DB db 0
.data:100610DC dd offset aZemGetLicenseI ; "zem_get_license_info"
.data:100610E0 dd offset zem_get_license_info_sub_10002455
.data:100610E4 align 8
.data:100610E8 db 0FFh
.data:100610E9 db 0FFh
.data:100610EA db 0FFh
.data:100610EB db 0FFh
.data:100610EC db 0
.data:100610ED db 0
.data:100610EE db 0
.data:100610EF db 0
.data:100610F0 dd offset aZendIsConfigur ; "zend_is_configuration_changed"
.data:100610F4 dd offset sub_10001A78
.data:100610F8 db 0
.data:100610F9 db 0
.data:100610FA db 0
.data:100610FB db 0
.data:100610FC db 0FFh
.data:100610FD db 0FFh
.data:100610FE db 0FFh
.data:100610FF db 0FFh
.data:10061100 db 0
.data:10061101 db 0
.data:10061102 db 0
.data:10061103 db 0
.data:10061104 dd offset aZendSetConfigu ; "zend_set_configuration_changed"
.data:10061108 dd offset sub_10001262
.data:1006110C align 10h
.data:10061110 db 0FFh
.data:10061111 db 0FFh
.data:10061112 db 0FFh
.data:10061113 db 0FFh
.data:10061114 db 0
.data:10061115 db 0
.data:10061116 db 0
.data:10061117 db 0
.data:10061118 dd offset aZendGetCfgVar ; "zend_get_cfg_var"
.data:1006111C dd offset sub_10001604
.data:10061120 db 0
.data:10061121 db 0
.data:10061122 db 0
.data:10061123 db 0
.data:10061124 db 0FFh
.data:10061125 db 0FFh
.data:10061126 db 0FFh
.data:10061127 db 0FFh
.data:10061128 db 0
.data:10061129 db 0
.data:1006112A db 0
.data:1006112B db 0
.data:1006112C dd offset aZendRestartPhp ; "zend_restart_php"
.data:10061130 dd offset sub_100026A3
.data:10061134 align 8
.data:10061138 db 0FFh
.data:10061139 db 0FFh
.data:1006113A db 0FFh
.data:1006113B db 0FFh
.data:1006113C db 0
.data:1006113D db 0
.data:1006113E db 0
.data:1006113F db 0
.data:10061140 dd offset aZendGetFileSiz ; "zend_get_file_size"
.data:10061144 dd offset sub_1000146F
.data:10061148 db 0
.data:10061149 db 0
.data:1006114A db 0
.data:1006114B db 0
.data:1006114C db 0FFh
.data:1006114D db 0FFh
.data:1006114E db 0FFh
.data:1006114F db 0FFh
.data:10061150 db 0
.data:10061151 db 0
.data:10061152 db 0
.data:10061153 db 0
.data:10061154 dd offset aZendGetCpuArch ; "zend_get_cpu_arch"
.data:10061158 dd offset sub_10002D6F
.data:1006115C align 10h
.data:10061160 db 0FFh
.data:10061161 db 0FFh
.data:10061162 db 0FFh
.data:10061163 db 0FFh
.data:10061164 db 0
.data:10061165 db 0
.data:10061166 db 0
.data:10061167 db 0
.data:10061168 dd offset aZemSerialNumbe ; "zem_serial_number_info"
.data:1006116C dd offset sub_100015B9
.data:10061170 db 0
.data:10061171 db 0
.data:10061172 db 0
.data:10061173 db 0
.data:10061174 db 0FFh
.data:10061175 db 0FFh
.data:10061176 db 0FFh
.data:10061177 db 0FFh
.data:10061178 db 0
.data:10061179 db 0
.data:1006117A db 0
.data:1006117B db 0
看到了函数zem_serial_number_info:
.data:10061168 dd offset aZemSerialNumbe ; "zem_serial_number_info"
.data:1006116C dd offset sub_100015B9
跟进sub_100015B9看下:
php
int __fastcall sub_100015B9(int a1, int *a2)
{
return sub_1000A730(a1, a2);
}
php
int __fastcall sub_1000A730(int a1, int *a2)
{
int *v2; // edi
int result; // eax
unsigned int v4; // ecx
const struct QString *v5; // esi
int v6; // eax
int v7; // eax
volatile signed __int32 *v8; // [esp+Ch] [ebp-30h]
volatile signed __int32 *v9; // [esp+10h] [ebp-2Ch]
char v10; // [esp+14h] [ebp-28h]
char v11; // [esp+18h] [ebp-24h]
char v12; // [esp+1Ch] [ebp-20h]
char v13; // [esp+20h] [ebp-1Ch]
int v14; // [esp+24h] [ebp-18h]
const char *v15; // [esp+28h] [ebp-14h]
int v16; // [esp+2Ch] [ebp-10h]
int v17; // [esp+38h] [ebp-4h]
v2 = a2;
result = zend_parse_parameters(*(_DWORD *)(a1 + 28), "ss", &v15, &v12, &v14, &v13);
if ( result == -1 )
{
*v2 = -1;
v2[2] = 4;
}
else
{
if ( v15 )
v4 = strlen(v15);
else
v4 = -1;
v16 = QString::fromAscii_helper(v15, v4);
sub_10001267((int)&v8);
sub_10002306(&v8, (int)&v16);
v5 = (const struct QString *)sub_1000272F((int)&v16);// allocate_license_db
sub_10011270(v2, v5); // 许可证各个字段解析
j_free_license_db(v5);
QString::~QString((QString *)&v11);
QString::~QString((QString *)&v10);
v17 = 0;
v6 = *((_DWORD *)v9 + 2);
if ( !v6 || v6 != -1 && _InterlockedExchangeAdd(v9 + 2, 0xFFFFFFFF) == 1 )
QHashData::free_helper((QHashData *)v9, (void (__cdecl *)(struct Node *))sub_100026B2);
v17 = 1;
v7 = *((_DWORD *)v8 + 2);
if ( !v7 || v7 != -1 && _InterlockedExchangeAdd(v8 + 2, 0xFFFFFFFF) == 1 )
QHashData::free_helper((QHashData *)v8, (void (__cdecl *)(struct Node *))sub_10001109);
result = QString::~QString((QString *)&v16);
}
return result;
}
发现了,两个关键的函数:
v5 = (const struct QString *)sub_1000272F((int)&v16);// allocate_license_db
sub_10011270(v2, v5); // 许可证各个字段解析
最关键的是:sub_1000272F调用了allocate_license_db,看函数名字就知道了
经分析allocate_license_db在ZendExtensionManager.dll文件里面,我们转去分析ZendExtensionManager.dll的导出函数, 看到了get_license_db ,应该就是它了
php
void *get_license_db()
{
return get_license_db_0();
}
void *get_license_db_0()
{
return Memory;
}
IDA分析很简单,只是返回了一个内存地址:Memory,那就是计算好的许可证信息了。
交叉引用Memory看看哪些地方写了Memory,找到了关键函数sub_10014FC0
php
QString *__thiscall sub_10014FC0(QString *this, const struct QString *Memory, unsigned int a3, int pUserName)
{
QString *pThis; // ebx
unsigned int v5; // ecx
const struct QString *v6; // eax
QHashData **v7; // eax
bool v8; // al
int v9; // ecx
char v10; // al
int v11; // ecx
int v12; // ecx
QDate *v13; // eax
bool v14; // al
int v15; // ecx
int v16; // eax
int v17; // ecx
const struct QString *v18; // edi
int v19; // ecx
char v20; // bl
int v21; // esi
int *v22; // edi
int v23; // esi
int v24; // ebx
int *v25; // eax
int v26; // eax
_DWORD *v27; // edi
int v28; // ecx
int v29; // ecx
int v30; // esi
struct QListData::Data *v31; // ebx
int v32; // edi
void **i; // esi
struct QListData::Data *v34; // ebx
int v35; // edi
void **j; // esi
int v38; // [esp-8h] [ebp-54h]
const struct ZPrintable *v39; // [esp-4h] [ebp-50h]
void **v40; // [esp+10h] [ebp-3Ch]
const struct QString *v41; // [esp+14h] [ebp-38h]
int v42; // [esp+18h] [ebp-34h]
char v43; // [esp+1Ch] [ebp-30h]
char v44; // [esp+20h] [ebp-2Ch]
char v45; // [esp+28h] [ebp-24h]
const struct QString *v46; // [esp+30h] [ebp-1Ch]
int *v47; // [esp+34h] [ebp-18h]
QString *v48; // [esp+38h] [ebp-14h]
int v49; // [esp+3Ch] [ebp-10h]
int v50; // [esp+48h] [ebp-4h]
pThis = this;
v48 = this;
QString::QString(this, Memory);
v50 = 0;
if ( pUserName )
v5 = strlen((const char *)pUserName);
else
v5 = -1;
*((_DWORD *)pThis + 1) = QString::fromAscii_helper(pUserName, v5);// 用户名
v6 = (const struct QString *)operator new(0x10u);// 分配16个字节
Memory = v6;
LOBYTE(v50) = 2;
if ( v6 )
v7 = sub_100010FA((int)v6);
else
v7 = 0;
*((_DWORD *)pThis + 2) = v7;
*((_WORD *)pThis + 6) = 0;
*((_BYTE *)pThis + 14) = 0;
*((_DWORD *)pThis + 4) = 4;
*((_DWORD *)pThis + 5) = 1;
QDate::QDate((QString *)((char *)pThis + 24));// 时间?
v47 = (int *)((char *)pThis + 32);
*v47 = QHashData::shared_null;
v49 = QListData::shared_null;
v39 = (QString *)((char *)pThis + 4);
v38 = (int)pThis + 32;
LOBYTE(v50) = 4;
sub_10001974((QListData *)&v38, (int)&v49);
v8 = sub_10001479(*((char **)pThis + 2), (int)pThis, a3, v38, (int)v39);
*((_BYTE *)pThis + 12) = v8;
if ( v8 )
{
*((_BYTE *)pThis + 14) = 1;
}
else
{
v9 = *((_DWORD *)pThis + 2);
v10 = sub_1000105A(pThis);
*((_BYTE *)pThis + 14) = v10;
if ( v10 && (v11 = *((_DWORD *)pThis + 2), (unsigned __int8)sub_100015D7(pThis, 6)) )
{
v12 = *((_DWORD *)pThis + 2);
*((_BYTE *)pThis + 13) = sub_100010DC(pThis);
}
else
{
*((_BYTE *)pThis + 13) = 0;
}
}
v13 = sub_1000169F(*((_DWORD **)pThis + 2), (int)&v45, (int)pThis);// 计算过期时间
*((_DWORD *)pThis + 6) = *(_DWORD *)v13; // date_lock 0是永不过期
*((_DWORD *)pThis + 7) = *((_DWORD *)v13 + 1);// version_lock
v14 = *((_QWORD *)pThis + 3) < *(_QWORD *)QDate::currentDate(&v44);// 判断许可证是否过期
*((_BYTE *)pThis + 13) = v14;
if ( v14 )
*((_BYTE *)pThis + 12) = 0; // license_ok赋值false了
v15 = *((_DWORD *)pThis + 2);
v16 = sub_1000185C(pThis);
v17 = *((_DWORD *)pThis + 2);
v39 = pThis;
*((_DWORD *)pThis + 5) = v16;
v18 = 0;
*((_DWORD *)pThis + 4) = sub_1000182A(v39);
Memory = 0;
a3 = 0;
do
{
pUserName = QListData::shared_null;
LOBYTE(v50) = 5;
v46 = v18;
sub_1000160E(&v46);
v19 = *((_DWORD *)pThis + 2);
v20 = sub_100015D7(pThis, v18);
v21 = sub_1000150F(v18);
ZPrintable::ZPrintable((ZPrintable *)&v40);
v40 = &FeatureDescriptor::`vftable';
v41 = v18;
v42 = v21;
v43 = v20;
v22 = v47;
LOBYTE(v50) = 6;
sub_100017A3(v47);
v23 = *v22;
v24 = a3 ^ *(_DWORD *)(*v22 + 28);
v39 = (const struct ZPrintable *)(a3 ^ *(_DWORD *)(*v22 + 28));
v25 = sub_10001032(v22, (int)&Memory, (int)v39);
a3 = (unsigned int)v25;
v26 = *v25;
if ( v26 == v23 )
{
if ( *(_DWORD *)(v23 + 12) >= *(_DWORD *)(v23 + 24) )
{
QHashData::rehash((QHashData *)v23, *(signed __int16 *)(v23 + 22) + 1);
v23 = *v22;
a3 = (unsigned int)sub_10001032(v22, (int)&Memory, v24);
}
v27 = QHashData::allocateNode((QHashData *)v23, 4);
v28 = *(_DWORD *)a3;
v27[2] = Memory;
*v27 = v28;
v39 = (const struct ZPrintable *)&v40;
v27[1] = v24;
ZPrintable::ZPrintable((ZPrintable *)(v27 + 3), v39);
v29 = (int)v47;
v27[3] = &FeatureDescriptor::`vftable';
v27[4] = v41;
v27[5] = v42;
*((_BYTE *)v27 + 24) = v43;
*(_DWORD *)a3 = v27;
++*(_DWORD *)(*(_DWORD *)v29 + 12);
}
else
{
v30 = v26 + 12;
ZPrintable::ZPrintable((ZPrintable *)(v26 + 12), (const struct ZPrintable *)&v40);
*(_DWORD *)v30 = &FeatureDescriptor::`vftable';
*(_DWORD *)(v30 + 4) = v41;
*(_DWORD *)(v30 + 8) = v42;
*(_BYTE *)(v30 + 12) = v43;
}
ZPrintable::~ZPrintable((ZPrintable *)&v40);
LOBYTE(v50) = 7;
if ( !*(_DWORD *)pUserName
|| *(_DWORD *)pUserName != -1 && _InterlockedExchangeAdd((volatile signed __int32 *)pUserName, 0xFFFFFFFF) == 1 )
{
v31 = (struct QListData::Data *)pUserName;
v32 = pUserName + 4 * (*(_DWORD *)(pUserName + 8) + 4);
for ( i = (void **)(pUserName + 4 * (*(_DWORD *)(pUserName + 12) + 4)); i != (void **)v32; sub_10001807(*i) )
{
--i;
v39 = (const struct ZPrintable *)4;
}
QListData::dispose(v31);
}
pThis = v48;
v18 = (const struct QString *)((char *)Memory + 1);
Memory = v18;
a3 = (unsigned int)v18;
}
while ( (signed int)v18 < 15 );
LOBYTE(v50) = 8;
if ( !*(_DWORD *)v49
|| *(_DWORD *)v49 != -1 && _InterlockedExchangeAdd((volatile signed __int32 *)v49, 0xFFFFFFFF) == 1 )
{
v34 = (struct QListData::Data *)v49;
v35 = v49 + 4 * (*(_DWORD *)(v49 + 8) + 4);
for ( j = (void **)(v49 + 4 * (*(_DWORD *)(v49 + 12) + 4)); j != (void **)v35; sub_10001807(*j) )
{
--j;
v39 = (const struct ZPrintable *)4;
}
QListData::dispose(v34);
}
return v48;
}
php
QDate *__thiscall sub_1000169F(_DWORD *this, int a1, int a2)
{
return sub_1001D510(this, (QDate *)a1, a2);
}
QDate *__thiscall sub_1001D510(_DWORD *this, QDate *a2, int a3)
{
_DWORD *v3; // edi
int v4; // esi
QString *v5; // eax
int v6; // eax
int v7; // eax
int expiredDay; // ebx
int v9; // eax
int v10; // eax
int expiredMonth; // edi
int v12; // eax
int v13; // eax
int expiredYear; // esi
char v16; // [esp+10h] [ebp-1Ch]
_DWORD *v17; // [esp+14h] [ebp-18h]
char v18; // [esp+18h] [ebp-14h]
char v19; // [esp+1Ch] [ebp-10h]
int v20; // [esp+28h] [ebp-4h]
v3 = this;
v17 = this;
v4 = a3;
QString::operator=(this + 2, a3);
v5 = sub_10001C2B(v3, (int)&v18, v4);
QString::operator=(v3 + 3, v5);
QString::~QString((QString *)&v18);
sub_10001C2B(v3, (int)&v19, v4);
v20 = 0;
QString::mid(&v19, &a3, 10, 16);
LOBYTE(v20) = 1;
v6 = QString::mid(&a3, &v18, 0, 5);
LOBYTE(v20) = 2;
v7 = sub_10001604(v6); // 计算过期时间 日
LOBYTE(v20) = 1;
expiredDay = v7;
QString::~QString((QString *)&v18);
v9 = QString::mid(&a3, &v18, 5, 4);
LOBYTE(v20) = 3;
v10 = sub_10001604(v9); // 计算过期时间 月
LOBYTE(v20) = 1;
expiredMonth = v10;
QString::~QString((QString *)&v18);
v12 = QString::mid(&a3, &v16, 9, 7);
LOBYTE(v20) = 4;
v13 = sub_10001604(v12); // 计算过期时间 年
LOBYTE(v20) = 1;
expiredYear = v13 + 2000;
QString::~QString((QString *)&v16);
QDate::QDate(a2, expiredYear, expiredMonth, expiredDay);// 过期时间
QString::~QString((QString *)&a3);
QString::~QString((QString *)&v19);
return a2;
}
到此真相大白了,我本想改成9999年,但是32位程序最大日期是:2038年01月19日
因此我改成了2038年01月01日,还有种修改方法,在判断是否过期的地方,我们改成永不过期就行。
注:OD分析时,分析zendserver\ZendServer\php\7.4\bin\php-cgi.exe就好
为了关闭zend server替换文件,可以关闭服务:ZendApache
