com.test.xxx
检查更新&绕过逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Java.perform(function() { var AppInfos = Java.use("com.test.xxx.bean.AppInfos"); AppInfos.getForceUpdate.implementation = function() { console.log("[*] getForceUpdate() → false (bypass)"); return Java.use("java.lang.Boolean").FALSE; };
var UserInfo = Java.use("com.test.xxx.bean.UserInfo"); var Boolean = Java.use("java.lang.Boolean"); UserInfo.getVipStatus.implementation = function() { console.log("[*] getVipStatus() → TRUE"); return Boolean.TRUE; }; UserInfo.getVipEndDate.implementation = function() { return "2099-12-31 23:59:59"; }; UserInfo.getVipPaymentCycle.implementation = function() { return "5"; }; });
|
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
| ┌─────────────────────────────────────────────────────────────┐ │ 阶段1: SplashActivity 启动 │ │ ───────────────────────────────── │ │ 开屏页面 onResume → 触发版本检查 │ └──────────────────────┬──────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 阶段2: 第一阶段网络请求 - 系统维护状态检查 │ │ ───────────────────────────────── │ │ ComModel.systemUpgrade("anxin", onSuccess=C1215Hy(this,7),│ │ onError=C1215Hy(this,8)) │ │ → 访问服务器 /systemUpgrade 接口 │ └──────────────────────┬──────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 阶段3: case 8 回调处理 systemUpgrade 响应 │ │ ───────────────────────────────── │ │ SystemUpgrade { appCode, msg, name, status } │ │ │ │ ┌─ status == null → 数据异常分支 → 进入应用 │ │ ├─ status == false → 系统正常 → 调用 latestApp │ │ └─ status == true → 系统维护中 → 显示维护页 │ └──────────────────────┬──────────────────────────────────────┘ ↓ (status=false 路径) ┌─────────────────────────────────────────────────────────────┐ │ 阶段4: 第二阶段网络请求 - 获取应用版本详情 │ │ ───────────────────────────────── │ │ ComModel.latestApp("anxin", onSuccess=C1215Hy(this,2), │ │ onError=C1215Hy(this,3)) │ │ → 访问服务器 /latestApp 接口 │ └──────────────────────┬──────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 阶段5: case 3 回调处理 AppInfos 响应 ★方案B作用点★ │ │ ───────────────────────────────── │ │ AppInfos { versionNum, forceUpdate, ... } │ │ │ │ if (currentVersion < serverVersion │ │ && appInfos.getForceUpdate()) { ← 方案B 在此拦截 │ │ // 显示"发现新版本"强制更新弹窗 │ │ } else { │ │ SplashActivity.m34808j0(splashActivity); // 进入应用 │ │ } │ └─────────────────────────────────────────────────────────────┘
|
有更新要求时,强制更新
手动定位
- 算法助手定位弹窗、onclick、日志打印等
- 开发者助手定位弹窗ID
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 疑问: 1、找点击立即更新触发的接口 搜索关键词"立即更新"->得到资源文件id(txt_download)
<TextView android:textSize="@dimen/sp_16" android:textStyle="bold" android:textColor="@color/white" android:gravity="center" android:id="@+id/txt_download" android:background="@drawable/shape_theme_50" android:visibility="visible" android:layout_width="match_parent" android:layout_height="48dp" android:text="立即更新" app:layout_constraintTop_toTopOf="parent"/>
搜索id得到一堆绑定ID的
2、搜索更新关键词(词库)
3、启动流程
|
小结
1 2 3 4 5 6 7 8 9 10 11 12
| 1、anp.kad.sdk.C1215Hy,包含有SDK字样可能是回调
2、需要语感,已经定位到关键词文本,就要继续跟 View viewFindViewById4 = viewInflate.findViewById(R.id.txt_download); AbstractC0942Bg.m829r(viewFindViewById4, "findViewById(...)"); TextView textView = (TextView) viewFindViewById4; .... AbstractC0942Bg.m753D0(textView, new C1299Jy(progressBar, textView, splashActivity, str2, version));
- AbstractC0942Bg.m753D0(View, OnClickListener) = 就是 setOnClickListener 的封装 - 第一个参数 textView = 你点击的那个"立即更新"按钮(R.id.txt_download) - 第二个参数 new C1299Jy(...) = 点击后实际触发的功能函数所在的回调类!
|
系统更新关键词
1 2 3 4 5 6 7
| 第一梯队: systemUpgrade、forceUpdate (systemUpgrade|forceUpdate)
第二梯队: latestApp、SplashActivity、upgrade、update、checkVersion (latestApp|SplashActivity|upgrade|update|checkVersion)
|
VIP检测(VIP Check)关键词
1 2
| vipGift、getVipStatus、getVipEndDate、getVipPaymentCycle、vipStatus (vipGift|getVipStatus|getVipEndDate|getVipPaymentCycle|vipStatus)
|
VIP状态判断方法分析
📊 判断逻辑核心
根据 C1289Jo.invoke() 方法(case 7)的代码,系统通过以下字段判断用户VIP状态:
字段 类型 说明 vipStatus Boolean VIP会员状态(true=已开通) adFreeStatus Boolean 免广告会员状态(true=已开通) vipEndDate String VIP会员到期日期 adFreeEndDate String 免广告会员到期日期 userType String 用户类型标识
🔍 用户类型判定逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Boolean vipStatus = userInfo.getVipStatus(); boolean isVip = vipStatus != null ? vipStatus.booleanValue() : false;
Boolean adFreeStatus = userInfo.getAdFreeStatus(); boolean isAdFree = adFreeStatus != null ? adFreeStatus. booleanValue() : false;
if (!isVip && !isAdFree) { userType = "1"; // 普通用户 } else if (isVip) { userType = "3"; // VIP会员 } else { userType = "2"; // 免广告会员 }
|
用户类型对应关系:
userType 用户类型 说明 “1” 普通用户 未开通任何会员 “2” 免广告会员 仅开通免广告权益 “3” VIP会员 开通完整VIP权益
📅 会员到期判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| // 获取到期日期(空值保护) String vipEndDate = userInfo.getVipEndDate(); if (vipEndDate == null) { vipEndDate = ""; } String adFreeEndDate = userInfo.getAdFreeEndDate(); if (adFreeEndDate == null) { adFreeEndDate = ""; }
// 判断是否已过期 boolean isVipExpired = !AbstractC1468Nz.m1709g0(vipEndDate); // 日期非空表示有到期时间 boolean isAdFreeExpired = !AbstractC1468Nz.m1709g0(adFreeEndDate);
|
按钮文本逻辑:
条件 按钮文本 普通用户(userType=”1”)且无过期日期 “开通VIP享专属权益” 普通用户(userType=”1”)但会员已过期 “续费VIP享专属权益” 已开通会员(userType=”2”或”3”) “续费VIP享专属权益”
🎯 状态标识传递
用户VIP状态通过 userType 字段持久化到 MMKV:
1 2 3 4
| mmkv.m51759h("userType", mineFragment.f34428h); mmkv.m51759h("vipEndDate", str); // 到期日期 mmkv.m51759h("paymentCycle", str2); // 付费周期(如:月付、年 付、终身)
|
🔄 数据来源
VIP状态数据来自 UserInfo 对象,该对象由 UserModel.getUserInfo() 接口从服务端获取。在 MineFragment 中通过观察 LiveEventBus 或直接调用接口更新用户信息:
1 2
| ((UserModel) mineFragment.f34424d.getValue()).getUserInfo(new C1289Jo(mineFragment, 7));
|
💡 关键结论
判断当前用户是否开通VIP的核心方法:
- 直接判断 :检查 UserInfo.getVipStatus() 是否为 true
- 间接判断 :检查 userType 是否为 “3”
- 有效期判断 :检查 vipEndDate 是否存在且未过期
会员状态优先级:
- VIP会员( vipStatus=true )优先级最高,即使同时开通免广告会员,也判定为VIP会员
- 免广告会员( adFreeStatus=true )次之
- 普通用户(两者都为false)最低
1
| <package android:name="com.tencent.mm"/>
|
两个思路:
1、点击开通VIP时去伪造支付
2、登录后,获取当前用户状态时,伪造VIP状态
直接hook getUserInfo,没Hook到
点击”我的”会调用判断是否已签到
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
| public final Object invoke(Object obj) { case 5: .... if (todayIsSignIn) {
AbstractC2618p2.m3193x("已签到", textView);
} else {
AbstractC2618p2.m3193x("签到+7", textView);
} } else { Context context = mineFragment.getContext(); if (context != null) { AbstractC0942Bg.m759G0(context, "签到信息获取失败", 0, 6); } }
return c2972xC2;
|
VIP分析
核心问题:客户端有 30+ 处直接读缓存,不调用 UserInfo getter
1 2
| BaseActivity.m34835N() (userType缓存) → 被 36 处调用 BaseActivity.m34836O() (vipEndDate缓存) → 被 20 处调用
|
这些缓存字段( f30987G / f30988H / f30989I )是在 SplashActivity 启动时由 UserInfo 设置一次,之后所有业务点(账单、AI记账、自动记账、各 Activity)都 直接读 BaseActivity 的缓存 ,不再调用 UserInfo.getVipStatus() 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 应用启动流程: ┌─────────────────────────────────────────────────────────┐ │ 1. SplashActivity.onCreate │ │ └─ userInfo 回调 → 读 getVipStatus() → 设置 f30987G │ │ ★ 你的 Hook 在这里生效了 │ │ ★ 但 f30987G 已经被设置为 "1"(如果Hook时机晚) │ ├─────────────────────────────────────────────────────────┤ │ 2. MainActivity.onResume │ │ └─ userInfo 回调 → 读 getVipStatus() → 重设 f30987G │ │ ★ 你的 Hook 应该生效 │ ├─────────────────────────────────────────────────────────┤ │ 3. 各业务点(30+处) │ │ └─ 直接读 BaseActivity.m34835N() → 返回 f30987G │ │ ✗ 这里没有调用 UserInfo.getVipStatus() │ │ ✗ Hook 不到! │ └─────────────────────────────────────────────────────────┘
|
找到根因了! 自动记账有 独立的 VipGift 试用检查 ,会强制关闭开关并持久化。
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
| ┌─────────────────────────────────────────────────────────────┐ │ 1. AutomateActivity.onResume() │ │ └─ userInfo(C1429N1(this,5), C1429N1(this,6)) │ └──────────────────────┬──────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 2. C1429N1.invoke case 6 (UserInfo 回调) │ │ Boolean vipStatus = userInfo.getVipStatus(); │ │ f33062V = vipStatus.booleanValue(); │ │ // 计算 userType │ │ if (!equals(f30987G, "3")) { ← 不是VIP │ │ userModel.vipGiftOne(1, ...); ← 查询试用权益 │ │ } else { │ │ // VIP → 直接写入 SP │ │ } │ └──────────────────────┬──────────────────────────────────────┘ ↓ (userType != "3" 时) ┌─────────────────────────────────────────────────────────────┐ │ 3. C1429N1.invoke default (VipGift 回调) ★ 问题在这里!★ │ │ VipGift vipGift = (VipGift) obj; │ │ if (vipGift == null) { │ │ f31140q.setChecked(false); ← 关闭开关 │ │ mmkv.m51760i("autoSwitch", false); ← 持久化关闭 │ │ SP.putBoolean("userIsAuto", false); ← SP 持久化 │ │ } else { │ │ String endTime = vipGift.getEndTime(); │ │ if (isEmpty(endTime)) { ← endTime 为空 │ │ // 关闭开关 + 持久化 │ │ } else if (now > parseTime(endTime)) { ← 已过期 │ │ // 关闭开关 + 持久化 │ │ } │ │ // 未过期 → 保持开关 │ │ } │ └─────────────────────────────────────────────────────────────┘
|
问题根因
三个层面的持久化关闭 :
- VipGift.getEndTime() 未 Hook → 试用过期 → 触发关闭
- MMKV “autoSwitch” 被写入 false → mo34812V 读取到 false → 开关关闭
- SharedPreferences “userIsAuto” 被写入 false → 跨进程读取到 false
即使你 Hook 了 UserInfo.getVipStatus() 返回 TRUE ,如果 Hook 时机晚于 onResume,case 6 已经执行了非 VIP 分支,vipGiftOne 已经被调用,default case 已经关闭了开关并持久化。
现有绕过VIP&更新的脚本
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 135 136 137 138 139 140 141 142 143 144
| Java.perform(function () { var Boolean = Java.use("java.lang.Boolean"); console.log("[*] VIP bypass + 自动记账 完整脚本启动...");
try { var UserInfo = Java.use("com.test.xxx.bean.UserInfo"); UserInfo.getVipStatus.implementation = function() { return Boolean.TRUE; }; UserInfo.getVipEndDate.implementation = function() { return "2099-12-31 23:59:59"; }; UserInfo.getVipPaymentCycle.implementation = function() { return "5"; }; UserInfo.getAdFreeStatus.implementation = function() { return Boolean.TRUE; }; UserInfo.getAdFreeEndDate.implementation = function() { return "2099-12-31 23:59:59"; }; console.log("[+] A: UserInfo hooks installed"); } catch (e) { console.log("[!] A: " + e); }
try { var BaseActivity = Java.use("com.test.xxx.base.BaseActivity"); BaseActivity.m34835N.implementation = function() { return "3"; }; BaseActivity.m34836O.implementation = function() { return "2099-12-31 23:59:59"; }; BaseActivity.m34829H.implementation = function() { return "5"; }; console.log("[+] B: BaseActivity hooks installed"); } catch (e) { console.log("[!] B: " + e); }
try { var MMKV = Java.use("com.tencent.mmkv.MMKV"); MMKV.m51756c.overload('java.lang.String', 'java.lang.String').implementation = function(key, defaultVal) { if (key === "userType") return "3"; if (key === "vipEndDate") return "2099-12-31 23:59:59"; if (key === "paymentCycle") return "5"; return this.m51756c.overload('java.lang.String', 'java.lang.String').call(this, key, defaultVal); }; console.log("[+] C: MMKV getString hooks installed"); } catch (e) { console.log("[!] C: " + e); }
try { var AutoSettingProvider = Java.use("com.test.xxx.p768ui.receiver.AutoSettingProvider"); AutoSettingProvider.query.implementation = function(uri, projection, selection, selectionArgs, sortOrder) { var MatrixCursor = Java.use("android.database.MatrixCursor"); var cursor = MatrixCursor.$new(["userIsVip", "userIsAuto"]); cursor.addRow(["3", 1]); return cursor; }; console.log("[+] D: AutoSettingProvider hooks installed"); } catch (e) { console.log("[!] D: " + e); }
try { var VipActivity = Java.use("com.test.xxx.p768ui.activity.VipActivity"); VipActivity.f34012E0.value = true; VipActivity.f34013F0.value = false; console.log("[+] E: VipActivity static fields set"); } catch (e) { console.log("[!] E: " + e); }
try { var VipGift = Java.use("com.test.xxx.bean.VipGift"); VipGift.getEndTime.implementation = function() { return "2099-12-31 23:59:59"; }; VipGift.getBeginTime.implementation = function() { return "2024-01-01 00:00:00"; }; console.log("[+] F: VipGift hooks installed"); } catch (e) { console.log("[!] F: " + e); }
try { var MMKV = Java.use("com.tencent.mmkv.MMKV"); MMKV.m51754a.overload('java.lang.String').implementation = function(key) { if (key === "autoSwitch") return true; return this.m51754a.overload('java.lang.String').call(this, key); }; console.log("[+] G: MMKV getBoolean hooks installed"); } catch (e) { console.log("[!] G: " + e); }
try { var MMKV = Java.use("com.tencent.mmkv.MMKV"); MMKV.m51760i.overload('java.lang.String', 'boolean').implementation = function(key, value) { if (key === "autoSwitch" && !value) { return this.m51760i.overload('java.lang.String', 'boolean').call(this, key, true); } return this.m51760i.overload('java.lang.String', 'boolean').call(this, key, value); }; console.log("[+] H: MMKV putBoolean hooks installed"); } catch (e) { console.log("[!] H: " + e); }
try { var CompoundButton = Java.use("android.widget.CompoundButton"); CompoundButton.setChecked.implementation = function(checked) { if (!checked) { return this.setChecked(true); } return this.setChecked(checked); }; console.log("[+] I: CompoundButton hooks installed"); } catch (e) { console.log("[!] I: " + e); }
try { var SharedPreferencesImpl = Java.use("android.app.SharedPreferencesImpl"); SharedPreferencesImpl.getBoolean.implementation = function(key, defValue) { if (key === "userIsAuto") return true; return this.getBoolean.call(this, key, defValue); }; console.log("[+] J: SP getBoolean hooks installed"); } catch (e) { console.log("[!] J: " + e); }
try { var Editor = Java.use("android.app.SharedPreferencesImpl$EditorImpl"); Editor.putBoolean.implementation = function(key, value) { if (key === "userIsAuto" && !value) { return this.putBoolean(key, true); } return this.putBoolean(key, value); }; console.log("[+] K: SP putBoolean hooks installed"); } catch (e) { console.log("[!] K: " + e); }
try { var TextView = Java.use("android.widget.TextView"); TextView.setText.overload('java.lang.CharSequence').implementation = function(text) { if (text !== null) { var str = text.toString(); if (str.indexOf("已过期") !== -1 || str.indexOf("会员过期") !== -1 || str.indexOf("VIP过期") !== -1) { return this.setText.overload('java.lang.CharSequence').call(this, "尊贵的终生会员"); } } return this.setText.overload('java.lang.CharSequence').call(this, text); }; console.log("[+] L: UI text interception installed"); } catch (e) { console.log("[!] L: " + e); }
try { var AppInfos = Java.use("com.test.xxx.bean.AppInfos"); AppInfos.getForceUpdate.implementation = function() { return Boolean.FALSE; }; console.log("[+] AppInfos (force update) hook installed"); } catch (e) { console.log("[!] AppInfos: " + e); }
console.log("[*] All hooks installed. VIP bypass + 自动记账 complete.");
})
|
现有VIP功能
- 聊天记账
- 自动记账
- 定时记账(1)
- 存钱计划(1)
- 预算管理(添加预算分类)
点击立即更新链路跟踪
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
| 算法助手 → ID: 2131298275 (dec) │ ▼ ┌────────────────────────────────────────────────────┐ │ 第1步:十进制 → 十六进制 │ │ 2131298275 = 0x7F0907E3 │ │ (0x7F=资源前缀, 09=id类型, 07E3=具体编号) │ └────────────────────────────────────────────────────┘ │ ▼ 搜索 0x7F0907E3 / 0x7f0907e3 ┌────────────────────────────────────────────────────┐ │ 第2步:结果只有 1 个类 → com.test.xxx.R │ │ 判断:代码中不会直接写十六进制数字,都是 │ │ 引用 R.id.xxx 字段名,所以去R类里找字段名 │ └────────────────────────────────────────────────────┘ │ ▼ 在R类中按值查字段名(PowerShell搜 0x7f0907e3) ┌────────────────────────────────────────────────────┐ │ 第3步:找到字段名 → txt_download │ │ 即:R.id.txt_download = 0x7f0907e3 │ │ (虽然名叫txt_开头,但实际当按钮用) │ └────────────────────────────────────────────────────┘ │ ▼ 搜索 "txt_download" ┌────────────────────────────────────────────────────┐ │ 第4步:得到 5 个结果 → 用角色判断法筛选 │ │ │ │ ① com.test.xxx.R │ │ → 跳过,只是定义ID的地方 │ │ │ │ ② com.test.xxx.databinding.DialogUpdateBinding │ → 名字叫 DialogUpdate!= 更新对话框的Binding类 │ │ → 确认:这个txt_download就是更新弹窗上的按钮 │ │ → 布局文件: dialog_update.xml │ │ │ │ ③ anp.kad.sdk.C1215Hy │ │ → 混淆类 + SplashActivity有关(之前分析过) │ │ → 保留:启动时的版本检查回调处理类 │ │ │ │ ④ anp.kad.sdk.C2143e │ │ → 混淆类,但构造函数参数是 AboutUsActivity │ │ → 判断:是"关于我们"页面的手动检查更新按钮逻辑 │ │ → 不是启动弹窗,暂放一边 │ │ │ │ ⑤ com.test.xxx.p768ui.activity.SettingActivity │ → 设置页面,次要的手动检查入口 │ └────────────────────────────────────────────────────┘ │ ▼ 深挖 C1215Hy,搜索 txt_download 周围的代码 ┌────────────────────────────────────────────────────┐ │ 第5步:找到 findViewById + 点击事件绑定 │ │ View viewFindViewById4 = │ │ viewInflate.findViewById(R.id.txt_download); │ │ TextView textView = (TextView) viewFindViewById4; │ │ ... │ │ AbstractC0942Bg.m753D0(textView, │ │ new C1299Jy(progressBar, textView, │ │ splashActivity, str2, version)); │ │ │ │ 翻译一下: │ │ m753D0(View, OnClickListener) 就是 setOnClickListener │ 第一个参数 = 你点的"立即更新"按钮 │ │ 第二个参数 = 点击后真正调用的回调类! │ │ │ │ 结论:点击立即更新 → 触发 C1299Jy 的回调方法 │ └────────────────────────────────────────────────────┘ │ ▼ 打开 C1299Jy 类 ┌────────────────────────────────────────────────────┐ │ 第6步:最终目标!定位到"立即更新"的功能函数 │ │ │ │ 类名:anp.kad.sdk.C1299Jy │ │ 方法:m1473a(View view) → case 0 │ │ │ │ 功能逻辑分解: │ │ ① splashActivity.m34847d(json) │ │ → 上报埋点(type=3, actionType=3001) │ │ │ │ ② if (!存储权限已获取) │ │ → 弹存储权限申请对话框(dialog_permission) │ │ → 同意授权后才下载 │ │ │ │ ③ else(有权限) │ │ → 构建保存路径:/files/apk/{version}.apk │ │ → 调用 splashActivity.m34851f( │ │ str2=下载URL, │ │ string=本地保存路径, │ │ progressBar=进度条) │ │ → 开始下载新版APK并显示进度 │ └────────────────────────────────────────────────────┘
|