亿企赢逆向手记(三):X-Irc-Data 指纹头的伪造与随机化

张 张站长 · 2026-09-08 17:55 · 技术-逆向 · 26 次浏览 · 0 条评论

解开是 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 里动态计算——让你没法「解一次、造一个模板、永久复用」。

遇到陌生请求头,先问三件事

判断一个来路不明的头值不值得深挖:

  1. 它明文可解吗?(base64 / URL 编码 / JSON)——是,基本就是声明式指纹,构造回去即可。
  2. 它变的部分依赖什么?——只跟环境/时间相关、不依赖服务端种子,就能自己造。
  3. 去掉它请求会不会失败?——很多这类头「有更好、无也不致命」,实测一下能省大量功夫。

X-Irc-Data 三条全中:明文、可自造、弱校验。一个模板随机几个字段,这道墙就过了。

下一篇:登录之后那些看不见的会话状态——xsrf-token / companyId / accountId / ucAccountId,以及怎么在多个公司组之间切换上下文才能搜到客户。

评论 0

还没有评论
← 全部文章

找站长

站长微信二维码 扫码加微信
售后群二维码 扫码进售后群
站长收款码 建站不易,感谢打赏
微信 18581469390
本站不支持自助充值;需要充值或开通额度,请直接联系站长。
加微信可以找我重置密码、充值、问工具怎么用