TikTok IM协议逆向实战:解密WSS通信与模拟商品卡片发送

📅 2026/7/29 7:18:30 👁️ 阅读次数 📝 编程学习
TikTok IM协议逆向实战:解密WSS通信与模拟商品卡片发送

1. 项目概述:从一次“发送失败”开始的逆向之旅

如果你也尝试过在TikTok上通过私信发送一个商品链接,却发现对方收到的只是一个无法点击的纯文本,或者你好奇那些带货主播是如何在聊天中瞬间弹出精美的商品卡片,那么你大概能理解我最初的好奇心。这背后,是TikTok IM(即时通讯)协议在起作用,更具体地说,是WebSocket Secure连接上流动的、经过层层加密的消息。作为一个对网络协议和客户端逆向有浓厚兴趣的开发者,我决定亲手揭开这层神秘的面纱,目标很明确:逆向分析TikTok IM的WSS通信,理解其消息加密机制,并最终实现模拟发送一个完整的、可交互的商品卡片消息。这个过程不仅是对一个流行应用协议的探索,更是一次完整的网络协议逆向、密码学应用和客户端行为模拟的实战演练。无论你是安全研究员、爬虫工程师,还是单纯对移动应用底层通信机制着迷的极客,这篇手把手的记录都能为你提供一条清晰的路径和一堆实实在在的“坑位”提示。

2. 核心思路与技术栈选型

逆向一个像TikTok这样拥有庞大工程师团队和成熟风控体系的应用协议,不能靠蛮力。我的核心思路是“由外而内,动静结合”。首先,我们需要一个能够拦截和查看所有网络请求的工具,这是我们的“眼睛”。其次,由于核心逻辑往往封装在客户端内部,我们需要能够动态调试或静态分析客户端代码的能力,这是我们的“手术刀”。基于这个思路,我选择了以下技术栈组合,并解释为什么这么选。

2.1 抓包与协议分析工具:Fiddler Everywhere + 自签名证书

为什么是Fiddler Everywhere而不是Charles或Wireshark?对于HTTPS/WSS流量解密,三者原理类似,都需要在设备上安装根证书。Fiddler Everywhere的界面更现代,对WebSocket消息的展示和过滤非常直观,而且跨平台支持好。Wireshark更底层,能抓到所有网卡流量,但对于需要解密HTTPS的移动端场景,配置略繁琐。Charles是经典选择,但Fiddler Everywhere的个人免费版功能已经足够强大。

注意:在Android高版本(特别是Android 7+)上,系统不再信任用户安装的证书,除非将证书安装到系统证书目录,这通常需要Root权限。对于非Root设备,一个可行的方案是使用VirtualXposed、太极等虚拟环境,或者直接使用已经Root的测试设备/模拟器。iOS设备同样需要手动信任已安装的描述文件。这是逆向分析移动端App的第一道,也是劝退很多人的一道坎。

2.2 动态调试与代码分析:Jadx-GUI + Frida

Jadx-GUI:用于将TikTok的APK文件反编译成可读的Java/Kotlin代码。它的优势在于图形化界面,搜索、跳转、查看调用关系非常方便,是静态分析的起点。我们可以通过搜索关键词如“WebSocket”、“wss”、“encrypt”、“商品”、“commerce”、“card”等来定位相关代码模块。

Frida:这是本次逆向的“神器”。它是一个动态插桩框架,可以在应用程序运行时注入JavaScript代码,来Hook(钩子)任何函数,监控参数、返回值,甚至修改逻辑。当静态分析遇到混淆(TikTok肯定有重度混淆)导致代码难以阅读时,Frida可以让我们在运行时观察真实的数据流,验证我们的猜测。例如,我们可以Hook消息发送前的加密函数,直接打印出加密前的明文和加密后的密文。

2.3 协议模拟实现:Python +websockets

一旦我们分析清楚了消息格式和加密算法,就需要用代码来模拟客户端行为进行验证和发送。Python因其丰富的库和简洁的语法成为首选。websockets库提供了异步的WebSocket客户端实现,非常适合用于和服务器进行长连接通信。此外,我们可能还需要protobuf(如果TikTok使用Protocol Buffers序列化)、cryptography(用于实现加密算法)等库。

