乐于分享
好东西不私藏

安卓端某音乐类 APP 逆向分享(三):params 参数加密分析——上

安卓端某音乐类 APP 逆向分享(三):params 参数加密分析——上

脱敏说明:本文涉及的类名、包名、so 库文件名均已脱敏,真实的加密密钥、盐值、前后缀字符串以 **** 代替,不包含任何可直接用于攻击真实服务的敏感信息。本文仅用于逆向工程技术原理的学习与记录。


前言

在上一篇协议分析中,我们确认了歌曲搜索接口的 POST body 中只有一个字段 params,其值是一串十六进制大写密文。本篇(上)将分析 params 的生成全流程,从 Java 层调用链定位出发,一步步深入到 Native 层的 so 文件,最终在静态分析阶段发现加密算法线索(AES + MD5)。

下一篇(下)将继续通过 IDA Pro 动态调试、unidbg 模拟执行和汇编指令流分析,完整还原加密算法的每个细节。


一、Java 层代码分析

1.1 搜索 params 关键字

使用反编译工具 jadx(Java Decompiler X)搜索字符串 "params",结果多达 1286 条,无法直接判断哪一处负责生成 POST body 中的 params 字段。

由于 params 作为参数名一般以 ("params", 的形式被 put() 或 add() 到某个数据结构中,可以改为搜索 ("params", 这个更精确的字符串,大幅减少结果数量:

技巧补充:类似的精确搜索策略在 jadx 中非常实用。如果参数是通过 put 添加到 HashMap 或 Bundle 的,可以直接搜索形如 ("key_name", 的字符串;如果是通过 add 添加到 List,则可结合字段名进行过滤。

1.2 定位拦截器函数

通过上一步的精确搜索,分析得出 params 参数是在以下函数中生成的:

com.example.music.network.retrofit.q.a.intercept

该函数是一个 OkHttp 拦截器(Interceptor)的 intercept 方法,在 HTTP 请求发出前对请求体进行修改,将明文参数替换为加密后的 params 字段:

1.3 定位核心加密函数:serialdata

继续追踪调用链,最终定位到核心函数:

// 核心加密函数,接收请求路径和请求体,返回加密后的 params 字符串
com.example.music.utils.MusicUtils.serialdata(String str1, String str2)

jadx 显示该函数带有 native 关键字修饰,说明其实现在某个动态链接库(.so 文件)中,Java 层通过 JNI 调用它:

为什么用 Native 实现加密:将核心加密逻辑下沉到 Native 层(.so 文件)是 Android 应用常见的安全加固手段。Native 代码以二进制形式存储,比 Java 字节码更难被反编译和理解;同时,可以通过 so 文件加固(加壳、混淆)进一步提升分析难度。

1.4 Frida Hook serialdata 函数

既然已知 serialdata 是产生 params 的关键,可以直接用 Frida Hook 这个函数,观察它接收的参数和返回值,无需立即分析 Native 层:

// Frida 脚本:Hook serialdata 函数,打印入参和返回值
Java.perform(function({
// 获取 MusicUtils 类的引用
var utils = Java.use("com.example.music.utils.MusicUtils");

// 重写 serialdata 方法(指定参数类型以区分重载)
    utils.serialdata.overload("java.lang.String""java.lang.String")
        .implementation = function(str1, str2{
// 先执行原始方法,获取加密结果
var res = this.serialdata(str1, str2);

// 打印入参和结果
console.log("=== serialdata called ===");
console.log("str1 (path):", str1);
console.log("str2 (body):", str2);
console.log("res  (enc) :", res);
console.log("\n");

return res;  // 必须返回原始结果,否则会影响 App 正常运行
        };
});

用 objection 工具同样可以 Hook 该函数,下图展示了 Hook 得到的函数入参和返回值:

参数 1(str1):请求路径

/api/search/song/page

参数 2(str2):请求体 JSON(明文)

{
"offset""0",
"limit""20",
"channel""typing",
"keyword""天空之城",
"rqrefer""[F:63][1667359577935#545#8.8.50#221010200836][e][3][8]...",
"scene""normal",
"header""{}",
"e_r"true
}

返回值:加密后的十六进制字符串(即 params 的值)

74A595527B7A1647174ADDB4F261E92FF2AFA5F42E4694960F9C5A3746841C9B...

1.5 现阶段可行方案:函数服务化

至此,已经可以通过以下几种方式将 serialdata 函数服务化——即封装为 HTTP API,传入路径和请求体,返回加密结果,无需深入理解加密算法本身:

方案
原理
优点
缺点
Frida RPC
在手机上运行 Frida,通过 Python 调用 serialdata
实现简单,调用稳定
依赖真机/模拟器,性能有限
Xposed 模块
编写 Xposed 插件暴露 API
持久化运行,无需额外工具
需要 Root 环境,开发量较大
unidbg
在 PC 上模拟 Android 环境,直接运行 .so 文件
不依赖真机,性能好
需要处理 JNI 环境模拟

本文选择继续深入分析 serialdata 的具体实现,还原完整的加密算法,最终在 Python 中独立复现,彻底摆脱对设备环境的依赖。


二、Native 层代码分析

2.1 定位 serialdata 函数在哪个 .so 文件中实现

2.1.1 通过 loadLibrary 搜索(效果有限)

通常 Java 层使用 System.loadLibrary("xxx") 加载 .so 文件。用 jadx 搜索 loadLibrary,发现结果较多,难以快速定位是哪个库实现了 serialdata

2.1.2 Hook RegisterNatives(精准定位)

RegisterNatives 是 JNI 中的关键函数,其作用是将 Java 层声明的 native 方法与 Native 层的实现函数进行绑定。调用格式如下:

RegisterNatives(JNIEnv*, jclass clazz, const JNINativeMethod* methods, jint nMethods)

其中 methods 数组的每个元素包含三个字段:name(方法名)、signature(签名)、fnPtr(函数指针)。通过 Hook 这个函数,可以读出每个 native 方法对应的函数指针,再通过 Process.findModuleByAddress 确定其所在 .so 文件和相对偏移。

以下是基于 Frida 实现的 Hook RegisterNatives 脚本:

// 在 libart.so 中查找 ART 的 RegisterNatives 函数地址
functionfind_RegisterNatives({
// 枚举 libart.so 的所有符号,找到 RegisterNatives
let symbols = Module.enumerateSymbolsSync("libart.so");
let addrRegisterNatives = null;

for (let i = 0; i < symbols.length; i++) {
let symbol = symbols[i];
// 匹配条件:包含 art + JNI + RegisterNatives,排除 CheckJNI 变体
if (symbol.name.indexOf("art") >= 0 &&
            symbol.name.indexOf("JNI") >= 0 &&
            symbol.name.indexOf("RegisterNatives") >= 0 &&
            symbol.name.indexOf("CheckJNI") < 0) {
            addrRegisterNatives = symbol.address;
console.log("RegisterNatives found at:", symbol.address, symbol.name);
            hook_RegisterNatives(addrRegisterNatives);
        }
    }
}

// Hook RegisterNatives,打印每个注册的 native 方法信息
functionhook_RegisterNatives(addrRegisterNatives{
if (addrRegisterNatives == nullreturn;

    Interceptor.attach(addrRegisterNatives, {
onEnterfunction(args{
let java_class = args[1];
let methods_ptr = ptr(args[2]);
let method_count = parseInt(args[3]);

// 获取 Java 类名
let class_name = Java.vm.tryGetEnv().getClassName(java_class);
console.log("[RegisterNatives] class:", class_name, "method_count:", method_count);

// 遍历所有注册的方法,每条记录 3 个指针:name / signature / fnPtr
for (let i = 0; i < method_count; i++) {
let name_ptr = Memory.readPointer(methods_ptr.add(i * 3 * Process.pointerSize));
let sig_ptr  = Memory.readPointer(methods_ptr.add((i * 3 + 1) * Process.pointerSize));
let fnPtr    = Memory.readPointer(methods_ptr.add((i * 3 + 2) * Process.pointerSize));

let name   = Memory.readCString(name_ptr);
let sig    = Memory.readCString(sig_ptr);
// 通过函数指针找到所属 .so 文件及基址
letmodule = Process.findModuleByAddress(fnPtr);

console.log(
" name:", name,
"sig:", sig,
"fnPtr:", fnPtr,
"module:"module.name,
"base:"module.base,
"offset:", ptr(fnPtr).sub(module.base)
                );
            }
        }
    });
}

setImmediate(find_RegisterNatives);

执行脚本:

frida -U -l hook_RegisterNatives.js -f com.example.music --no-pause

输出结果如下图所示:

关键信息

  • serialdata 注册在 libmusic_core.so 中,偏移地址为 0x4e041
  • deserialdata(响应解密函数)同样在 libmusic_core.so 中,偏移地址为 0x4e125

记住 0x4e041 这个偏移地址,后面分析 so 文件时会用到。


2.2 libmusic_core.so 文件修复

2.2.1 直接用 IDA Pro 打开——失败

用 IDA Pro 直接打开 APK 包中的 libmusic_core.so 文件,提示文件已损坏,无法正常加载:

这说明 App 对该 so 文件进行了加固处理,在 APK 打包时人为破坏了文件头(ELF magic number)或修改了某些关键字段,使静态分析工具无法正常解析。而在 App 运行时,系统会在加载前动态修复这个文件,因此不影响实际运行。

常见的 so 加固手段:修改/清空 ELF 文件头的 magic number(\x7fELF)、破坏 Section Header Table、加密函数体、使用 .init_array 在运行时自解密等。

解决方案:从运行中的进程内存中 dump 出已被系统修复的 so 文件。

2.2.2 查看进程 ID 和内存映射

首先查看目标 App 的进程 ID:

读取该进程的 /proc/<pid>/maps 文件,找到 libmusic_core.so 在内存中的起始和结束地址,例如 0xbf942000 到 0xbf9da000

2.2.3 Frida + Python 内存 Dump 脚本

以下脚本通过 Frida 的 Memory.protect 获取 so 文件的读权限,再用 readByteArray 读出完整内存,写入本地文件:

import frida
import sys


defon_message(message, data):
"""Frida 消息回调(本脚本不需要处理消息)"""
pass


deffrida_script():
"""返回注入到目标进程的 Frida JS 脚本字符串"""
return"""
rpc.exports = {
    // 查找模块信息(基址 + 大小)
    findmodule: function(so_name) {
        return Process.findModuleByName(so_name);
    },
    // 从内存中读出整个 .so 文件
    dumpmodule: function(so_name) {
        var libso = Process.findModuleByName(so_name);
        if (libso == null) {
            return -1;  // 模块未加载
        }
        // 先将内存页权限改为可读可写可执行,确保 readByteArray 不会权限报错
        Memory.protect(ptr(libso.base), libso.size, 'rwx');
        // 读出整个模块内容作为 ArrayBuffer 返回
        var libso_buffer = ptr(libso.base).readByteArray(libso.size);
        return libso_buffer;
    },
};
"""



defdump(so_name: str) -> None:
"""
    连接前台应用,通过 Frida 从内存中 dump 指定 .so 文件
    :param so_name: 目标 .so 文件名(不含路径),如 "libmusic_core.so"
    """

    device = frida.get_usb_device()
    pid = device.get_frontmost_application().pid  # 获取前台应用 PID
    session = device.attach(pid)
    script = session.create_script(frida_script())
    script.on('message', on_message)
    script.load()

# 获取模块基址和大小
    module_info = script.exports.findmodule(so_name)
    base = module_info["base"]
    size = module_info["size"]
    print(f"base: {base}  size: {size}")

# 从内存读出 so 内容并写入文件
    module_buffer = script.exports.dumpmodule(so_name)
if module_buffer != -1:
        dump_so_name = so_name + ".dump.so"
with open(dump_so_name, "wb"as f:
            f.write(module_buffer)
        print(f"Dumped to: {dump_so_name}")
else:
        print("Error: module not found in memory")


if __name__ == "__main__":
    dump("libmusic_core.so")

执行后打印出 base: 0xbf942000, size: 622592,与 maps 文件中的内存区间完全对应。导出文件命名为 libmusic_core.so.dump.so

2.2.4 使用 SoFixer 修复文件头

用 IDA Pro 尝试打开 libmusic_core.so.dump.so,发现仍然是纯二进制内容,无法以 ELF 格式解析。这说明内存 dump 出的文件的 ELF 文件头仍然是被篡改过的状态。

使用开源工具 SoFixer 修复文件头,将文件和工具推送到手机:

adb push libmusic_core.so.dump.so /data/local/tmp
adb push SoFixer /data/local/tmp

在手机上执行修复命令(-m 参数指定内存基址):

./SoFixer -m 0xbf942000 -s ./libmusic_core.so.dump.so -o ./libmusic_core.so.fix.so

SoFixer 报错:文件头中的 ELF 魔数(magic number)无效,文件头损坏:

手动修复魔数:使用 010 Editor 打开原始 APK 包中的 libmusic_core.so,将其文件头的前 64 字节(ELF Header)复制,粘贴覆盖到 libmusic_core.so.dump.so 的文件头位置:

ELF 文件头结构简介:标准 ELF 文件的前 16 字节为 e_ident,其中前 4 字节必须是 \x7fELF(魔数)。加固处理通常将这 4 字节改为无效值,使 IDA 等工具拒绝加载。将正确的文件头覆盖后,SoFixer 可以利用运行时地址信息重建 Section Header Table。

修复魔数后再次执行 SoFixer,成功导出 libmusic_core.so.fix.so


2.3 静态分析 libmusic_core.so.fix.so

2.3.1 偏移 0x4e041 处无法反编译

用 IDA Pro 跳转到偏移 0x4e041(即 serialdata 函数入口),发现该函数无法被 IDA 的 F5 伪代码功能反编译,只能看到原始汇编指令:

img

这说明该函数的代码段可能经过了混淆反反编译处理(如使用不规则的 THUMB/ARM 混合指令、插入无效指令序列、修改调用约定等),增加了静态分析的难度。

2.3.2 发现 AES 和 MD5 函数声明

虽然 serialdata 函数本身难以直接分析,但通过 IDA Pro 的 Exports 窗口(导出函数列表),可以看到该 so 文件声明了多个明文可见的加密函数:

关键函数及其偏移地址:

函数名
偏移地址
说明
MD5_Init 0x0000BA0C
MD5 上下文初始化
MD5_Update 0x0000C688
MD5 数据输入
MD5_Final 0x0000C7A0
MD5 最终计算,输出 16 字节摘要
MD5_Transform 0x0000C798
MD5 核心压缩函数
md5_block_data_order 0x0000BA58
MD5 分组数据处理
AES_set_encrypt_key 0x0000C96C
AES 加密密钥扩展
AES_encrypt 0x0000CEC0
AES 单块加密(ECB 模式)
AES_set_decrypt_key 0x0000C970
AES 解密密钥扩展
AES_decrypt 0x0000DA00
AES 单块解密
private_AES_set_encrypt_key 0x0000D100
AES 内部密钥扩展实现

这些函数名直接以明文形式暴露在导出表中,强烈暗示 serialdata 的加密逻辑涉及 AES 和 MD5

2.3.3 加密算法特征魔数识别

当 so 文件经过深度混淆、函数名无法看到时,可以通过特征魔数(Magic Constants)识别使用了哪种加密算法:

MD5 特征魔数(初始化向量,固定值):

0x67452301  0xefcdab89  0x98badcfe  0x10325476
0xd76aa478  0xe8c7b756  ...(T 表常量)

AES 特征魔数(S-Box 相关):

0xd66b6bbd  0xde6f6fb1  0x91c5c554  0x60303050  0x2010103  ...

在 IDA 中使用 Search → Immediate Value 搜索这些魔数,即便函数名已被混淆,也能八九不离十地识别出所使用的加密算法。这是逆向工程中定位加密逻辑的通用技巧。


本篇小结

分析阶段
结论
Java 层搜索
params

 由拦截器 q.a.intercept 生成,最终调用 MusicUtils.serialdata(path, body)
Hook 函数入参
str1=请求路径,str2=请求体 JSON,返回值=十六进制密文
RegisterNatives 定位
serialdata

 在 libmusic_core.so 中,偏移 0x4e041
so 文件修复
ELF 文件头被篡改,通过内存 dump + 010 Editor 手动修复魔数 + SoFixer 重建结构
静态分析线索
导出表包含明文的 AES、MD5 函数声明,确认加密算法为 AES + MD5 组合

下一篇将通过 IDA Pro 动态调试unidbg 模拟执行 和 汇编指令流逐步追踪,还原 AES 加密的明文组成方式、密钥、以及 MD5 的输入内容,最终给出可独立运行的 Python 加密还原实现。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 安卓端某音乐类 APP 逆向分享(三):params 参数加密分析——上

评论 抢沙发

7 + 8 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