Android实战-逆向某li某li视频APP

当然不是bilibili,是某不良视频APP,没想到不良软件都能上梆梆加固企业版了,因此xposed模块编写失败,被检测了,进修后再来看看吧

Frida hook分析

MT看了下是梆梆加固企业版(不知道真假),先看看能不能一把梭

image-20251119114414291

frida-dexdump出现了点问题

JavaScript agent(Frida 注入脚本)在执行过程中被目标进程销毁/结束,导致 RPC 调用失败。

为什么RPC会失败?

原因是:

JS Agent 已经死亡,但 Python 还在尝试 RPC 调用

1
2
3
4
5
searchdex → RPC 调用

Frida JS agent 被杀死(反调试 / 崩溃)

script has been destroyed

RPC = 电脑端调用 Frida 注入手机里的 JS 函数。

JS 崩溃 → RPC 失败 → InvalidOperationError。

在安卓中,应用大多运行在不同的进程中,彼此不能直接访问对方的内存。于是 Android 设计了 Binder IPC(进程间通信),它使用 RPC 模型

就像你调用一个函数,但这个函数实际上在另一个进程里执行。

可能导致rpc失败的点很多

  • ptrace 反调试
  • native 层 anti-frida
  • Java 层 isDebuggerConnected
  • hook 监控
  • detect Frida server 端口
  • detect /data目录中 frida 名称
  • 判断 frida-gadget 特征内存
  • 主动 kill 注入线程

导致 Frida agent 被瞬间 kill → script 被销毁

经过测试,不启动frida时是可以正常启动的

问题可能就是frida-server被识别

56.al

image-20251119114827518

网站试了下,梭出来了点信息

jadx查看了下,是广告vip和一些简单的视频vip

而不包含userVip

要凭借这些信息绕过也行,但最根本的还得凭借user的一些注入和hook

用了Releases · hzzheyang/strongR-frida-android