2.4 目标设备与环境

  • 测试设备:一台已经Root的Android手机或性能足够的Android模拟器(如雷电模拟器、夜神模拟器)。Root是为了方便安装系统级证书和进行更深度的Hook。
  • TikTok版本:选择一个相对较旧但功能稳定的版本(例如,v28.x左右)。新版本可能增加了更强的防护或修改了协议,增加逆向难度。可以从第三方APK下载站获取历史版本。
  • 网络环境:确保测试设备可以正常访问TikTok服务。这需要自行解决网络连通性问题,但请注意,我们的所有操作仅限于协议研究和学习,必须在法律和TikTok用户协议允许的范围内进行。

3. 逆向分析实战:定位、抓包与解密

理论准备就绪,现在让我们戴上手套,拿起手术刀,开始实操。

3.1 第一步:建立抓包环境

  1. 配置Fiddler Everywhere:启动Fiddler Everywhere,在设置中开启HTTPS解密功能。它会生成一个根证书。
  2. 安装证书到测试设备
    • 确保手机和电脑在同一局域网。
    • 在手机Wi-Fi设置中,配置代理为手动,主机填电脑的IP地址,端口填Fiddler的监听端口(默认8888)。
    • 用手机浏览器访问http://<电脑IP>:8888,下载并安装Fiddler的根证书。
    • 对于Root设备:使用adb shellmount命令将系统分区挂载为可写,然后将证书文件(通常需要从PEM格式转换为DER格式并重命名)推送到/system/etc/security/cacerts/目录,并修改权限为644。重启后,证书即被系统信任。
  3. 验证抓包:在手机上打开任意一个使用HTTPS的App(如浏览器),在Fiddler中应该能看到解密的HTTPS流量。如果看到的是Tunnel to ... 443,说明证书未成功被系统信任。

3.2 第二步:捕获TikTok IM的WSS连接

  1. 在配置好代理并信任证书的手机上,打开TikTok。
  2. 进行会触发IM通信的操作:打开私信对话框,发送一条普通文本消息。
  3. 回到Fiddler Everywhere,你应该能看到大量的tiktokv.comtiktokcdn.com等域名的请求。我们需要从中找到WebSocket连接。
  4. 在Fiddler的会话列表里,查找Protocol列为HTTP/1.1 101 Switching Protocols的请求。这通常就是WebSocket的升级请求。查看其完整URL,可能会是类似于wss://webcast3-ws-web-*.tiktokv.com/...这样的格式。这就是IM的WSS连接。
  5. 选中这个WebSocket会话,在右侧的Inspectors选项卡中选择WebSocket视图。在这里,你可以实时看到客户端和服务器之间来回发送的消息帧。不过,此时你看到的Payload很可能是乱码或二进制数据——因为它们被加密了。

3.3 第三步:静态分析定位加密逻辑

现在我们知道WSS连接在哪,但消息是加密的。下一步是找出加密发生在代码的哪个位置。

  1. 使用Jadx-GUI打开TikTok APK。这个过程可能需要几分钟,因为APK很大且混淆严重。
  2. 关键词搜索
    • 搜索连接相关WebSocket,wss://,okhttp3.WebSocketListener(TikTok很可能使用OkHttp作为网络库)。
    • 搜索消息发送sendMessage,encode,encrypt。可以尝试搜索“消息”、“发送”的中文拼音或常见翻译,如xiaoxi,fasong,message,send
    • 搜索商品相关commerce,product,goods,card,商品,卡片。这有助于我们定位到生成商品卡片消息体的具体代码。
  3. 分析调用链路:通过搜索,你可能会找到几个候选的类和方法。例如,一个名为com.bytedance.ies.im.core.service.a(类名是混淆后的)的类,其中有一个sendMessage方法。点进去,查看它的代码逻辑。通常,发送流程会是:构造消息体 -> 序列化(可能是JSON或Protobuf)-> 加密 -> 通过WebSocket发送。
  4. 寻找加密函数:在sendMessage方法内部或它调用的方法里,寻找诸如Cipher.getInstance,AES/ECB/PKCS5Padding,encrypt,encode等调用。注意查看方法的参数和返回值。你可能会发现类似byte[] encryptData(byte[] plainText, byte[] key)这样的方法签名。

