
1. 项目概述与核心挑战上次我们聊了如何定位一个音乐APP的关键API接口并初步分析了其请求参数。今天我们深入其安全体系的核心Cookie和设备指纹。这不仅仅是“拿到数据”那么简单更是理解现代移动应用如何构建其防御壁垒的绝佳案例。很多朋友在逆向时卡在登录后的请求总是返回“token无效”或“设备异常”其根源多半就在这里。这个音乐APP的防护策略相当典型它没有采用单一的校验方式而是构建了一个由动态Cookie和混合加密的设备指纹组成的双层验证体系。简单来说服务器不仅要看你的“门票”Cookie还要验明你的“身份证”设备指纹是不是原装的、是不是在有效期内。这套机制的核心目的是为了对抗自动化脚本和模拟器。APP会采集你手机的一堆硬件和软件特征比如IMEI、Android ID、屏幕分辨率、CPU型号等等然后将这些信息用一套复杂的算法本次涉及AES和RSA加密打包形成一个唯一的、且每次启动都可能变化的“设备指纹”。同时服务端下发的Cookie也并非一成不变其生命周期和有效性与其他参数深度绑定。如果你直接用固定的Cookie去请求或者用随机生成的假设备信息很快就会触发风控。因此我们的目标就是完整还原这套设备指纹的生成逻辑并理解Cookie的维护机制从而让我们的自动化脚本能够“伪装”成一个合法的、真实的设备。2. Cookie体系全解不只是那个“小饼干”很多人对Cookie的理解还停留在Web端认为它就是一个服务器下发的、存储在浏览器里的键值对。在移动端尤其是在强安全需求的APP里Cookie体系要复杂得多。它更像一个由服务器签发、客户端保管的“动态通行证”其有效性和内容与当前会话状态、设备状态紧密相关。2.1 Cookie的结构与生命周期分析通过抓包观察登录后的请求你会发现请求头里通常不止一个Cookie字段可能类似这样Cookie: sessionidxyz789; device_fpabc123; main_login_tokendef456这只是一个示例实际字段名可能被混淆。我们需要关注的是它们的来源和变化规律。来源这些Cookie绝大多数是在登录接口或初始化接口的响应头Set-Cookie字段中下发的。你需要仔细检查登录成功后的那个响应包。生命周期会话型Cookie如sessionid其有效期可能较短或者标记为HttpOnly、Secure旨在防止XSS攻击直接读取。它在APP进程存活期间有效或者有一个明确的Max-Age。持久型Cookie如main_login_token有效期可能长达数天或数周用于实现“记住我”或长期免登录功能。它通常会被安全地存储在本地的SharedPreferences或加密数据库中。设备关联型Cookie如device_fp这是本次重点。它的生成和验证很可能与我们在本地计算的设备指纹密文有关。服务器在登录时会将客户端上传的设备指纹信息与自己计算或存储的版本进行比对通过后签发这个Cookie。后续请求中这个device_fpCookie需要与其他地方如请求体携带的设备指纹密文保持一致或形成某种对应关系。实操心得不要只盯着一个请求看。你需要对比首次登录、二次打开APP、退出后重新登录、清除数据后登录这几种场景下的Cookie变化。特别是device_fp观察它在不同设备、或同一设备清除数据后是否会变化这能帮你判断它是否与本地生成的、可变的设备指纹绑定。2.2 Cookie的维护与同步策略在自动化脚本中维护Cookie的核心是模拟APP的原生行为。这不仅仅是把服务器返回的Cookie保存下来然后在后续请求中塞回去那么简单。自动管理使用成熟的HTTP客户端库如Python的requests.Session()或Go的cookiejar。这些库会自动处理响应中的Set-Cookie并在后续请求中携带符合域名路径要求的Cookie省去手动解析的麻烦。关键Cookie提取对于需要参与逻辑计算的Cookie比如device_fp你需要将其值提取出来因为它可能就是其他加密函数的输入参数之一。过期与刷新监听请求的响应状态码。如果收到401/403可能意味着sessionid过期。此时脚本应触发一个“令牌刷新”流程如果有专门的刷新接口或者重新执行登录流程。对于device_fp过期则可能需要重新生成设备指纹并走一遍设备注册或验证流程。一个典型的坑APP可能采用Cookie Body/Header参数双重校验。即请求体里有一个fingerprint字段其值是通过加密计算得到的而请求头里的device_fpCookie值可能是这个fingerprint的某种哈希或映射。服务器会校验两者是否匹配。如果你只复制了Cookie而请求体里的fingerprint是乱填的请求就会失败。3. 设备指纹的采集与明文构造设备指纹的生成第一步是采集。我们需要找到APP收集了哪些设备信息。通常有两种方式一是通过静态分析搜索TelephonyManager、Build、Settings.Secure等系统API的调用二是通过动态Hook监控这些API的返回值。3.1 关键信息采集点通过逆向分析这个音乐APP很可能采集了以下信息具体字段名已被混淆但类型可推测信息类型可能来源Android API示例值/作用设备唯一标识Build.SERIAL,Settings.Secure.ANDROID_ID相对稳定的设备ID硬件信息Build.MODEL,Build.BRAND,Build.HARDWARE“Xiaomi Mi 10”, “qcom”系统信息Build.VERSION.RELEASE,Build.VERSION.SDK_INT“12”, “31”屏幕信息DisplayMetrics(widthPixels, heightPixels, densityDpi)“1080x2340”, “440dpi”CPU信息/proc/cpuinfo或Build.SUPPORTED_ABIS“arm64-v8a”传感器列表SensorManager.getSensorList存在哪些传感器用于检测模拟器网络信息WifiManager.getConnectionInfo,TelephonyManagerBSSID, NetworkOperator其他环境信息是否Root、是否调试、安装应用列表哈希等用于风险检测注意事项采集这些信息需要相应的Android权限。APP会在安装或首次运行时申请。在逆向时我们可以直接模拟这些值但模拟的值必须自洽。例如一个标注为“Xiaomi Mi 10”的设备其屏幕分辨率大概率是1080x2340CPU架构是arm64-v8a。胡乱组合的参数容易被识别为伪造。3.2 明文JSON的构造逻辑采集到的信息不会直接发送而是先被组装成一个JSON对象。这个组装顺序和键名非常关键因为后续的加密过程可能是对整个JSON字符串进行的。假设我们通过Hook或代码分析找到了构造这个JSON的类和方法。还原出来的明文结构可能类似这样{ “v”: “1.0”, “ts”: 1648886400000, “d_id”: “a1b2c3d4”, “brand”: “Xiaomi”, “model”: “Mi 10”, “os_ver”: “12”, “screen”: “1080*2340”, “cpu_abi”: “arm64-v8a”, “sensor_list”: “accelerometer,gyroscope”, // ... 其他字段 “nonce”: “7a8f9e” }关键字段解析v: 版本号可能用于兼容性。ts: 当前时间戳毫秒。这是动态变化的核心确保每次生成的指纹密文都不同。d_id: 一个相对稳定的设备ID可能由ANDROID_ID等加工而来。nonce: 随机数增加熵值防止重放攻击。实操心得找到这个JSON的构造方法是突破口。你可以搜索JSONObject.put,Gson.toJson等方法的调用。然后动态调试在调用加密函数前打印或Hook这个JSON字符串。拿到明文样本后对比多次启动的明文找出变化的部分如ts,nonce和不变的部分这对后续理解加密逻辑至关重要。4. 混合加密算法AES与RSA的协作这是整个逆向过程中最硬核的部分。该APP采用了“RSA加密AES密钥AES加密实际数据”的混合加密模式这是HTTPS等安全通信中常见的方式兼顾了对称加密的高效和非对称加密的安全密钥交换。4.1 算法流程还原整个流程可以分解为以下几步生成随机AES密钥与IV在客户端每次生成设备指纹时都会动态生成一个随机的AES密钥例如256位的aes_key和一个随机的初始化向量IV。IV用于CBC等分组模式确保同样的明文加密出不同的密文。AES加密明文JSON使用上一步生成的aes_key和iv以AES-CBC-PKCS7Padding模式将构造好的设备信息JSON明文加密得到密文A。RSA加密AES密钥客户端内置了服务器的RSA公钥。它将aes_key和iv有时会拼接在一起用这个RSA公钥进行加密得到密文B。由于RSA加密速度慢且对数据长度有限制所以只用来加密短的AES密钥。组装最终请求体将密文AAES加密的设备信息和密文BRSA加密的AES密钥以某种格式如Base64编码后用特定分隔符拼接或放入一个更大的JSON中组合发送给服务器。服务器端流程则相反用自己的RSA私钥解密密文B拿到客户端生成的aes_key和iv。用这个aes_key和iv解密密文A拿到设备信息明文JSON。校验JSON的合法性时间戳是否新鲜、设备ID是否已知等然后进行业务逻辑处理。4.2 关键代码定位与参数提取要还原这个流程我们需要在反编译的代码中定位几个关键点1. 定位AES加密调用 搜索关键词Cipher.getInstance(“AES/CBC/PKCS7Padding”),AES/CTR/NoPadding,SecretKeySpec,IvParameterSpec。找到初始化Cipher对象并调用doFinal方法的地方。2. 定位RSA加密调用 搜索关键词Cipher.getInstance(“RSA/ECB/PKCS1Padding”)(较老),RSA/ECB/OAEPWithSHA-256AndMGF1Padding(较新),PublicKey,X509EncodedKeySpec。寻找加载公钥并加密的代码段。3. 提取RSA公钥 公钥通常以字符串形式硬编码在代码中可能被分割或简单编码或者从某个配置接口动态获取。在代码中搜索-----BEGIN PUBLIC KEY-----或MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A这样的Base64编码的PEM格式开头。找到后需要将其还原成标准的PEM格式。4. 确定AES密钥长度和模式 查看SecretKeySpec的第二个参数是“AES”。密钥字节数组的长度决定是AES-12816字节、AES-19224字节还是AES-25632字节。查看Cipher.getInstance的参数确定是CBC、CTR还是GCM模式。踩坑记录一个常见的混淆手段是将算法字符串如“AES”拆分成“A” “ES”或者将“RSA”的字符存储在int数组里再拼接。你需要耐心地跟踪字符串的生成过程。另外注意Java的PKCS7Padding在标准库中实际叫PKCS5Padding但很多第三方库如BouncyCastle支持PKCS7Padding代码里写的是什么就是什么不要想当然。5. 完整算法还原与Python实现理论清晰后我们用Python来完整复现这个流程。这里假设我们通过逆向分析确定了以下参数AES模式CBC填充PKCS7密钥长度256位。RSA填充方案PKCS1_v1_5对应Java的RSA/ECB/PKCS1Padding。RSA公钥已提取为PEM格式字符串。5.1 依赖库安装我们需要pycryptodome这个强大的密码学库。pip install pycryptodome5.2 核心代码实现import json import base64 import time import random import string from Crypto.Cipher import AES, PKCS1_v1_5 from Crypto.PublicKey import RSA from Crypto.Util.Padding import pad, unpad from Crypto.Random import get_random_bytes class DeviceFingerprintGenerator: def __init__(self, rsa_public_key_pem): 初始化传入服务器RSA公钥(PEM格式) self.rsa_public_key RSA.import_key(rsa_public_key_pem) # 模拟固定的设备ID实际应从ANDROID_ID等计算 self.device_id “simulated_device_123456” def _generate_random_string(self, length8): 生成随机字符串模拟nonce return .join(random.choices(string.ascii_letters string.digits, klength)) def construct_plaintext_json(self): 构造设备指纹的明文JSON plain_dict { “v”: “1.0”, “ts”: int(time.time() * 1000), # 当前毫秒时间戳关键动态参数 “d_id”: self.device_id, “brand”: “Xiaomi”, “model”: “Mi 10”, “os_ver”: “12”, “sdk_int”: “31”, “screen”: “1080*2340”, “density_dpi”: “440”, “cpu_abi”: “arm64-v8a”, “sensor_list”: “accelerometer,gyroscope,proximity”, “nonce”: self._generate_random_string(6) # 6位随机数 } # 确保JSON序列化的顺序有时顺序会影响最终的加密结果如果服务端做字符串比对 # 这里按定义顺序输出更稳妥的方法是使用sort_keysTrue return json.dumps(plain_dict, separators(,, :), ensure_asciiFalse) def generate_fingerprint(self): 生成完整的加密设备指纹 # 1. 构造明文 plaintext_json self.construct_plaintext_json() plaintext_bytes plaintext_json.encode(utf-8) print(f“[DEBUG] 明文JSON: {plaintext_json}”) # 2. 生成随机AES密钥和IV aes_key get_random_bytes(32) # AES-256 iv get_random_bytes(16) # AES block size is 16 bytes print(f“[DEBUG] 随机AES Key (hex): {aes_key.hex()}”) print(f“[DEBUG] 随机IV (hex): {iv.hex()}”) # 3. AES加密明文 cipher_aes AES.new(aes_key, AES.MODE_CBC, iv) # 使用PKCS7填充在Crypto库中pad函数默认使用PKCS7 ciphertext_aes cipher_aes.encrypt(pad(plaintext_bytes, AES.block_size)) ciphertext_aes_b64 base64.b64encode(ciphertext_aes).decode(utf-8) print(f“[DEBUG] AES密文 (Base64): {ciphertext_aes_b64}”) # 4. RSA加密AES密钥将key和iv拼接后加密 # 注意实际实现中可能只加密key或者用特定格式拼接key和iv data_to_encrypt_by_rsa aes_key iv # 常见拼接方式 cipher_rsa PKCS1_v1_5.new(self.rsa_public_key) # RSA加密有长度限制但AES-256 key(32) IV(16) 48字节远小于RSA-2048的密钥长度限制 ciphertext_rsa cipher_rsa.encrypt(data_to_encrypt_by_rsa) ciphertext_rsa_b64 base64.b64encode(ciphertext_rsa).decode(utf-8) print(f“[DEBUG] RSA密文 (Base64): {ciphertext_rsa_b64}”) # 5. 组装最终请求体假设服务端要求JSON格式 final_payload { “encrypted_data”: ciphertext_aes_b64, “encrypted_key”: ciphertext_rsa_b64, “version”: “1.0” # 可能还有其他字段如算法标识符 “algo”: “AES/RSA” } return final_payload # 使用示例 if __name__ “__main__”: # 替换成你从APP中逆向出来的公钥 PUBLIC_KEY_PEM “““-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyour_public_key_here... -----END PUBLIC KEY-----””” generator DeviceFingerprintGenerator(PUBLIC_KEY_PEM) fingerprint_payload generator.generate_fingerprint() print(“\n[INFO] 最终生成的请求体:”) print(json.dumps(fingerprint_payload, indent2))代码关键点解释时间戳ts和随机数nonce这是保证每次加密结果不同的核心。服务端会校验时间戳的新鲜度如允许5分钟内误差防止重放攻击。AES密钥与IV的生成每次都是随机的确保了“一次一密”。RSA加密的内容我们模拟了常见的做法将AES密钥和IV拼接后整体加密。有些实现可能会分别加密或用JSON格式包装后再加密具体需根据逆向结果调整。最终组装将两个密文以及可能的算法版本号组装成JSON作为请求体发送。6. 请求集成与Cookie联动生成了加密的设备指纹后我们需要将其与Cookie体系联动完成一次完整的合法请求。6.1 完整的请求流程模拟假设登录接口为/api/login它接受用户名密码和device_fingerprint参数并在响应中返回Set-Cookie。import requests def simulate_full_login(username, password, device_fp_payload): session requests.Session() headers { ‘User-Agent’: ‘Your_Music_App_Client/1.0 (模拟)’, ‘Content-Type’: ‘application/json; charsetutf-8’, } # 1. 构造登录请求体 login_data { ‘username’: username, ‘password’: password, # 注意密码很可能也是加密的这里简化处理 ‘device_fingerprint’: device_fp_payload # 即上一步generate_fingerprint()的返回值 } # 2. 发送登录请求 login_url “https://api.music-app.com/api/login” resp session.post(login_url, jsonlogin_data, headersheaders) if resp.status_code 200: print(“[INFO] 登录成功”) # 3. 检查并保存Cookie (requests.Session会自动处理) # 但我们可以打印出来看看 print(f“[INFO] 获得的Cookies: {session.cookies.get_dict()}”) # 特别关注device_fp device_fp_cookie session.cookies.get(‘device_fp’) if device_fp_cookie: print(f“[INFO] 关键Cookie device_fp: {device_fp_cookie}”) return session, device_fp_cookie else: print(f“[ERROR] 登录失败: {resp.status_code}, {resp.text}”) return None, None # 后续的API请求直接使用这个session即可 def get_user_playlist(session): api_url “https://api.music-app.com/api/user/playlist” resp session.get(api_url) if resp.status_code 200: print(“[INFO] 获取歌单成功”) return resp.json() else: print(f“[ERROR] 请求失败: {resp.status_code}”) return None6.2 Cookie与指纹的关联验证这是最需要小心的地方。服务器如何关联Cookie和指纹有两种常见模式模式ACookie作为指纹的“句柄”。客户端首次登录时上传加密的设备指纹。服务端解密后在数据库为这个设备指纹创建一个记录并生成一个唯一的device_fp_token。服务端将device_fp_token通过Set-Cookie: device_fpxxxx下发。后续请求客户端在Cookie中携带这个device_fp_token在请求体不再需要上传完整的设备指纹。服务端通过Cookie中的token查到之前存储的设备信息完成校验。逆向对策这种情况下我们只需要在首次登录时成功伪造一次设备指纹拿到Cookie后就可以长期使用直到过期。重点在于首次指纹的伪造要足够逼真。模式BCookie与指纹参数双向校验。每次关键请求甚至每次请求客户端都需要在请求体中上传加密的设备指纹。同时Cookie中也携带一个device_fp字段。服务端会解密请求体中的指纹计算出一个哈希值比如MD5或SHA256然后与Cookie中的device_fp值进行比对。两者必须一致。逆向对策这种情况下Cookie值不是服务器下发的随机令牌而是客户端自己计算并可能通过某个初始化接口“注册”或“同步”给服务器的。我们需要找到计算这个Cookie值的算法。它很可能就是encrypted_data或明文JSON的某种哈希。你需要逆向查找设置Cookie的代码看它的值是从哪里来的。排查技巧如何判断是哪种模式抓包对比。在登录后的第一个非登录请求如获取首页信息中观察请求体是否还包含庞大的device_fingerprint数据。如果还有很可能是模式B如果没有只有Cookie则是模式A。对于模式B你需要搜索设置device_fp这个Cookie的代码逆向其生成逻辑。7. 常见问题与排查技巧实录在实际逆向和复现过程中你会遇到各种各样的问题。这里记录一些典型场景和解决思路。7.1 加密结果与服务端不匹配这是最常遇到的问题。你的Python代码运行无误但生成的密文服务器就是不认。排查清单明文JSON格式确保JSON字符串完全一致。包括字段顺序、空格、缩进、Unicode转义。使用json.dumps(..., separators(‘,’, ‘:’), ensure_asciiFalse)来获得最紧凑且无转义的控制。与服务端通信时ensure_asciiFalse可能导致中文乱码有时需要设为True具体看原APP行为。AES参数密钥长度确认是128192还是256位。看SecretKeySpec的字节数组长度。工作模式CBCCTR还是GCM看Cipher.getInstance的参数。填充方式PKCS5Padding, PKCS7Padding, 还是NoPaddingJava的PKCS5Padding实际处理PKCS7填充。但如果你在Python用了PKCS7而Java端是NoPadding就会失败。IV处理IV是否正确使用在CBC模式下加解密必须使用相同的IV。确认你是将IV作为参数传入还是从密文中提取有些实现会把IV拼在密文前面。RSA参数公钥确认你提取的公钥是正确的、完整的PEM格式。可以尝试用openssl rsa -pubin -in key.pem -text检查一下。填充方案这是最大的坑PKCS1_v1_5和OAEP完全不同。Java代码中RSA/ECB/PKCS1Padding对应Python的PKCS1_v1_5。如果是RSA/ECB/OAEPWithSHA-256AndMGF1Padding则需要使用Crypto.Cipher.PKCS1_OAEP并指定对应的哈希算法。加密内容RSA到底加密了什么是只加密了AES key还是keyiv还是keyiv其他数据你需要动态调试在RSA加密前打印其输入字节确认其内容。编码问题所有步骤的输入输出是字节串(bytes)还是Base64字符串加密函数通常处理bytes。确保在需要字符串传输时进行Base64编码并且没有多余的换行符。7.2 请求被风控返回“设备异常”或“行为可疑”即使加密通过了请求也可能被更高级的风控系统拦截。可能原因及对策设备信息模拟不真实你模拟的设备信息过于“完美”或自相矛盾。例如一个2020年的机型却有着2023年才发布的系统版本。尽量使用真实存在的设备型号和对应的合理参数。可以从真机抓包获取一套真实的设备信息明文作为模板。时间戳问题服务器检查时间戳。你的脚本时间可能与服务器有较大误差或者时间戳格式不对可能是秒而不是毫秒。确保使用服务器时间或与之同步。请求频率过高模拟的“用户”行为不像真人。添加随机延迟模拟人的操作间隔。网络环境特征如果你的脚本运行在服务器或海外VPS上IP地址、TCP窗口大小等网络特征可能与移动网络不同。可以考虑使用ADB连接真机在真机上运行Python脚本或者使用更接近移动端的请求库设置。缺少其他签名参数除了设备指纹请求可能还对URL、请求体、时间戳等有其他签名算法如HMAC-SHA256。你需要检查每个请求是否都有sign、token之类的参数并找到其生成算法。7.3 动态Hook与调试技巧静态分析遇到阻碍时动态调试是利器。使用Frida进行动态Hook这是最强大的方法。你可以写Frida脚本Hook关键函数如Cipher.doFinal,MessageDigest.digest,JSONObject.toString直接打印出输入输出参数。// 示例Hook Cipher.doFinal 并打印输入输出 Java.perform(function() { var Cipher Java.use(‘javax.crypto.Cipher’); Cipher.doFinal.overload(‘[B’).implementation function(input) { console.log(‘[Cipher.doFinal] input: ‘ JSON.stringify(input)); var result this.doFinal(input); console.log(‘[Cipher.doFinal] output: ‘ JSON.stringify(result)); console.log(‘[Cipher.doFinal] output (base64): ‘ base64.encode(result)); return result; }; });使用Xposed模块如果你能root设备并安装Xposed框架可以编写更稳定的注入模块来记录日志。日志分析APP本身可能有调试日志通过logcat可以抓取。搜索包名和特定关键字如fingerprint,encrypt,device有时会有意外收获。网络抓包对比用你的脚本生成请求同时用原APP抓取相同操作的请求。对比两个请求的每一个字段从差异处入手逆向。Burp Suite或Charles的Diff功能非常好用。整个逆向过程就像侦探破案需要耐心、细心和对技术细节的执着。从Cookie的维护到AES/RSA混合加密的还原每一步都需要严谨的推理和验证。当你最终用自己的代码成功模拟出一个被服务器认可的“合法设备”时那种成就感是无与伦比的。这不仅是为了获取数据更是对现代移动应用安全机制的一次深刻理解。记住这些技术知识应当用于安全研究、自动化测试和个人学习切勿用于破坏他人服务或侵犯用户隐私。