还是被kill掉了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java.perform(function(){
//枚举当前加载的模块
var process_Obj_Module_Arr = Process.enumerateModules();
for(var i = 0; i < process_Obj_Module_Arr.length; i++) {
//包含"lib"字符串的
if(process_Obj_Module_Arr[i].path.indexOf("lib")!=-1)
{
console.log("模块名称:",process_Obj_Module_Arr[i].name);
console.log("模块地址:",process_Obj_Module_Arr[i].base);
console.log("大小:",process_Obj_Module_Arr[i].size);
console.log("文件系统路径",process_Obj_Module_Arr[i].path);
}
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Java.perform(function(){
var dlopen_interceptor = hook_dlopen();

function hook_dlopen() {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
{
onEnter: function (args) {
this.fileName = args[0].readCString()
console.log(`dlopen onEnter: ${this.fileName}`)
}, onLeave: function(retval){
console.log(`dlopen onLeave fileName: ${this.fileName}`)
}
}
);
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hook_dlopen() {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
{
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
console.log("load " + path);
}
}
}
);
}


Java.perform(function(){
hook_dlopen();
});

hookandroid_dlopen_ext>查看so文件的加载流程

1
2
3
4
5
6
dlopen onEnter: libmonochrome_64.so
dlopen onLeave fileName: libmonochrome_64.so
dlopen onEnter: /product/app/TrichromeLibrary64/TrichromeLibrary64.apk!/lib/arm64-v8a/libmonochrome_64.so
dlopen onLeave fileName: /product/app/TrichromeLibrary64/TrichromeLibrary64.apk!/lib/arm64-v8a/libmonochrome_64.so
dlopen onEnter: /system/lib64/libwebviewchromium_plat_support.so
dlopen onLeave fileName: /system/lib64/libwebviewchromium_plat_support.so

libwebviewchromium_plat_support.so

这个里面可能放反调试的东西?

但是之前用MT看的时候是某梆加固企业版怪问题是用查壳工具并没有发现特征

image-20251119211509768

越看越奇怪

1
2
3
./hs -l 0.0.0.0:9999
adb forward tcp:9999 tcp:9999(需要另外开一个终端),前面的是电脑端口
frida -H 127.0.0.1:9999 -f xxx -l r0tracer.js

直接换了个端口就不闪退了

frida默认端口号是20742

1
frida-dexdump -H 127.0.0.1:9999 -f com.ctf -o  E:\Desktop\dump\

成功运行

image-20251119223705732

有几处错误地方很正常

看来那个某梆企业版感觉是吓唬人的…

抓下包

image-20251120211208201

image-20251120214618691

1
2
@o("v1/user/info")
pl.d<vo.o<String>> g(@zo.a w wVar);

@o 很可能是 Retrofit 的 @POST

@zo.a 很可能是 Retrofit 的 @Body

通过抓包字段viptril找到了ResponseUserInfo

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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
package com.palipali.model.response;

import a0.b;
import android.support.v4.media.d;
import ib.e;
import java.io.Serializable;
import ym.f;

/* compiled from: ResponseUserInfo.kt */
/* loaded from: E:\Desktop\dump\classes16.dex */
public final class ResponseUserInfo implements Serializable {
private final int _id;
private boolean binding_alert;
private int checkin_recounting_times;
private String checkin_time;
private int download_limit;
private long expiry;
private String forever_code;
private ResponseInnerAnnouncement informAnnouncement;
private String invite_code;
private int invite_deadline;
private int invite_total;
private int level;
private String login_type;
private String mail;
private boolean new_avkey_success;
private int no_purchase;
private ResponseOrderGa orderGa;
private ResponseInnerAnnouncement paymentAnnouncement;
private String phone;
private String platform;
private boolean set_password;
private String share_url;
private int showVipTime;
private String trialMessage;
private boolean trialResult;
private String unicode;
private int user_id;
private int verify_mail;
private int verify_phone;
private boolean vipTrial;

public ResponseUserInfo() {
this(0, 1, null);
}

public ResponseUserInfo(int i10) {
this._id = i10;
this.login_type = "";
this.mail = "";
this.phone = "";
this.platform = "";
this.invite_code = "";
this.share_url = "";
this.unicode = "";
this.forever_code = "";
this.trialMessage = "";
this.checkin_time = "";
}

public static /* synthetic */ ResponseUserInfo copy$default(ResponseUserInfo responseUserInfo, int i10, int i11, Object obj) {
if ((i11 & 1) != 0) {
i10 = responseUserInfo._id;
}
return responseUserInfo.copy(i10);
}

public final int component1() {
return this._id;
}

public final ResponseUserInfo copy(int i10) {
return new ResponseUserInfo(i10);
}

public boolean equals(Object obj) {
if (this == obj) {
return true;
}
return (obj instanceof ResponseUserInfo) && this._id == ((ResponseUserInfo) obj)._id;
}

public final boolean getBinding_alert() {
return this.binding_alert;
}

public final int getCheckin_recounting_times() {
return this.checkin_recounting_times;
}

public final String getCheckin_time() {
return this.checkin_time;
}

public final int getDownload_limit() {
return this.download_limit;
}

public final long getExpiry() {
return this.expiry;
}

public final String getForever_code() {
return this.forever_code;
}

public final ResponseInnerAnnouncement getInformAnnouncement() {
return this.informAnnouncement;
}

public final String getInvite_code() {
return this.invite_code;
}

public final int getInvite_deadline() {
return this.invite_deadline;
}

public final int getInvite_total() {
return this.invite_total;
}

public final int getLevel() {
return this.level;
}

public final String getLogin_type() {
return this.login_type;
}

public final String getMail() {
return this.mail;
}

public final boolean getNew_avkey_success() {
return this.new_avkey_success;
}

public final int getNo_purchase() {
return this.no_purchase;
}

public final ResponseOrderGa getOrderGa() {
return this.orderGa;
}

public final ResponseInnerAnnouncement getPaymentAnnouncement() {
return this.paymentAnnouncement;
}

public final String getPhone() {
return this.phone;
}

public final String getPlatform() {
return this.platform;
}

public final boolean getSet_password() {
return this.set_password;
}

public final String getShare_url() {
return this.share_url;
}

public final int getShowVipTime() {
return this.showVipTime;
}

public final String getTrialMessage() {
return this.trialMessage;
}

public final boolean getTrialResult() {
return this.trialResult;
}

public final String getUnicode() {
return this.unicode;
}

public final int getUser_id() {
return this.user_id;
}

public final int getVerify_mail() {
return this.verify_mail;
}

public final int getVerify_phone() {
return this.verify_phone;
}

public final boolean getVipTrial() {
return this.vipTrial;
}

public final int get_id() {
return this._id;
}

public int hashCode() {
return Integer.hashCode(this._id);
}

public final void setBinding_alert(boolean z10) {
this.binding_alert = z10;
}

public final void setCheckin_recounting_times(int i10) {
this.checkin_recounting_times = i10;
}

public final void setCheckin_time(String str) {
e.g(str, "<set-?>");
this.checkin_time = str;
}

public final void setDownload_limit(int i10) {
this.download_limit = i10;
}

public final void setExpiry(long j10) {
this.expiry = j10;
}

public final void setForever_code(String str) {
e.g(str, "<set-?>");
this.forever_code = str;
}

public final void setInformAnnouncement(ResponseInnerAnnouncement responseInnerAnnouncement) {
this.informAnnouncement = responseInnerAnnouncement;
}

public final void setInvite_code(String str) {
e.g(str, "<set-?>");
this.invite_code = str;
}

public final void setInvite_deadline(int i10) {
this.invite_deadline = i10;
}

public final void setInvite_total(int i10) {
this.invite_total = i10;
}

public final void setLevel(int i10) {
this.level = i10;
}

public final void setLogin_type(String str) {
e.g(str, "<set-?>");
this.login_type = str;
}

public final void setMail(String str) {
e.g(str, "<set-?>");
this.mail = str;
}

public final void setNew_avkey_success(boolean z10) {
this.new_avkey_success = z10;
}

public final void setNo_purchase(int i10) {
this.no_purchase = i10;
}

public final void setOrderGa(ResponseOrderGa responseOrderGa) {
this.orderGa = responseOrderGa;
}

public final void setPaymentAnnouncement(ResponseInnerAnnouncement responseInnerAnnouncement) {
this.paymentAnnouncement = responseInnerAnnouncement;
}

public final void setPhone(String str) {
e.g(str, "<set-?>");
this.phone = str;
}

public final void setPlatform(String str) {
e.g(str, "<set-?>");
this.platform = str;
}

public final void setSet_password(boolean z10) {
this.set_password = z10;
}

public final void setShare_url(String str) {
e.g(str, "<set-?>");
this.share_url = str;
}

public final void setShowVipTime(int i10) {
this.showVipTime = i10;
}

public final void setTrialMessage(String str) {
e.g(str, "<set-?>");
this.trialMessage = str;
}

public final void setTrialResult(boolean z10) {
this.trialResult = z10;
}

public final void setUnicode(String str) {
e.g(str, "<set-?>");
this.unicode = str;
}

public final void setUser_id(int i10) {
this.user_id = i10;
}

public final void setVerify_mail(int i10) {
this.verify_mail = i10;
}

public final void setVerify_phone(int i10) {
this.verify_phone = i10;
}

public final void setVipTrial(boolean z10) {
this.vipTrial = z10;
}

public String toString() {
return b.a(d.a("ResponseUserInfo(_id="), this._id, ')');
}

public /* synthetic */ ResponseUserInfo(int i10, int i11, f fVar) {
this((i11 & 1) != 0 ? -1 : i10);
}
}

注入似乎只能改变某些东西

如用户页面vip显示,视频页面的vip高清和快速通道选项

广告vip和视频时长似乎仍是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
function hook_dlopen() {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
{
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
console.log("load " + path);
}
}
}
);
}