实操心得:面对重度混淆,类名和方法名可能毫无意义,如a.a.b.c()。此时,不要纠结于名字,而要关注代码“结构”和“常量”。例如,查找硬编码的字符串常量(Jadx可以搜索字符串),如算法名称"AES"、模式"ECB"、填充"PKCS5Padding",或者查找特定的数字常量(如密钥长度16、24、32对应AES-128/192/256)。这些是破解混淆的锚点。

3.4 第四步:动态Hook验证与密钥提取

静态分析给出了可疑的加密函数位置,但密钥从哪里来?加密模式是否正确?需要用Frida在运行时验证。

  1. 编写Frida脚本:假设我们通过静态分析,怀疑com.bytedance.ies.im.core.utils.Encryptor.encryptAES是加密函数。
    // hook_im.js Java.perform(function() { var Encryptor = Java.use("com.bytedance.ies.im.core.utils.Encryptor"); // Hook encryptAES方法,假设它接收两个参数:明文byte数组和密钥byte数组 Encryptor.encryptAES.implementation = function(plainData, key) { console.log("\n[+] EncryptAES Called!"); // 打印明文(可能是Hex或Base64) console.log("Plaintext (hex): " + bytesToHex(plainData)); console.log("Plaintext (str): " + Java.use("java.lang.String").$new(plainData)); // 打印密钥 console.log("Key (hex): " + bytesToHex(key)); // 调用原方法获取加密结果 var result = this.encryptAES(plainData, key); console.log("Ciphertext (hex): " + bytesToHex(result)); // 同时,我们可以尝试在Fiddler中抓取同一时刻发送的WSS帧,对比这个Ciphertext是否一致 console.log("---"); return result; }; // 辅助函数:将byte数组转为Hex字符串 function bytesToHex(bytes) { if (!bytes) return "null"; var hex = []; for (var i = 0; i < bytes.length; i++) { hex.push((bytes[i] & 0xFF).toString(16).padStart(2, '0')); } return hex.join(''); } });
  2. 注入并触发
    • 在电脑上启动Frida Server(需推送到手机并运行)。
    • 使用命令frida -U -l hook_im.js -f com.zhiliaoapp.musically(包名)附加到TikTok进程。
    • 在手机上再次发送一条消息。此时,你的终端应该会打印出Hook到的加密信息。
  3. 关联抓包数据:在Frida脚本运行的同时,确保Fiddler正在抓包。当脚本打印出一次加密调用时,立刻去Fiddler的WebSocket视图里,找到最新的一条客户端发送的消息帧。对比Frida打印出的Ciphertext (hex)和Fiddler中消息帧的Payload(可能需要将Payload从Base64或原始字节转换为Hex进行比较)。如果两者匹配,恭喜你,你已经精准定位了加密函数和密钥!

注意事项:密钥可能不是硬编码的,而是从服务器下发的,或者在登录时协商的。你的Hook脚本可能第一次捕获到的密钥就是正确的。但也可能需要Hook密钥的生成或获取函数。此外,加密可能不止一层,可能有先压缩再加密,或者对消息头、消息体分别加密的情况。需要耐心地层层剥离。

4. 消息结构拆解与商品卡片模拟

找到了加密方法,我们就可以尝试解密一条正常的消息,看看它的结构。

