乐于分享
好东西不私藏

安卓端某音乐类 APP 逆向分享(五):NMDI 参数加密分析

安卓端某音乐类 APP 逆向分享(五):NMDI 参数加密分析

NMDI 参数的加密分析是本系列逆向过程中技术难度最高的环节。NMDI 由一个名为 caesarson 的自研加密算法生成,本文将从 Java 层调用链溯源出发,经由 Frida Hook、IDA 静态分析、unidbg 动态追踪,最终完整还原 caesarson 加密算法的 Python 实现。


一、参数定位

使用 Jadx 搜索关键字 NMDI,可定位到 NMDI 参数的生成位置位于:

com.example.music.core.security.c.intercept

其中第 22 行将变量 c 赋值给 NMDI,c 由第 17 行调用 b.c() 函数返回。

继续追踪 b.c() 函数调用链,最终可追溯到 CaesarsonCryptor.encrypt 函数。该函数在 Java 层仅有声明,标注为 native,其真正的实现位于 Native 库 libcaesarson.so 中。

Frida Hook 验证

使用 Frida Hook CaesarsonCryptor.encrypt 函数,观察传入参数及返回值。

注意:需要在首次启动 APP(或清除现有数据后)以 spawn 模式执行 Hook,确保在加密初始化之前介入。

// hook_caesarson.jsJava.perform(function ({var CaesarsonCryptor = Java.use("com.example.music.crypto.caesarson.CaesarsonCryptor"    );    CaesarsonCryptor.encrypt.overload("java.lang.String").implementation = function (str{var res = this.encrypt(str);console.log("[caesarson] param  : " + str);console.log("[caesarson] result : " + res);return res;    };});

以 spawn 模式运行:

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

Hook 输出如下:

入参(JSON 对象):

{"ie":"[MASKED_IMEI]","mc":"[MASKED_MAC]","ydid":"[MASKED_YDID]"}

返回值(Base64 密文):

Q1NKTQkBDAD7j0c5Ehyf9tpVp/trAAAA9EOlfKiR9O8qT7ldbUCgy0d0934KFsERECA4NUn8KHTI9IEOc3QXlpIMz8tWLrlVsRA3JACQsNksXZxW+w/7K4pJinJv2JRRctdr84bNQKbIHo+DXnkbVrCF1ll78Rs9q045NwID7Y+hWik=

二、参数分析及伪造

传入 CaesarsonCryptor.encrypt 的 JSON 参数包含三个字段:

字段
含义
取值来源
ie
设备 IMEI 串号
真实设备值,需按规则伪造
mc
设备 MAC 地址
真实设备值,需按规则伪造
ydid
疑似设备 ID
暂时随机生成 MD5 值

对于爬虫场景,真实设备数量有限,需要按照字段的内在约束规则伪造合法值。

2.1 IMEI 串号伪造

IMEI(国际移动设备识别码)由 15 位数字组成,其结构如下:

位数
含义
伪造策略
前 8 位
TAC(类型分配码),标识品牌和型号
取自真机,不修改
9-14 位
序列号
随机生成 6 位数字
第 15 位
校验码
由前 14 位经 Luhn 算法 计算得出
import randomdefrandom_imei(imei: str) -> str:"""    基于真实 IMEI 伪造新 IMEI。    保留前 8 位 TAC,随机化序列号(9-14位),重新计算 Luhn 校验码。    :param imei: 真实 IMEI(15位)    :return: 伪造的合法 IMEI    """# 随机化序列号部分(第9-14位)    imei = imei[:8] + str(random.randint(01_000_000)).zfill(6)# 计算 Luhn 校验码    checksum = 0for n in range(14):        digit = int(imei[n])if n % 2 == 1:# 奇数位(0-indexed):乘2后十位与个位相加            checksum += (digit * 2) % 10 + (digit * 2) // 10else:            checksum += digit    remainder = checksum % 10    imei += str(0if remainder == 0else10 - remainder)return imei

2.2 MAC 地址伪造