function hook_user() {
Java.perform(function () {
var User = Java.use("com.palipali.model.response.ResponseUserInfo");
console.log("[+] Hooking ResponseUserInfo ...");
// 强制 vipTrial = true
User.setVipTrial.implementation = function (v) {
console.log("[VIP] force vipTrial = true");
return this.setVipTrial(true);
};
// 强制 level = 2
User.setLevel.implementation = function (v) {
console.log("[VIP] force level = 2");
return this.setLevel(1);
};

// 强制 expiry(有效期拉满)
User.setExpiry.implementation = function (v) {
console.log("[VIP] force expiry = 9999999999999");
return this.setExpiry(9999999999999);
};

// 强制试用成功
User.setTrialResult.implementation = function (v) {
console.log("[VIP] force trialResult = true");
return this.setTrialResult(true);
};

// 设置 showVipTime
User.setShowVipTime.implementation = function (v) {
console.log("[VIP] force showVipTime = 300");
return this.setShowVipTime(3000);
};
User.toString.implementation = function () {
var ret = this.toString();
console.log("[UserInfo] " + ret);
console.log(" level =", this.getLevel());
console.log(" vipTrial =", this.getVipTrial());
console.log(" expiry =", this.getExpiry());
console.log(" trialResult =", this.getTrialResult());
return ret;
};

});
}


Java.perform(function(){
hook_dlopen();
hook_user();
});

陷入了漫长的寻找

ResponseUserInfo,可能只改了用户信息,真正用于播放鉴权的是 ik.v(MemberBean)。

发现黄鸟抓包要全一点

image-20251121133808157

image-20251121165438371

image-20251121165453581

这两个类均有video_end和duration类似信息

先hook了ResponseVideoInfo

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
function hook_response_vip() {

var Info = Java.use("com.palipali.model.response.ResponseVideoInfo");

console.log("[+] Hooking ResponseVideoInfo...");

// 打印 video_duration
Info.getVideo_duration.implementation = function () {
var d = this.getVideo_duration();
console.log("[VIDEO] getVideo_duration =", d);
return d; // 不修改
};

// 打印 video_end
Info.getVideo_end.implementation = function () {
var end = this.getVideo_end();
console.log("[VIDEO] getVideo_end =", end);
return end; // 不修改
};

// 打印 setVideo_end
Info.setVideo_end.implementation = function (v) {
console.log("[VIDEO] setVideo_end called, value =", v);
return this.setVideo_end(v); // 不修改
};

}

