mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1131 字
3 分钟
PixPin 会员功能解锁
2026-04-29

PixPin 会员门禁逆向记录#

正常状态下,PixPin 的部分功能入口会带一个 👑 图标,点击后会弹出升级 VIP 的提示;右键托盘图标里也能看到“升级为 PixPin”的入口。

表面上看,这是界面层的提示,但真正决定功能是否可用的,是后面的会员状态判断。

PixPin 主界面与会员提示

我这里分析的是 PixPin 3.1.4.0,顺着 IDA 里的链路往下看:fetchUserInfo 先发请求,回包进入 replyFinished,再往下是 loadFromJson,最后落到 isProUser / isVip

顺序关注点目的
0fetchUserInfo找到请求入口
1replyFinished看回包如何写回状态
2loadFromJson追对象字段如何落地
3isProUser / 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 里的字段逐项写回对象。

userIdavatarnickNamephoneemail 这些都是基础资料,真正需要关注的是 vipInfo

通过 vipInfo 这个分支,程序会把会员相关的数据继续交给 VipInfo::loadFromJson

换句话说,会员状态并不单独存放,而是随着用户资料一起写入对象结构里。

关注点作用
userId先确认这是一个完整用户对象
vipInfo继续进入会员信息分支

2.1 UserInfo::loadFromJson#

// 普通资料先一项项写进去
// 真正和会员相关的是 vipInfo
void __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();
}

它只看 statusvalidUntilMs,可以帮助我们理解订阅信息的结构,但不是最终落点。

函数角色
UserInfo::isProUser最外层开关
VipInfo::isVip直接会员判断
Subscription::isVip订阅态辅助判断

4. 结论#

把前面的链路串起来后,补丁思路就很清晰了。

最后需要对照的只有这两个判断:

  • PixAuth::UserInfo::isProUser
  • PixAuth::VipInfo::isVip
样本说明

下面这两个地址是这版样本中的落点;不同版本或不同编译结果,地址会变化。

判断地址
PixAuth::UserInfo::isProUser0x1800ADB00
PixAuth::VipInfo::isVip0x1800DEFF0

思路也简单,直接让这两个判断稳定返回 true,后面的状态和界面就会统一起来。

免责声明#

仅用于记录逆向分析过程和学习思路,不构成任何授权、补丁建议或使用许可。请勿将文中的内容用于未获授权的软件修改、规避会员限制或任何违反软件许可与法律法规的用途。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

PixPin 会员功能解锁
https://20200626.xyz/posts/pixpin-auth-analysis/
作者
Luo
发布于
2026-04-29
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录