夜雨聆风学习资料网

ARTICLE · 1009896

实战不正经APP之黄果短剧

实战不正经APP之黄果短剧
AI短剧 是最近两年非常火爆的赛道,更是吸引了灰产扎堆,其中黄果短剧最近很是火热,官方也上线了APP端,今天仅合法范围类的技术交流。
看到软件架构为Flutter,Dart 代码被 AOT 编译成 ARM64 机器码塞进 libapp.so

一、字符串情报收集

对 libapp.so 的 rodata 做关键词搜索,命中率极高:

付费模型字段:

freeEpisode        ← 免费集数
isFreeChapter      ← 是否免费章节
chapterPrice       ← 章节单价
unlockType         ← 解锁类型
Pro|realPlayUrl    ← "Pro|" 前缀是它家协议字段命名习惯

核心 API 路径:

/playlet-chapter/list/      ← 拉取剧集章节列表
/playlet-chapter/extra/     ← 章节附加信息
/playlet-permission/unlock  ← 付费解锁接口
/api/app/ping/check         ← 存活检查

VIP 体系:mine_vip/online_pay_page.dartVipCorePrivilegeCardclickAdToVipdoCheckinClaimVip(签到送 VIP)……

到这里业务全貌已经清晰:金币按集解锁 + VIP 包月双轨付费,短剧数据模型围绕 PlayletChapter(章节)构建。

二、还原数据模型

反汇编 PlayletChapter::fromJson(0x64666C,3460 字节)可以看到完整的 JSON 字段到对象偏移的映射。Dart AOT 的 JSON 解析代码特征非常明显——每个字段都是"从 Map 取 key → 类型转换 → StoreField"三连:

0x646D78:  ldr  x2, [x27, #0xca8]   ; "videoUrl"
0x646D88:  blr  xlr                 ; map["videoUrl"]
0x646DD0:  stur w0, [x3, #0x2f]     ; this->field_2f = videoUrl

关键映射结果:

JSON key
对象偏移
语义
videoUrlfield_2f章节真实播放路径
(字段_6b)
field_6b
预览路径(试看)
chapterPrice
章节价格
id
章节ID

VideoModel(剧集聚合对象)侧还有两个关键字段:field_7 = 当前集数下标(int),field_c3 = 章节列表 List<PlayletChapter>

三、还原播放链路

从播放器控制器入手,反汇编 ShortVideoPlayerLogic::initVideoPlayer(0x92F29C):

0x92F434:  bl  #0x8d1794        ; ::Pro.realVideoUrl(item)
0x92F444:  stur w0, [x2, #0x5b] ; logic->playUrl = url
0x92F464:  bl  #0x9318e4        ; PlayerFactory::network(url)

顺藤摸瓜:

// 伪代码还原
String realVideoUrl(VideoModel v) {
  final path = Pro.currentChapter(v)?.videoUrl ?? "";   // currentChapter = v.chapters[v.episodeIndex]
  if (path.isEmpty) return "";
  if (path.startsWith("http")) return path;
  return "$cdnBase/vid/h5/m3u8/$path?token=$token&c=$c"; // token/c 是全局静态字段
}

注意两个细节:

  1. URL 组装用的 token 和 c 是全局静态字段
    ,不是按集签发的——这是一个很弱的授权设计,对客户端锁定方案是重大利好;
  2. Pro.videoUrl
     里有一个被丢弃返回值的 isCanPreview 调用(读原始字节 09 00 00 14 确认是无条件 B 跳过了预览分支)——编译器遗留的痕迹,还原伪代码时要小心别被带偏。

然后我把 realVideoUrl 的全部 13 个调用方枚举了一遍(播放器、长视频页、缓存下载模块):没有任何一处存在"切集/滑动"层面的强制拦截ShortVideoLogic::onPageChanged 反汇编后确认只做下标更新和预加载。也就是说:

客户端唯一的内容门槛,就是"当前章节的 videoUrl 字段里有没有货"。

ComicBuyAlert::show(购买弹窗)全库只有一个调用者 onBuyTapEvent——用户主动点"解锁"按钮才弹,滑动切集时不会自动弹。

四、还原付费判定链

UI 层的付费墙来自 ShortVideoPlayerLogic 的两个 getter:

bool get needBuyVipBool   => longVideoStatusDesc(video).needBuyVip;
bool get needBuyVideoBool => longVideoStatusDesc(video).needBuyVideo;

中央状态函数 ::longVideoStatusDesc(0x9321E4)的反汇编翻译成 Dart:

Status longVideoStatusDesc(VideoModel video) {
  final r = Status(needBuyVip: false, needBuyVideo: false, desc: "");
  if (video == null) return r;
  FreePlayCountManager.instance; globalStore;          // 触发懒加载
  final ch = Pro.currentChapter(video);
  if (ch == null) return r..desc = "预览视频";
  if (video.freeArea) return r..desc = "免费视频";       // ← 分水岭
  if (!Pro.isCoinVideo(video)) {
    if (ch.field_57[0].field_7 == true) return r;      // VIP剧已解锁
    return r..needBuyVip = true..desc = "跳过预览";
  } else {
    if (ch.field_57[0].field_7 == true) return r;      // 金币剧已解锁
    return r..needBuyVideo = true..desc = "跳过预览";
  }
}

两个子判定也全部还原:

bool freeArea(VideoModel v) {
  if (v.field_83 == null) return true;                 // 无付费标记 → 免费
  return v.currentEpisode < v.freeEpisode;             // 当前集 < 免费集数
}

bool isCoinVideo(VideoModel v) => v.payType == VideoPayType.Price;  // Price/None/Vip 三态

顺带一提,分析过程中通过返回值指令实测确认了 Dart AOT ARM64 的布尔编码:true = xNULL+0x20false = xNULL+0x30(xNULL=x22 寄存器)。这个细节在写补丁判断分支极性时非常关键,搞反了整个分析都会错。

五、解锁协议

购买弹窗侧 _ComicBuyAlertState 的几个关键函数还原后,解锁协议非常直白:

POST /playlet-permission/unlock
{
"unlockType": <int>,          // 金币/VIP 等解锁方式
"playletChapterId": <章节ID>
}

这是一个纯记账型接口:服务端记录"这集你买过了",并不给客户端签发任何按集的播放凭证(对比第一节的 CDN URL 用全局 token,进一步佐证)。试看体系(FreePlayCountManager)则负责扣减免费试看次数并异步上报观看记录。

六、修改方案

分析到这里,补丁点其实有很多候选:状态 getter、freeArea、isCoinVideo、章节解锁标志……这次选择两个点:

P1 —— longVideoStatusDesc 内的分水岭跳转(0x9322F8)

0x9322F4:  bl   #0x932ba4     ; 调 freeArea(video)
0x9322F8:  tbnz w0, #4, +5    ; false(付费区)→走付费分支   ← 改成 nop
0x9322FC:  (true 分支:desc="免费视频",needBuy 标志保持 false)

效果:付费状态计算恒返回"免费视频",所有读取 needBuy 标志和状态文案的 UI(解锁按钮、跳过预览、购买弹窗入口)全部哑火。

P2 —— freeArea 的集数比较(0x932BFC)

0x932BF8:  cmp  x3, x1        ; currentEpisode vs freeEpisode
0x932BFC:  b.ge #0x932c08     ; >= 免费集数 → return false  ← 改成 nop
0x932C00:  add  x0, x22, #0x20 ; return true

效果:数据层所有"免费区"判定恒为 true(含试看额度管理里的引用),从源头把"付费区"抹掉。

最后肯定就是构建验证了,测试完美解锁付费剧集

__________

End.

感 谢 阅 读

相关学习资料

返回首页浏览学习资料