Photo Recovery(com.files.photo.recovery.restore.pro)源码逆向分析报告#
分析对象:Google Play 上架应用 Photo Recovery
版本:V1.0.2(versionCode = 3)
开发者:Technology Hub Pro
目标 SDK:Android 15(compileSdk=35 / minSdk=24)
分析日期:2026-07-13
分析方式:apkeep 从 Google Play 下载 XAPK → jadx-1.5.1 反编译 → 静态源码走查 + 关键组件动态调用关系还原
摘要(TL;DR)#
这是一款以 "照片/文件恢复 + 垃圾清理" 为壳、以 通知栏骚扰 + 顽固保活 为核心运营手段的商业化应用。代码层同时命中「骚扰级本地推送 + 顽固保活 + 通知劫持」三类灰产特征,全部核心攻击参数通过 Firebase Remote Config 远程收放,具备典型「上架审核期低压、上线后灰度组放量」的操作空间。
- 通知系统:4 层叠加(本地推送 + 前台常驻 + FCM 远程 + NotificationListener 劫持),核心通知节奏由 Firebase Remote Config 下发;灰度组
Online_B_V2_topon默认参数为 每 2 分钟一条、每天上限 99 条、广告类通知无上限。 - 小组件系统:一颗「拉活 + 引流」钩子,通过设置页 / 退桌挽留 / 主页多入口反复诱导 pin;
updatePeriodMillis=24h让系统每天至少唤醒进程一次;三个点击路由全部强制先经MainActivity中转,喂给CleanActivity/MainActivity/ContactFunSelectActivity三大广告场景。 - 保活链:前台 Service(
specialUse=quick entry)+ 2 个 JobService(4-6s 兜底 / 24 min 心跳)+ 8h AlarmManager + FCM 高优先级远程唤醒 + BOOT_COMPLETED + Home/最近任务键动态监听 + Activity onResume 反补拉活 —— 7 路并行、互为兜底,onTaskRemoved和通知deleteIntent双向自愈。 - 未采用的重型武器:1 像素 Activity、双进程守护、Native fork —— 这些会触发 Google Play 严查,作者刻意避开,属"上架合规粉饰 + 运行时灰产实操"典型套路。
一、应用基础信息#
1.1 Google Play 元数据#
| 项目 | 值 |
|---|---|
| 应用名 | Photo Recovery(中文名:文件恢复) |
| 包名 | com.files.photo.recovery.restore.pro |
| 版本 | V1.0.2(versionCode = 3) |
| 开发者 | Technology Hub Pro |
| 分类 | Tools |
| 最低 Android 版本 | 7.0(API 24) |
| 目标 Android 版本 | 15(API 35) |
| APK 大小 | ≈ 42 MB(base.apk)+ 拆分 APK 共 44 MB |
1.2 声明的权限(AndroidManifest.xml)#
网络:INTERNET, ACCESS_NETWORK_STATE, ACCESS_WIFI_STATE
存储:WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE, MANAGE_EXTERNAL_STORAGE(All Files Access)
通信录:READ_CONTACTS, WRITE_CONTACTS
后台/保活:RECEIVE_BOOT_COMPLETED, WAKE_LOCK, FOREGROUND_SERVICE, FOREGROUND_SERVICE_SPECIAL_USE
通知:POST_NOTIFICATIONS(重复声明两次)
使用统计:PACKAGE_USAGE_STATS(声明但代码中未主动调用 UsageStatsManager,可能为后续版本保留)
广告:com.google.android.gms.permission.AD_ID, ACCESS_ADSERVICES_ATTRIBUTION, ACCESS_ADSERVICES_AD_ID, ACCESS_ADSERVICES_TOPICS, ACCESS_ADSERVICES_CUSTOM_AUDIENCE
FCM:com.google.android.c2dm.permission.RECEIVE
支付:com.android.vending.BILLING, com.android.vending.CHECK_LICENSE
1.3 集成的第三方 SDK#
| SDK | 用途 |
|---|---|
| Firebase(Analytics / Crashlytics / Messaging / Remote Config / Sessions / Installations) | 数据分析、崩溃、远程推送、远程配置 |
Google AdMob(ca-app-pub-5103256378795836~2856087682) |
广告主渠道 |
| Meta Audience Network(FAN) | 广告 |
| Bytedance Pangle SDK v7.3.0.4 | 广告 |
| Mintegral Ads(含 mbridge / thinkup / tramini / topon) | 广告聚合 |
| InMobi Ads | 广告 |
| Vungle Ads | 广告 |
| ThinkUp(Mintegral 系) | 广告聚合 |
| Adjust SDK | 归因/追踪 |
| PairIP License(Google Play Licensing 加固) | 防盗版 |
| Google Play Billing 7.1.1 | 内购 |
1.4 应用入口链#
- Application:
com.pairip.application.Applicationextendscom.tools.photo.recovery.App—attachBaseContext先跑LicenseClient.checkLicense(context)(Google Play 加固)。 - 首个 Activity:
LauncherActivity(android:launchMode="singleTask"),带 4 个 shortcuts。
二、通知系统(Notification)逻辑#
代码层实现了 4 类通知机制,彼此叠加、互相唤起:
| 层 | 类型 | 触发方 | 显示方 |
|---|---|---|---|
| A | 本地推送(自研) | S8.T 周期协程 / 冷启动 / 前台切换 |
S8.M.notify(...) |
| B | 前台常驻 + 保活链 | iqQiiOL.qnfE(FCM)/iLiOQl.qnfE(Job)/Ql1lqlL.qnfE(Job)/lefrHZ9.qnfE(Alarm) |
第三方 SDK a.C1448b + xb.g 频道 |
| C | 远程推送 | FCM | iqQiiOL.qnfE.c(RemoteMessage) 收 google.delivered_priority=high 时启动前台服务 |
| D | NotificationListenerService 劫持 | 系统 onNotificationPosted 回调 |
MyNotificationListenerService + S8.C1056h 主动 cancelNotification 他人通知 |
2.1 关键类#
本地推送引擎(Layer A):
S8.M(约 3221 行)—— 通知构造/调度核心枢纽(前置门槛:通知权限、屏亮、未锁、竖屏、装机满 5 分钟)。构造四类通知:ad_ntf(广告召回,IMPORTANCE_HIGHAdRecall频道 + 自定义 RemoteViews)、filter_ntf(常驻 ongoing)、res_ntf(残留式大通知)、normal_ntf。用RelativeSizeSpan(1.3f/2.28f)+ 高饱和红粉色_FF5A8B强调数字与关键词;用RemoteViews塞"距上次恢复 X 天""清理 Y 天"等伪计数徽章。S8.T—— 周期协程调度器,读 Firebase RC 里notification.normalNtf.intervalTime,若ntfSwitch==1则以该间隔重复触发。NotificationConfig/NormalNtf/NtfItem—— 分类桶(ad_ntf / normal_ntf / res_ntf / apk_ntf),每个桶有intervalTime(下次触发间隔)、upperLimit(日上限)、ntfCoolingTime(冷却)、ntfSwitch(开关)、repetitions(同素材复用次数)、maxVisibleNtf(下拉屏最多堆几条)等参数。NotificationTempActivity—— 所有通知点击的收敛跳板。读取notification_type_key/notification_router_key/notification_id_key,做完埋点后App.f35499u=router并FLAG_ACTIVITY_CLEAR_TASK拉主 Activity(照片/视频/清理等 tab)。
NotificationListenerService(Layer D):
MyNotificationListenerService—— 系统级通知监听。S8.C1055g.onListenerConnected—— 连接时批量扫描getActiveNotifications()并入库。S8.C1056h.onNotificationPosted—— 非白名单包名的通知会被主动cancelNotification(sbn.key),然后转成NotificationBean落库供"通知管理"界面显示。表面是"通知隐藏/清理骚扰",本质是吃掉竞品/系统通知的曝光机会。NotificationManagementActivity.onDestroy—— 退出时触发AD_SCENE_NOTIFICATION_EXIT_INT插屏广告,将系统权限功能商业化。
远程推送(Layer C):
iqQiiOL.qnfE(FirebaseMessagingService 子类,混淆名)——onCreate主动强拉 FCM token;c(RemoteMessage)判google.delivered_priority == "high"→ 直接调AbstractC1449c.d(this)(启动前台服务)。
2.2 远程配置数据源#
Firebase Remote Config(A9.C1503o.f15838d)拉到 RemoteConfigBean 后写入 f15839e。硬编码兜底两组:
| 桶 | 参数 | Offline_A_V2_topon(默认离线) | Online_B_V2_topon(在线灰产型) |
|---|---|---|---|
res_ntf |
switch / upper_limit / interval | 1 / 2 / 14400000ms (4h) | 1 / 2 / 14400000ms |
ad_ntf |
switch / upper_limit / interval | 1 / -1(无上限) / -1 | 1 / -1 / -1 |
normal_ntf |
upper_limit / interval / repetitions | 5 / 1800000ms(30min) / 1 | 99 / 120000ms(2min) / 3 |
结论:日常参数尚可(30 min/条、日上限 5),但灰度组 Online_B 默认参数是 2 分钟一条、每天最多 99 条、素材可复用 3 次,广告类通知无日上限。
2.3 关键代码片段#
通知置顶技巧:when = now + 365 天(a/C1448b.java:34-43)—— 通知栏按 when 倒序排列,等价于永远置顶:
public static long b() {
boolean isXiaomi = "Xiaomi".equalsIgnoreCase(Build.BRAND) || "Xiaomi".equalsIgnoreCase(Build.MANUFACTURER);
if (isXiaomi) return System.currentTimeMillis();
return TimeUnit.DAYS.toMillis(365L) + System.currentTimeMillis(); // 未来 1 年时间戳
}
通知构造(MAX 优先级 + 灯 + 常驻)(a/C1448b.java:95-120):
notification.deleteIntent = g9; // ← 用户下滑清除 = 触发前台服务再次启动
vVar.f46530k = false; // setAutoCancel(false)
notification.when = b(); // now + 365 天
notification.defaults = -1; // DEFAULT_ALL
notification.flags |= 1; // FLAG_SHOW_LIGHTS
vVar.f46529j = 2; // PRIORITY_MAX
vVar.f46536s = 1; // VISIBILITY_PUBLIC
deleteIntent 指向前台服务(划掉即复活)(a/C1448b.java:279-310):
Intent intent = new Intent(contextWrapper, iIL0OiOQ.qnfE.class);
return PendingIntent.getForegroundService(contextWrapper, reqId, intent, flag);
FCM 高优先级 → 前台服务(iqQiiOL/qnfE.java:10-46):
if (Constants.HIGH.equals(delivered_priority)) {
AbstractC1449c.d(this); // ← 启动前台服务
}
NotificationListener 干掉他人通知(S8/C1056h.java,语义还原):
if (!C1062n.f10033f.contains(sbn.packageName)) { // 非白名单
if ((sbn.notification.flags and FLAG_ONGOING_EVENT) == 0) {
cancelNotification(sbn.key) // ★ 主动取消
dao.insert(NotificationBean(...))
}
}
2.4 通知系统灰产味道评估#
按危害度递减:
notification.when = now + 365 天—— 永远排到通知栏最上方。deleteIntent → getForegroundService(iIL0OiOQ.qnfE)—— 划掉通知反而重启前台服务。iIL0OiOQ.qnfE.onTaskRemoved—— 用户从最近任务划掉 App,服务立即复活。- 4 秒 heads-up 二次弹窗:
xb.i.f46310a.postDelayed(4000ms)后重推更高级别频道xb.g.f46295e,制造抖屏。 - 小米专供旧版高优先级:
Build.BRAND=="Xiaomi"且 SDK<35 时把 importance 打到 5(旧 IMPORTANCE_MAX),钻 MIUI 策略空子。 - 伪计数徽章:"距离上次恢复 X 天"、"清理 Y 天" + 高饱和红粉色 + 2.28× 字号 —— 制造"未处理危险"错觉。
- AdRecall 频道 IMPORTANCE_HIGH + ad_ntf.upper_limit=-1 —— 广告类通知披上"重要提醒"外衣,且无日上限。
- 权限功能商业化:
NotificationManagementActivity退出触发插屏。 - NotificationListener 反向利用:非白名单包被主动
cancelNotification,实质抢占其他 App 通知曝光。 - 点击
FLAG_ACTIVITY_CLEAR_TASK:无论用户当前在做什么,通知点开即换成主界面,中断上下文。
三、桌面小组件(AppWidget)逻辑#
3.1 组件概览#
| 项目 | 值 |
|---|---|
| Provider 类 | com.tools.photo.recovery.widget.MyWidgetProvider |
| Pin 回调 Receiver | com.tools.photo.recovery.widget.AppWidgetPinReceiver |
| Layout | @layout/widget_layout(横向 LinearLayout) |
| 元数据 | minWidth=300dp, minHeight=100dp, updatePeriodMillis=86400000(24h), resizeMode=vertical|horizontal, widgetCategory=home_screen |
外观:中央圆形区展示可清理缓存大小(数字 + 单位),按 4 档换色 —— <1MB 绿 "very clean"、<20MB 绿、<100MB 橙、≥100MB 红。三块可点击区域:中央清理块、右上恢复块、右下备份块。
3.2 Pin(加桌)流程#
唯一 pin API 入口:d.AbstractC4279a.H(Context, String from_key)(D/AbstractC4279a.java:166-178)
public static void H(Context context, String str) {
AppWidgetManager m = AppWidgetManager.getInstance(context);
if (m != null && Build.VERSION.SDK_INT >= 26 && m.isRequestPinAppWidgetSupported()) {
Intent intent = new Intent(context, AppWidgetPinReceiver.class);
intent.putExtra("from_key", str);
m.requestPinAppWidget(new ComponentName(context, MyWidgetProvider.class), null,
PendingIntent.getBroadcast(context, (int)System.currentTimeMillis(), intent, 201326592));
}
}
Pin 成功后 AppWidgetPinReceiver.onReceive 触发 add_widget_success 埋点(J8.b.f5600G1),携带 source=from_key。
明确的诱导入口:
| from_key 取值 | 触发场景 | 位置 |
|---|---|---|
main_page |
主页 / 扫描页"加桌"按钮 | MainActivity.java:198, B2/W.java:85 |
screen_hot_remove |
截屏发热清理场景 | A3/C0458a.java:186 |
screenshot |
截图事件触发 | MainActivity.java:64 |
launcher_page |
启动页 / 开屏后引导 | LauncherActivity.java:175 |
uninstall |
卸载监听后诱导 | B/W.java |
Settings |
设置页"添加小组件" | U7/C1156q.java(先发 settings_click,type=widget) |
Exit |
退出挽留弹窗 | x8/C1423y0.java:107-108 |
3.3 数据来源#
小组件展示的核心数据 = MyWidgetProvider.f35733a(public static long)。写入路径:
D8.t(junk/cache 扫描聚合)
└─ tVar.f2817g = ib.a0(MutableStateFlow<Long>)
└─ x8.C5733g(App 冷启动协程,Dispatchers.IO)
.collect( new I8.h(app, 12) )
└─ default 分支 I8/h.java:143-152:
MyWidgetProvider.f35733a = value
RemoteViews rv = new RemoteViews(pkg, R.layout.widget_layout)
AbstractC4279a.y(rv, context)
appWidgetManager.updateAppWidget(componentName, rv)
缓存 Flow 每次 emit,都会同步刷写静态字段并整刷 RemoteViews。
3.4 刷新机制(双路径)#
(a) 系统周期刷新:updatePeriodMillis=86400000(24h),系统 AlarmManager 每天至少一次派发 APPWIDGET_UPDATE(即使进程死亡也能被拉起)。
(b) 事件驱动刷新(进程存活期):ib.a0 StateFlow 每次值变直推 updateAppWidget。
无 WorkManager / JobScheduler 做小组件专用刷新,仅依赖上述两路。
3.5 点击路由(三个 router_key_widget_*)#
RemoteViews 的三个 setOnClickPendingIntent 目标由 Z6.b.H(context, key) 构造,通过 v8.AbstractActivityC5616b(实现类 com.tools.cornerstone.router.RouterRelayActivity,透明主题)中转,最终派发到目标 Activity:
| 视图 | router extra | 静态注册项 | 最终行为 |
|---|---|---|---|
ll(中央清理块) |
router_key_widget_clean |
MyWidgetProvider.f35734b = A3.Y(12) |
跳 CleanActivity,fileSource=J8.c.f5700h |
ll_recovery |
router_key_widget_recovery |
f35735c = A3.Y(13) |
直接进入 MainActivity(照片恢复主流程) |
ll_back_up |
router_key_widget_back_up |
f35736d = A3.Y(14) |
MainActivity.y() gate → ContactFunSelectActivity |
派发时机由 u8.AbstractActivityC5547b.onResume(基 Activity)承担 —— 当当前活动为 MainActivity 时才执行。所以三个 widget 点击都会先经 relay → 落到 MainActivity → 才真正跳目标页,保证 App 处在前台可见状态,触发 App 内 AdScene 生命周期。
3.6 小组件与保活 / 变现关系#
- 保活侧:
updatePeriodMillis=86400000使系统每 24h 至少唤起进程一次(即使 App 被杀);每次点击 widget 都强制经过MainActivity,触发 App 生命周期回调,顺带执行AbstractC1449c.d()拉活。 - 变现侧:三条点击路径直接抵达三大插屏/原生广告场景(清理、恢复、联系人备份)。
- 诱导闭环:Settings + Exit 两处 pin 请求 + 通知权限流水线一起构成"用户关不掉 App"的 dark pattern。
四、保活机制(进程保活 / 后台唤醒)#
4.1 保活组件清单(混淆类真实用途)#
| Manifest 组件 | 真实身份 | 关键行为 |
|---|---|---|
lefrHZ9.qnfE |
AlarmManager 触发的 Receiver | 收闹钟后 ①上报事件;②AbstractC1449c.a() 重订 FCM topic;③f() 重排下一次闹钟(自续期) |
iIL0OiOQ.qnfE |
常驻前台 Service(specialUse / quick entry) |
onStartCommand 调 C1448b.i() 启动 startForeground(2084960406, ntf);onDestroy / onTaskRemoved 立即反手拉自己 |
iLiOQl.qnfE |
快速拉活 JobService | onCreate 就直接 startForegroundService(iIL0OiOQ.qnfE);参数 minLatency=3999ms, overrideDeadline=5999ms, priority=400(HIGH), persisted=true |
Ql1lqlL.qnfE |
周期心跳 JobService | onStartJob 里再次调 AbstractC1449c.e(this) 自我重排(minLatency=1446000ms ≈ 24 min),绕开 setPeriodic() 的 15min 强制约束 |
Qi0l0QQ.qnfE |
开机/装包更新自启 Receiver | 匹配 BOOT_COMPLETED / MY_PACKAGE_REPLACED / QUICKBOOT_POWERON,进入 Db.c 初始化链 |
iqQiiOL.qnfE |
FirebaseMessagingService | onCreate 强拉 FCM token;onMessageReceived 判 google.delivered_priority=="high" → 直接调 AbstractC1449c.d(this) 拉起前台服务 |
a.C1450d(动态注册) |
Home/最近任务键监听 | E0/c.java:173-177 registerReceiver(new a.C1450d(), IntentFilter("CLOSE_SYSTEM_DIALOGS"), RECEIVER_EXPORTED) — 按 Home / 最近任务时触发 AbstractC1449c.a() 补拉活 |
4.2 拉活触发链路#
① 开机 / 装包更新
BOOT_COMPLETED / MY_PACKAGE_REPLACED / QUICKBOOT_POWERON
→ Qi0l0QQ.qnfE.onReceive → E0.c.J() ── 一次性初始化:
├─ AbstractC1449c.f(app) → AlarmManager.set(ELAPSED_REALTIME_WAKEUP, +8h)
├─ AbstractC1449c.d(app) → startForegroundService(iIL0OiOQ.qnfE)
├─ AbstractC1449c.e(app) → JobScheduler → Ql1lqlL.qnfE (24 min)
├─ enqueueUniquePeriodicWork "day" → WorkManager Worker IqHBn.iLiOQl (18 min)
└─ registerReceiver(C1450d) → Home / 最近任务键监听
② 前台 Service 意外死亡
iIL0OiOQ.qnfE.onDestroy / onTaskRemoved
→ AbstractC1449c.d(context) ← 兜底 startForegroundService
catch(Exception)
→ AbstractC1449c.b(context) ← 转 JobScheduler(latency=3999ms) → iLiOQl.qnfE
→ iLiOQl.qnfE.onCreate → 再 startForegroundService ★闭环★
③ AlarmManager 到点
PendingIntent → lefrHZ9.qnfE.onReceive
├─ AbstractC1449c.a() (FCM 订阅)
└─ AbstractC1449c.f(ctx) 重排下次闹钟(+8h)
④ 周期 JobService
Ql1lqlL.qnfE.onStartJob → AbstractC1449c.e(this) 重排(24 min)+ AbstractC1449c.a()
⑤ 远程唤醒(FCM)
FCM push (priority=high) → iqQiiOL.qnfE.c() → AbstractC1449c.d(this)
⑥ 用户 Activity 回前台
xb.e.onActivityResumed → AbstractC1449c.a() + AbstractC1449c.d(app)
⑦ 按 Home / 最近任务
a.C1450d.onReceive → C1448b.c().h() + AbstractC1449c.a()
4.3 周期任务清单#
| 类型 | 目标 | 频率 | 关键参数 |
|---|---|---|---|
| AlarmManager | Broadcast lefrHZ9.qnfE |
elapsedRealtime + 28,799,365 ms ≈ 8h |
type=ELAPSED_REALTIME_WAKEUP(2), 用 set(...)(非 exact) |
| JobScheduler #1(重启兜底) | iLiOQl.qnfE |
前台服务挂掉后 4-6s 内 | minLatency=3999ms, overrideDeadline=5999ms, network=NONE, priority=400, persisted=true |
| JobScheduler #2(心跳) | Ql1lqlL.qnfE |
≈ 24 min | network=NONE, persisted=true, priority=400;onStartJob 内自我重排 |
| WorkManager PeriodicWork | Worker IqHBn.iLiOQl |
18 min | uniqueName = "4LwP"("day"),Worker 里只上报事件后返回 Retry() |
注意:AlarmManager 使用 set()(非 setExactAndAllowWhileIdle),Doze 下会被系统合并;作者依赖的是 前台 Service 常驻 + 多路互备 而非严格计时。
4.4 前台服务行为#
startForeground(id=2084960406, notification)(C1448b.i(...)@C1448b.java:317-327)。- 通知:
priority=IMPORTANCE_HIGH,ongoing=true,showWhen=false, 自定义RemoteViews。 - 反监管小把戏:
notification.when = now + 365 天(C1448b.b()),规避某些 ROM 折叠常驻通知。 - MIUI 特判:
getprop ro.miui.ui.version.name检测 MIUI,用headsUpContentView单独配置。 - 通知渠道:
xb.g.f46293c/f46294d/f46295e(前台常驻 + 弹性 + heads-up 三档)。 specialUse+subtype="quick entry":作者无对应快捷操作面板功能,纯粹是为了满足 Android 14+FOREGROUND_SERVICE_SPECIAL_USE的强制声明要求 —— 明显与 Google PlayspecialUse政策原意不符,Play Console 高概率判违规。
4.5 FCM 是否被用作远程唤醒#
是。 iqQiiOL/qnfE.java:17-30:
String string = bundle.getString("google.delivered_priority");
if (Constants.HIGH.equals(string)) {
AbstractC1449c.d(this); // ← 拉起前台服务
((A.Q) cVar.a().f35921a).K(Db.b.f2871b); // "wake" 事件上报
}
- 仅
delivered_priority == "high"的 push 会触发拉起。 AbstractC1449c.a()里还有 topic 订阅逻辑,加密 topic 字符串p7.u0.D("0bkJ"),指数退避(30s→24h)保证 FCM 通道稳定。这意味着服务端可通过对该 topic 广播 high-priority push 来唤醒所有装机。
4.6 广告 SDK 的隐性保活贡献#
| SDK | 组件 | 贡献 |
|---|---|---|
| Mintegral (Tramini) | com.tramini.plugin.api.TraminiContentProvider (initOrder=100) |
Provider.onCreate 起单独线程 抢在 Application.onCreate 之前完成初始化,绕开主初始化流程 |
| Adjust | com.adjust.sdk.SystemLifecycleContentProvider |
Provider.onCreate 抢跑 registerActivityLifecycleCallbacks;主初始化调 enableSendingInBackground()(x8/C5732f.java:451) |
| Mintegral / mbridge / thinkup / Vungle / InMobi / Pangle | 动态注册 SCREEN_ON / SCREEN_OFF / USER_PRESENT / CONNECTIVITY_CHANGE Receiver |
屏幕状态 / 网络变化都会唤醒进程 |
| Mintegral / Thinkup | PlatformScheduler(含 JobInfo.Builder) |
各广告模块调用即 schedule 后台 job |
总结:三方 SDK 提供的都是"顺势保活",真正主动保活的关键武器全在应用自研的 a/、Db/、xb/、E0/ 包和 qnfE 混淆类里。
4.7 灰产 / 激进保活手法评估#
| 手法 | 是否使用 |
|---|---|
| 前台 Service + 常驻通知 | ✅ 滥用 foregroundServiceType="specialUse" + subtype "quick entry" |
通知 when 时间戳造假(+365 天) |
✅ 规避 ROM 折叠,MIUI 特判走 heads-up |
| Service ⇄ JobService 互拉 | ✅ AbstractC1449c.b/d 双向兜底 |
| 手写周期 JobService(绕开 15min 限制) | ✅ Ql1lqlL.qnfE 自我重排 |
| AlarmManager 长周期心跳 | ✅ 8 小时兜底 |
| BOOT / MY_PACKAGE_REPLACED / QUICKBOOT_POWERON | ✅ |
| FCM 高优先级远程唤醒 | ✅ |
| WorkManager PeriodicWork | ✅ 18 min(主要用于上报,间接保活) |
| Home / 最近任务键动态监听 | ✅ 动态注册 a.C1450d |
| Activity onResume 反补拉活 | ✅ |
| 网络变化拉活 | ✅ S8.C1059k 动态 Receiver |
| ContentProvider 抢跑 | ✅ Mintegral Tramini + Adjust |
| PACKAGE_USAGE_STATS 权限声明 | ✅(代码中未见主动调用 UsageStatsManager) |
| 1 像素 Activity | ❌ |
| 双进程 / 多 process 守护 | ❌(无自定义 android:process=) |
| Native fork 守护 | ❌ |
| Accessibility 服务保活 | ❌ |
综合评级:中偏高的灰产保活套路。 未用 1-pixel、双进程、native fork 这类"重型武器"(避免触发 Google Play 严查),但把 Android 官方允许机制(前台服务 + JobService + Alarm + FCM + Provider)多路并行 + 互备闭环,另外滥用 specialUse 前台服务类型。字符串加密(p7.u0.D("..."))和类名混淆(qnfE / iIL0OiOQ 等)显示作者刻意规避静态扫描。
4.8 关键代码片段#
A. Service onTaskRemoved 反手拉自己(iIL0OiOQ/qnfE.java:63-70)
public final void onTaskRemoved(Intent intent) {
super.onTaskRemoved(intent);
AbstractC1449c.f15192a = false;
AbstractC1449c.d(getApplicationContext()); // 反手再拉起自己
}
B. JobService onCreate 立即启动前台服务(iLiOQl/qnfE.java:15-30)
if (!AbstractC1449c.f15192a) {
Intent intent = new Intent(this, iIL0OiOQ.qnfE.class);
if (Build.VERSION.SDK_INT >= 26) startForegroundService(intent);
else startService(intent);
}
C. 24 min 心跳 JobService(自我重排)(a/AbstractC1449c.java:126-144)
JobInfo.Builder builder = new JobInfo.Builder(hashCode,
new ComponentName(ctx.getPackageName(), Ql1lqlL.qnfE.class.getName()));
builder.setOverrideDeadline(1449999L);
builder.setPriority(400);
builder.setMinimumLatency(1446000L); // ≈ 24 min
jobScheduler.schedule(builder.build());
D. 8 小时 AlarmManager(a/AbstractC1449c.java:148-162)
Intent intent = new Intent(context, lefrHZ9.qnfE.class);
PendingIntent broadcast = PendingIntent.getBroadcast(context, 15254, intent, flags);
alarmManager.cancel(broadcast);
alarmManager.set(2, SystemClock.elapsedRealtime() + 28799365, broadcast); // 8h
E. Home / 最近任务键动态监听(E0/c.java:173-177)
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("android.intent.action.CLOSE_SYSTEM_DIALOGS");
z1.AbstractC5885a.registerReceiver(application, new a.C1450d(), intentFilter, RECEIVER_EXPORTED);
F. WorkManager 18 分钟 unique periodic(E0/c.java:141-168)
U3.x xVar = new U3.x(i10, IqHBn.iLiOQl.class);
long millis = TimeUnit.MINUTES.toMillis(18L);
h5.AbstractC4562b.F(wVar, "enqueueUniquePeriodic_" + D12, u3, new V3.v(...));
五、总体风险评估与结论#
5.1 风险画像#
从代码证据看,这是一款 以"文件恢复+清理"为壳、以通知栏拉活为核心运营手段的灰产 App:
- 通知系统同时命中:
- 骚扰级本地推送(灰度组默认 2 min/条、日上限 99、无广告上限、素材复用 3 次)
- 前台常驻通知(IMPORTANCE_MAX +
when=+365天+deleteIntent反打) - FCM 远程"精准"唤醒 + 通知投放
- NotificationListener 反向劫持竞品通知
- 保活链具备完整灰产拓扑(前台服务 + 2 JobService + 8h Alarm + FCM + BOOT + Home 键 + Activity 回前台 + 网络变化 + 广告 Provider 抢跑),任一环存活即可再拉全部。
- 广告变现极度密集:AdMob / Meta / Pangle / Mintegral / InMobi / Vungle / ThinkUp / Adjust 全家桶,几乎所有可展示广告的场景都被规划出插屏/原生位(甚至权限管理界面退出都插屏)。
- 合规粉饰:
com.pairip.application.Application+LicenseClient.checkLicense走 Google Play Licensing 加固,字符串 XOR 加密(p7.u0.D("..."))、类名混淆(qnfE/iIL0OiOQ等)刻意规避静态扫描。 - 未采用重型武器:1-pixel Activity、双进程、native fork —— 这些会触发 Play 严查,说明作者对 Google Play 政策边界有清晰认识。
5.2 Google Play 政策风险#
FOREGROUND_SERVICE_SPECIAL_USEsubtype"quick entry"与实际保活用途不符,违反 Google Play Foreground Service 政策。POST_NOTIFICATIONS双重声明 + 灰度组骚扰量级通知参数,违反用户体验政策。PACKAGE_USAGE_STATS权限声明缺少实际功能对应。- NotificationListenerService 对非白名单包主动
cancelNotification是明显的通知劫持,违反通知权限使用条款。
5.3 潜在的其他保活方式(应用未使用但业界常见的)#
作为分析补充,以下是 Android 生态里常见但本应用未采用的保活手段:
- 1-pixel Activity:屏幕关闭时用 1×1 透明 Activity 占位保持前台状态。
- 双进程 / 多进程守护:通过
android:process=":daemon"拆分子进程,两个进程互相监视对方存活并拉起。 - Native fork 守护进程:JNI + fork 出脱离 JVM 的守护进程。
- AccessibilityService 挂住系统:无障碍服务在系统层持续运行,很难被杀。
- SYSTEM_ALERT_WINDOW / 悬浮窗:借悬浮窗维持前台可见性。
- Wallpaper Service:注册为动态壁纸,与桌面进程一起活。
- VPN Service:申请 VPN 权限后开个空 VpnService 保活。
- 利用系统账号 SyncAdapter:注册 ContentProvider + Account + SyncAdapter 强制定期同步。
- INSTANT_RUN / DEX 加载多进程共享:老套路,通过多 DEX 分包制造额外进程。
- 短信/电话广播反射:老年套路,收到短信/来电时被系统临时拉起。
- JobIntentService(Android 8 前):借系统调度维持后台。
- KeepAlive 系列黑科技(如网易云的双 Service 互 startForeground 传通知 id 的技巧)。
作者显然懂这些,也刻意避开 —— 是有意识规避 Play 严打的灰产团队。
5.4 建议#
如果这是作为竞品分析:可参考其 Firebase Remote Config 驱动的骚扰节奏调控设计,但不建议照搬其 notification.when=+365天、deleteIntent→ForegroundService、NotificationListener 劫持等做法(法律/合规风险 + 用户体验 + Google Play 政策全部踩线)。
如果是作为安全/合规调查:本 APK 明确符合灰产广告变现型应用特征,Play Console 政策审查上处于高风险;用户端可通过 "限制后台活动 + 关闭通知权限 + 卸载" 三步降低骚扰。
附:分析产物文件位置#
- 反编译源码:
/Users/sandy/.qoderwork/workspace/mrikv3zxxlkamrep/apk_analysis/decompiled/ - AndroidManifest.xml(已解码):
apk_analysis/manifest_extracted/resources/AndroidManifest.xml - XAPK 原文件:
apk_analysis/com.files.photo.recovery.restore.pro.xapk - 主 APK:
apk_analysis/xapk_extracted/com.files.photo.recovery.restore.pro.apk
本报告基于 2026-07-13 从 Google Play 抓取的 V1.0.2(versionCode=3)APK 静态反编译分析,不涉及运行时动态行为观测。远程配置(Firebase Remote Config)内容可能被开发者动态调整;上述灰度组参数为反编译时的兜底 JSON 硬编码内容。