image-20251121165626439

证明其实不该hook这,在这hook也晚了,或者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
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
package com.palipali.model.response;

import android.support.v4.media.d;
import bi.t;
import com.google.gson.annotations.SerializedName;
import e2.b;
import ib.e;
import java.io.Serializable;
import miui.telephony.phonenumber.CountryCodeConverter;
import ym.f;

/* compiled from: ResponseVideoUrl.kt */
/* loaded from: E:\Desktop\51f8fb02c9617292c82517301744238c.zip\..\dump\classes16.dex */
public final class ResponseVideoUrl implements Serializable {
@SerializedName("intro")
private String introUrl;
@SerializedName(CountryCodeConverter.GQ)
private String qvgaUrl;
@SerializedName("480")
private String vgaUrl;

public ResponseVideoUrl() {
this(null, null, null, 7, null);
}

public ResponseVideoUrl(String str, String str2, String str3) {
t.a(str, "introUrl", str2, "qvgaUrl", str3, "vgaUrl");
this.introUrl = str;
this.qvgaUrl = str2;
this.vgaUrl = str3;
}

public static /* synthetic */ ResponseVideoUrl copy$default(ResponseVideoUrl responseVideoUrl, String str, String str2, String str3, int i10, Object obj) {
if ((i10 & 1) != 0) {
str = responseVideoUrl.introUrl;
}
if ((i10 & 2) != 0) {
str2 = responseVideoUrl.qvgaUrl;
}
if ((i10 & 4) != 0) {
str3 = responseVideoUrl.vgaUrl;
}
return responseVideoUrl.copy(str, str2, str3);
}

public final String component1() {
return this.introUrl;
}

public final String component2() {
return this.qvgaUrl;
}

public final String component3() {
return this.vgaUrl;
}

public final ResponseVideoUrl copy(String str, String str2, String str3) {
e.g(str, "introUrl");
e.g(str2, "qvgaUrl");
e.g(str3, "vgaUrl");
return new ResponseVideoUrl(str, str2, str3);
}

public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ResponseVideoUrl)) {
return false;
}
ResponseVideoUrl responseVideoUrl = (ResponseVideoUrl) obj;
return e.b(this.introUrl, responseVideoUrl.introUrl) && e.b(this.qvgaUrl, responseVideoUrl.qvgaUrl) && e.b(this.vgaUrl, responseVideoUrl.vgaUrl);
}

public final String getIntroUrl() {
return this.introUrl;
}

public final String getQvgaUrl() {
return this.qvgaUrl;
}

public final String getVgaUrl() {
return this.vgaUrl;
}

public int hashCode() {
return this.vgaUrl.hashCode() + g1.e.a(this.qvgaUrl, this.introUrl.hashCode() * 31, 31);
}

public final void setIntroUrl(String str) {
e.g(str, "<set-?>");
this.introUrl = str;
}

public final void setQvgaUrl(String str) {
e.g(str, "<set-?>");
this.qvgaUrl = str;
}

public final void setVgaUrl(String str) {
e.g(str, "<set-?>");
this.vgaUrl = str;
}

public String toString() {
StringBuilder a10 = d.a("ResponseVideoUrl(introUrl=");
a10.mo3321append(this.introUrl);
a10.mo3321append(", qvgaUrl=");
a10.mo3321append(this.qvgaUrl);
a10.mo3321append(", vgaUrl=");
return b.a(a10, this.vgaUrl, ')');
}

public /* synthetic */ ResponseVideoUrl(String str, String str2, String str3, int i10, f fVar) {
this((i10 & 1) != 0 ? "" : str, (i10 & 2) != 0 ? "" : str2, (i10 & 4) != 0 ? "" : str3);
}
}
字段 含义 可能内容
introUrl 试看/简介视频(通常几十秒 → 限制版) 60 秒试看
qvgaUrl 低清视频(完整) 240P 正片
vgaUrl 标清视频(完整) 480P 正片

看了下免费的:

image-20251121171715328

写了个新的hook方法

因为1分钟短片是intro

qvga是240p

vga是480p

我看了免费的验证了这之间只差一个参数,因此获取intro之后可以重新拼接,只要再给vga或qvag即可

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
function hook_response_url() {

var Url = Java.use("com.palipali.model.response.ResponseVideoUrl");
console.log("[+] Hooking ResponseVideoUrl with auto full URL...");
var url_240 = ''
var url_480 = ''

Url.getIntroUrl.implementation = function () {
var intro = this.getIntroUrl();
console.log("[URL] intro =", intro);
var filename = intro.substring(intro.lastIndexOf("/") + 1);
console.log("[URL] extracted filename =", filename);

url_240 = "/media/240/" + filename;
url_480 = "/media/480/" + filename;

console.log("[URL] FULL video =", url_240);
console.log("[URL] FULL video 480 =", url_480);

this.setQvgaUrl(url_240);
this.setVgaUrl(url_480);

console.log("[URL] qvgaUrl overridden to FULL");
return intro;
};

Url.getQvgaUrl.implementation = function () {
var q = this.getQvgaUrl();
if (url_240) q = url_240;
console.log("[URL] qvga =", q);
return q;
};

Url.getVgaUrl.implementation = function () {
var vga = this.getVgaUrl();
if (url_480) vga = url_480;
console.log("[URL] vga =", vga);
return vga;
};
}

