1. 项目概述与核心价值
最近在移动安全研究圈里,快手APP的签名算法一直是大家讨论的热点。无论是做数据采集、风控研究,还是单纯想理解大型APP如何保护其API通信安全,逆向分析其签名机制都是一个绝佳的实战案例。这个项目标题“快手APP逆向实战:手把手教你用Python复现sig3和tokensig签名算法”直指核心,它不是一个泛泛而谈的教程,而是瞄准了快手APP中两个关键的、用于接口请求认证的签名参数:
sig3
和
tokensig
。对于刚接触逆向的新手,可能会觉得无从下手;对于有经验的开发者,也可能在复杂的混淆和加密逻辑中迷失方向。这篇文章的目的,就是充当你的“领航员”,用Python作为主要工具,带你一步步拆解、分析并最终复现这两个签名算法,让你不仅能拿到可运行的代码,更能透彻理解其背后的设计思路和实现逻辑。
简单来说,
sig3
和
tokensig
是快手APP与服务器端进行API交互时的“身份证”和“通行证”。服务器通过验证这两个签名的有效性,来判断请求是否来自合法的、未被篡改的客户端。我们的目标,就是通过逆向工程,弄清楚这个“身份证”和“通行证”是如何制作出来的,并用Python代码模拟这个过程。这个过程会涉及到安卓APK的反编译、Java代码的静态分析、关键算法的定位、以及最终用Python进行算法还原和模拟请求。无论你是想学习APP逆向的实战技巧,还是需要解决特定的数据采集需求,这篇文章提供的思路和代码都将是一份宝贵的参考资料。我会尽量用通俗的语言解释每个步骤,并提供完整的、可运行的代码片段,确保你能够跟着做下来。
2. 逆向工程的核心思路与准备工作
逆向工程不是盲目地乱翻代码,它需要一套清晰的策略。对于快手APP的签名算法逆向,我们的核心思路可以概括为“由外及内,动态结合静态”。首先,我们需要理解签名的应用场景:它出现在网络请求中。因此,我们的切入点就是捕获和分析快手APP发出的网络请求,特别是关注请求头(Headers)或请求体(Body)中那些看起来像随机字符串的参数,
sig3
和
tokensig
通常就在其中。
2.1 工具链准备:你的数字手术刀
工欲善其事,必先利其器。在开始之前,你需要准备好以下工具,它们构成了我们逆向分析的“手术台”:
抓包工具
:这是我们的“眼睛”。推荐使用
Charles
或
Fiddler
。你需要配置好手机代理,并安装抓包工具的CA证书到手机中,以便能够解密HTTPS流量(对于快手这类APP,几乎全部是HTTPS)。抓包的目的很明确:找到包含
sig3
和
tokensig
的请求,记录下完整的URL、Headers、Body。一个典型的发现可能是一个视频列表请求,其URL参数或Headers里带着长长的
sig3
字符串。
反编译与静态分析工具
:这是我们的“显微镜”。我们需要拆解APK文件,查看其Java/Smali代码。
-
APK提取
:可以从手机中提取已安装的APP,或者从可靠的第三方市场下载特定版本的APK。
注意
:为了分析稳定性和代码可读性,建议选择一个较旧但功能完整的版本,因为最新版本的混淆和加固可能更强。
-
反编译工具
:
JADX-GUI
是首选。它可以将APK中的DEX文件反编译成可读性非常高的Java代码,并且支持全局搜索、跳转引用,极大提升了分析效率。
-
备用工具
:
Apktool
可以用来解包APK资源文件和
Smali
代码(一种安卓字节码的汇编语言)。当
JADX
反编译出的Java代码逻辑混乱时,查看
Smali
代码有时能获得更准确的控制流信息。
动态调试工具(可选但推荐)
:这是我们的“内窥镜”。静态分析有时会陷入复杂的调用链。使用动态调试工具,如
Frida
,可以在APP运行时,注入我们的脚本,实时打印函数参数、返回值、甚至修改逻辑。这对于验证算法猜测、追踪加密函数的输入输出至关重要。
开发环境
:这是我们的“复原工作台”。我们需要
Python 3.7+
环境,以及一些关键的库:
-
requests
: 用于模拟最终的网络请求。
-
frida-tools
: 如果你使用Frida进行动态分析。
-
可能用到的加密库:
hashlib
,
hmac
,
Crypto
(来自
pycryptodome
),用于复现各种哈希和加密算法。
2.2 逆向策略:从流量到代码
准备好工具后,我们开始实施逆向策略:
流量捕获与参数定位
:启动抓包工具和快手APP,进行一些常规操作,如刷新首页、观看视频、查看评论。在抓包工具中过滤
kuaishou.com
或相关域名,仔细寻找请求中包含
sig3
和
tokensig
的接口。记录下这个请求的所有细节,包括时间戳、其他参数(如
__NS_sig3
、
__NStokensig
等可能的不同名称)。这是我们的“已知结果”,后续所有分析都为了复现生成这个结果的过程。
搜索与定位
:将APK文件拖入
JADX-GUI
。反编译完成后,利用其强大的搜索功能,直接搜索字符串
sig3
和
tokensig
。你可能会找到许多包含这些字符串的类,比如参数拼接的代码、网络拦截器的代码、或者签名计算类的定义。重点关注那些看起来像是在“计算”、“生成”、“签名”的类和方法。通常,签名生成的代码会集中在一个或多个专门的
XXXSigner
、
SignatureHelper
或
SecurityUtils
这样的类中。
关键代码分析
:找到疑似签名生成的函数后(例如一个名为
calculateSig3
或
getTokenSig
的方法),仔细阅读其代码。你需要关注:
-
输入参数
:这个方法接收哪些参数?通常是URL路径、查询参数、请求体、时间戳、设备信息等。
-
算法流程
:代码中调用了哪些加密相关函数?如
MessageDigest.getInstance(“SHA-256”)
、
Mac.getInstance(“HmacSHA256”)
、
Cipher.getInstance(“AES/…”)
等。这些是算法类型的直接提示。
-
密钥来源
:加密或HMAC所需的密钥从哪里来?可能是硬编码在代码中的字符串(经过简单编码),也可能是从服务器动态获取后保存在本地。寻找密钥的赋值和加载过程。
-
输出处理
:计算出的字节数组是如何变成最终的字符串的?常见的是Base64编码或十六进制(Hex)字符串。
注意
:商业APP的代码通常经过了混淆(Proguard)。类名、方法名、变量名可能变成了
a
,
b
,
c
这种无意义的字符。这时,你需要更多地依赖对代码逻辑(如循环、条件判断、API调用)的分析,以及结合动态调试来确认函数功能。
3. sig3签名算法深度解析与复现
sig3
签名在快手的接口中扮演着请求完整性和身份验证的角色。根据对多个版本快手的逆向分析,其核心思想是对请求的特定要素(如URL路径、排序后的查询参数、请求体等)进行拼接,然后使用一种哈希算法(可能是变种的HMAC或自定义哈希)进行计算,最后进行编码输出。请注意,不同版本、不同接口的
sig3
算法细节可能有细微差别,以下是一个典型的、经过简化的逻辑还原,旨在阐明其核心原理和复现方法。
3.1 算法输入与参数收集
sig3
的生成通常依赖于一组动态和静态的参数。我们的Python复现代码首先需要能模拟收集这些参数:
请求路径(Path)
:例如
/api/v1/feed
。
查询参数(Query Params)
:将URL中的所有查询参数(如
?type=hot&page=1
)进行收集。
关键步骤
:需要按照参数名的字典序(ASCII码顺序)进行排序。这是防止参数顺序不同导致签名不一致的常见做法。
请求体(Body)
:对于POST请求,需要获取其请求体内容。如果是JSON格式,通常直接使用原始JSON字符串(或去除空白字符后的字符串)。
时间戳(Timestamp)
:一个当前时间的Unix时间戳(秒级或毫秒级)。
设备标识符
:如
device_id
、
install_id
、
openudid
等,这些值通常从APP的本地存储或系统API中获取。在模拟时,我们可以从一个有效的抓包请求中提取并固定使用。
盐值(Salt)或密钥
:一个固定的、硬编码在APP中的字符串,用于增加哈希的复杂度,防止被轻易破解。这是算法的核心秘密之一,需要通过逆向分析在代码中寻找。
# 示例:参数收集与预处理
import time
import urllib.parse
def collect_sig3_params(url, body_json=None, device_info=None):
"""
收集并预处理用于计算sig3的参数。
:param url: 完整的请求URL,如 https://api.kuaishou.com/api/v1/feed?type=hot&page=1
:param body_json: POST请求的JSON体,字符串格式。
:param device_info: 字典,包含 device_id, install_id 等信息。
:return: 处理后的参数字典和排序后的查询字符串。
"""
parsed_url = urllib.parse.urlparse(url)
path = parsed_url.path # 例如:/api/v1/feed
# 解析并排序查询参数
query_params = urllib.parse.parse_qs(parsed_url.query)
# 注意:parse_qs返回的值是列表,需要处理。这里简化,假设每个键对应单个值。
sorted_query_items = sorted([(k, v[0] if isinstance(v, list) and len(v)==1 else v) for k, v in query_params.items()])
sorted_query_str = '&'.join([f'{k}={v}' for k, v in sorted_query_items])
# 请求体处理
body_str = ''
if body_json:
# 实际中可能需要去除JSON中的空白字符,确保与APP行为一致
import json
body_str = json.dumps(json.loads(body_json), separators=(',', ':')) # 紧凑格式
timestamp = int(time.time()) # 秒级时间戳
params = {
'path': path,
'sorted_query': sorted_query_str,
'body': body_str,
'timestamp': timestamp,
'device_id': device_info.get('device_id', ''),
'install_id': device_info.get('install_id', ''),
# … 其他设备参数
}
return params, sorted_query_str
3.2 核心计算逻辑还原
参数准备好后,接下来就是核心的签名计算。逆向分析发现,
sig3
的计算并非简单的标准HMAC,它可能包含多轮哈希、特定拼接规则和自定义的变换。
拼接规则
:将上述参数按照一个固定的顺序和分隔符拼接成一个大的字符串。例如:
path + “|” + sorted_query_str + “|” + body_str + “|” + str(timestamp) + “|” + device_id + …
。分隔符可能是竖线
|
、冒号
:
或空字符串,这需要逆向确认。
哈希算法
:对拼接后的字符串进行哈希运算。常见的选择是
SHA-256
。但快手可能使用了
HMAC-SHA256
,即需要一个密钥(上文提到的盐值)。
关键点
:这个密钥可能不是直接使用,而是经过了某种预处理(如与另一个字符串拼接后再取哈希作为实际密钥)。
编码输出
:计算得到的哈希值(字节数组)会进行Base64编码。有时,Base64结果还会进行一些字符替换(如将
+
换成
–
,
/
换成
_
,以符合URL安全要求),并可能截取特定长度。
# 示例:sig3计算核心函数(假设为HMAC-SHA256变种)
import hmac
import hashlib
import base64
def calculate_sig3_core(param_string, secret_salt):
"""
计算sig3的核心哈希函数。
:param param_string: 拼接好的参数字符串。
:param secret_salt: 逆向得到的盐值或密钥。
:return: sig3签名字符串。
"""
# 步骤1:密钥预处理(示例:将盐值与固定字符串拼接后取SHA256作为HMAC密钥)
key_seed = secret_salt + "a_fixed_string_from_app"
hmac_key = hashlib.sha256(key_seed.encode('utf-8')).digest()
# 步骤2:使用预处理后的密钥进行HMAC-SHA256计算
signature = hmac.new(hmac_key, param_string.encode('utf-8'), hashlib.sha256).digest()
# 步骤3:Base64编码并做URL安全处理
sig_b64 = base64.urlsafe_b64encode(signature).decode('utf-8').rstrip('=')
# 可能还有额外的字符替换,例如:
# sig_b64 = sig_b64.replace('+', '-').replace('/', '_')
# 步骤4:可能截取前N位(例如前16位)作为最终的sig3
final_sig3 = sig_b64[:16]
return final_sig3
# 整合函数
def generate_sig3(url, body_json, device_info, secret_salt):
params, sorted_query = collect_sig3_params(url, body_json, device_info)
# 按照逆向分析的顺序拼接字符串
param_string_to_sign = f"{params['path']}|{params['sorted_query']}|{params['body']}|{params['timestamp']}|{params['device_id']}"
sig3 = calculate_sig3_core(param_string_to_sign, secret_salt)
return sig3, params['timestamp']
实操心得
:
secret_salt
的寻找是逆向的难点。它可能被隐藏在字符串常量池里,可能被分割成多段,也可能经过了简单的XOR或Base64编码。在JADX中,可以搜索一些常见的加密算法初始化代码附近出现的字符串常量。动态调试
Frida
在这里可以大显身手:在疑似计算签名的函数入口处打印传入的密钥参数,是最直接有效的方法。
4. tokensig签名算法深度解析与复现
tokensig
(或类似名称)通常与会话(Session)或令牌(Token)的生命周期绑定,用于验证用户身份的持续有效性。它可能依赖于一个长期有效的
token
(例如登录后下发的
access_token
),并结合其他动态因子生成。其安全等级通常比
sig3
更高,算法也可能更复杂,可能涉及非对称加密或更复杂的密钥派生。
4.1 tokensig的生成依赖
Access Token
:用户登录后获得的核心凭证,是生成
tokensig
的基础。
动态因子
:如当前时间戳、请求的某种特征(可能是URL的哈希值)、一个随机数(Nonce)等,用于防止重放攻击。
设备指纹
:与
sig3
类似,会包含设备唯一标识。
私钥或固定密钥
:可能是存储在APP内的一个固定RSA私钥(PEM格式或其衍生值),或者一个用于HMAC的密钥。这个密钥的安全性非常高,可能被深度混淆或放在so库(Native代码)中。
4.2 算法流程推测与复现
由于
tokensig
涉及更高的安全性,其算法变种更多。这里提供两种最常见的思路:
思路A:基于HMAC的增强令牌签名
这种方式与
sig3
类似,但输入和密钥不同。
def generate_tokensig_hmac(access_token, timestamp, device_fingerprint, secret_key):
"""
假设tokensig是HMAC-SHA512的变种。
"""
# 拼接规则需要逆向确定,例如:token + timestamp + device_fp
message = f"{access_token}:{timestamp}:{device_fingerprint}"
# 使用一个独立的、更强的密钥
signature = hmac.new(secret_key.encode('utf-8'), message.encode('utf-8'), hashlib.sha512).digest()
# 输出可能为Hex字符串,而非Base64
tokensig = signature.hex()[:32] # 取前32位十六进制字符
return tokensig
思路B:基于非对称加密的签名
这种更为安全,APP端使用私钥对某个消息进行签名。
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
def generate_tokensig_rsa(access_token, timestamp, private_key_pem):
"""
假设tokensig是RSA-PKCS1_v1_5签名。
:param private_key_pem: 从APP中逆向提取出的PEM格式私钥字符串(可能被混淆)。
"""
# 1. 拼接签名字符串
message = f"{access_token}|{timestamp}"
# 2. 计算消息的哈希
digest = SHA256.new(message.encode('utf-8'))
# 3. 加载私钥并签名
private_key = RSA.import_key(private_key_pem)
signer = pkcs1_15.new(private_key)
signature_bytes = signer.sign(digest)
# 4. 编码输出,可能是Base64
import base64
tokensig = base64.urlsafe_b64encode(signature_bytes).decode('utf-8').rstrip('=')
return tokensig
注意事项
:在实际的逆向中,私钥几乎不可能以明文PEM格式存在。它很可能被分割、编码(如Base64)、或与其它数据XOR后存储。还原过程需要仔细分析密钥的加载和解码函数。此外,签名算法也可能是
ECDSA
等。动态调试
Frida
在这里几乎是必需品,可以挂钩到密码学库(如
OpenSSL
或
BouncyCastle
)的相关函数,直接捕获输入、输出和密钥。
4.3 整合与请求模拟
当我们成功复现了
sig3
和
tokensig
的生成函数后,最后一步就是将它们整合到网络请求中,模拟一个完整的、可以被服务器接受的请求。
import requests
import json
def make_kuaishou_request(api_url, method='GET', post_data=None, access_token=None, device_info=None, secrets=None):
"""
模拟发送一个快手API请求。
:param secrets: 字典,包含 sig3_salt, tokensig_key 等逆向得到的密钥。
"""
# 1. 生成sig3
body_json_str = json.dumps(post_data) if post_data else None
sig3, timestamp = generate_sig3(api_url, body_json_str, device_info, secrets.get('sig3_salt'))
# 2. 生成tokensig (如果有token)
tokensig = None
if access_token:
# 假设使用HMAC方式
device_fp = device_info.get('device_id', '') + device_info.get('install_id', '')
tokensig = generate_tokensig_hmac(access_token, timestamp, device_fp, secrets.get('tokensig_key'))
# 3. 构造请求头
headers = {
'User-Agent': 'Kwai-Android/…', # 模拟真实的快手UA
'Content-Type': 'application/json',
'X-REQUEST-ID': str(timestamp), # 或其他生成ID的方式
}
if sig3:
headers['X-SIG3'] = sig3 # 实际Header名需根据抓包确定
if tokensig:
headers['X-TOKENSIG'] = tokensig # 实际Header名需根据抓包确定
if access_token:
headers['Authorization'] = f'Bearer {access_token}'
# 4. 发送请求
if method.upper() == 'GET':
response = requests.get(api_url, headers=headers)
else:
response = requests.post(api_url, headers=headers, json=post_data)
return response
# 使用示例
device_info = {'device_id': 'your_device_id', 'install_id': 'your_install_id'}
secrets = {'sig3_salt': '逆向得到的盐值', 'tokensig_key': '逆向得到的密钥'}
access_token = 'your_access_token_if_logged_in'
api_url = 'https://api.kuaishou.com/api/v1/feed?type=hot'
resp = make_kuaishou_request(api_url, device_info=device_info, secrets=secrets, access_token=access_token)
print(resp.status_code, resp.json())
5. 逆向过程中的常见问题与排查技巧
即使有了清晰的思路和工具,逆向过程也绝不会一帆风顺。下面记录了一些典型问题和我的解决经验。
5.1 代码混淆严重,找不到关键函数
-
问题
:在JADX中搜索
sig3
,可能只找到一些字符串常量,而计算逻辑被隐藏在以
a.a.a.a
命名的类中。
-
排查技巧
:
-
关注调用栈
:在抓包工具中,你可以看到请求发出。尝试在JADX中全局搜索你抓包到的完整URL路径的一部分,或者搜索明显的网络库类(如
OkHttp
的
Interceptor
接口实现类)。网络拦截器是签名注入的常见位置。
-
动态跟踪
:使用
Frida
Hook所有
java.security.MessageDigest
或
javax.crypto.Mac
类的
getInstance
和
update
/
doFinal
方法。当APP运行时,这些Hook点会打印出堆栈信息,告诉你是在哪个类的方法里调用了加密函数,从而逆向定位到入口。
-
特征字符串搜索
:搜索算法名称的字符串片段,如
“SHA”
、
“HMAC”
、
“AES”
、
“/ECB/”
等,这些地方附近往往是加密相关代码。
5.2 算法逻辑复杂,拼接顺序难以确定
-
问题
:知道了用HMAC-SHA256,但不知道哪些参数、以什么顺序和分隔符拼接。
-
排查技巧
:
-
对比实验法
:用
Frida
Hook疑似签名函数,在多个不同请求(不同URL、不同参数)下,打印出函数的
输入字符串
和
输出签名
。通过对比多组数据,可以反推拼接规则。例如,如果两个请求只有时间戳不同,那么输入字符串中变化的部分很可能就包含了时间戳。
-
参数篡改测试
:在Hook中,尝试修改传入函数的某个参数(如将时间戳加1),观察输出签名的变化。如果签名完全变了,说明该参数参与了计算;如果没变,则可能没参与。
注意
:篡改可能引发APP崩溃,需谨慎。
-
日志分析
:有些APP在调试版本或特定条件下会输出详细的日志。可以尝试在
Logcat
中过滤APP的日志标签,寻找签名计算相关的打印信息。
5.3 密钥被隐藏或动态生成
-
问题
:找到了算法,但用于HMAC的密钥或用于RSA签名的私钥不是简单的字符串,而是一段看起来像乱码的数据,或者是从另一个函数调用返回的。
-
排查技巧
:
-
追溯来源
:在JADX中,查看该密钥变量的赋值语句,向上追溯它的来源。它可能来自一个
getKey()
方法,该方法可能从本地文件、数据库或一个复杂的初始化函数中读取。
-
内存DUMP
:对于深度混淆或Native层的密钥,可以在APP运行起来、密钥已加载到内存后,使用
Frida
脚本搜索内存中的特定模式(如PEM密钥的头尾
—–BEGIN PRIVATE KEY—–
),或者直接Dump持有密钥对象的内存区域。
-
静态分析Native库(.so文件)
:如果密钥逻辑在C/C++层,需要使用
IDA Pro
或
Ghidra
反汇编so文件进行分析。这难度较大,通常需要先确认Java层调用了哪些Native方法。
5.4 复现的签名与服务端校验不匹配
-
问题
:所有代码都写好了,但生成的签名服务器不认可,返回签名错误。
-
排查技巧
:
-
严格比对输入
:确保你的Python代码收集的参数与APP运行时完全一致。包括:URL编码(空格是
%20
还是
+
?)、JSON字符串格式(是否有多余空格、换行符?字典顺序是否一致?)、时间戳精度(秒还是毫秒?)。
-
检查编码细节
:哈希前的字符串编码是
UTF-8
还是
GBK
?Base64编码是标准还是URL安全的?是否有额外的字符替换或截取?
-
分步验证
:如果可能,用
Frida
在APP运行时,将你的Python代码生成的中间结果(拼接后的字符串)与APP内部的中间结果进行比对。也可以将APP计算出的最终签名与你生成的签名进行比对,看差异在哪里。
-
版本差异
:确认你分析的APK版本与你抓包使用的APP版本是否一致。不同版本的算法可能有升级。
下表总结了一些常见错误和排查方向:
| 签名完全不对 | 算法根本错误 | 重新确认Hook到的加密函数类型(SHA256? HMAC?)。检查密钥是否正确。 |
| 签名部分匹配(前几位相同) | 编码或截取问题 | 检查Base64/Hex编码方式,检查签名是否被截断(取前N位)。 |
| 只有带特定参数的请求失败 | 参数拼接顺序或遗漏 | 对比成功和失败请求的Hook输入字符串,找出差异点。检查是否遗漏了某些固定参数(如版本号)。 |
| 时间戳相关错误 | 时间戳格式或同步问题 | 检查时间戳是10位(秒)还是13位(毫秒)。检查是否需要与服务器时间同步。 |
| 仅在登录后请求失败 | tokensig算法错误或token状态 |
确认
access_token 是否正确。检查 tokensig 算法是否依赖了其他动态因子(如请求ID)。 |
逆向工程是一场与开发者的智力博弈,需要耐心、细心和大量的尝试。每一次成功的复现,不仅解决了一个具体问题,更是对移动应用安全机制的一次深刻理解。希望这份详细的指南和代码框架,能为你打开快手APP逆向,乃至更广阔的移动安全研究之门。记住,核心思路和调试方法比某一行代码更重要。