PixPin 会员门禁逆向记录
正常状态下,PixPin 的部分功能入口会带一个 👑 图标,点击后会弹出升级 VIP 的提示;右键托盘图标里也能看到“升级为 PixPin”的入口。
表面上看,这是界面层的提示,但真正决定功能是否可用的,是后面的会员状态判断。

我这里分析的是 PixPin 3.1.4.0,顺着 IDA 里的链路往下看:fetchUserInfo 先发请求,回包进入 replyFinished,再往下是 loadFromJson,最后落到 isProUser / isVip。
| 顺序 | 关注点 | 目的 |
|---|---|---|
| 0 | fetchUserInfo | 找到请求入口 |
| 1 | replyFinished | 看回包如何写回状态 |
| 2 | loadFromJson | 追对象字段如何落地 |
| 3 | isProUser / isVip | 定位最终门禁 |
fetchUserInfo
// 这个函数只负责把请求发出去:// 1. 先检查当前用户状态是否有效// 2. 通知上层开始拉取用户信息// 3. 组装请求参数并调用 PixNetwork::get// 4. 把 replyError / replyFinished 绑定回调接上void __fastcall PixAuth::Application::fetchUserInfo(PixAuth::Application *this, char a2){ __int64 v4; // rsi const struct QUrlQuery *v5; // rax int v6; // ebx __int64 v7; // rsi __int64 v8; // rax __int64 v9; // rax _BYTE *v10; // [rsp+50h] [rbp-49h] BYREF __int64 v11; // [rsp+58h] [rbp-41h] BYREF _BYTE v12[8]; // [rsp+60h] [rbp-39h] BYREF int v13; // [rsp+68h] [rbp-31h] __int64 v14; // [rsp+70h] [rbp-29h] BYREF _QWORD v15[2]; // [rsp+80h] [rbp-19h] BYREF _BYTE v16[8]; // [rsp+90h] [rbp-9h] BYREF _BYTE v17[8]; // [rsp+98h] [rbp-1h] BYREF _BYTE v18[8]; // [rsp+A0h] [rbp+7h] BYREF _BYTE v19[8]; // [rsp+A8h] [rbp+Fh] BYREF _BYTE v20[8]; // [rsp+B0h] [rbp+17h] BYREF _BYTE v21[8]; // [rsp+B8h] [rbp+1Fh] BYREF int v22; // [rsp+C0h] [rbp+27h] __int64 v23; // [rsp+C8h] [rbp+2Fh] void (*v24)(PixNetworkReply *__hidden, const struct QByteArray *); // [rsp+100h] [rbp+67h] BYREF __int64 v25; // [rsp+110h] [rbp+77h] BYREF __int64 v26; // [rsp+118h] [rbp+7Fh] BYREF
LODWORD(v24) = 0; if ( PixAuth::UserInfo::isValid((PixAuth::Application *)((char *)this + 16)) ) { // 先通知上层开始拉取 PixAuth::Application::sigStartFetchUserInfo(this); if ( !byte_1803A20B4 ) { // 同一轮请求只发一次 byte_1803A20B4 = 1; v4 = PixNetwork::instance(); v10 = v20; // 请求名固定就是 FetchUserInfo v11 = QString::fromAscii_helper("FetchUserInfo", 13); if ( a2 ) { // a2 为真时附带 active=true v26 = QString::fromAscii_helper("active", 6); LODWORD(v24) = 1; v25 = QString::fromAscii_helper("true", 4); QString::QString((QString *)v16, (const struct QString *)&v26); QString::QString((QString *)v17, (const struct QString *)&v25); LODWORD(v24) = 15; v15[0] = v16; v15[1] = v18; v5 = (const struct QUrlQuery *)QUrlQuery::QUrlQuery(v19, v15); v6 = 31; } else { v5 = QUrlQuery::QUrlQuery((QUrlQuery *)v18); v6 = 32; } // 这里开始把临时参数整理成最终 query LODWORD(v24) = v6; QUrlQuery::QUrlQuery((QUrlQuery *)v12, v5); v13 = 30000; v14 = QMapDataBase::shared_null; QString::QString(v20, &v11); QUrlQuery::QUrlQuery((QUrlQuery *)v21, (const struct QUrlQuery *)v12); v22 = v13; v23 = v14; v14 = QMapDataBase::shared_null; // 真正发起请求 v7 = PixNetwork::get(v4, v20, this); sub_1800737D0(&v14); QUrlQuery::~QUrlQuery((QUrlQuery *)v12); QString::~QString((QString *)&v11); if ( (v6 & 0x20) != 0 ) { v6 &= ~0x20u; QUrlQuery::~QUrlQuery((QUrlQuery *)v18); } if ( (v6 & 0x10) != 0 ) { v6 &= ~0x10u; QUrlQuery::~QUrlQuery((QUrlQuery *)v19); } if ( (v6 & 8) != 0 ) { v6 &= ~8u; LODWORD(v24) = v6; sub_1801C18BC(v16, 16, 1, sub_1800751F0); } if ( (v6 & 2) != 0 ) { LOBYTE(v6) = v6 & 0xFD; QString::~QString((QString *)&v25); } if ( (v6 & 1) != 0 ) QString::~QString((QString *)&v26); // 错误回调 v24 = PixNetworkReply::replyError; v8 = sub_1801C1984(0x18u); v15[0] = v8; if ( v8 ) { *(_DWORD *)v8 = 1; *(_QWORD *)(v8 + 8) = &sub_180099BA0; *(_QWORD *)(v8 + 16) = this; } else { v8 = 0; } QObject::connectImpl(&v10, v7, &v24, this, 0, v8, 0, 0, PixNetworkReply::staticMetaObject); QMetaObject::Connection::~Connection((QMetaObject::Connection *)&v10); // 完成回调 v24 = PixNetworkReply::replyFinished; v9 = sub_1801C1984(0x18u); v15[0] = v9; if ( v9 ) { *(_DWORD *)v9 = 1; *(_QWORD *)(v9 + 8) = &sub_180099FB0; *(_QWORD *)(v9 + 16) = this; } else { v9 = 0; } QObject::connectImpl(&v10, v7, &v24, this, 0, v9, 0, 0, PixNetworkReply::staticMetaObject); QMetaObject::Connection::~Connection((QMetaObject::Connection *)&v10); } }}1. 回包更新:replyFinished
通过分析 replyFinished 这个回调,我们可以先知道它负责的是解析回包并更新状态,而不是做最终判断。
上游的 fetchUserInfo 只是发起请求,真正的状态写回发生在这里;后面还要继续追对象字段怎么落地。
// replyFinished 做的事情很明确:// 1. 解析回包 JSON// 2. 检查有没有 userId// 3. 更新用户状态// 4. 如果 Pro 状态变了,再通知上层void __fastcall sub_180099FB0(int a1, PixAuth::Application **a2, __int64 a3, __int64 a4){ __int64 v5; // rax bool v6; // bl bool isProUser; // bl __int64 v8; // rax QMessageLogger *v9; // rax __int64 v10; // rax PixAuth::Application *v11; // rbx bool v12; // al __int64 v13; // [rsp+20h] [rbp-48h] BYREF _BYTE v14[8]; // [rsp+28h] [rbp-40h] BYREF _BYTE v15[16]; // [rsp+30h] [rbp-38h] BYREF _BYTE v16[40]; // [rsp+40h] [rbp-28h] BYREF
if ( a1 ) { if ( a1 == 1 ) { // 先把回包转成 JSON v5 = QJsonDocument::fromJson(v14, *(_QWORD *)(a4 + 8), 0); QJsonDocument::object(v5, v15); QJsonDocument::~QJsonDocument((QJsonDocument *)v14);
// 只有包含 userId,才继续更新内部用户信息 v13 = QString::fromAscii_helper("userId", 6); v6 = QJsonObject::contains((QJsonObject *)v15, (const struct QString *)&v13); QString::~QString((QString *)&v13);
if ( v6 ) { // 更新前先记一次旧状态,方便对比 Pro 是否变化 isProUser = PixAuth::UserInfo::isProUser((PixAuth::Application *)((char *)a2[2] + 16)); // 把回包字段写回用户对象 PixAuth::UserInfo::loadFromJson((PixAuth::Application *)((char *)a2[2] + 16), (const struct QJsonObject *)v15); v8 = sub_18009D4A0(); sub_18009DA30(v8, v15);
// 状态变化就通知上层刷新 token / UI if ( isProUser != PixAuth::UserInfo::isProUser((PixAuth::Application *)((char *)a2[2] + 16)) ) { v9 = QMessageLogger::QMessageLogger((QMessageLogger *)v16, 0, 0, 0); v10 = QMessageLogger::warning(v9, &v13); QDebug::operator<<(v10, "User VIP status changed, refresh access token"); QDebug::~QDebug((QDebug *)&v13); sub_180074600(&qword_1803A1F40); v11 = a2[2]; v12 = PixAuth::UserInfo::isProUser((PixAuth::Application *)((char *)v11 + 16)); PixAuth::Application::sigProStatusChanged(v11, v12); }
PixAuth::Application::sigUserInfoRefreshed(a2[2]); }
// 请求结束,重置标志位 byte_1803A20B4 = 0; QJsonObject::~QJsonObject((QJsonObject *)v15); } } else if ( a2 ) { j_j_free(a2); }}从这段代码可以得到第一层结论:
| 结论 | 说明 |
|---|---|
replyFinished | 负责解析回包并更新状态 |
fetchUserInfo | 只负责发起请求 |
| 真正门禁 | 在后续对象状态判断里 |
顺着它继续追,才能定位到会员信息写回的入口。
2. 会员信息如何写回对象
继续看 UserInfo::loadFromJson,这里的任务是把 JSON 里的字段逐项写回对象。
userId、avatar、nickName、phone、email 这些都是基础资料,真正需要关注的是 vipInfo。
通过 vipInfo 这个分支,程序会把会员相关的数据继续交给 VipInfo::loadFromJson。
换句话说,会员状态并不单独存放,而是随着用户资料一起写入对象结构里。
| 关注点 | 作用 |
|---|---|
userId | 先确认这是一个完整用户对象 |
vipInfo | 继续进入会员信息分支 |
2.1 UserInfo::loadFromJson
// 普通资料先一项项写进去// 真正和会员相关的是 vipInfovoid __fastcall PixAuth::UserInfo::loadFromJson(PixAuth::UserInfo *this, const struct QJsonObject *a2){ bool v4; // bl __int64 v5; // rax __int64 v6; // rax bool v7; // bl __int64 v8; // rax __int64 v9; // rax bool v10; // bl __int64 v11; // rax __int64 v12; // rax bool v13; // bl __int64 v14; // rax __int64 v15; // rax bool v16; // bl __int64 v17; // rax __int64 v18; // rax bool v19; // bl __int64 v20; // rax __int64 v21; // rax char v22; // bl __int64 v23; // rax bool v24; // r14 __int64 v25; // rax const struct QJsonObject *v26; // rax bool v27; // bl __int64 v28; // rax __int64 v29; // rax bool v30; // bl QJsonValue *v31; // rax _QWORD v32[3]; // [rsp+20h] [rbp-30h] BYREF _BYTE v33[24]; // [rsp+38h] [rbp-18h] BYREF __int64 v34; // [rsp+80h] [rbp+30h] BYREF __int64 v35; // [rsp+88h] [rbp+38h] BYREF
LODWORD(v34) = 0; // 清空旧状态后重新写入 PixAuth::UserInfo::clear(this);
// userId v34 = QString::fromAscii_helper("userId", 6); v4 = QJsonObject::contains(a2, (const struct QString *)&v34); QString::~QString((QString *)&v34); if ( v4 ) { // userId 作为基础字段先写回 v34 = QString::fromAscii_helper("userId", 6); v5 = QJsonObject::operator[](a2, v32, &v34); v6 = QJsonValue::toString(v5, &v35); QString::operator=((char *)this + 40, v6); QString::~QString((QString *)&v35); QJsonValue::~QJsonValue((QJsonValue *)v32); QString::~QString((QString *)&v34); }
// avatar v34 = QString::fromAscii_helper("avatar", 6); v7 = QJsonObject::contains(a2, (const struct QString *)&v34); QString::~QString((QString *)&v34); if ( v7 ) { // avatar 直接写回对象 v34 = QString::fromAscii_helper("avatar", 6); v8 = QJsonObject::operator[](a2, v32, &v34); v9 = QJsonValue::toString(v8, &v35); QString::operator=(this, v9); QString::~QString((QString *)&v35); QJsonValue::~QJsonValue((QJsonValue *)v32); QString::~QString((QString *)&v34); }
// nickName v34 = QString::fromAscii_helper("nickName", 8); v10 = QJsonObject::contains(a2, (const struct QString *)&v34); QString::~QString((QString *)&v34); if ( v10 ) { // nickName 同样是普通资料 v34 = QString::fromAscii_helper("nickName", 8); v11 = QJsonObject::operator[](a2, v32, &v34); v12 = QJsonValue::toString(v11, &v35); QString::operator=((char *)this + 8, v12); QString::~QString((QString *)&v35); QJsonValue::~QJsonValue((QJsonValue *)v32); QString::~QString((QString *)&v34); }
v34 = QString::fromAscii_helper("phone", 5); v13 = QJsonObject::contains(a2, (const struct QString *)&v34); QString::~QString((QString *)&v34); if ( v13 ) { // phone 也是普通资料字段,存在就直接写回 v34 = QString::fromAscii_helper("phone", 5); v14 = QJsonObject::operator[](a2, v32, &v34); v15 = QJsonValue::toString(v14, &v35); QString::operator=((char *)this + 16, v15); QString::~QString((QString *)&v35); QJsonValue::~QJsonValue((QJsonValue *)v32); QString::~QString((QString *)&v34); }
v34 = QString::fromAscii_helper("email", 5); v16 = QJsonObject::contains(a2, (const struct QString *)&v34); QString::~QString((QString *)&v34); if ( v16 ) { // email 的处理方式和 phone 一样 v34 = QString::fromAscii_helper("email", 5); v17 = QJsonObject::operator[](a2, v32, &v34); v18 = QJsonValue::toString(v17, &v35); QString::operator=((char *)this + 24, v18); QString::~QString((QString *)&v35); QJsonValue::~QJsonValue((QJsonValue *)v32); QString::~QString((QString *)&v34); }
v34 = QString::fromAscii_helper("wechat", 6); v19 = QJsonObject::contains(a2, (const struct QString *)&v34); QString::~QString((QString *)&v34); if ( v19 ) { // wechat 同样只是基础资料,不参与最终会员判断 v34 = QString::fromAscii_helper("wechat", 6); v20 = QJsonObject::operator[](a2, v32, &v34); v21 = QJsonValue::toString(v20, &v35); QString::operator=((char *)this + 32, v21); QString::~QString((QString *)&v35); QJsonValue::~QJsonValue((QJsonValue *)v32); QString::~QString((QString *)&v34); }
v32[0] = QString::fromAscii_helper("vipInfo", 7); v22 = 1; LODWORD(v34) = 1; v24 = 0; if ( QJsonObject::contains(a2, (const struct QString *)v32) ) { // vipInfo 存在且类型正确,才继续往下 v35 = QString::fromAscii_helper("vipInfo", 7); LODWORD(v34) = 3; v23 = QJsonObject::operator[](a2, v33, &v35); v22 = 7; LODWORD(v34) = 7; if ( (unsigned int)QJsonValue::type(v23) == 5 ) v24 = 1; }
if ( v24 ) { v34 = QString::fromAscii_helper("vipInfo", 7); v25 = QJsonObject::operator[](a2, v33, &v34); v26 = (const struct QJsonObject *)QJsonValue::toObject(v25, v32); PixAuth::VipInfo::loadFromJson((PixAuth::UserInfo *)((char *)this + 64), v26); QJsonObject::~QJsonObject((QJsonObject *)v32); QJsonValue::~QJsonValue((QJsonValue *)v33); QString::~QString((QString *)&v34); }
v34 = QString::fromAscii_helper("config_hash", 11); v27 = QJsonObject::contains(a2, (const struct QString *)&v34); QString::~QString((QString *)&v34); if ( v27 ) { // config_hash 存在就刷新,不存在就回退到默认值 v34 = QString::fromAscii_helper("config_hash", 11); v28 = QJsonObject::operator[](a2, v33, &v34); v29 = QJsonValue::toString(v28, &v35); QString::operator=((char *)this + 56, v29); QString::~QString((QString *)&v35); QJsonValue::~QJsonValue((QJsonValue *)v33); QString::~QString((QString *)&v34); } else { QString::operator=((char *)this + 56, byte_1801CD04D); }
v34 = QString::fromAscii_helper("allowTrialDays", 14); v30 = QJsonObject::contains(a2, (const struct QString *)&v34); QString::~QString((QString *)&v34); if ( v30 ) { // allowTrialDays 是一个额外的数值配置,没有就置 0 v34 = QString::fromAscii_helper("allowTrialDays", 14); v31 = (QJsonValue *)QJsonObject::operator[](a2, v33, &v34); *((_DWORD *)this + 12) = QJsonValue::toInt(v31, 0); QJsonValue::~QJsonValue((QJsonValue *)v33); QString::~QString((QString *)&v34); } else { *((_DWORD *)this + 12) = 0; }}从这里可以看出,VipInfo::loadFromJson 先处理 prepaid,再处理 subscription。
2.2 VipInfo::loadFromJson
| 字段 | 含义 | 作用 |
|---|---|---|
prepaid.endAtMs | 预付费到期时间 | 提供第一层有效期判断 |
subscription | 订阅信息 | 提供状态和到期时间 |
这两组数据就是会员有效期和订阅状态的来源,后面的判断都会围绕这些字段展开。
void __fastcall PixAuth::VipInfo::loadFromJson(PixAuth::VipInfo *this, const struct QJsonObject *a2){ char *v4; // rbx __int64 v5; // rax QJsonValue *v6; // rax __int64 v7; // rax __int128 v8; // [rsp+20h] [rbp-50h] BYREF __int128 v9; // [rsp+30h] [rbp-40h] BYREF _BYTE v10[24]; // [rsp+40h] [rbp-30h] BYREF _BYTE v11[24]; // [rsp+58h] [rbp-18h] BYREF
*(_QWORD *)this = 0; v4 = (char *)this + 8; *((_DWORD *)this + 2) = 4; // 先把 VipInfo 清空 QString::clear((PixAuth::VipInfo *)((char *)this + 16)); *((_QWORD *)v4 + 2) = 0; *((_DWORD *)v4 + 6) = 8; *((_QWORD *)v4 + 4) = 0; *((_QWORD *)v4 + 5) = 0; *((_QWORD *)v4 + 6) = 0; QString::clear((QString *)(v4 + 56)); QString::clear((QString *)(v4 + 64));
// prepaid.endAtMs:预付费到期时间 LODWORD(v8) = 7; *((_QWORD *)&v8 + 1) = "prepaid"; v9 = v8; if ( (unsigned __int8)QJsonObject::contains(a2, &v9) ) { // 先处理 prepaid.endAtMs LODWORD(v8) = 7; *((_QWORD *)&v8 + 1) = "prepaid"; v9 = v8; v5 = QJsonObject::value(a2, v10, &v9); QJsonValue::toObject(v5, &v8); QJsonValue::~QJsonValue((QJsonValue *)v10); if ( !QJsonObject::isEmpty((QJsonObject *)&v8) ) { *(_QWORD *)this = 0; LODWORD(v9) = 7; *((_QWORD *)&v9 + 1) = "endAtMs"; v6 = (QJsonValue *)QJsonObject::value(&v8, v11, &v9); *(_QWORD *)this = (unsigned int)(int)QJsonValue::toDouble(v6, 0.0); QJsonValue::~QJsonValue((QJsonValue *)v11); } QJsonObject::~QJsonObject((QJsonObject *)&v8); }
// subscription:交给下一层 LODWORD(v9) = 12; *((_QWORD *)&v9 + 1) = "subscription"; if ( (unsigned __int8)QJsonObject::contains(a2, &v9) ) { // 再处理 subscription LODWORD(v9) = 12; *((_QWORD *)&v9 + 1) = "subscription"; v7 = QJsonObject::value(a2, v11, &v9); QJsonValue::toObject(v7, &v8); QJsonValue::~QJsonValue((QJsonValue *)v11); if ( !QJsonObject::isEmpty((QJsonObject *)&v8) ) PixAuth::Subscription::loadFromJson((PixAuth::Subscription *)v4, (const struct QJsonObject *)&v8); QJsonObject::~QJsonObject((QJsonObject *)&v8); }}Subscription::loadFromJson 负责把订阅类型、状态、到期时间这些字段装进去。
可以把它理解成“把订阅侧的元数据完整装箱”。
2.3 Subscription::loadFromJson
void __fastcall PixAuth::Subscription::loadFromJson(PixAuth::Subscription *this, const struct QJsonObject *a2){ __int64 v4; // rax __int64 v5; // rax __int64 v6; // rax __int64 v7; // rax QJsonValue *v8; // rax __int64 v9; // rax __int64 v10; // rax QJsonValue *v11; // rax QJsonValue *v12; // rax QJsonValue *v13; // rax __int64 v14; // rax __int64 v15; // rax __int64 v16; // rax __int64 v17; // rax int v18; // [rsp+20h] [rbp-30h] BYREF const char *v19; // [rsp+28h] [rbp-28h] _BYTE v20[32]; // [rsp+30h] [rbp-20h] BYREF char v21; // [rsp+70h] [rbp+20h] BYREF
*(_DWORD *)this = 4; // Subscription 先清空再写 QString::clear((PixAuth::Subscription *)((char *)this + 8)); *((_QWORD *)this + 2) = 0; *((_DWORD *)this + 6) = 8; *((_QWORD *)this + 4) = 0; *((_QWORD *)this + 5) = 0; *((_QWORD *)this + 6) = 0; QString::clear((PixAuth::Subscription *)((char *)this + 56)); QString::clear((PixAuth::Subscription *)((char *)this + 64));
// type 和 status 决定订阅种类和状态 v18 = 4; // type 决定订阅类型 v19 = "type"; v4 = QJsonObject::value(a2, v20, &v18); v5 = QJsonValue::toString(v4, &v21); *(_DWORD *)this = PixAuth::subscriptionTypeFromString(v5); QString::~QString((QString *)&v21); QJsonValue::~QJsonValue((QJsonValue *)v20);
v18 = 14; v19 = "subscriptionID"; v6 = QJsonObject::value(a2, v20, &v18); v7 = QJsonValue::toString(v6, &v21); QString::operator=((char *)this + 8, v7); QString::~QString((QString *)&v21); QJsonValue::~QJsonValue((QJsonValue *)v20);
v18 = 25; v19 = "subscriptionTrialEndsAtMs"; v8 = (QJsonValue *)QJsonObject::value(a2, v20, &v18); *((_QWORD *)this + 2) = (unsigned int)(int)QJsonValue::toDouble(v8, 0.0); QJsonValue::~QJsonValue((QJsonValue *)v20);
v18 = 6; // status 决定订阅状态 v19 = "status"; v9 = QJsonObject::value(a2, v20, &v18); v10 = QJsonValue::toString(v9, &v21); *((_DWORD *)this + 6) = PixAuth::subscriptionStatusFromString(v10); QString::~QString((QString *)&v21); QJsonValue::~QJsonValue((QJsonValue *)v20);
v18 = 10; v19 = "cancelAtMs"; v11 = (QJsonValue *)QJsonObject::value(a2, v20, &v18); *((_QWORD *)this + 4) = (unsigned int)(int)QJsonValue::toDouble(v11, 0.0); QJsonValue::~QJsonValue((QJsonValue *)v20);
v18 = 13; v19 = "periodEndAtMs"; v12 = (QJsonValue *)QJsonObject::value(a2, v20, &v18); *((_QWORD *)this + 5) = (unsigned int)(int)QJsonValue::toDouble(v12, 0.0); QJsonValue::~QJsonValue((QJsonValue *)v20);
v18 = 12; // validUntilMs 作为最终到期时间 v19 = "validUntilMs"; v13 = (QJsonValue *)QJsonObject::value(a2, v20, &v18); *((_QWORD *)this + 6) = (unsigned int)(int)QJsonValue::toDouble(v13, 0.0); QJsonValue::~QJsonValue((QJsonValue *)v20);
v18 = 16; v19 = "subscriptionName"; v14 = QJsonObject::value(a2, v20, &v18); v15 = QJsonValue::toString(v14, &v21); QString::operator=((char *)this + 56, v15); QString::~QString((QString *)&v21); QJsonValue::~QJsonValue((QJsonValue *)v20);
v18 = 21; v19 = "subscriptionPriceText"; v16 = QJsonObject::value(a2, v20, &v18); v17 = QJsonValue::toString(v16, &v21); QString::operator=((char *)this + 64, v17); QString::~QString((QString *)&v21); QJsonValue::~QJsonValue((QJsonValue *)v20);}3. 最终判断链
这一节才是核心。
UserInfo::isProUser
通过 UserInfo::isProUser 的交叉引用,我们就能把最外层的判断点定位出来。
这个函数很短,核心逻辑只有两条:
- 外层用户对象状态要有效
VipInfo::isVip(...)必须为真
bool __fastcall PixAuth::UserInfo::isProUser(PixAuth::UserInfo *this){ return *(_DWORD *)(*((_QWORD *)this + 5) + 4LL) && PixAuth::VipInfo::isVip((PixAuth::UserInfo *)((char *)this + 64));}这里可以把它理解成外层总开关。
第一项只是基础状态检查,真正决定是不是 Pro 的,还是 VipInfo::isVip(...)。
VipInfo::isVip
继续往下追,真正决定会员状态的就是这里。
它不是只看一个布尔值,而是把时间字段、状态号和 validUntilMs 一起纳入判断。
bool __fastcall PixAuth::VipInfo::isVip(PixAuth::VipInfo *this){ if ( *(_QWORD *)this > QDateTime::currentMSecsSinceEpoch() ) return 1; if ( *((_DWORD *)this + 8) && *((_DWORD *)this + 8) != 6 ) return 0; return *((_QWORD *)this + 7) > QDateTime::currentMSecsSinceEpoch();}到这一步,门槛就已经说清楚了。
| 参与项 | 作用 |
|---|---|
prepaid | 提供预付费到期时间 |
status | 影响订阅是否被认为有效 |
validUntilMs | 最终到期时间 |
前面的有效期、状态号和最终到期时间共同决定它会不会直接返回真。
Subscription::isVip
Subscription::isVip 也是一层状态判断,不过它更偏订阅态的辅助逻辑。
bool __fastcall PixAuth::Subscription::isVip(PixAuth::Subscription *this){ int v1; // eax
v1 = *((_DWORD *)this + 6); if ( v1 && v1 != 6 ) { QDateTime::currentMSecsSinceEpoch(); return 0; } return *((_QWORD *)this + 6) > QDateTime::currentMSecsSinceEpoch();}它只看 status 和 validUntilMs,可以帮助我们理解订阅信息的结构,但不是最终落点。
| 函数 | 角色 |
|---|---|
UserInfo::isProUser | 最外层开关 |
VipInfo::isVip | 直接会员判断 |
Subscription::isVip | 订阅态辅助判断 |
4. 结论
把前面的链路串起来后,补丁思路就很清晰了。
最后需要对照的只有这两个判断:
PixAuth::UserInfo::isProUserPixAuth::VipInfo::isVip
样本说明下面这两个地址是这版样本中的落点;不同版本或不同编译结果,地址会变化。
| 判断 | 地址 |
|---|---|
PixAuth::UserInfo::isProUser | 0x1800ADB00 |
PixAuth::VipInfo::isVip | 0x1800DEFF0 |
思路也简单,直接让这两个判断稳定返回 true,后面的状态和界面就会统一起来。
免责声明
仅用于记录逆向分析过程和学习思路,不构成任何授权、补丁建议或使用许可。请勿将文中的内容用于未获授权的软件修改、规避会员限制或任何违反软件许可与法律法规的用途。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时