这个方法只是治标不治本,根本上还得分析谁调用了video,因为什么信息才导致不是vip,之前的hook_user用不了

分析了下,这个是后端校验为主

Hook 改写的是:

  • ResponseUserInfo.level
  • isVip
  • video_end
  • ResponseVideoUrl

这些都只是 本地数据模型

但 app 一旦执行需要写入服务器的操作,例如:

  • 收藏视频(POST 请求)
  • 取消收藏(DELETE)
  • 点赞视频
  • 评论

这些都是发请求给服务器的。

服务器当然不会根据你本地代码判断你是不是 VIP,它根据:

服务器数据库真正记录的账号状态

1
2
3
4
5
6
{
"user_id": 12345,
"level": 0, // 真实账号是非 VIP
"is_vip": false,
"vip_expiry": null
}

解决方案有哪些

  1. Hook 收藏接口的 请求参数 —— 伪造 VIP token

    若收藏接口需要带 token:

    1
    2
    3
    POST /favorite
    token=xxxxxx
    video_id=123

    可以 Hook OkHttp / retrofit,把 token 换成一个真正 VIP 的 token(需要你自己有 VIP 账号)。

    缺点:需要一个 VIP 账号。

    有点脱裤子放屁了

  2. Hook OkHttp,让收藏接口永远“本地成功”

    跟之前hook本地model一样

  3. Hook retrofit/okhttp,劫持整个后端返回,伪造 ResponseUserInfo

    这样整个 app 都以为你是真 VIP,甚至收藏接口服务器也不会再拒绝(因为它会读取 local token?取决于接口)。

    但通常收藏/点赞是根据真实服务器账号认证,不是本地字段。

    即便伪造本地 ResponseUserInfo,服务器还是知道:

    user_id=12345 的账号不是 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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
function hook_dlopen() {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr) {
var path = ptr(pathptr).readCString();
console.log("[dlopen] load => " + path);
}
}
});
}

function hook_JNI_OnLoad(){
let module = Process.findModuleByName("libAppGuard.so")
Interceptor.attach(module.base.add(0x32ADC), {
onEnter(args){
console.log("call JNI_OnLoad")
}
})
}

function hook_dlopen2(soName = '') {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
{
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
if (path.indexOf(soName) >= 0) {
this.is_can_hook = true;
}
}
},
onLeave: function (retval) {
if (this.is_can_hook) {
hook_JNI_OnLoad()
}
}
}
);
}

setImmediate(hook_dlopen2, "libAppGuard.so")


// hook_dlopen();

function hook_user() {
Java.perform(function () {
var User = Java.use("com.palipali.model.response.ResponseUserInfo");
console.log("[+] Hooking ResponseUserInfo ...");
// 强制 vipTrial = true
// User.setVipTrial.implementation = function (v) {
// console.log("[VIP] force vipTrial = true");
// return this.setVipTrial(true);
// };
// 强制 level = 2 尊荣VIP
User.setLevel.implementation = function (v) {
console.log("[VIP] force level = 2");
return this.setLevel(2);
};

// 强制 expiry(有效期拉满)
User.setExpiry.implementation = function (v) {
console.log("[VIP] force expiry = 9999999999999");
return this.setExpiry(1799999999);
};

// 强制试用
// User.setTrialResult.implementation = function (v) {
// console.log("[VIP] force trialResult = true");
// return this.setTrialResult(true);
// };

// 如果 App 有读取 showVipTime 也顺便设成 300 秒
// User.setShowVipTime.implementation = function (v) {
// console.log("[VIP] force showVipTime = 300");
// return this.setShowVipTime(3000);
// };

User.toString.implementation = function () {
var ret = this.toString();
console.log("[UserInfo] " + ret);
console.log(" level =", this.getLevel());
console.log(" vipTrial =", this.getVipTrial());
console.log(" expiry =", this.getExpiry());
console.log(" trialResult =", this.getTrialResult());
return ret;
};

});
}

