黄果短剧不老实啊,实战1.0.6最新版
- 2026-10-06 21:09:32
免责声明:本文所有分析仅用于技术研究与学习交流。请勿将文中技术用于破解并传播非法内容或任何商业用途,由此产生的一切后果自负。
〇、写在前面/文末看落地验证
上一篇实战不正经APP之黄果短剧发出后,有读者提到:
“最新版的黄果短剧该如何修改?”
我今晚看完电影回来拿到了1.0.6新包,完整记录第二轮逆向的全过程。开发者确实针对上一版的破解思路做了防御升级(话说黄果一点也不老实啊,新版本还增加了片库), 但那又如何呢:

一、确认逆向基础设施还在
两步验证:
第一步,搜 JSON 字段名 isFreeChapter——JSON key 不会被混淆,它还在,偏移从 0x52760 漂移到 0x53571,说明付费模型延续;
第二步,符号表搜 PlayletChapter——15 个符号全部在列,类名、源码路径 package:hgdj/... 完好无损。
混淆?不存在的。 开发者的防御预算显然花在了别处(后面会看到)。库哈希从
@1324390399变成了@1408390399,这是正常的版本演进标识。
这时候直接搜 1.0.3 的老朋友,会发现一批“失踪人口”:
longVideoStatusDesc—— 搜不到了 freeArea—— 还在,但体积变了(0x929CEC,252 字节) needBuyVipBool/ needBuyVideoBool—— 还在Pro.currentChapter/ Pro.realVideoUrl—— 还在新面孔: PlayletChapter::copyByNoVideoUrl—— 1.0.3 没有这个函数
失踪与新增,就是这一版攻防的战场。逐个来看。
二、freeArea 重写
1.0.3 的 freeArea 是“集数区间”模型:
// 1.0.3bool get freeArea => field_83 == null || currentEpisode < freeEpisode;
反汇编 1.0.6 的 freeArea(0x929CEC),发现逻辑整个换掉了:
// 1.0.6bool get freeArea { final ch = Pro.currentChapter(this); if (ch == null) return payType == VideoPayType.None; if (ch.field_57 == 2) return true; // ← 解锁状态字段 if (ch.field_57 == 0 && payType == None) return true; return false;}
从“客户端自己算是不是免费集”,改成了“读章节对象上服务端下发的解锁状态整数”。field_57 是个枚举式的状态位(0=未标记、2=已解锁),== 2 即免费可播。
这是一次正确的防御方向:状态判定从“客户端推导”改为“服务端下发”,理论上服务端不给状态,客户端就无米下锅。但注意——它依然是个纯客户端读取的整数,这正是后文补丁的突破口。
三、中央状态函数改名 + 登录态判定
longVideoStatusDesc 在 1.0.6 里“失踪”了。顺藤摸瓜:needBuyVipBool 的反汇编里有一个新调用 ShortVideoPlayerLogic::videoStatus,它最终指向一个新函数——::videoStatusDesc @0x92984c(还在 detail_top_right_view.dart,只是改了名)。
反汇编还原(节选核心判定):
// 1.0.6 videoStatusDesc(video, flag)Status videoStatusDesc(VideoModel video, bool flag) { if (video == null) return Status(needBuyVip: false, needBuyVideo: false); // ← 注意这里 final isLogin = globalStore?.field_1f ?? false; // 新增:登录态 final r = Status(false, false); if (flag == true) { if (video.freeArea) return r; // 免费区 → 不买 if (Pro.isCoinVideo(video)) { // 金币剧 if (video.field_b?.field_1f == true) return r; // 新增字段判定 if (video.field_8f != 0 || !isLogin) r.needBuyVideo = true; } else { // VIP 剧 if (!isLogin) r.needBuyVip = true; // 未登录 → 要 VIP } } else { // 章节级判定:currentChapter.field_5f[0].field_7 == true → 已解锁 // 未解锁 → 按剧型置 needBuyVideo / needBuyVip } return r;}
和 1.0.3 相比有两个变化:章节“已解锁”标志从 field_57 挪到了 field_5f[0].field_7(对象布局重排),以及新增了未登录直接 needBuyVip 的强制逻辑。
但结构上有个对我们极其友好的细节:函数入口的 video == null 分支,本身就是一条现成的“构造 {false, false} 并返回”路径。这个性质上一版也有,这一版依然在——先记住,补丁设计时要用。
四、copyByNoVideoUrl 防盗链
新版最扎眼的新函数是 PlayletChapter::copyByNoVideoUrl @0x937CC8。名字太有指向性了:“复制章节但去掉 videoUrl”。
反汇编确认:它逐字段复制了 field_7 到 field_5b 的全部字段,唯独跳过 field_2f(videoUrl)。这不是防盗链是什么?如果播放链上用这个副本去播,未解锁章节的 URL 就被客户端主动抹掉了——那是真正的硬防御,客户端补丁无法凭空造 URL。
必须查清它的调用方。 交叉引用结果只有一个:
ShortVideoPlayerLogic::_refreshOperateStatus() async → bl #0x937cc8 (copyByNoVideoUrl)
_refreshOperateStatus 是刷新播放器操作栏状态的函数,属于 UI 状态链,不是播放链。也就是说:这个函数只是给“未解锁章节”造一个无 URL 的副本用于界面展示,真正的播放判定根本不用它,虚惊一场。
五、播放链验证:URL 组装公式原样未动
防御升级了,那播放链呢?这是决定补丁可行性的根本。逐环验证:
Pro.realVideoUrl @0x8C8D44——与 1.0.3 几乎逐指令同构,仅新增一个 field_107 优先级字段的读取(非空则优先用,否则回落 Pro.videoUrl):
String realVideoUrl(VideoModel v) { final p = v.field_107; final url = (p != null && p.path非空) ? p.path : Pro.videoUrl(v); if (url.isEmpty) return ""; if (url.startsWith("http")) return url; return "$cdnBase/vid/h5/m3u8/$url?token=$token&c=$c";}
Pro.videoUrl @0x62C48C——主路径依然是 Pro.currentChapter(v)?.field_2f ?? ""(章节真实 videoUrl),试看回落字段从 field_6b 挪到 field_73,结构未变。
initVideoPlayer / onBuyTapEvent / unlockPlaylet——全部在位,购买弹窗依旧只由用户点按触发,解锁 API 依旧 POST /playlet-permission/unlock {unlockType, playletChapterId}。
结论:播放链的客户端唯一门槛,依然是“章节对象里有没有 videoUrl”+ 一组纯客户端的布尔判定。 三处防御升级全部作用在 UI 状态层,没有一处伤到播放主干。
六、修改方案
1.0.3 的补丁是:freeArea 的 b.ge、longVideoStatusDesc 的 tbnz、外加数据层保险。1.0.6 重新推演后:
P1 —— videoStatusDesc 入口 @0x929870
0x92986C: cmp w1, w22 ; video == null ?0x929870: b.ne +5; ≠null → 走完整付费判定 ← 改成 nop0x929874: (null 分支:分配 Status,写入 false/false,返回)
效果:无论 video 是否为 null,恒走 null 分支返回 {needBuyVip:false, needBuyVideo:false}。这个函数是 1.0.6 全部 UI 购买判定的汇点——needBuyVipBool、needBuyVideoBool、isNeedBuy(付费遮罩)三个 getter 全部从它取值,一个 NOP 全灭。而且它覆盖了 1.0.3 需要单独考虑的“登录态分支”:不管登录没登录,判定都到不了。
P2 —— freeArea @0x929D0C
0x929D0C: mov x1, x0 ; 函数第一个工作指令 ← 改成 b 0x929d3c0x929D3C: add x0, x22, #0x20 ; return true0x929D40: (LeaveFrame; ret)
效果:数据层“免费区”判定恒 true,兜住所有不经过 videoStatusDesc 的引用点。落点选在函数内既有的 return-true 尾部——栈帧已建立,跳转完全合法,依然坚持“复用现有代码路径、不手写对象构造”的原则。
补丁后反汇编回读验证:
0x929870: nop0x929874: bl 0x929de8 ; 分配 Status0x92987c: add x0, x22, 0x30 ; false0x929880: stur w0, [x1, 7] ; needBuyVip = false0x929d0c: b 0x929d3c0x929d3c: add x0, x22, 0x20 ; true0x929d44: ret
顺带确认了 1.0.6 的运行时编码体系与 1.0.3 完全一致:x22 为 NULL 基址,true = x22+0x20,false = x22+0x30。这类底层不变量是跨版本快速定位的锚点。
七、落地并验证

__________
End.