MAC 地址由 6 字节(48 位) 组成,通常以冒号分隔的十六进制表示,如 xx:xx:xx:xx:xx:xx

字节范围
含义
伪造策略
前 3 字节(OUI)
制造商编码,由 IEEE 分配
取自真机,不修改
后 3 字节
产品序列号
随机生成 3 字节
import randomdefrandom_mac(mac: str) -> str:"""    基于真实 MAC 地址伪造新 MAC 地址。    保留前 3 字节 OUI,随机化后 3 字节序列号。    :param mac: 真实 MAC 地址,格式 'xx:xx:xx:xx:xx:xx'    :return: 伪造的 MAC 地址    """    hex_chars = "0123456789abcdef"    random_parts = ["".join(random.choices(hex_chars, k=2))for _ in range(3)    ]return mac[:8] + ":".join(random_parts)

2.3 ydid 伪造

ydid 字段疑似为某种设备指纹 ID,具体生成逻辑尚未深入分析。目前策略是随机生成一个 32 位十六进制字符串(MD5 格式),后续如有需要再进行函数级逆向分析。

import uuiddefrandom_ydid() -> str:"""随机生成 ydid,格式为 32 位十六进制字符串(不含连字符)。"""return uuid.uuid4().hex

三、NMDI 加密分析

3.1 IDA 静态代码分析

使用 IDA Pro 打开 libcaesarson.so,在导出函数窗口中找到加密函数入口:

定位到 JNI 导出函数 Java_com_example_music_crypto_caesarson_CaesarsonCryptor_native_1encrypt

继续追踪内部调用链:CaesarsonCryptor::encryptAsBase64 ↓

→ CaesarsonCryptorImpl::encrypt ↓

→ sub_6630(内部核心加密函数,逻辑较复杂,静态分析难以直接理解):

静态分析到 sub_6630 后已难以直接读懂逻辑,下一步改用 unidbg 动态追踪


3.2 unidbg 调用加密函数

使用 unidbg 加载 libcaesarson.so,直接调用 JNI 导出函数,同时开启汇编指令流追踪(traceCode),将执行过程输出到文件供后续分析。