function hook_response_vip() {

var Info = Java.use("com.palipali.model.response.ResponseVideoInfo");

console.log("[+] Hooking ResponseVideoInfo...");

// 打印 video_duration(总时长)
Info.getVideo_duration.implementation = function () {
var d = this.getVideo_duration();
console.log("[VIDEO] getVideo_duration =", d);
return d; // 不修改
};

// 打印 video_end(试看截止点)
Info.getVideo_end.implementation = function () {
var end = this.getVideo_end();
console.log("[VIDEO] getVideo_end =", end);
return end; // 不修改
};

// 打印 setVideo_end(服务器写入的限制)
Info.setVideo_end.implementation = function (v) {
console.log("[VIDEO] setVideo_end called, value =", v);
return this.setVideo_end(v); // 不修改
};

}

function hook_response_url() {
var Url = Java.use("com.palipali.model.response.ResponseVideoUrl");
console.log("[+] Hooking ResponseVideoUrl with auto full URL...");
var url_240 = ''
var url_480 = ''

Url.getIntroUrl.implementation = function () {
var intro = this.getIntroUrl();
console.log("[VID] intro =", intro);
var filename = intro.substring(intro.lastIndexOf("/") + 1);
console.log("[VID] extracted filename =", filename);
if(filename){
url_240 = "/media/240/" + filename;
url_480 = "/media/480/" + filename;

console.log("[VID] FULL video =", url_240);
console.log("[VID] FULL video 480 =", url_480);

this.setQvgaUrl(url_240);
this.setVgaUrl(url_480);

console.log("[VID] qvgaUrl overridden to FULL");
}
return intro;
};

Url.getQvgaUrl.implementation = function () {
var q = this.getQvgaUrl();
if (url_240) q = url_240;
console.log("[URL] qvga =", q);
return q;
};

Url.getVgaUrl.implementation = function () {
var vga = this.getVgaUrl();
var q = this.getQvgaUrl();
var filename = q.substring(q.lastIndexOf("/") + 1);
console.log("[URL] extracted filename =", filename);
if (filename) url_480 = "/media/480/" + filename;
if (url_480) vga = url_480;
console.log("[URL] vga =", vga);
return vga;
};
}

function hook_missing_video_urls() {
Java.perform(function () {

var Info = Java.use("com.palipali.model.response.ResponseVideoInfo");
var Url = Java.use("com.palipali.model.response.ResponseVideoUrl");

console.log("[+] Hooking fallback URL generator...");

Info.getVideo_urls.implementation = function () {

var urls = this.getVideo_urls();
var vid = this.getVideo_id(); // 关键字段:video_id,例如 "123"
console.log("ID:" + vid);

if (!vid || vid.length === 0) {
console.log("[VID] video_id missing, return raw urls");
return urls;
}

// 原始 URL(intro/qvga/vga)
var intro = urls.getIntroUrl();
var qvga = urls.getQvgaUrl();
var vga = urls.getVgaUrl();

// 如果三者中有一个为空,即需要补全
if (!intro && !qvga && !vga) {

console.log("[VID] Missing URLs for video:", vid);
var file = vid + ".m3u8";

var url240 = "/media/240/" + file;
var url480 = "/media/480/" + file;

console.log("[VID] fallback 240 =", url240);
console.log("[VID] fallback 480 =", url480);

urls.setQvgaUrl(url240);
urls.setVgaUrl(url480);

// intro 可选:有些逻辑会读取
urls.setIntroUrl(url240);
}else if(intro){
console.log("[VID] Missing URLs for video:", vid);
var file = vid + ".m3u8";

var url240 = "/media/240/" + file;
var url480 = "/media/480/" + file;

console.log("[VID] fallback 240 =", url240);
console.log("[VID] fallback 480 =", url480);

urls.setQvgaUrl(url240);
urls.setVgaUrl(url480);
}

return urls;
};

});
}


Java.perform(function(){
hook_user();
hook_response_vip();
// hook_response_url();
hook_missing_video_urls();
});

可能要改数据库只能渗透了hhh

渗透有空再学吧

Xpoed模块编写 [失败]

到此为止想收个尾,写个模块

没想到直接被kill了

因为没找到如何定位哪kill的,找了很久,看了挺多博客都是java层的

但是java层没发现什么,后面猜测是native层的

直接找so

发现了可以文件libAppGuard.so

似乎有反调但不确定

ida分析,发现这几个函数

image-20251122215950382

image-20251122220007679

是检测,但是a1点进去只是个偏移并不知道是对什么做了匹配

猜测是xposed这种?因为使用frida并没有出现这个情况

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
145
146
147
148
149
150
151
152
package com.example.prometronome;


import java.io.BufferedReader;
import java.io.FileReader;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.XposedBridge;