4.1 解密与解析消息格式

  1. 录制一条样本消息:在Fiddler中,找到一条你发送普通文本时对应的客户端WebSocket发送帧,复制其Payload(可能是Base64编码的)。
  2. 编写解密脚本:根据Hook到的算法(例如AES-ECB-PKCS5Padding)和密钥,用Python的cryptography库编写解密函数。
    from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend import base64 def decrypt_aes_ecb(ciphertext_b64, key_hex): key = bytes.fromhex(key_hex) cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend()) decryptor = cipher.decryptor() ciphertext = base64.b64decode(ciphertext_b64) # 注意:ECB模式不需要IV plaintext_padded = decryptor.update(ciphertext) + decryptor.finalize() # 去除PKCS5/PKCS7填充 padding_len = plaintext_padded[-1] plaintext = plaintext_padded[:-padding_len] return plaintext # 使用Hook捕获的密钥和抓包的Payload key = "你的16/24/32字节Hex密钥" payload_b64 = "从Fiddler复制的Base64" decrypted = decrypt_aes_ecb(payload_b64, key) print(decrypted.decode('utf-8', errors='ignore'))
  3. 分析明文结构:解密后的数据很可能是一种序列化格式。常见的有:
    • JSON:最易读,直接json.loads()即可。
    • Protocol Buffers (Protobuf):二进制格式,需要.proto定义文件才能正确解析。如果没有定义文件,解析起来非常困难,但可以通过分析代码中com.google.protobuf相关的使用来寻找线索,或者使用protobuf-inspector这类工具进行盲解析。
    • Thrift自定义二进制格式:可能性较小但存在。 如果是JSON,你就能清晰地看到消息结构,例如包含message_typecontentsendertimestamp等字段。content字段里可能就包含了文本或卡片信息。

4.2 构造商品卡片消息

这是本次逆向的终极目标。我们需要模仿客户端,构造一个能生成商品卡片的消息体。

  1. 定位卡片生成代码:回到Jadx,利用之前找到的与“商品”、“卡片”相关的类。搜索product_card,commerce_card,generateCard等方法。找到负责构建卡片消息体的类和方法。注意观察它需要哪些参数:商品ID (product_id)、商品标题、图片URL、价格、跳转链接等等。
  2. Hook卡片构建方法:使用Frida Hook你找到的卡片构建方法。在真实的TikTok App中,分享一个商品到私信,触发这个Hook。打印出该方法构建出的完整消息对象(或其中的关键字段)。这是获取正确消息结构的最直接方法。
  3. 组装完整消息:根据Hook得到的数据结构,用Python字典(如果最终是JSON)或Protobuf对象(如果使用Protobuf)来组装一个一模一样的消息体。消息体中message_type字段很可能是一个特定的枚举值,比如2代表富媒体消息,content字段内再嵌套具体的卡片类型和数据。
  4. 序列化与加密:将组装好的消息体,按照分析出的格式(JSON字符串或Protobuf序列化)转换成字节流。然后,调用我们逆向出来的加密函数(用Python复现),对字节流进行加密。
  5. 建立WSS连接并发送
    import asyncio import websockets import json async def send_product_card(): # 1. 建立WSS连接(需要从抓包中获取准确的WSS URL和必要的Headers,如鉴权Token) uri = "wss://webcast3-ws-web-*.tiktokv.com/..." headers = { "User-Agent": "...", "Authorization": "Bearer ...", # 关键:需要有效的登录态Token } async with websockets.connect(uri, extra_headers=headers) as websocket: # 2. 构造商品卡片消息明文 card_message = { "type": 2, # 假设2是卡片消息类型 "seq_id": 123456, "content": { "card_type": "product", "product_id": "123456789", "title": "测试商品", "image_url": "https://...", "price": "¥99.99", "jump_url": "https://www.tiktok.com/product/..." } } plaintext = json.dumps(card_message).encode('utf-8') # 3. 加密 ciphertext = encrypt_aes_ecb(plaintext, key) # 使用之前复现的加密函数 # 4. 发送(可能需要Base64编码) await websocket.send(base64.b64encode(ciphertext).decode()) print("商品卡片消息已发送!") asyncio.run(send_product_card())