package every.app.unidbg;import com.github.unidbg.AndroidEmulator;import com.github.unidbg.Emulator;import com.github.unidbg.Module;import com.github.unidbg.hook.hookzz.*;import com.github.unidbg.linux.android.AndroidEmulatorBuilder;import com.github.unidbg.linux.android.AndroidResolver;import com.github.unidbg.linux.android.dvm.*;import com.github.unidbg.memory.Memory;import com.github.unidbg.utils.Inspector;import com.sun.jna.Pointer;import java.io.File;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.PrintStream;import java.util.Arrays;import java.util.List;// 继承 AbstractJni 处理 JNI 回调publicclassMusicAppextendsAbstractJni{privatefinal VM vm;privatefinal Module module;privatefinal AndroidEmulator emulator;privatefinal String traceFile = "unidbg-workspace/src/main/resources/musicapp/trace.txt";    MusicApp() throws FileNotFoundException {// 创建 32 位 ARM 模拟器,进程名与真实 APP 保持一致以规避进程名检测        emulator = AndroidEmulatorBuilder                .for32Bit()                .setProcessName("com.example.music")                .build();final Memory memory = emulator.getMemory();        memory.setLibraryResolver(new AndroidResolver(23));// 加载 APK 以辅助完成签名等校验        vm = emulator.createDalvikVM(new File("unidbg-workspace/src/main/resources/musicapp/musicapp-8.8.50.apk")        );// 加载目标 SO,true 表示调用 JNI_OnLoad        DalvikModule dm = vm.loadLibrary("caesarson"true);module = dm.getModule();// 开启汇编指令流追踪,重定向到文件        PrintStream traceStream = new PrintStream(new FileOutputStream(traceFile), true);        emulator.traceCode(module.base, module.base + module.size).setRedirect(traceStream);        vm.setJni(this);        vm.setVerbose(true);        dm.callJNI_OnLoad(emulator);    }// 处理 JNI setIntField 回调@OverridepublicvoidsetIntField(BaseVM vm, DvmObject<?> dvmObject, String signature, int value){if ("com/example/music/crypto/caesarson/ErrorObject->errorCode:I".equals(signature)) {// 忽略错误码写入        }    }// 处理 JNI setObjectField 回调@OverridepublicvoidsetObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature, DvmObject<?> value){if ("com/example/music/crypto/caesarson/ErrorObject->message:Ljava/lang/String".equals(signature)) {// 忽略错误信息写入        }    }/** 调用 native encrypt 函数 */public String native_encrypt(String str){        DvmClass ErrorObject = vm.resolveClass("com/example/music/crypto/caesarson/ErrorObject");        List<Object> list = Arrays.asList(                vm.getJNIEnv(), 0,                vm.addLocalObject(new StringObject(vm, str)),                vm.addLocalObject(ErrorObject.newObject(null))        );        Number number = module.callFunction(                emulator,"Java_com_example_music_crypto_caesarson_CaesarsonCryptor_native_1encrypt",                list.toArray()        );return vm.getObject(number.intValue()).getValue().toString();    }/** 调用 native init 函数(传入 Base64 编码的初始化密钥) */publicvoidinit(String str){        List<Object> list = Arrays.asList(                vm.getJNIEnv(), 0,                vm.addLocalObject(new StringObject(vm, str))        );module.callFunction(                emulator,"Java_com_example_music_crypto_caesarson_CaesarsonCryptor_native_1init",                list.toArray()        );    }/**     * Hook sub_6B14 函数(核心加密子函数),     * 打印入参与出参内存内容,辅助分析数据流。     */publicvoidhookSub6B14(){        IHookZz hookZz = HookZz.getInstance(emulator);        hookZz.wrap(module.base + 0x6B14 + 1new WrapCallback<HookZzArm32RegisterContext>() {@OverridepublicvoidpreCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info){                Pointer input1 = ctx.getPointerArg(0);                Pointer input2 = ctx.getPointerArg(1);                Inspector.inspect(input1.getByteArray(00x150), "6B14 arg1");                Inspector.inspect(input2.getByteArray(00x150), "6B14 arg2");            }@OverridepublicvoidpostCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info){                Inspector.inspect(ctx.getR4Pointer().getByteArray(0300), "6B14-R4 output");            }        });    }publicstaticvoidmain(String[] args)throws FileNotFoundException {        MusicApp app = new MusicApp();        app.hookSub6B14();// 传入 Base64 编码的初始化参数(脱敏)        app.init("Ce****==");// 使用脱敏后的设备信息调用加密        String result = app.native_encrypt("{\"ie\":\"[MASKED_IMEI]\",\"mc\":\"[MASKED_MAC]\",\"ydid\":\"[MASKED_YDID]\"}"        );        System.out.println(result);    }}

unidbg 验证结果

调用 native_encrypt 后,返回的 Base64 密文经解码后转为字节序列:

b'\x43\x53\x4a\x4d\x09\x01\x0c\x00\x12\x1e\x33\x83\x85\xf3\xff\x7d  \x6d\x8d\xf9\xdd\x6b\x00\x00\x00\x55\xad\xe1\xac\xe9\x4f\x78\xcf  ...'

逐字节对照分析,验证了输出结构如下:

字节范围
十六进制值
含义
1-4
0x43 0x53 0x4a 0x4d
魔数 CSJM(大端)
5
0x09
版本/标志字段
6
0x01
版本/标志字段
7
0x0c
nonce 字节长度(12)
8
0x00
填充
9-20
0x12 1e 33 83 85 f3 ff 7d 6d 8d f9 dd
随机 nonce(12 字节)
21
0x6b

(= 107 = 91 + 16)
明文长度 + 16
22-24
0x00 0x00 0x00
小端高位填充
25 – (24+N)
密文字节
AES-CTR 加密密文(N 字节)
最后 16 字节
auth tag
GHASH 类认证标签

结论:caesarson 是一种 AES-GCM 变体,采用 CTR 模式加密 + 自实现的 GHASH 类认证,整体结构与 AEAD 加密方案一致。


