分享链接除了内容 ID,还可能携带由分享账号生成的参数。这里只记三种已经通过样本回算确认的算法:小红书 shareRedId、B站 vd_source 和抖音 u_code。
真实样本中的短链码、哈希、UID、sec_uid 和主页地址统一用 ****** 占位。代码里的 UID 是人工构造的演示值,不对应这些样本中的账号。
小红书:shareRedId 可逆编码
样本
原始短链
https://xhslink.cn/o/<短链码>
跳转目标中的身份字段
shareRedId=******
xhslink.cn 和 xhslink.com 的路径只是重定向索引。先跟随 HTTP Location 取得小红书长链接,再读取其中的 shareRedId。
编码原理
小红书用户 ID 是 24 位十六进制字符串。样本中的 shareRedId 按以下步骤生成:
- 将用户 ID 转为大写。
- 将每个字符的 ASCII 码分别加上固定偏移量。
- 对得到的 24 字节字符串执行 Base64URL 编码并删除末尾
=。
固定偏移量:
[2, 6, 2, 0, 3, 5, 4, 9, 6, 7, 5, 2, 9, 8, 0, 6, 6, 3, 9, 7, 4, 5, 6, 9]
解码是上述过程的逆运算:
decoded = Base64URLDecode(shareRedId)
uid[i] = chr(ord(decoded[i]) - shifts[i])
还原实现
import base64
import re
SHIFTS = [
2, 6, 2, 0, 3, 5, 4, 9, 6, 7, 5, 2,
9, 8, 0, 6, 6, 3, 9, 7, 4, 5, 6, 9,
]
def base64url_decode(value):
padding = "=" * (-len(value) % 4)
data = base64.urlsafe_b64decode(value + padding)
return data.decode("latin1")
def base64url_encode(value):
data = base64.urlsafe_b64encode(value.encode("latin1"))
return data.decode("ascii").rstrip("=")
def decode_share_red_id(share_red_id):
transformed = base64url_decode(share_red_id)
if len(transformed) != len(SHIFTS):
raise ValueError("unexpected shareRedId length")
uid = "".join(
chr(ord(char) - SHIFTS[index])
for index, char in enumerate(transformed)
).lower()
if not re.fullmatch(r"[0-9a-f]{24}", uid):
raise ValueError("decoded value is not a 24-character user ID")
return uid
def encode_share_red_id(uid):
if not re.fullmatch(r"[0-9a-fA-F]{24}", uid):
raise ValueError("invalid user ID")
transformed = "".join(
chr(ord(char) + SHIFTS[index])
for index, char in enumerate(uid.upper())
)
return base64url_encode(transformed)
验证
使用人工构造的 UID:
uid
0123456789abcdef01234567
逐位加偏移后的字符串
27437::@>@FDLLEL64;:8:<@
Base64URL 编码
Mjc0Mzc6OkA-QEZETExFTDY0Ozo4OjxA
将最后一行交给 decode_share_red_id,会恢复为 0123456789abcdef01234567。实际样本同样执行“解码、格式检查、重新编码”三步校验,但不在文章中公开其身份值。
B站:vd_source 是 UID 的 MD5
样本
样本里的 vd_source 覆盖了两种算法。
标准字符串 MD5 样本
https://www.bilibili.com/video/BV1hngA6XErb/?share_source=copy_web&vd_source=******
数字字节 MD5 样本
https://www.bilibili.com/video/BV1JDJZzAExt?vd_source=******
b23.tv 短链需要先跟随 HTTP Location 取得 bilibili.com 长链接,再读取 vd_source。
编码原理
样本中存在两种 UID 序列化方式。
第一种直接对 UID 的十进制 ASCII 字符串计算 MD5:
A(uid) = ASCII(String(uid))
vd_source = MD5(A(uid))
第二种先把十进制字符串的每一位转换为数值字节,再计算 MD5:
B(uid) = [Number(digit) for digit in String(uid)]
vd_source = MD5(B(uid))
例如字符串 "12345678" 的两种字节表示为:
ASCII 字节: 31 32 33 34 35 36 37 38
数字字节: 01 02 03 04 05 06 07 08
MD5 不可逆,但数字 UID 是有限候选空间,因此可以通过原像搜索找到匹配 UID。
还原实现
import hashlib
def md5(data):
return hashlib.md5(data).hexdigest()
def standard_vd_source(uid):
return md5(str(uid).encode("ascii"))
def digit_bytes_vd_source(uid):
data = bytes(int(digit) for digit in str(uid))
return md5(data)
def find_uid(target, max_uid=999_999_999):
for uid in range(1, max_uid + 1):
if standard_vd_source(uid) == target:
return uid, "ascii"
if digit_bytes_vd_source(uid) == target:
return uid, "digit-bytes"
return None
搜索复杂度为 O(N)。可以把连续 UID 区间分给多个线程并行计算,但并行只改变速度,不改变匹配结果。
验证
使用人工构造的 UID 12345678:
MD5(ASCII("12345678"))
= 25d55ad283aa400af464c76d713c07ad
MD5([1, 2, 3, 4, 5, 6, 7, 8])
= 0ee0646c1c77d8131cc8f4ee65c7673b
把任一哈希交给 find_uid,都会返回 UID 12345678 和对应的序列化方式。真实的两条样本分别命中这两个分支,其哈希和还原 UID 使用 ****** 占位。
抖音:u_code 是 UID 的 23 进制编码
样本
视频短链
https://v.douyin.com/<短链码>/
第一跳 activity_info
u_code=******
social_share_user_id=******
social_author_id=******
直播短链
https://v.douyin.com/<短链码>/
第一跳参数
u_code=******
share_user_id=******
sec_relation_user_id=******
v.douyin.com 的路径是重定向索引。跟随第一跳 HTTP Location 后,需要同时读取普通查询参数和 URL 编码 JSON 中的 activity_info、share_extra_params、ecom_share_track_params。
编码原理
u_code 使用 23 个字符表示数字 UID:
字符: 0 1 2 3 4 5 6 7 8 9 a b c d e f g h i j k l m
数值: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
从左向右按普通进位制累加:
uid = 0
uid = uid * 23 + value(char)
u_code 可以还原数字 UID,但不能据此计算 sec_uid。如果同一跳转地址同时包含 share_user_id 和 sec_relation_user_id,只能说明该链接直接给出了两者的对应关系。
还原实现
ALPHABET = "0123456789abcdefghijklm"
def decode_douyin_u_code(u_code):
uid = 0
for char in u_code.lower():
digit = ALPHABET.find(char)
if digit < 0:
raise ValueError("invalid u_code")
uid = uid * 23 + digit
return uid
def encode_douyin_uid(uid):
uid = int(uid)
if uid == 0:
return "0"
result = ""
while uid > 0:
uid, digit = divmod(uid, 23)
result = ALPHABET[digit] + result
return result
Python 整数支持任意精度,可以直接处理长 UID。
验证
使用人工构造的 UID:
uid
1234567890
23 进制 u_code
87ifcgi
encode_douyin_uid(1234567890) 得到 87ifcgi,再执行 decode_douyin_u_code("87ifcgi") 会恢复为 1234567890。
真实视频样本中,u_code 的解码结果与 activity_info.social_share_user_id 完全一致;social_author_id 是内容作者 UID,不参与计算。真实直播样本中,u_code 的解码结果与 share_user_id 一致,并且同一跳转地址另外给出了对应的 sec_relation_user_id。这些身份值均用 ****** 占位。
其他字段的算法分类
| 结果类型 | 示例字段 | 可推导结论 |
|---|---|---|
| 明文用户 ID | uid、shareuid、shareUid、appuid |
格式通过后可直接得到用户标识 |
| 可逆编码或加密 | 小红书 shareRedId、网易云 uct2 |
解码后可通过反向计算或格式校验 |
| 进位制编码 | 抖音 u_code |
按 23 进制还原数字 UID |
| 有限空间哈希 | B站 vd_source |
枚举候选并精确匹配原始哈希 |
| 不透明标识 | hosteuin、ruk、utm_oi、sec_sharer_id |
只能比较是否相同,不能直接还原公开 UID |
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。