核心难点与避坑

  1. 鉴权(Token):WSS连接建立时,往往需要携带用户登录凭证(Token)。这个Token通常存在于App的本地存储或内存中。可以通过Frida Hook登录后的Token存储函数来获取。没有有效的Token,服务器会拒绝连接或发送消息。
  2. 消息序列号(seq_id):IM协议通常要求消息有严格递增的序列号,用于保证消息顺序和去重。你需要从客户端代码或服务器响应中维护一个正确的序列号。
  3. 心跳保活:WSS连接需要定期发送心跳包(Ping/Pong或特定指令)来保持连接。你需要从抓包中分析出心跳包的格式和间隔,并在你的Python客户端中模拟。
  4. 风控策略:TikTok有完善的风控。短时间内发送大量消息、消息内容异常、从未知IP建立连接等行为都可能导致连接被断开或账号受限。模拟行为应尽量贴近真实用户,间隔时间随机化。

5. 常见问题排查与经验总结

在整个逆向和模拟过程中,我遇到了无数问题,以下是几个最具代表性的排查思路:

问题1:Fiddler抓不到TikTok的HTTPS/WSS流量。

  • 排查:首先确认手机代理设置正确,且电脑防火墙允许Fiddler端口连接。然后检查TikTok是否使用了证书绑定(SSL Pinning)。证书绑定会验证服务器证书是否与App内硬编码的证书匹配,绕过系统证书,导致Fiddler解密失败。
  • 解决:使用Frida脚本来禁用SSL Pinning。网上有通用的禁用脚本,也可以针对TikTok使用的网络库(如OkHttp3)进行Hook。例如,HookOkHttpClient.BuildersslSocketFactoryhostnameVerifier方法,将其替换为信任所有证书的实现。

问题2:Hook加密函数时,打印出的明文是乱码或看起来不像消息内容。

  • 排查:可能Hook错了函数,或者消息在加密前经过了压缩(如GZIP)或另一种编码(如Protobuf)。
  • 解决:向上追溯调用栈。在Frida脚本中,可以使用Java.use("android.util.Log").e("TAG", "调用栈: " + Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))来打印堆栈,看看是谁调用了这个加密函数。从而找到更早的、处理原始消息体的函数进行Hook。

问题3:模拟发送的消息,服务器不响应或返回错误。

  • 排查:这是一个综合问题。需要逐一检查:
    1. WSS连接URL和Headers:是否完全正确?特别是HostOriginUser-Agent和鉴权Header。
    2. 消息格式:加密前的明文结构是否完全正确?字段名、类型、嵌套关系是否与抓包解密后的一致?可以使用json.dumps(..., indent=2)美化打印后仔细对比。
    3. 加密算法和密钥:是否100%还原?包括算法、模式、填充、初始向量(如果有)。可以用Hook到的明文和密钥,用你的Python加密函数加密,看结果是否与Hook到的密文一致。
    4. 序列号和时序seq_id是否连续且大于上一次的值?消息发送的节奏是否过快?
  • 解决:采用“最小化验证”策略。先不发送复杂的商品卡片,而是尝试发送最简单的文本消息("hello")。如果文本消息能成功发送并被对方接收,说明连接、基础消息格式、加密都没问题,问题就出在商品卡片消息体的具体构造上。

问题4:账号因模拟行为被限制功能。

  • 经验:这是进行此类逆向模拟的最大风险。务必使用测试专用的小号,切勿使用主力账号。模拟的行为应尽可能“人性化”:连接后等待一会儿再发消息,消息间隔加入随机延迟(如3-10秒),消息内容不要完全一致。即便如此,也不能保证100%安全,要有账号被暂时限制的心理准备和备用方案。

这次对TikTok IM协议的逆向之旅,更像是一次系统的工程训练。它不仅仅关乎一个加密算法或一个API端点,而是涵盖了环境搭建、流量分析、静态逆向、动态调试、协议模拟和风控对抗等多个环节。每一个环节都可能遇到意想不到的困难,需要耐心、细致的观察和逻辑推理。最终成功发送出商品卡片的那一刻,所有的折腾都变得值得。更重要的是,这套方法论可以迁移到对其他移动应用协议的分析中。记住,逆向工程的乐趣在于探索和理解,请务必在法律和道德框架内进行,尊重知识产权和用户隐私。