ARTICLE · 1019328
某影视APP-P2P去签实战
前言
AI太牛逼了 。感觉发什么都没意思。。。
看一堆人发dy、xhs、好像这些都是有手就行了。
算了。别搞技术了,研究思路就行了。
随便玩玩吧
这里随便水一下。后面都随缘发了。
版本:5.0.0
包名:com.pathrecall.gj
加固:360 pp_hlsProtected(libjiagu_sdk_pp_hlsProtected.so + libpp_hls.so)
目标两件:
无限观影 重打包
广告
主要体现有两个 如下图:
1、
2、

这里弹窗hook 就不多说了 直接hook外层activity 也可以。
后面播放判断的逻辑 如下图所示

C5758.m19826() || m19824() >= m19808() → 直接播 → 否则走激励广告混淆后类名:com.fnmobi.sdk.library.ᐧʻˎ,这里要看下对应的关系 方便后续改smail 最后确认在 classes8.dex。
如下图所示,可以看到还是有一些莫名其妙的符号混淆

这里总结了下:
ʽﹶ | m19826 | KEY_PREF_FREE_AD |
|---|---|---|
ʽᵢ | KEY_PREF_IS_FREE_AD_ALLBoolean | |
ʽゼ | m19824 | KEY_PREF_PLAYVIEWNUM |
ʽי | m19808 | KEY_PREF_MAXVIEWNUM |
每天会走 C4006.m14325 把 PLAYVIEWNUM 清零。
Frida
Java 层直接改四个 getter 的返回值。ʽᵢ 返回的是 Boolean 对象,不是 boolean,要用 Boolean.valueOf(true)。
functionlog() {var a = [];for (var i = 0; i < arguments.length; i++) a.push(String(arguments[i]));console.log("[unlimited] " + a.join(" "));}Java.perform(function () {var C5758 = Java.use("com.fnmobi.sdk.library.ᐧʻˎ");// KEY_PREF_FREE_AD C5758["ʽﹶ"].implementation = function () { log("C5758.isFreeAd is called, return true");returntrue; };// KEY_PREF_IS_FREE_AD_ALL,返回 Boolean C5758["ʽᵢ"].implementation = function () { log("C5758.isFreeAdAll is called, return true");return Java.use("java.lang.Boolean").valueOf(true); };// KEY_PREF_PLAYVIEWNUM 已看次数 C5758["ʽゼ"].implementation = function () { log("C5758.getPlayViewNum is called, return 99999");return99999; };// KEY_PREF_MAXVIEWNUM 上限 C5758["ʽי"].implementation = function () { log("C5758.getMaxViewNum is called, return 0");return0; }; log("unlimited watch hooks installed");});这样就能播放了。其实这个逻辑比较简单。
重打包
我觉得这一块比较麻烦。因为你重打包 正常都直接过了。但是你使用mt 或者其他工具 直接就会闪退。。。。
这一版会比较麻烦。。他不闪退 就是视频无法播放
上文提到了 主要逻辑是在 classes8.dex
这里我们看看

这里改完classes8.dex 需要保持原大小。因为我发现改完要重新算下hash 。用zip -d 去签名,再 zip -u 只更新这一个 dex
字节码
const/4 v0, 1 是 12 10,不是 12 01。第一次写反了,就会导致VerifyError。
FREE_AD / MAX / PLAY 三个方法 insns_size = 12,FREE_AD_ALL 是 16,后面还要留 Boolean.valueOf。
12 10 0f 00 # const/4 v0,1 ; return v0 ʽﹶ FREE_AD12 00 0f 00 # const/4 v0,0 ; return v0 ʽי MAXVIEWNUM14 00 9f 86 01 00 0f 00 # const v0, 99999 ʽゼ PLAYVIEWNUM360 加固
重打包还会触发360加固 可能会导致闪退。。 安装报毒。
具体逻辑如下

MyRootApplication.attachBaseContext 里会调
invoke-static {p0}, Lcom/jiagu/sdk/pp_hlsProtected;->install(Landroid/app/Application;)V对应 opcode:71 10 43 40 00 00(offset 0x409326)。
Java 层 install() 是空壳,活在 JNI_OnLoad。改过 dex 再让它跑,MultiDex 之后直接 SIG 9。
所以把这 6 字节 nop 掉:
71 10 43 40 00 00 → 00 00 00 00 00 00这样就能直接过了。但是过了之后 依然不行。
因为直接nop掉会导致 链接拼接错误。哎 真麻烦。。。
最后的体现如下

怎么刷新没用。
其实后面通过抓包发现 他应该是播放地址有问题。
当 CommonEachBean.getIs_p2p() == 1 时,不走原始 vod_url,而是:
http://127.0.0.1:{MyRootApplication.ʽℼ}/resource.m3u8?src={Base64(vod_url)}&ck={ck}封装在 com.fnmobi.sdk.library.ˉـ.ʽᵢ(C4006.m14331)。无 ck 的版本是 ʽﹳ(m14333)。
端口来自 com.pp.hls.load:
new com.pp.hls().load("<p2p-app-key>", // 32 位 hex,so 里 / smali 里都有"com.xxx.xxx","3", extDir, extDir, p2pConfig, // ᐧʻˎ.ʽ͋() 拼出来的配置串"1");返回值会重新 写进 me.genuine.cher.sub.app.base.MyRootApplication.ʽℼ
这里重新用下frida 去hook看看。

frida如下:
functionlog() {var a = [];for (var i = 0; i < arguments.length; i++) a.push(String(arguments[i]));console.log("[play] " + a.join(" "));}Java.perform(function () {var Bean = Java.use("me.genuine.cher.sub.app.contentEntry.detail.CommonEachBean"); Bean.getIs_p2p.implementation = function () {var r = this.getIs_p2p(); log("getIs_p2p=" + r + " vod=" + this.getVod_url() + " ck=" + this.getCk());return r; };var App = Java.use("me.genuine.cher.sub.app.base.MyRootApplication"); log("p2p_port_field=" + App.ʽℼ.value);var Hls = Java.use("com.pp.hls"); Hls.load.overload("java.lang.String", "java.lang.String", "java.lang.String","java.lang.String", "java.lang.String", "java.lang.String", "java.lang.String" ).implementation = function (a, b, c, d, e, f, g) { log("hls.load key=" + a + " pkg=" + b + " mode=" + c);var r = this.load(a, b, c, d, e, f, g); log("hls.load ret=" + r + " portField=" + App.ʽℼ.value);return r; };var C4006 = Java.use("com.fnmobi.sdk.library.ˉـ"); C4006["ʽᵢ"].implementation = function (s) {var r = this["ʽᵢ"](s); log("wrap.m14331 in=" + s + " out=" + r);return r; }; C4006["ʽﹳ"].implementation = function (s) {var r = this["ʽﹳ"](s); log("wrap.m14333 in=" + s + " out=" + r);return r; };});结果:
hls.load ret=-1 portField=-1getIs_p2p=1vod=http://cdn.xxx.com/vod/m3u8/.../index.m3u8wrap.m14331 out=http://127.0.0.1:-1/resource.m3u8?src=...端口 -1,Exo 报地址异常。原包同一条 hook,hls.load ret 是正常正数端口。
这里看到了 portField=-1, ret=-1 代表有问题。那这里就要重新拼接。
这里注意几个点:
1. 把 ˉـ.ʽᵢ / ʽﹳ 整段改成 return p0
2. 只把 CommonEachBean.getIs_p2p 改成 return 0
片源要走本地 P2P(libpp_hls.so)。它会读安装签名;重签后 hls.load 返回 -1,地址变成 http://127.0.0.1:0/...,界面就是「播放异常」。CDN 直链是 403,不走代理播不了。
P2P 签名校验
刚刚讲到了 P2P 代理。所以代表P2P 其实有个签名校验。
这里也不想用ida看了。直接让ai帮我读一下libpp_hls.so 的明文串:
最后发现
getPackageManagergetPackageInfo(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;signatures[Landroid/content/pm/Signature;toByteArrayjava/security/MessageDigestJNI 里 flags 写死 0x40(GET_SIGNATURES),把证书 toByteArray 再 MD5 成 hex。这个 hash 当应用身份交给 tracker。
重签之后 hash 对不上,load 直接 -1。
这里不能直接nop掉。。 因为你nop掉就读不到原证书了。
所以要从 META-INF/CERT.RSA 那这个东西是什么。
其实这玩意就是是 APK v1 签名里的证书文件。
本质上欺骗签名就是伪造这个。
具体frida代码如下: 这里具体hash我就不贴了。。。
var PKG = "com.xxx.xxx";var CER_HEX = "<原包证书 DER hex>";functionhexToBytes(hex) {var arr = [];for (var i = 0; i < hex.length; i += 2) arr.push(parseInt(hex.substr(i, 2), 16));return Java.array("byte", arr);}Java.perform(function () {var Signature = Java.use("android.content.pm.Signature");var origSig = Signature.$new(hexToBytes(CER_HEX));var PM = Java.use("android.app.ApplicationPackageManager"); PM.getPackageInfo.overload("java.lang.String", "int").implementation = function (name, flags) {var pi = this.getPackageInfo(name, flags);if (name === PKG && (flags & 0x40)) { pi.signatures.value = Java.array("android.content.pm.Signature", [origSig]);console.log("[spoof] getPackageInfo flags=0x" + (flags >>> 0).toString(16)); }return pi; };var App = Java.use("me.genuine.cher.sub.app.base.MyRootApplication");var Hls = Java.use("com.pp.hls");var ctx = Java.use("android.app.ActivityThread").currentApplication();var path = ctx.getExternalFilesDir("").getAbsolutePath();var cfg = Java.use("com.fnmobi.sdk.library.ᐧʻˎ")["ʽ͋"]();var r = Hls.$new().load("<p2p-app-key>", PKG,"3", path, path, cfg, "1" );console.log("[spoof] hls.load ret=" + r + " portField=" + App.ʽℼ.value);if (r > 0) App.ʽℼ.value = r;});实际结果:
getPackageInfo flags=0x40hls.load ret=7000port written 7000设备上:
LISTEN 0.0.0.0:7000签名骗过,P2P 就起来了。后面就是把这套写进包。
最后:还要Hook一个内置方法。ActivityThread.sPackageManager。不能直接让ai改getPackageInfo。因为他默认会返回本包。
结论:
最终结果如下:

classes8.dex(原大小不变):
ᐧʻˎ四个 getter → 免广告 / 次数 99999 / 上限 0pp_hlsProtected.installnop(0x409326),避免改 dex 被杀MyRootApplication去掉 final(0x182994:11 00 00 00→01 00 00 00)
classes10.dex:IPackageManager代理,把本包signatures换成原包证书AndroidManifest.xml:Application 换成SigSpoofAppImplXX重签 v1/v2/v3