3.3 汇编指令流分析

Step 1:从密文逆向 XOR 链定位 AES 函数

密文第 25-28 字节为 0x55 0xad 0xe1 0xac,小端存储为 0xace1ad55

在汇编指令流中搜索该值:

发现 0xace1ad55 由两数 XOR 得到:

0xace1ad55 = 0xc9888f2e XOR 0x7b226965

其中 0x7b226965 以大端还原为 {"ie,正是明文 JSON 字符串的前 4 字节——这验证了 CTR 模式的 XOR 加密特征。

继续搜索 0xc9888f2e(即 AES keystream 的前 4 字节):

发现 0xc9888f2e 由 0xa5cfe38c XOR 0x8b406b45 得到,位于偏移 0x854a

Step 2:结合 IDA 定位 AES 函数

跳转到偏移 0x854a 对应的 IDA 反编译代码,确认这是 sub_838c 函数中的一条语句:

上层调用函数为 sub_8340

该函数的代码特征与 AES 加密函数高度吻合。使用 IDA 插件 Findcrypt 进一步验证:

Findcrypt 识别出 RijnDael_AES_7E00(AES S-Box 特征魔数),查看其引用关系:

确认 sub_8340 即为 AES ECB 加密函数(用于生成 CTR keystream 块)。

Step 3:提取 AES 明文与密钥

在汇编指令流中向上追溯 sub_8340 的调用位置(从第 34231 行向上搜索):

提取到的 AES 输入参数:

参数
值(十六进制)
明文

(nonce + counter)
0x121e338385f3ff7d6d8df9dd00000002
密钥

(固定,来自 .so)
0xf3645cc3db63ae5828925612412de6d9
AES 输出

(keystream 块)
0x2e8f88c9cb755af7cfca4ace2f5dbda7...

验证:AES 输出的前 4 字节 0x2e8f88c9,以小端存储为 0xc9888f2e,与 Step 1 中的追踪结果完全吻合。

Step 4:CTR 模式结构归纳

定义 CTR keystream 块序列(nonce = 随机 12 字节):

AesData0 = AES_ECB(nonce || 0x00000000)   # 用于认证标签初始化AesData1 = AES_ECB(nonce || 0x00000001)   # 用于认证标签最终 XORAesData2 = AES_ECB(nonce || 0x00000002)   # 加密明文第 1-16 字节AesData3 = AES_ECB(nonce || 0x00000003)   # 加密明文第 17-32 字节AesData4 = AES_ECB(nonce || 0x00000004)   # 加密明文第 33-48 字节...

密文 Part 4 的加密规则:

ciphertext[0:16]  = plaintext[0:16]  XOR AesData2[0:16]ciphertext[16:32] = plaintext[16:32] XOR AesData3[0:16]...

举例验证(明文第 1-16 字节 {"ie":"[MASKED_I):

plaintext hex : 0x7b226965223a22383637393739303231AesData2      : 0x2e8f88c9cb755af7cfca4ace2f5dbda7XOR result    : 0x55ade1ace94f78cff9fd73f9166d8f96  ← 对应密文第 25-40 字节 ✓

Step 5:最后 16 字节(认证标签)分析

最后 16 字节的计算较为复杂,涉及三张内部数据表和两个核心函数:

// 初始化常量(硬编码于 .so 中)data_bb6e = [0xaa0x450x6e0xc7,0xd80x290x720x0b,0x0f0xce0xe30xfa,0x0e0xbd0x940x3f]// GF(2^128) 多项式约减表data_b9c0 = [0x00000x1C200x38400x2460,0x70800x6CA00x48C00x54E0,0xE1000xFD200xD9400xC560,0x91800x8DA00xA9C00xB5E0]// GHASH 乘法系数表(16x4 的 uint32 矩阵)data_6b14 = [    [0x000000000x000000000x000000000x00000000],    [0xf5ee727b0xd665e73c0xacb31f700x5829b1d9],// ... 共 16 行(外加最后一行边界条目)]

认证标签的完整计算步骤:

  1. b9f0(ciphertext_blocks, part4):对所有完整密文块执行 GHASH 类多项式乘累加,更新 data_bb6e 状态
  2. 末尾不足 16 字节的部分块:与对应 keystream 字节 XOR 后累加入 data_bb6e
  3. bb14()(第一次):对 data_bb6e 进行一轮完整的多项式变换
  4. 长度编码处理data_bb6e[7] ^= 0x40,并将 len(plaintext) * 8(比特数)编码后 XOR 入 data_bb6e[12:16]
  5. bb14()(第二次):再进行一轮多项式变换
  6. 最终 XORauth_tag[i] = data_bb6e[i] ^ AesData1[i]

算法本质:caesarson 加密是 AES-GCM 的自定义实现。AES key 硬编码于 .so 中,nonce 每次随机生成,认证标签通过 GF(2^128) 上的 GHASH 类运算计算。


四、Python 完整实现

以下是逆向还原的完整 Python 实现,包含 IMEI/MAC 伪造和 caesarson 加密两部分:

import uuidimport randomimport base64from binascii import b2a_hex, unhexlifyfrom Crypto.Cipher import AESfrom typing import Optional# ============================# 设备标识伪造# ============================defrandom_imei(imei: str) -> str:"""    基于真实 IMEI 伪造合法 IMEI。    - 保留前 8 位 TAC(厂商标识码)    - 随机化第 9-14 位序列号    - 按 Luhn 算法计算第 15 位校验码    """    imei = imei[:8] + str(random.randint(01_000_000)).zfill(6)    checksum = 0for n in range(14):        digit = int(imei[n])if n % 2 == 1:            checksum += (digit * 2) % 10 + (digit * 2) // 10else:            checksum += digit    rem = checksum % 10return imei + ("0"if rem == 0else str(10 - rem))defrandom_mac(mac: str) -> str:"""    基于真实 MAC 地址伪造合法 MAC。    - 保留前 3 字节 OUI    - 随机化后 3 字节    """    hex_chars = "0123456789abcdef"    suffix = ":".join("".join(random.choices(hex_chars, k=2)) for _ in range(3))return mac[:8] + suffixdefrandom_ydid() -> str:"""生成随机 32 位十六进制字符串作为 ydid。"""return uuid.uuid4().hex# ============================# Caesarson 加密实现# ============================classCaesarson:"""    caesarson 加密算法的 Python 还原实现。    本质是 AES-GCM 变体:CTR 模式加密 + GHASH 类认证标签。    AES key 硬编码于 libcaesarson.so,由逆向分析提取(脱敏展示)。    """# AES 密钥(从 .so 静态分析提取,16 字节)    AES_KEY: bytes = b'\xf3\x64\x5c\xc3\xdb\x63\xae\x58\x28\x92\x56\x12\x41\x2d\xe6\xd9'def__init__(self) -> None:# GF(2^128) 多项式约减表(用于 GHASH 乘法)        self._b9c0 = [0x00000x1C200x38400x2460,0x70800x6CA00x48C00x54E0,0xE1000xFD200xD9400xC560,0x91800x8DA00xA9C00xB5E0,        ]# 认证标签初始状态(硬编码常量)        self._bb6e: list[int] = [0xaa0x450x6e0xc7,0xd80x290x720x0b,0x0f0xce0xe30xfa,0x0e0xbd0x940x3f,        ]# GHASH 乘法系数矩阵(16 行 x 4 列 uint32)        self._6b14: list[list[int]] = [            [0x000000000x000000000x000000000x00000000],            [0xf5ee727b0xd665e73c0xacb31f700x5829b1d9],            [0xebdce4f60xaccbce790x59663ee10xb05363b3],            [0x1e32968d0x7aae29450xf5d521910xe87ad26a],            [0xd7b9c9ed0x59979cf30xb2cc7dc30xa2a6c766],            [0x2257bb960x8ff27bcf0x1e7f62b30xfa8f76bf],            [0x3c652d1b0xf55c528a0xebaa43220x12f5a4d5],            [0xc98b5f600x2339b5b60x47195c520x4adc150c],            [0xaf7393db0xb32f39e70x6598fb860x874d8ecd],            [0x5a9de1a00x654adedb0xc92be4f60xdf643f14],            [0x44af772d0x1fe4f79e0x3cfec5670x371eed7e],            [0xb14105560xc98110a20x904dda170x6f375ca7],            [0x78ca5a360xeab8a5140xd75486450x25eb49ab],            [0x8d24284d0x3cdd42280x7be799350x7dc2f872],            [0x9316bec00x46736b6d0x8e32b8a40x95b82a18],            [0x66f8ccbb0x90168c510x2281a7d40xcd919bc1],            [0x4000bb150x4000b9f10x4000641b0x00000000],        ]# ---- 内部工具方法 ----def_hex2bytes(self, s: str, length: Optional[int] = None) -> bytes:"""十六进制字符串 -> bytes,支持补零对齐。"""        s = s.lstrip("0x"if s.startswith("0x"else sif len(s) % 2:            s = "0" + sif length and length > len(s):            s = "0" * (length - len(s)) + sreturn unhexlify(s.encode())def_aes_ecb(self, block: bytes) -> bytes:"""AES ECB 加密单块(无 padding,block 必须为 16 字节)。"""        cipher = AES.new(self.AES_KEY, AES.MODE_ECB)return cipher.encrypt(block)def_random_nonce(self) -> bytes:"""生成 12 字节随机 nonce(uuid4 hex 截取前 32 位)。"""return self._hex2bytes(uuid.uuid4().hex.replace("-"""))[:12]# ---- GHASH 核心函数 ----def_b9f0(self, data_bytes: bytes, part4: list[int]) -> None:"""        GHASH 类多项式乘累加,处理所有完整的 16 字节密文块。        更新 self._bb6e 状态。        """        n_blocks = len(data_bytes) // 16        v4 = part4[15]        v5 = self._bb6e[15]for i in range(n_blocks):            v6 = v4 ^ v5            v7 = 14            row = self._6b14[v6 & 0xF]            v9, v10, v11, v12 = row            row13 = self._6b14[(v6 & 0xF0) >> 4]            v14 = self._b9c0[v9 & 0xF]            v15 = (row13[0] ^ (v9 >> 4) ^ (v10 << 28)) & 0xFFFFFFFF            v16 = (row13[1] ^ (v10 >> 4) ^ (v11 << 28)) & 0xFFFFFFFF            v17 = (row13[2] ^ (v11 >> 4) ^ (v12 << 28)) & 0xFFFFFFFF            v20 = (row13[3] ^ (v12 >> 4) ^ (v14 << 16)) & 0xFFFFFFFF            v18 = part4[i * 16 + 14] ^ self._bb6e[14]            v19 = v18 & 0xF0            v4 = v18 & 0xFwhile v7 >= 0:                v7 -= 1                v21 = self._6b14[v4]                v4 = 2 * (v15 & 0xF)                v22, v23, v24, v25 = v21[0], v21[3], v21[1], v21[2]                v26 = (v22 ^ (v15 >> 4) ^ (v16 << 28)) & 0xFFFFFFFF                v27 = (v24 ^ (v16 >> 4) ^ (v17 << 28)) & 0xFFFFFFFF                v28 = self._b9c0[v4 // 2]                v29 = (v25 ^ (v17 >> 4)) & 0xFFFFFFFFif v7 >= 0:                    v4 = part4[i * 16 + v7]                v30 = (v29 ^ (v20 << 28)) & 0xFFFFFFFF                v31 = (v23 ^ (v20 >> 4)) & 0xFFFFFFFF                v32 = self._6b14[v19 // 16]                v33 = (v31 ^ (v28 << 16)) & 0xFFFFFFFF                v19 = 2 * (v26 & 0xF)                v34, v35, v36, v37 = v32[0], v32[1], v32[2], v32[3]                v38 = (v34 ^ (v26 >> 4)) & 0xFFFFFFFFif v7 >= 0:                    v34 = self._bb6e[v7]                v15 = (v38 ^ (v27 << 28)) & 0xFFFFFFFF                v39 = (v35 ^ (v27 >> 4)) & 0xFFFFFFFF                v40 = self._b9c0[v19 // 2]                v16 = (v39 ^ (v30 << 28)) & 0xFFFFFFFF                v17 = (v36 ^ (v30 >> 4) ^ (v33 << 28)) & 0xFFFFFFFFif v7 >= 0:                    v4 ^= v34                v41 = (v37 ^ (v33 >> 4)) & 0xFFFFFFFFif v7 >= 0:                    v19 = v4 & 0xF0                    v4 &= 0xF                v20 = (v41 ^ (v40 << 16)) & 0xFFFFFFFF# 将本轮输出写回 _bb6e            v5 = v15if i + 1 < n_blocks:                v4 = part4[(i + 1) * 16 + 15]for j, val in enumerate([v20, v17, v16, v15]):                chunk = self._hex2bytes(hex(val), length=8)                self._bb6e[j * 4: j * 4 + 4] = list(chunk)def_bb14(self) -> None:"""        对 _bb6e 执行一轮完整的 GHASH 多项式变换(不输入新数据)。        用于认证标签的最终化处理。        """        v2 = self._bb6e[15]        v3 = v2 & 0xF0        v4 = 14        row = self._6b14[v2 & 0xF]        v6, v7, v8, v9 = row        v10 = self._bb6e[14]        row2 = self._6b14[v3 // 16]        v11, v12, v13, v14 = row2        v15 = 2 * (v6 & 0xF)        v16 = (v11 ^ (v6 >> 4)) & 0xFFFFFFFF        v17 = self._b9c0[v15 // 2]        v18 = (v16 ^ (v7 << 28)) & 0xFFFFFFFF        v19 = (v12 ^ (v7 >> 4) ^ (v8 << 28)) & 0xFFFFFFFF        v20 = (v13 ^ (v8 >> 4) ^ (v9 << 28)) & 0xFFFFFFFF        v21 = v10 & 0xF0        v22 = (v14 ^ (v9 >> 4) ^ (v17 << 16)) & 0xFFFFFFFF        v23 = v10 & 0xFwhile v4 >= 0:            v4 -= 1            v24 = self._6b14[v23]            v23 = 2 * (v18 & 0xF)            v25, v26, v27, v28 = v24[0], v24[3], v24[1], v24[2]            v29 = (v25 ^ (v18 >> 4) ^ (v19 << 28)) & 0xFFFFFFFF            v30 = (v27 ^ (v19 >> 4) ^ (v20 << 28)) & 0xFFFFFFFF            v31 = self._b9c0[v23 // 2]            v32 = (v28 ^ (v20 >> 4)) & 0xFFFFFFFFif v4 >= 0:                v23 = self._bb6e[v4]            v33 = (v32 ^ (v22 << 28)) & 0xFFFFFFFF            v34 = (v26 ^ (v22 >> 4)) & 0xFFFFFFFF            v35 = self._6b14[v21 // 16]            v36 = (v34 ^ (v31 << 16)) & 0xFFFFFFFF            v21 = 2 * (v29 & 0xF)            v18 = (v35[0] ^ (v29 >> 4) ^ (v30 << 28)) & 0xFFFFFFFF            v37 = self._b9c0[v21 // 2]            v19 = (v35[1] ^ (v30 >> 4) ^ (v33 << 28)) & 0xFFFFFFFF            v20 = (v35[2] ^ (v33 >> 4) ^ (v36 << 28)) & 0xFFFFFFFF            v38 = (v35[3] ^ (v36 >> 4)) & 0xFFFFFFFFif v4 >= 0:                v21 = v23 & 0xF0                v23 &= 0xF            v22 = (v38 ^ (v37 << 16)) & 0xFFFFFFFFfor j, val in enumerate([v22, v20, v19, v18]):            chunk = self._hex2bytes(hex(val), length=8)            self._bb6e[j * 4: j * 4 + 4] = list(chunk)# ---- 主加密函数 ----defencrypt(self, plaintext: str) -> str:"""        caesarson 加密。        :param plaintext: 待加密的 JSON 字符串        :return: Base64 编码的密文        """        data_bytes = plaintext.encode("utf-8")# 重置认证状态(每次加密独立)        self._bb6e = [0xaa0x450x6e0xc7,0xd80x290x720x0b,0x0f0xce0xe30xfa,0x0e0xbd0x940x3f,        ]# 生成随机 nonce(12 字节)        nonce = self._random_nonce()# Part 1: 魔数头部        part1 = [0x430x530x4a0x4d0x090x010x0c0x00]# Part 2: nonce        part2 = list(nonce)# Part 3: 输出长度字段(明文长度 + 16,4 字节小端)        total_len = len(data_bytes) + 16        part3 = list(self._hex2bytes(hex(total_len), length=8))# 生成 CTR keystream 并加密明文        n_blocks = (len(data_bytes) // 16) + 1        aes_stream: list[int] = []for i in range(n_blocks):            counter = self._hex2bytes(hex(i + 2), length=8)  # counter 从 2 开始            block = self._aes_ecb(nonce + counter)            aes_stream.extend(block[:16])        part4 = [data_bytes[i] ^ aes_stream[i] for i in range(len(data_bytes))]# 计算认证标签        self._b9f0(data_bytes, part4)# 处理末尾不足 16 字节的余量        remainder = len(data_bytes) % 16for i, j in enumerate(range(remainder, 0-1)):            self._bb6e[i] ^= (data_bytes[len(data_bytes) - j] ^ aes_stream[len(data_bytes) - j])        self._bb14()# 长度编码最终化        self._bb6e[7] ^= 0x40        bit_len_bytes = self._hex2bytes(hex(len(data_bytes) << 3), length=8)for i, b in enumerate(bit_len_bytes):            self._bb6e[12 + i] ^= b        self._bb14()# 最终 XOR with AesData1(nonce || 0x00000001)        aes_data1 = self._aes_ecb(nonce + b"\x00\x00\x00\x01")        part5 = [self._bb6e[i] ^ aes_data1[i % 16for i in range(16)]        result = bytes(part1 + part2 + part3 + part4 + part5)return base64.b64encode(result).decode()# ============================# 示例:生成完整 NMDI 参数# ============================defgenerate_nmdi(real_imei: str, real_mac: str) -> str:"""    生成伪造设备标识并加密,返回 NMDI 参数值。    :param real_imei: 真实设备 IMEI(仅用于提取前 8 位 TAC)    :param real_mac:  真实设备 MAC(仅用于提取前 3 字节 OUI)    :return: Base64 编码的 NMDI 密文    """    ie = random_imei(real_imei)    mc = random_mac(real_mac)    ydid = random_ydid()    payload = f'{{"ie":"{ie}","mc":"{mc}","ydid":"{ydid}"}}'return Caesarson().encrypt(payload)if __name__ == "__main__":# 示例(使用脱敏的真实设备前缀)    nmdi = generate_nmdi(        real_imei="86000000000000",   # 替换为真实设备 IMEI        real_mac="xx:xx:xx:00:00:00"# 替换为真实设备 MAC    )    print("NMDI:", nmdi)

附录:caesarson 与标准 AES-GCM 对比

特性
标准 AES-GCM
caesarson
加密模式
CTR
CTR(counter 从 2 开始)
认证标签
GHASH
自实现 GHASH(b9f0 + bb14
Nonce 生成
随机 12 字节
随机 12 字节(uuid hex 前 12B)
Key 来源
外部传入
硬编码于 libcaesarson.so
输出格式
IV|Ciphertext|Tag
魔数头 | nonce | 长度 | 密文 | Tag,整体 Base64
输出魔数
CSJM

(0x43534A4D)

输出结构图

图注:caesarson 输出字节布局全览,展示各 Part 与 AES CTR keystream 的对应关系及认证标签生成流程

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

评论 抢沙发

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