public class MainHook implements IXposedHookLoadPackage {
private String url240 = "";
private String url480 = "";

@Override
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {

if (!lpparam.packageName.equals("com.palipali")) return;

XposedBridge.log("[+] Loaded com.palipali, start hooking...");
dumpProcMaps();
hookUserInfo(lpparam);
hookResponseUrl(lpparam);
}
private void dumpProcMaps() {
try {
BufferedReader br = new BufferedReader(new FileReader("/proc/self/maps"));
String line;
while ((line = br.readLine()) != null) {
XposedBridge.log("[MAPS] " + line);
}
br.close();
} catch (Throwable e) {
XposedBridge.log("ERROR dumpProcMaps: " + e);
}
}

// -----------------------------
// Hook ResponseUserInfo:强制 VIP
// -----------------------------
private void hookUserInfo(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> cls = XposedHelpers.findClass(
"com.palipali.model.response.ResponseUserInfo",
lpparam.classLoader
);

XposedBridge.log("[+] Hooking ResponseUserInfo ...");

// 强制 level=2 (VIP)
XposedHelpers.findAndHookMethod(cls, "setLevel", int.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log("[VIP] force level = 2");
param.args[0] = 2;
}
});

// 强制 expiry 很久
XposedHelpers.findAndHookMethod(cls, "setExpiry", long.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log("[VIP] force expiry = 1799999999");
param.args[0] = 1799999999L;
}
});

// 打印检查
XposedHelpers.findAndHookMethod(cls, "toString", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object thiz = param.thisObject;

int level = (int) XposedHelpers.callMethod(thiz, "getLevel");
boolean vipTrial = (boolean) XposedHelpers.callMethod(thiz, "getVipTrial");
long expiry = (long) XposedHelpers.callMethod(thiz, "getExpiry");
boolean trialResult = (boolean) XposedHelpers.callMethod(thiz, "getTrialResult");

XposedBridge.log("[UserInfo] " + param.getResult());
XposedBridge.log(" level = " + level);
XposedBridge.log(" vipTrial = " + vipTrial);
XposedBridge.log(" expiry = " + expiry);
XposedBridge.log(" trialResult = " + trialResult);
}
});

} catch (Throwable e) {
XposedBridge.log("ERROR hookUserInfo: " + e);
}
}

// -----------------------------
// Hook ResponseVideoUrl → 返回完整清晰度视频
// -----------------------------
private void hookResponseUrl(XC_LoadPackage.LoadPackageParam lpparam) {
try {
Class<?> cls = XposedHelpers.findClass(
"com.palipali.model.response.ResponseVideoUrl",
lpparam.classLoader
);

XposedBridge.log("[+] Hooking ResponseVideoUrl ...");

// Hook getIntroUrl
XposedHelpers.findAndHookMethod(cls, "getIntroUrl", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
String intro = (String) param.getResult();
if (intro == null) return;

XposedBridge.log("[URL] intro = " + intro);

String filename = intro.substring(intro.lastIndexOf("/") + 1);

url240 = "/media/240/" + filename;
url480 = "/media/480/" + filename;

XposedBridge.log("[URL] FULL video 240 = " + url240);
XposedBridge.log("[URL] FULL video 480 = " + url480);

XposedHelpers.callMethod(param.thisObject, "setQvgaUrl", url240);
XposedHelpers.callMethod(param.thisObject, "setVgaUrl", url480);

XposedBridge.log("[URL] Overridden qvga/vga to FULL");
}
});

// Hook getQvgaUrl
XposedHelpers.findAndHookMethod(cls, "getQvgaUrl", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
param.setResult(url240);
XposedBridge.log("[URL] qvga = " + url240);
}
});

// Hook getVgaUrl
XposedHelpers.findAndHookMethod(cls, "getVgaUrl", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
param.setResult(url480);
XposedBridge.log("[URL] vga = " + url480);
}
});

} catch (Throwable e) {
XposedBridge.log("ERROR hookResponseUrl: " + e);
}
}
}

image-20251122220233142

看了下用的这个so的是脱壳时才会用到,闪退也是没进入广告就闪退了

因此就是脱壳时so文件做了检测

向上追溯ida就是一个init方法

现在的问题是如何绕过

为了方便可以先用fridahook下确定

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
const so_name = "libAppGuard.so";
const sub_2F02F8_offset = 0x2F02F8;

function hexDump(ptr, len=64) {
try {
return hexdump(ptr, {length: len});
} catch (e) {
return "(hex dump failed)";
}
}

setTimeout(function() {
const module = Process.findModuleByName(so_name);
if (!module) {
console.log("[-] Cannot find module:", so_name);
return;
}
console.log("[+] Module base =", module.base);
let target = module.base.add(sub_2F02F8_offset);
console.log("[+] sub_2F02F8 address =", target);

Interceptor.attach(target, {
onEnter(args) {
this.arg_i = args[0];
this.arg_a1 = args[1];

console.log("\n========== sub_2F02F8 called ==========");

console.log("i =", this.arg_i);
console.log("a1 ptr =", this.arg_a1);
try {
let s = Memory.readUtf8String(this.arg_a1);
console.log("[+] a1 string:", s);
} catch(e) {
console.log("[-] a1 is not valid UTF-8:", e);
}
console.log("[+] Hex dump a1:");
console.log(hexDump(this.arg_a1, 128));
}
});

}, 1000);

