亿企赢逆向手记(三):X-Irc-Data 指纹头的伪造与随机化
解开是 7 字段的 base64 浏览器指纹——无签名无种子无时间戳,一个模板随机几个字段就过;附完整字段来源表。
「亿企赢逆向手记」第三篇。逐字段拆开每个请求都带的
X-Irc-Data指纹头,讲它的完整结构、伪造方法、随机化技巧,以及为什么它属于「构造回去即可」的弱指纹。自己账号自动化,技术学习向。
抓亿企赢的包,几乎每个请求头里都挂着一个长长的 X-Irc-Data,值是一串 base64。这一篇把它解开、还原、再伪造回去。
解码:一段声明式浏览器指纹
把 X-Irc-Data 的值 base64 解码,是一段紧凑 JSON,字段固定 7 个:
{
"browserLang": "zh-CN",
"screenResolution": "1512*982",
"color": "30",
"ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
"referer": "https://www.17dz.com/manage/index.html",
"domain": "https://www.17dz.com",
"url": "https://www.17dz.com/home/login.html"
}
对照来源,每个字段前端 JS 都能直接读到:
| 字段 | 前端来源 |
|---|---|
browserLang |
navigator.language |
screenResolution |
screen.width * "*" * screen.height |
color |
screen.colorDepth |
ua |
navigator.userAgent |
referer |
document.referrer |
domain |
location.origin |
url |
location.href |
没有签名、没有加密、没有服务端下发的种子、没有时间戳。就是把一堆环境值拼成 JSON、base64 一下。这决定了它属于「声明式指纹」——数据由客户端自己声明、自己编码,服务端只能选择相信。
伪造:一个模板 + 每请求覆写几个字段
既然是明文 base64,伪造 = 构造同样结构的 JSON 再编码。做法是存一份真实抓包解出的模板,每次请求只改动会变的字段:
def _parse_irc_template(self):
# 把一次真实抓包的 X-Irc-Data 存成 base64 常量,解开当模板
decoded = base64.b64decode(ORIGINAL_B64).decode('utf-8')
return json.loads(decoded) # → 上面那个 7 字段 dict
def _generate_irc_data(self, referer, url):
irc = self.irc_template.copy()
irc.update({
'screenResolution': f"{random.randint(1000,1920)}*{random.randint(700,1080)}",
'color': str(random.randint(24, 32)),
'referer': referer, # 跟着当前请求语境走
'url': url,
})
json_str = json.dumps(irc, separators=(',', ':')) # 紧凑分隔符,和前端一致
return base64.b64encode(json_str.encode()).decode()
三个细节:
① referer / url 必须跟当前请求语境对齐。 指纹里带的 url/referer 要和你这次实际请求的页面对得上:请求登录页就填登录页 url,请求 API 就填对应的业务 referer。所以代码里分了两套头装配——页面请求用 _headers_page,API 请求用 _headers_api,各自把正确的 referer/url 塞进 X-Irc-Data。若所有请求都写死同一个 url,序列一眼假。
② screenResolution / color 每次随机。 真人多次请求这两个值是稳定的,这里反而随机化——目的不是模仿某台真机,而是不让多次请求的指纹逐字节完全一致。完全一致的重复指纹比略有浮动的更像脚本。这是个反直觉小技巧:适度抖动 > 死板复读。
③ separators=(',', ':') 不能漏。 前端 JSON.stringify 出的是紧凑格式(逗号冒号后无空格),Python 默认 json.dumps 会加空格。这里服务端大概率不校验,但在「对整个字符串做签名/哈希」的场景里,一个空格就会让签名对不上——养成紧凑分隔符的习惯能避开一类坑。
两套请求头怎么装配
def _headers_page(self, referer, url): # 访问 HTML 页面
return {**self.headers_common,
'Accept': 'text/html,application/xhtml+xml,...',
'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate',
'Upgrade-Insecure-Requests': '1',
'Referer': referer,
'X-Irc-Data': self._generate_irc_data(referer, url)}
def _headers_api(self, referer): # 打 XHR 接口
return {**self.headers_common,
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/x-www-form-urlencoded',
'Origin': 'https://www.17dz.com',
'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors',
'Referer': referer,
'X-Irc-Data': self._generate_irc_data(referer, "https://www.17dz.com/home/login.html")}
区别就是浏览器发这两类请求时的天然区别:页面请求带 Sec-Fetch-Dest: document、Upgrade-Insecure-Requests;XHR 带 Origin、Sec-Fetch-Dest: empty、JSON 的 Accept。把这些 Sec-Fetch-* 也照抄对,是让请求「像浏览器发的」而非「像脚本发的」的一部分。
为什么它这么好绕
X-Irc-Data 是典型的自证明指纹——它没有引入任何客户端无法伪造的信息:
- 没有服务端下发的一次性 challenge(否则你没法用静态模板);
- 没有基于密钥的签名 / HMAC(否则你得逆出密钥或算法);
- 没有和 cookie、时间强绑定的校验(否则模板会过期)。
所以它的实际作用是给正常流量打标、给最低级的脚本设个小门槛,而不是真能挡住定向逆向。真正难绕的方案会让一部分数据或签名密钥由服务端下发、或藏在混淆过的 JS 里动态计算——让你没法「解一次、造一个模板、永久复用」。
遇到陌生请求头,先问三件事
判断一个来路不明的头值不值得深挖:
- 它明文可解吗?(base64 / URL 编码 / JSON)——是,基本就是声明式指纹,构造回去即可。
- 它变的部分依赖什么?——只跟环境/时间相关、不依赖服务端种子,就能自己造。
- 去掉它请求会不会失败?——很多这类头「有更好、无也不致命」,实测一下能省大量功夫。
X-Irc-Data 三条全中:明文、可自造、弱校验。一个模板随机几个字段,这道墙就过了。
下一篇:登录之后那些看不见的会话状态——xsrf-token / companyId / accountId / ucAccountId,以及怎么在多个公司组之间切换上下文才能搜到客户。
评论 0