原始逆向报告
原始逆向报告返回统一总览

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 AdMobca-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 应用入口链#

  • Applicationcom.pairip.application.Application extends com.tools.photo.recovery.AppattachBaseContext 先跑 LicenseClient.checkLicense(context)(Google Play 加固)。
  • 首个 ActivityLauncherActivityandroid: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_HIGH AdRecall 频道 + 自定义 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=routerFLAG_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 通知系统灰产味道评估#

按危害度递减:

  1. notification.when = now + 365 天 —— 永远排到通知栏最上方。
  2. deleteIntent → getForegroundService(iIL0OiOQ.qnfE) —— 划掉通知反而重启前台服务。
  3. iIL0OiOQ.qnfE.onTaskRemoved —— 用户从最近任务划掉 App,服务立即复活。
  4. 4 秒 heads-up 二次弹窗xb.i.f46310a.postDelayed(4000ms) 后重推更高级别频道 xb.g.f46295e,制造抖屏。
  5. 小米专供旧版高优先级Build.BRAND=="Xiaomi" 且 SDK<35 时把 importance 打到 5(旧 IMPORTANCE_MAX),钻 MIUI 策略空子。
  6. 伪计数徽章:"距离上次恢复 X 天"、"清理 Y 天" + 高饱和红粉色 + 2.28× 字号 —— 制造"未处理危险"错觉。
  7. AdRecall 频道 IMPORTANCE_HIGH + ad_ntf.upper_limit=-1 —— 广告类通知披上"重要提醒"外衣,且无日上限。
  8. 权限功能商业化NotificationManagementActivity 退出触发插屏。
  9. NotificationListener 反向利用:非白名单包被主动 cancelNotification,实质抢占其他 App 通知曝光。
  10. 点击 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.f35733apublic 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) CleanActivityfileSource=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 常驻前台 ServicespecialUse / quick entry onStartCommandC1448b.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;onMessageReceivedgoogle.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=400onStartJob 内自我重排
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 Play specialUse 政策原意不符,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 小时 AlarmManagera/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 periodicE0/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

  1. 通知系统同时命中:
    • 骚扰级本地推送(灰度组默认 2 min/条、日上限 99、无广告上限、素材复用 3 次)
    • 前台常驻通知(IMPORTANCE_MAX + when=+365天+ deleteIntent 反打)
    • FCM 远程"精准"唤醒 + 通知投放
    • NotificationListener 反向劫持竞品通知
  2. 保活链具备完整灰产拓扑(前台服务 + 2 JobService + 8h Alarm + FCM + BOOT + Home 键 + Activity 回前台 + 网络变化 + 广告 Provider 抢跑),任一环存活即可再拉全部。
  3. 广告变现极度密集:AdMob / Meta / Pangle / Mintegral / InMobi / Vungle / ThinkUp / Adjust 全家桶,几乎所有可展示广告的场景都被规划出插屏/原生位(甚至权限管理界面退出都插屏)。
  4. 合规粉饰com.pairip.application.Application + LicenseClient.checkLicense 走 Google Play Licensing 加固,字符串 XOR 加密(p7.u0.D("..."))、类名混淆(qnfE / iIL0OiOQ 等)刻意规避静态扫描。
  5. 未采用重型武器:1-pixel Activity、双进程、native fork —— 这些会触发 Play 严查,说明作者对 Google Play 政策边界有清晰认识。

5.2 Google Play 政策风险#

  • FOREGROUND_SERVICE_SPECIAL_USE subtype "quick entry" 与实际保活用途不符,违反 Google Play Foreground Service 政策
  • POST_NOTIFICATIONS 双重声明 + 灰度组骚扰量级通知参数,违反用户体验政策。
  • PACKAGE_USAGE_STATS 权限声明缺少实际功能对应。
  • NotificationListenerService 对非白名单包主动 cancelNotification 是明显的通知劫持,违反通知权限使用条款。

5.3 潜在的其他保活方式(应用未使用但业界常见的)#

作为分析补充,以下是 Android 生态里常见但本应用未采用的保活手段:

  1. 1-pixel Activity:屏幕关闭时用 1×1 透明 Activity 占位保持前台状态。
  2. 双进程 / 多进程守护:通过 android:process=":daemon" 拆分子进程,两个进程互相监视对方存活并拉起。
  3. Native fork 守护进程:JNI + fork 出脱离 JVM 的守护进程。
  4. AccessibilityService 挂住系统:无障碍服务在系统层持续运行,很难被杀。
  5. SYSTEM_ALERT_WINDOW / 悬浮窗:借悬浮窗维持前台可见性。
  6. Wallpaper Service:注册为动态壁纸,与桌面进程一起活。
  7. VPN Service:申请 VPN 权限后开个空 VpnService 保活。
  8. 利用系统账号 SyncAdapter:注册 ContentProvider + Account + SyncAdapter 强制定期同步。
  9. INSTANT_RUN / DEX 加载多进程共享:老套路,通过多 DEX 分包制造额外进程。
  10. 短信/电话广播反射:老年套路,收到短信/来电时被系统临时拉起。
  11. JobIntentService(Android 8 前):借系统调度维持后台。
  12. KeepAlive 系列黑科技(如网易云的双 Service 互 startForeground 传通知 id 的技巧)。

作者显然懂这些,也刻意避开 —— 是有意识规避 Play 严打的灰产团队

5.4 建议#

如果这是作为竞品分析:可参考其 Firebase Remote Config 驱动的骚扰节奏调控设计,但不建议照搬其 notification.when=+365天deleteIntent→ForegroundServiceNotificationListener 劫持等做法(法律/合规风险 + 用户体验 + 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 硬编码内容。

Photo Recovery V1.0.3 竞品逆向分析 · 2026-08-13