跟xposed配合使用调试

image-20251122220445613

果然在libAppGuard.so被kill了

在这里被kill了

解密出来

image-20251124223447864

分析不动了

解混淆代码

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
from pathlib import Path

path = Path("libAppGuard.so").read_bytes()
src_off, src_len, dst_len = 0x2fad0, 0x126bf2, 0x289144
s = path[src_off:src_off + src_len]
dst = bytearray(dst_len)

w7 = w9 = w4 = 0
w11 = 0x7fffffff
w5 = 1
while True:
w6 = w4 & 0x7f
w4 = (w4 << 1) & 0xffffffff
if w6 == 0:
if w9 >= len(s): break
w4 = ((s[w9] << 1) + 1) & 0xffffffff; w9 += 1
if w4 & 0x100:
if w9 >= len(s): break
dst[w7] = s[w9]; w9 += 1; w7 += 1
continue
w6 = 1
while True:
w8 = w4 & 0x7f
w6 = (w6 << 1) & 0xffffffff
w4 = (w4 << 1) & 0xffffffff
if w8 == 0:
if w9 >= len(s): break
w4 = ((s[w9] << 1) + 1) & 0xffffffff; w9 += 1
w8 = (w4 >> 8) & 1; w6 = (w6 + w8) & 0xffffffff
w8 = w4 & 0x7f
w4 = (w4 << 1) & 0xffffffff
if w8 == 0:
if w9 >= len(s): break
w4 = ((s[w9] << 1) + 1) & 0xffffffff; w9 += 1
if w4 & 0x100: break
w8 = w4 & 0x7f
w6 = (w6 + w11) & 0xffffffff
w6 = (w6 << 1) & 0xffffffff
w4 = (w4 << 1) & 0xffffffff
if w8 == 0:
if w9 >= len(s): break
w4 = ((s[w9] << 1) + 1) & 0xffffffff; w9 += 1
w8 = (w4 >> 8) & 1; w6 = (w6 + w8) & 0xffffffff
continue
if w6 != 2:
if w9 >= len(s): break
w6 = (w6 << 8) & 0xffffffff
w5 = s[w9]; w9 += 1
w6 = (w6 - 0x300) & 0xffffffff
w5 = (w5 + w6) & 0xffffffff
if w5 == 0xffffffff: break
w6 = (w5 & 1) ^ 1
w5 = (w5 >> 1) + 1
else:
w8 = w4 & 0x7f
w4 = (w4 << 1) & 0xffffffff
if w8 == 0:
if w9 >= len(s): break
w4 = ((s[w9] << 1) + 1) & 0xffffffff; w9 += 1
w6 = (w4 >> 8) & 1
while True:
w8 = w4 & 0x7f
w6 = (w6 << 1) & 0xffffffff
w4 = (w4 << 1) & 0xffffffff
if w8 == 0:
if w9 >= len(s): break
w4 = ((s[w9] << 1) + 1) & 0xffffffff; w9 += 1
w8 = (w4 >> 8) & 1; w6 = (w6 + w8) & 0xffffffff
if w6 != 0: break
w6 = 1
while True:
w8 = w4 & 0x7f
w6 = (w6 << 1) & 0xffffffff
w4 = (w4 << 1) & 0xffffffff
if w8 == 0:
if w9 >= len(s): break
w4 = ((s[w9] << 1) + 1) & 0xffffffff; w9 += 1
w8 = (w4 >> 8) & 1; w6 = (w6 + w8) & 0xffffffff
w8 = w4 & 0x7f
w4 = (w4 << 1) & 0xffffffff
if w8 == 0:
if w9 >= len(s): break
w4 = ((s[w9] << 1) + 1) & 0xffffffff; w9 += 1
if not (w4 & 0x100): continue
w6 = (w6 + 2) & 0xffffffff
break
break
if w5 > 0x500:
w6 = (w6 + 1) & 0xffffffff
dst[w7] = dst[w7 - w5]; w7 += 1
x8 = 0
while True:
dst[w7 + x8] = dst[w7 - w5 + x8 + 1]
x8 += 1
if w6 == x8: break
w7 = (w7 + w6) & 0xffffffff

patched = bytearray(path)
patched[src_off:src_off + len(dst)] = dst
Path("libAppGuard_unpacked.so").write_bytes(patched)
print("done", w7, "bytes")

暂时告一段落了

Frida持久化也不太好搞…