尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

抖音私信接口reuqest_sign签名逆向分析:从抓包到算法还原实战

抖音私信接口reuqest_sign签名逆向分析:从抓包到算法还原实战 1. 项目概述与核心价值最近在做一个企业级自动化工具的项目其中有一个模块需要模拟抖音的私信发送功能。这听起来像是做营销或者客服机器人对吧但实际操作起来第一个拦路虎就出现了私信接口的加密参数reuqest_sign。这个参数是抖音服务端用来校验请求合法性的核心不搞定它任何模拟发送的请求都会被直接拒绝。网上关于抖音爬虫、无水印下载的资料一抓一大把但深入到企业版私信接口特别是这个关键签名参数逆向分析的公开记录却非常零散要么语焉不详要么方法已经失效。所以我决定把这次完整的逆向分析过程记录下来一方面给自己做个备忘另一方面也给遇到同样问题的朋友提供一个清晰的、可复现的路径。这篇记录不会涉及任何灰色或违规操作纯粹是技术层面的探索旨在理解大型应用如何设计其API安全机制。2. 逆向分析环境与工具准备工欲善其事必先利其器。逆向分析移动应用尤其是像抖音这样防护严密的应用选择合适的工具链至关重要。我的分析主要基于安卓平台。2.1 核心工具选型与配置我选择的工具组合是FridaJadxCharles/FiddlerIDA Pro。下面详细说说为什么选它们以及怎么配置。Frida这是动态插桩的瑞士军刀。它允许你在应用运行时注入自己的JavaScript代码来Hook Java方法和NativeSO库函数。对于追踪reuqest_sign这种很可能在Native层计算的加密逻辑Frida几乎是唯一的选择。我使用的是Frida 15.2.2版本服务端运行在Root过的安卓测试机上客户端通过Python脚本在电脑上连接控制。Jadx用于静态反编译APK将Dex文件转换成可读的Java代码。虽然抖音的核心逻辑可能不在Java层但Java层是发起网络请求、准备参数、调用Native接口的入口从这里入手能找到关键的线索。我使用Jadx-gui它的图形界面方便搜索和跳转。Charles/Fiddler抓包工具。目的是捕获到包含reuqest_sign参数的真实请求包拿到原始的请求URL、Headers、Body作为我们逆向分析的“输入样本”和“输出目标”。配置时需要注意抖音使用了证书绑定SSL Pinning直接抓包会失败。这里就需要Frida出场通过脚本绕过证书校验。一个常用的脚本是frida-ssl-unpinning可以Hook掉常见的证书验证方法。IDA Pro静态分析SO库的利器。当Frida定位到签名计算发生在某个SO文件如libcms.so,libsscronet.so或抖音自定义的库的某个函数时就需要用IDA Pro加载这个SO文件进行反汇编和伪代码分析理解其算法细节。注意所有工具和测试请在合规的测试环境中进行使用从官方渠道获取的测试版本应用并确保你的操作符合相关服务条款和法律法规。分析目的是学习安全技术而非破坏或滥用。2.2 测试环境搭建要点测试机一台已经Root的安卓手机或模拟器如夜神、雷电但需注意模拟器可能被应用检测。我推荐使用真机稳定性更高。抖音版本选择相对较旧但功能完整的企业版APK。太新的版本可能加固和混淆策略更强增加逆向难度。我分析时使用的是某个历史版本具体版本号因合规原因不公开其核心加密逻辑具有代表性。Frida Server部署将对应安卓CPU架构的frida-server文件push到手机/data/local/tmp/目录赋予执行权限并以root身份运行。确保电脑端frida-ps -U能列出手机进程。抓包环境在电脑上设置好代理Charles手机Wi-Fi配置手动代理指向电脑。安装Charles根证书到手机系统证书目录需要Root。然后使用Frida脚本禁用抖音的SSL Pinning。3. 请求抓包与参数初步定位一切准备就绪后启动抓包工具和Frida脚本然后在抖音企业版App中手动发送一条私信。成功抓包后你会看到一条指向https://aweme.snssdk.com或类似域名的POST请求路径可能是/aweme/v1/chat/msg/send/。查看这个请求的Form Data或JSON Body你会看到一堆参数例如to_user_id: 接收者IDtext: 消息内容client_msg_id: 客户端生成的消息IDdevice_id: 设备标识iid: 安装ID...以及我们的目标reuqest_sign这个reuqest_sign通常是一长串32位或64位的十六进制字符串例如a1b2c3d4e5f678901234567890abcdef0。它的值每次请求都会变化即使其他参数不变这说明它是一个基于请求内容、时间戳或随机数生成的签名而不是简单的加密。第一步我们需要确定这个签名是在客户端哪个环节被添加进去的。用Jadx打开APK全局搜索字符串reuqest_sign。你可能会在某个网络请求封装类如com.ss.android.common.applog.NetUtil、com.ss.sys.ces或com.bytedance.retrofit2.client相关的类中找到它被拼接到请求参数里的代码。找到这里就找到了签名的“调用点”。通常你会看到类似这样的代码String sign SignUtils.getSign(paramMap); paramMap.put(“reuqest_sign”, sign);那么下一步的目标就是逆向这个SignUtils.getSign方法。4. Java层Hook与算法入口追踪通过静态分析我们找到了签名方法的调用处。现在用Frida进行动态验证和深入追踪。首先写一个Frida脚本Hook这个SignUtils.getSign方法类名和方法名可能不同需要根据你的搜索结果调整Java.perform(function() { var SignUtils Java.use(‘com.bytedance.common.utility.SignUtils’); // 示例类名 SignUtils.getSign.overload(‘java.util.Map’).implementation function(paramMap) { console.log(“[] getSign called!”); // 打印入参看看签名前所有的参数 var it paramMap.keySet().iterator(); while (it.hasNext()) { var key it.next(); var value paramMap.get(key); console.log(“Key: ” key “, Value: ” value); } // 调用原方法获取签名结果 var result this.getSign(paramMap); console.log(“[] Calculated Sign: ” result); // 打印调用栈看看是谁调用了签名方法 console.log(Java.use(“android.util.Log”).getStackTraceString(Java.use(“java.lang.Exception”).$new())); return result; }; });运行这个脚本然后再次发送私信。如果Hook成功控制台会输出所有待签名的参数和计算出的reuqest_sign值。关键发现与常见情况参数排序你可能会发现传入getSign的Map其键值对Key-Value已经按照特定规则如字母升序排序。这是签名算法的常见第一步确保服务端能以同样规则还原。添加固定盐值Map里可能多出了一些你在抓包时没看到的参数比如_rticket时间戳、ts、或者一个固定的secret字符串。这些是签名算法的一部分。算法可能不在Java层getSign方法内部可能非常简单仅仅是调用了一个Native方法return NativeHelper.sign(paramStr);。如果遇到这种情况恭喜你也“恭喜”你核心逻辑已经转移到SO库了难度升级。5. Native层SO库深度逆向分析当发现签名计算通过System.loadLibrary加载的SO库完成时工作重心就要从Java转向Native。5.1 定位关键SO文件与函数首先从Hook到的Java Native方法入手。在Jadx中查看NativeHelper.sign的声明通常对应C/C层的函数名遵循Java_包名_类名_方法名的格式例如Java_com_bytedance_common_utility_NativeHelper_sign。使用Frida的Module.enumerateExports或Module.enumerateSymbols来枚举所有加载的SO库及其导出函数寻找包含上述模式或可疑字符串如“sign”、“encrypt”、“md5”、“sha”、“aes”的函数。更直接的方法是在调用签名前后用Frida拦截所有SO库的加载并追踪对常见加密函数如OpenSSL的MD5_Init,SHA256_Update,HMAC等的调用。这里给出一个示例脚本用于Hooklibcrypto.soOpenSSL的MD5_Final函数Interceptor.attach(Module.findExportByName(“libcrypto.so”, “MD5_Final”), { onEnter: function(args) { // args[0] 是 MD5_CTX 上下文args[1] 是输出缓冲区 console.log(“[MD5_Final] called, output buffer addr:”, args[1]); }, onLeave: function(retval) { // 函数执行后输出缓冲区里就是MD5结果 var hash Memory.readByteArray(args[1], 16); // MD5结果16字节 console.log(“[MD5_Final] result:”, hexdump(hash)); } });通过这种方式结合调用栈回溯可以精准定位到计算reuqest_sign的最终函数。5.2 静态分析与算法还原假设我们定位到了目标函数在libsscronet.so的Java_com_ss_android_ugc_aweme_network_SSSecurity_sign中。接下来用IDA Pro加载这个SO文件。找到函数在IDA的Exports窗口或通过搜索函数名找到目标函数。分析伪代码IDA的F5功能可以生成可读性较高的C语言伪代码。这是最耗时的部分需要耐心。识别算法在伪代码中你需要关注字符串常量搜索“md5”、“sha256”、“hmac”等字符串IDA可能会识别出。加密函数调用识别对标准库函数如MD5_Init,SHA256_Init,EVP_DigestUpdate或内部实现的加密函数的调用。逻辑流程通常的流程是拼接字符串 - 添加密钥盐 - 计算哈希MD5/SHA256 - 可能进行二次处理如Base64、Hex编码。密钥查找算法中的盐Secret Key可能硬编码在SO文件中以字符串或字节数组形式也可能通过网络请求动态获取。在IDA的Strings窗口或通过分析数据段查找可疑的长字符串或字节序列。一个典型的简化算法可能是这样的伪代码string generate_sign(mapstring, string params) { // 1. 参数排序并拼接成 key1value1key2value2... string sorted_str sort_and_concat(params); // 2. 拼接固定的盐值可能在so中硬编码为“douyin_secret_2023” string secret get_hardcoded_secret(); string to_be_hashed sorted_str “” secret; // 3. 计算MD5或SHA256 unsigned char hash[MD5_DIGEST_LENGTH]; MD5((unsigned char*)to_be_hashed.c_str(), to_be_hashed.length(), hash); // 4. 将二进制hash转为16进制小写字符串 return bytes_to_hex(hash, MD5_DIGEST_LENGTH); }5.3 动态调试验证算法静态分析得出的算法需要验证。我们可以用Frida写一个主动调用的脚本模拟这个Native函数。首先用Frida找到函数的绝对地址var func_addr Module.findExportByName(“libsscronet.so”, “Java_com_ss_android_ugc_aweme_network_SSSecurity_sign”);然后使用NativeFunction创建可调用的函数对象。这需要知道函数的参数类型和返回类型。通过分析IDA伪代码可以推断。假设它接收一个Java的JNIEnv指针、一个jobjectthis、和一个jstring参数字符串返回jstring。var native_sign new NativeFunction(func_addr, ‘pointer’, [‘pointer’, ‘pointer’, ‘pointer’]);最后构造参数进行调用并与真实抓包得到的签名对比。如果一致说明算法还原成功。实操心得Native逆向最头疼的是混淆。抖音的SO库很可能使用了控制流扁平化、指令替换、字符串加密等混淆技术。IDA的伪代码可能会变得极其混乱。这时需要动态跟踪多下断点用Frida或调试器如GDB单步跟踪观察数据流。关注输入输出始终牢记函数的输入请求参数Map和输出32位签名在混乱的逻辑中寻找处理这些数据的关键节点。大胆假设小心验证先假设它是标准哈希算法尝试用常见方式排序加盐MD5去模拟再通过动态调用验证。6. 算法复现与Python实现示例经过动态和静态分析我们假设算法为将所有请求参数不包括reuqest_sign本身按Key字典序升序排列拼接成key1value1key2value2的格式然后在末尾拼接一个固定盐值字符串最后计算其MD5值32位小写十六进制。下面是一个Python实现的示例import hashlib import urllib.parse def generate_request_sign(params, secret_key‘your_decrypted_secret’): “”” 生成抖音私信请求签名 (reuqest_sign) :param params: dict, 所有请求参数字典 :param secret_key: str, 从SO中分析得到的固定盐值 :return: str, 32位小写的reuqest_sign “”” # 步骤1: 移除已存在的sign参数如果有并按key排序 filtered_params {k: v for k, v in params.items() if k ! ‘reuqest_sign’} sorted_items sorted(filtered_params.items(), keylambda x: x[0]) # 步骤2: 拼接成 keyvalue 格式 query_string ‘’.join([f“{k}{v}” for k, v in sorted_items]) # 步骤3: 拼接盐值 string_to_sign query_string ‘’ secret_key # 或者可能是 query_string secret_key具体需根据分析确定 # 步骤4: 计算MD5 m hashlib.md5() m.update(string_to_sign.encode(‘utf-8’)) request_sign m.hexdigest() return request_sign # 使用示例 params { ‘to_user_id’: ‘123456789’, ‘text’: ‘你好这是测试消息’, ‘client_msg_id’: ‘abcdefg123456’, ‘device_id’: ‘888888888888888’, ‘iid’: ‘9999999999’, ‘ts’: str(int(time.time())), # 时间戳 ‘_rticket’: str(int(time.time() * 1000)), # 毫秒时间戳 } secret ‘xxxxxxxx’ # 此处替换为逆向分析得到的真实盐值 sign generate_request_sign(params, secret) print(f“Generated reuqest_sign: {sign}”)关键点secret_key是这个算法的核心机密需要从SO文件中逆向得到。它可能是一个明文字符串也可能是一个经过简单变换如Base64解码、与固定值异或后的字符串。参数中通常包含时间戳ts,_rticket这意味着签名是动态的每次请求都需要重新计算。空值参数的处理有些实现会过滤掉value为null或空字符串的参数这也需要根据实际分析确定。7. 常见问题、反调试对抗与排查技巧在实际逆向过程中绝不会一帆风顺。以下是几个我踩过的坑和对应的解决思路。7.1 抓包失败或网络错误现象配置好代理后抖音App无法联网或提示网络错误。原因SSL Pinning证书绑定生效。解决使用Frida脚本Hook证书验证相关方法。常用脚本如frida-ssl-unpinning或针对性Hook抖音使用的网络库如Cronet的证书检查方法。如果通用脚本无效需要自己分析是哪个类的方法进行了校验。7.2 Frida无法附加或进程崩溃现象执行frida -U -f com.ss.android.ugc.aweme后App闪退或Frida报错。原因App检测到了Frida。抖音这类大型应用普遍集成了反调试、反注入机制。解决重命名Frida Server将frida-server文件改名为其他名字如fs128并相应修改连接脚本。使用非常规端口Frida默认使用27042端口可以修改server启动参数换端口。使用隐藏性更好的工具如objection基于Frida的anti-root-detection插件可以尝试绕过一些检测。Patch应用修改APK去除反调试代码。这需要一定的逆向功底找到检测点如读取/proc/self/status检查TracerPid或检查frida-gadget等特征并NOP掉相应的指令。7.3 签名算法频繁变更或有多版本现象今天还原的算法明天就失效了。或者同一个App的不同接口、不同版本使用了不同的签名算法。原因服务端可能支持多种签名算法通过某个版本号或字段标识或者App定期更新更换了密钥或算法。解决动态获取算法标识分析请求参数中是否有一个如sign_method、version或as、cp这样的字段它可能指示了使用的算法版本。Hook多个疑似函数不要只Hook一个点对项目中所有与sign、encrypt、hash相关的方法都进行监控。关注SO库更新每次App升级都要重新检查核心SO文件是否有更新算法逻辑是否迁移。7.4 算法复杂度高或混淆严重现象IDA反编译的伪代码逻辑极其复杂充斥着大量无意义变量和跳转。解决黑盒测试如果目标只是生成可用的签名而非完全理解算法可以采用“黑盒”方式。用Frida Hook最终生成签名的函数记录其输入和输出构建一个巨大的“输入-输出”映射表。对于简单参数变化的请求甚至可以用机器学习的方法去拟合这个函数。但这只适用于参数空间有限的情况。聚焦数据流在IDA中不追求理解每一行代码而是专注于跟踪原始参数字符串和最终签名结果之间的数据流。使用IDA的交叉引用Xrefs功能跟踪关键数据的传递过程。利用符号执行高阶对于极其复杂的混淆可以考虑使用angr等符号执行框架但学习成本很高。8. 总结与安全思考通过这一整套从抓包、Java层Hook到Native层逆向的分析流程我们最终揭开了reuqest_sign参数的神秘面纱。它本质上是一个基于哈希的消息认证码HMAC的变种核心是确保请求的完整性和来源合法性。客户端使用一个与服务端共享的密钥盐对特定的请求参数序列进行计算服务端用同样的规则验签不一致则拒绝请求。这个过程让我深刻体会到大型互联网企业在API安全设计上的考量移动端核心逻辑下沉将关键算法放在Native层SO库增加逆向难度和成本。代码混淆与加固普遍使用商业加固方案对SO库和Java代码进行混淆、加壳防止静态分析。动态防御集成反调试、反注入机制对抗动态分析工具。算法与密钥分离签名算法可能相对固定但密钥可能动态更新或根据不同场景变化。作为开发者从防御角度我们可以学习这种多层防御的思路而从安全研究角度这个过程锻炼的是在复杂对抗环境下综合运用静态分析、动态调试、代码跟踪等多种技术解决问题的能力。最后再次强调所有技术研究应在法律允许和道德规范的范围内进行尊重知识产权切勿将技术用于破坏系统安全或从事非法活动。理解是为了更好地建设和防御。
返回列表