攻克 12306 前端加密算法

郑重声明: 本文仅供学习使用,禁止用于非法用途,否则后果自负,如有侵权,烦请告知删除,谢谢合作!

开篇明义

本文针对自主开发抢票脚本在抢票过程中常常遇到的请求无效等问题,简单分析了 12306 网站的前端加密算法,更准确的说,是探究 RAIL_DEVICEID 的生成过程.

因为该 cookie 值是抢票请求的核心基础,没有它将无法正确发送请求,或者一段时间后就会到期失效需要重新获取,或者明明更改了浏览器用户代理(navigator.userAgent)标识却还是被限制访问...

因为它并不是真正的客户端标识,只是迷惑性战术,浏览器唯一标识其实是 RAIL_OkLJUJ 而它却被 12306 网站设计者故意没有添加到 cookie ,因此造成了很强的欺骗性,编程真的是一门艺术!

你以为你的爬虫已经可以正常模仿浏览器,殊不知,只要没搞懂谁才是真正的浏览器标识,那么再怎么换马甲也难逃造假事实.

12306-algorithm-web-js-index-cookie-id.png

上图展示了 RAIL_OkLJUJ 的存在位置,可能是为了兼容市面上绝大数浏览器,也可能是为了联合各种前端缓存技术作为特征码,总是除了 cookie 之外,RAIL_OkLJUJ 存在于 Local Storage , Session Storage , IndexedDBWeb SQL 等.

值得注意的是,cookie 中故意没有设置 RAIL_OkLJUJ ,如果清空全部缓存后再次刷新网页,你就会发现 RAIL_DEVICEID 已经发生变化了而 RAIL_OkLJUJ 依旧没变!

12306-algorithm-web-js-index-cookie-RAIL_DEVICEID.png

下面简单验证一下说明谁才是真正的浏览器唯一标识:

  • step 1 : 复制当前获取到的 RAIL_DEVICEIDRAIL_OkLJUJ 的值

打开控制台(Console),通过 js 代码方式取出本地存储(localStorage) 的值:

控制台会立即返回该值,接下来需要手动复制到其他地方等待和第二次结果作比较.

但是程序员总是喜欢能偷懒就偷懒,手动复制也懒得复制怎么办?

当然,继续使用js 代码复制了啊!

比如这句代码就会把文本'Ryen欢迎您的访问,https://adsryen.cn'复制到剪贴板,接下来选择文本编辑器右键粘贴就能看到效果啦!

所以改造一下代码就能复制第一次访问 12306 网站获取到的 RAIL_DEVICEIDRAIL_OkLJUJ 的值.

12306-algorithm-web-js-index-copy-cookie-id.png
  • step 2 : 等待 5 min 后再次获取 RAIL_DEVICEIDRAIL_OkLJUJ 的值

或者清空网站 cookie 后再次刷新当前网页,总之就是想办法触发浏览器再次运行相关逻辑重新生成 RAIL_DEVICEIDRAIL_OkLJUJ .

  • step 3 : 对比第一次和第二次获取到的 RAIL_DEVICEIDRAIL_OkLJUJ 的值

显而易见,肉眼直接就能看出两次请求时 RAIL_OkLJUJ 的值并没有变化而 RAIL_DEVICEID 的值很大可能会发生改变.

因此,RAIL_DEVICEID 应该并不是浏览器唯一标识,而 RAIL_OkLJUJ 才是真正的唯一标识!

本文并不适合全部读者,如果你属于以下情况之一,那么本文对你绝对帮助甚多,否则对你来说只能算是浪费生命.

  • 适合对自主抢票或者脚本抢票有需求的天涯游子

  • 适合拥有一定 web 前端开发相关知识的开发者

  • 适合耐得住寂寞能够独自研究加密算法的孤独人

最后的核心前提是有网,当然WiFi更佳,否则流量真的吃不消啊!

故事背景

不知道你是否遭遇过一票难求的困境,尽管网络上关于第三方工具的加速包是否加速有过辟谣,但是每逢节假日总是会遇到抢不到车票的问题,大部分人还是会选择买个心理安慰吧!

目前为止,12306 官方线上售票渠道仅仅包括 12306 网站以及手机 app 客户端,因此市面上流行的第三方抢票软件均为非正常途径,而这些第三方渠道中最简单的实现方式应该就算是爬虫技术了.

不论是网页端还是手机端,统统称为客户端,客户端的作用仅仅是传声筒,真正负责执行命令的人就是服务端.

当你提交购票需求时,客户端会把这些车票信息一起打包发送给服务端,如果服务端有票的话,那么有可能就会返回给客户端成功信息,恭喜你订票成功.

但是尽管有票也不一定会给你,唯一确定的是无票一定会失败,总之不管结果如何服务端和客户端总是按照既定的约定协议在默默交流着.....

尽管官方渠道最可靠也最准确,可官方也还是没能给你买到车票啊!

所以想要抢票还是得亲自动手,不能完全依靠官方,这里就诞生了爬虫技术来冒充客户端,想要成功骗过服务端就要先了解真正的客户端到底有哪些特征?

由于本文篇幅有限,暂时不做关于抢票方面的相关论述,直奔重点,讲解 RAIL_DEVICEID 的请求过程,带你一步一步还原 12306 网站的前端加密算法的实现逻辑!

效果预览

在浏览器控制台运行 chromeHelper.prototype.encryptedFingerPrintInfo() 方法时会计算真实浏览器信息,如果发现计算结果中的 value 值和真正请求 https://kyfw.12306.cn/otn/HttpZF/logdevicehashcode 值相同,那么恭喜您,说明 12306 相关算法还没更新,如果不相同估计算法又稍微调整了!

事实证明,12306 算法虽然在变但都是小打小闹,根本没有伤筋动骨,所以自己动手改改又能满血复活了哟!

  • step 1 : 使用 Chrome 浏览器打开 12306 网站并清空该站点全部缓存数据.

请确保当前正在使用的是谷歌 Chrome 浏览器,IE和 firefox 等浏览器暂未测试.

12306-algorithm-web-js-website-clear-storage.png
  • step 2 : 手动清空 window.name 属性,保证浏览器处于首次打开 12306 网站状态.

因为非首次加载会携带上一次的请求信息,不方便学习验证,经过分析试验发现历史状态还保存在 window 对象的 name 属性,因此仅仅清空缓存还不够,还需要手动清空 name 属性的值.

12306-algorithm-web-js-website-clear-name.png
  • step 3 : 强制刷新当前页面并保持记录请求信息,过滤请求类型 js ,找到 /otn/HttpZF/logdevice 请求.

在找到该请求保存查询参数名为 hashCode: owRJc8M4EkFMvcTkzibRFJoDSkUKCx6N9ictZIJLIeY ,方便和之后的计算方式生成的结果做对比.

12306-algorithm-web-js-website-find-logdevice.png

除了查询请求信息外,更为重要的是查看响应信息,当初次请求 /otn/HttpZF/logdevice 时除了返回过期时间 expdfp 设备信息之外,还会返回 cookieCode 设备唯一标识.

如果等到过期时间或手动清空站点缓存后,/otn/HttpZF/GetJS 脚本中的相关逻辑会再次发起 /otn/HttpZF/logdevice 请求,那时候的响应内容再也没有 cookieCode 参数了.

让我们再好好看一看初次请求的响应信息吧!

如果将 callbackFunction() 回调函数去掉,不难发现其实返回数据是 json 格式,格式化后发现响应内容如下:

这里不得不佩服 12306 的设计思路了,故布疑阵,当你误以为自己已经更新了 RAIL_DEVICEID 的值,实际上 cookieCode 的值才是唯一标识而它恰恰没有设置到 cookie 中去,仅仅作为本地缓存保持了,用于再次请求 RAIL_DEVICEID.

12306-algorithm-web-js-website-cache-OkLJUJ.png
  • step 4 : 复制源码实现到控制台,输入 chromeHelper.prototype.encryptedFingerPrintInfo() 获取请求 /otn/HttpZF/logdevice 的查询参数,提取出其中的 value 值和真正的请求参数作对比.

假设真正请求参数 hashcode 的值已设置成变量,chromeHelper.prototype.encryptedFingerPrintInfo().value === hashcode 返回结果 true 说明复现算法实现还在正常运行,否则很可能是相关算法又更新了!

12306-algorithm-web-js-website-generate-compare.png

直奔重点

如果你正在学习自动抢票或者打算研究如何自动抢票,那么我可以负责任得告诉你,RAIL_DEVICEID 的值绝对是绕不过去的坎,堪称 12306 反爬虫技术的最精华手段!

现在目标已经锁定,赶紧动手和我一起去探究 12306 到底是如何处理 RAIL_DEVICEID 的值吧!

无痕模式下访问网站

众所周知,谷歌 Chrome 浏览器是程序员专属的浏览器,是因为提供了强大的开发调试能力,简单网站请求甚至根本不需要借助第三方专业的抓包工具就能独立完成分析整过程.

如果你还没有听说过 Chrome 浏览器或者正在使用其他浏览器,那么建议你先自行下载最新版 Chrome 浏览器,和文章使用一样的工具有助于顺利复现相关步骤,否则遇到莫名奇怪的问题只能自己研究了.

首先打开 Chrome 浏览器的无痕模式,处于无痕模式最大的特点就是不会保存 cookie,在一定程度上对目标网站而言是新用户(主要指的是新的客户端终端).

12306-algorithm-web-js-non-trace-mode.png

输入 12306 官网后,打开开发者控制台(F12或右键检查),选择网络(network)选项卡,确保一直处于监听网咯请求并实时记录状态.

具体而言,最左边的监听状态圆心是红色,保持日志(Preserve log)的复选框已勾选,禁用缓存(Disable cache) 的复选框已勾选,这三者是分析所有网络请求的基础.

12306-algorithm-web-js-network-listen.png

准备工作就绪后开始完整走一遍购票流程,即从首页进入登录页,登录并买票等过程,请求步骤越完整可提供分析的资料越多,也就基本上不会遗漏重要步骤,离真相越逼近.

12306-algorithm-web-js-query-ticket.png

凡是涉及到登录操作,建议先故意输出错误的账号密码等信息,这样有利于登录成功后重定向次数过多而导致无法找到之前的登录请求,如果网络(network)选项卡中的保持日志(Preserve log)的功能没有开启的话,这一现象将会更加严重!

12306-algorithm-web-js-login-wrong.png

再次输入正确的登录信息成功登录后进行买票行为等操作,但是无需付款,只要正常操作到下单完成即可视为整个购票流程.

12306-algorithm-web-js-purse-ticket.png
12306-algorithm-web-js-order-ticket.png

下单成功后整个购票流程已经基本完成,接下来开始全局搜索关键字 RAIL_DEVICEID 查看在哪里生成又在何处使用?

全局模糊查找关键字

现在整个购票流程基本上已经完成,接下来开始全局搜索全部请求中是否包含关键字 RAIL_DEVICEID 吧!

首先打开网络(network)选项卡,从左往右数第四个放大镜图标就是搜索功能,输入搜素关键字 RAIL_DEVICEID 会过滤符合条件的网络请求.

12306-algorithm-web-js-network-search.png

不搜不要紧,一搜一大把,只能看出来大部分网络请求都会自动携带该 cookie,反而淹没了到底是哪个网络请求生成的 cookie?

12306-algorithm-web-js-network-search-result.png

所以必须想办法精确搜索,过滤出生成该 cookie 的网络请求,所以接下来的问题就变成了如果 RAIL_DEVICEID 属于后端直接设置的行为,那么这样的网络请求应该长啥样的?

最好的学习就是模仿,假设并不知道真实的设置过程如何,但是我们可以查看其它 cookie 的设置过程啊!

同样地,在网络(network)选项卡选择第三个过滤器漏斗图标,展开网络请求类型,大致分为 All|XHR|JS|CSS|Img|Media|Font|Doc|WS|Manifest|Other等类型.

简单说一下网络请求类型的相关含义,整理出表格直观感受一下:

类型
名称
描述
代码

XHR

XHR adn Fetch

ajax异步请求

X-Requested-With: XMLHttpRequest

JS

Scripts

js脚本

Sec-Fetch-Dest: script

CSS

Stylesheets

css样式

Sec-Fetch-Dest: style

Img

Images

图片

Sec-Fetch-Dest: image

Media

Media

音视频媒体

Sec-Fetch-Dest: audio

Font

Fonts

字体

Sec-Fetch-Dest: font

Doc

Documents

html文档

Sec-Fetch-Dest: document

WS

WebSockets

长连接通信

暂无

Manifest

Manifest

版本文件

暂无

由于 12306 暂未包括后两种请求类型,所以无法判断该请求有什么特点,除了 ajax 异步请求外,其余类型的网络请求都是通过请求头 Sec-Fetch-Dest 属性标识的,当然设置浏览器的 cookie 也不例外,只不过大多数是通过服务端进行设置的,也就是网络请求的响应头标志了如何设置 cookie 的行为.

在前端 web 开发的过程中并不是一上来就前后端分离的,很长一段时间内前端页面也是由后端人员完成的,因此好多网站至今为止还保留着新旧交替的痕迹.

在上述网络请求类型中,最能体现这种变化特点就是 XHR 和 Doc 请求,XHR的常见封装实现 之一就是风靡全球的 ajax 异步请求,用于实现无刷新局部更新网页内容,而 Doc 是文档类型,无论是直接输出原生 html 还是使用模板技术动态渲染页面,最终输出展现结果一律是 html 文档,这一类的网络请求最容易设置 cookie 之类的请求,体现了上一代技术的一贯风格,恨不得一个人一次性把全部的活都干完!

但是随着技术的发展进步旧技术暴露的问题越来越多,引起了包括开发者在内的业内重视,各大企业已经逐步开始转变,也就是各司其职,物尽其用.

总结来说,XHR 和 Doc 有如下特点:

  • XHR 请求的数据绝大多数工作是在前端方面完成的,后端把相关数据返回给前端接口调用者,前端取到数据后进行业务组装展现.

  • XHR 请求绝大多数是异步ajax 请求,优点是当前页面不需要刷新就能看到最新内容,缺点是一旦涉及到相互依赖的业务就会出现请求等待噩梦.

  • XHR 请求是又前端发起也就是浏览器主动发出给后端,等后端服务器返回数据后继续由前端完成相关业务,这部分数据传输量极少但非常重要.

  • Doc 请求大多数是后端在控制,方便设置各种页面元素的表现形式,但是也不排除前端使用相关的模板引擎结合 XHR 数据在控制生成文档.

  • Doc 请求设置包括 cookie 在内的一系列网络行为,转发和重定向更是权限控制的常用做法.

下面我们已包括 RAIL_DEVICEID 关键字的网络请求,简单感受一下两者的差异性.

XHR 请求重点在于如何请求数据和接收数据,主要体现在 Request Data 和 Response Data 两方面,至于请求头一般都是默认设置.

12306-algorithm-web-js-network-search-result-xhr.png

Doc 请求的重点就不一样了,绝大多数请求就是输入网址后自动跳转页面,因而关注的重点应该放在请求头和响应头信息上,因为 cookie 的值就是通过请求头进行发送到后端服务器,后端如需新增或修改 cookie 值就是通过响应头进行设置的.

12306-algorithm-web-js-network-search-result-doc.png

柿子还要先捏软柿子,现在无法判定 RAIL_DEVICEID 到底是服务端直接设置还是客户端自行设置的,而客户端的行为不太直观,所以相对而言还是先捏服务端软柿子吧!

回顾刚才讲解的 Doc 网络请求,不难发现设置 cookie 的行为代码类似如下:

现在找到了学习对象,开始模仿查找类似请求的关键字应该是: Set-Cookie: RAIL_DEVICEID=

12306-algorithm-web-js-network-search-cookie-RAIL_DEVICEID.png

查无结果!

一般情况下出现无结果很可能是以下原因之一:

  • 恭喜您,真的查无结果,可以换条思路继续探索了.

  • 很遗憾,当前网络请求数据不足刚好缺失符合条件的请求.

  • 日了狗,操作不当误输多余符号或者本是关键字搜索实际上却开启了正则匹配等

所以逐一排查以上原因,首先考虑换一个关键字 Set-Cookie: JSESSIONID 能否查找出相应的结果.

12306-algorithm-web-js-network-search-cookie-JSESSIONID.png

事实胜于雄辩,查询过程并没有任何问题而是查询结果真的不存在,如此一来一次性排查两个原因,那么很有可能生成逻辑在于前端而非后端.

接下来只能在众多请求中碰碰运气寻找前端到底是在何处生成的 RAIL_DEVICEID ,然而请求众多还是要稍微讲究一下策略方向的.

既然是前端在控制 cookie 的生成逻辑,那么很有可能是某个 js 文件在起作用,当然也不排除其他类型的文件有操作浏览器行为的能力.

因此,通过分析进一步缩小范围:在请求类型为 JS 的网络请求中查找包括关键字 RAIL_DEVICEID 的全部请求.

12306-algorithm-web-js-network-search-js-cookie-RAIL_DEVICEID.png

理想很丰满,现实太骨感,本以为选中 js 再搜索关键字能够一起生效,结果并没有,请求类型依旧没有过滤出去,还是一大片的请求!

同时随便选中任意请求可以看到此时网络选项卡搜索匹配大结果其实是请求头这些基本信息,应该并没有包括 js 或者 doc 源码,方向错了,走再多路也是浪费时间.

12306-algorithm-web-js-network-search-js-cookie-analysis-RAIL_DEVICEID.png

虽然我不知道你从哪里来,中间经历了什么,但是我知道你的最终归宿一定会落到网络文件系统.

Chrome 浏览器除了可以看出网络请求也能看到最终呈现给用户的文件系统,既然中间过程找不到你,那么我直接到目的地去搜索吧!

打开源码(Source)选项卡,整个面板大致分为三部分,左侧文件数,中间文件区,右侧调试区.

12306-algorithm-web-js-source-workspace-pannel.png

其中左侧的文件结构可以清楚看到当前所处的层级结构,有利于快速掌握项目轮廓.右侧的调试区针对心中有想法但还不确定是否正确提供了非常好的验证工具,调试别人的代码就如本地开发那样边预言边验证.

中间的文件区域面积最大,功能自然也不能太弱,选中左侧具体文件后可以显示源码,方便查看,进而去调试验证心有所想.

所以问题来了,这一切的一切都要源于心有所想才能有行动,那么应该去哪里搜索包括关键字 RAIL_DEVICEID 的文件呢?

一般而言,良好的用户体验是不需要告诉你用户手册的,给你一大堆详尽的说明文档也未必会耐得住去看一篇,先用用再说!

人生苦短,不要浪费太多时间放在无聊的事情上面,简短的三行提醒富含哲理性,第一行告诉你怎么打开文件,第二行告诉你如何运行命令,第三行告诉你如何操作实现什么效果.

如果三行代码还不足以解决你的问题,阅读更多自己慢慢琢磨说明文档吧!

当然,这里和在文件系统中查找包含关键字 RAIL_DEVICEID 的需求最为接近的应该就是第二行,运行命令了,那就试试看吧!

12306-algorithm-web-js-source-command-search.png

输入搜索 search 后果然弹出了相关搜索命令,于是点击开发工具(DevTools)后出现了搜索框,顺理成章输入关键字 RAIL_DEVICEID 进行搜索了啊!

12306-algorithm-web-js-source-search-result.png

终于等到你,还好我没放弃,你就是我的唯一,看样子和 RAIL_DEVICEID 有关的处理逻辑全部都在这么一个 js 文件里,看你还往哪里跑!

直捣黄龙还往哪里跑

找到该文件后点击查看,红蓝黑密密麻麻一大片js 代码,绝对不是给人看的而是给机器看的,想要给人阅读还需要美化一下,将源码丑化混淆成难以阅读的代码也是防止他人偷窥复制拷贝自己的劳动成果,同时也能减少文件大小,加速网络传输数据,让你的网站速度更快一些.

12306-algorithm-web-js-source-getjs-pretty.png

点击中间区域的左下角格式化图标进行美化代码,然后在文件中搜素关键字 RAIL_DEVICEID 定位到具体代码.

非常人性化的是,搜索功能是通用的快捷键 Ctrl + F,现在定位到具体代码,截图留念下,接下来才是真正考验技术的时刻!

12306-algorithm-web-js-source-getjs-search.png

本地备份js方便复现

既然已经找到关键文件,自然需要留存快照进行存档操作,否则哪一天文件更新了都不知道哪里发生变化了,难不成还要从头再分析一遍,我选择差量更新而不是全量覆盖!

选中源文件右键弹出菜单,选择任意一款喜欢的方式复制源文件到本地留作学习备份,准备工作就绪后准备大干一场.

12306-algorithm-web-js-source-getjs-backup.png

高楼大厦寻关键线索

Js文件中关于网络请求最典型的就是异步回调,将原本简单的操作复杂化,非要你等我,我等他,他还等着他的她.

最终直接结果就是整个请求流程反过来了,假设正常流程:是 A->B->C-D-E-F,那么异步请求很可能陷入这样的陷阱: F <- E <- D <- C <- B <- A

所以一层又一层的回调函数真的是难以维护,这种技术也在慢慢淘汰更新成更容易维护的方式,还是不再展开了,回到正题上来,还是先找到程序到底什么时候开始调用的吧!

核心代码最外层函数是 initEc 函数,而该函数的写法明显是传统 js 的属性方法,因此判断挂载于该对象的属性方法应该都是完成某些相同的功能.

暂时先不着急继续寻找谁在调用 initEc 函数,先搞懂整个函数结构是什么轮廓.

如果熟悉 web 开发,那么你一定不难发现这是标准的面向对象的写法,ja 函数作为构造函数内置了一大堆成员变量,并且在原型链上继承了一大堆方法.

更何况,对象属性中还有三个带有 new 关键字的构造函数,估计也是类似于 ja 这种设计思路,高楼大厦平地起,还原相关算法之路预期并不简单!

但是想一想车票真难抢还动不动访问错误,是可忍孰不可忍,还是要研究算法一劳永逸搞定 RAIL_DEVICEID 的生成逻辑,自己用算法计算实现完美伪装浏览器!

现在以 initEc 函数名继续搜素,寻找到底是谁在调用,轻而易举又找到了新的函数名: getFingerPrint

同样地,不再过多停留,继续以 getFingerPrint 为关键字搜索,找到了 Pa 函数,终于不再是 ja 的方法了.

与此同时,Pa 函数也是 js 文件的第一行代码,来都来了,那就顺便看一眼 js 的整体结构代码吧!

自执行的匿名函数实现的闭包,这样的好处在于函数内的变量不会污染其他文件,更何况混淆之后的变量名称充斥着大量的变量 a,b,c,d,e,f之类的,不用闭包也不行啊!

现在继续以 Pa 为线索搜索,最终发现了函数入口,除此之外再无其他.

js 是典型的事件驱动型编程语言,当发生什么什么事件后我要干这个,页面加载时我要开始工作了,按钮被点击了我要登录了,页面关闭时我要下班了等等诸如此类的逻辑.

上述代码实现的就是页面元素加载成功后开始执行 Pa() 函数,而 Pa 函数又会执行 (new ja).getFingerPrint() ,紧接着又会执行 initEc 函数.

现在基本流程已经大致清楚了,总结一下基本代码逻辑如下:

从以上代码分析中,相信你会发现相关逻辑应该兼容 IE 浏览器,同时设置了定时程序反复更新 cookie 值,并且还有远程 RTC 保持通信,不得不说做得还真不错,不愧是国民出行的代步工具啊!

精力有限,这里选择最简单的一种情况进行算法还原过程的研究,浏览器选择谷歌 Chrome 浏览器,这样就可以屏蔽关于 IE 的兼容性补丁处理,同时也不考虑 RTCPeerConnection 的情况,于是乎,代码逻辑简化成这样:

所以现在问题的核心在于搞清楚 initEc 函数的数据流向,还原算法实现过程不是梦!

断点调试追踪调用栈

静态分析程序结构后开始断电调试观察一下数据流向,做到心中有数,同时为了该过程具有可重复性需要保持每一次操作环境一致.

具体而言,首先 Chrome 浏览器处于无痕模式,接着是每次试验时清空站点缓存,最后就可以愉快刷新当前网页等待进入下一轮断电调试了.

12306-algorithm-web-js-source-getjs-debug.png

提前在关键点打入断点(鼠标左键点击行号),然后等待程序进入调试模式,稍等一会后进入断点可以一步一步看到程序运行的值,在调试区还可以监控变量的值.

当然也可以有函数调用栈的关系,这一切只是辅助手段,最关键还是要靠自己分析弄清楚函数的调用顺序流程,原则上先大后小,先整体再细节.

12306-algorithm-web-js-source-getjs-debug-watch.png

函数最后会发送 ajax 请求获取 cookie 并写入本地以及 cookie 中,亲测数据如下:

12306-algorithm-web-js-source-getjs-debug-finally.png

一次请求完成后顺利生成了 cookie 也写入了本地缓存中,如果不清空那么进入下一次断点的流程就和这一次不一样了,所以为了可重复操作,再次断点调试时需要还原操作环境.

12306-algorithm-web-js-source-getjs-debug-write.png

首次加载时变量 a 并没有值,一不小心进入下一个过程时,这一次 a 已经有值了,多次试验后搞清楚了数据流向也明白了如何还原操作环境,保持实验结果的一致性.

12306-algorithm-web-js-source-getjs-debug-repeat.png

经过多次重复试验,先将基本数据流向还原如下:

异步请求 C <- B <- A 换算成实际情况是 : initEc 函数依赖于 this.ec.get("RAIL_OkLJUJ" 函数,等到 window.evercookie.get 运行完成后会调用 c.getDfpMoreInfo 函数,等到 getDfpMoreInfo 函数运行结束后会调用函数核心关键代码.

除了总的来看是各种异步请求相互回调之外,不少细节中也充斥着大量的回调函数,以 getDfpMoreInfo 函数为例,居然要收集这么多信息才开始做自己的事情!

getDfpMoreInfo 函数的执行过程中首先会运行 b.cfp.get 函数,通过搜索追根溯源发现是 aa.prototype.get 函数,这个函数除了获取浏览器简单信息外还涉及到字体的加密处理: var f = c.x64hash128(d.join("~~~"), 31);

然而想要继续分析 getDfpMoreInfo 函数需要先弄清楚 b.cfp.get(function(c, d) {getDfpMoreInfo: function(a) { 中的回调函数参数到底是什么,因此必须从头到尾逐一分析,这也是异步请求的陷阱.

看似请求逻辑一气呵成,真的要维护时却困难重重,想要分析 C 必须先分析 B,没想到 B 又要依赖于 A.

所以下一步的操作就是先从突破口 initEc 函数顺藤摸瓜,找到最初的函数 A 再根据断点调试看看是如何回调一步步回到 initEc 函数的,也就是将异步请求改造成同步请求的过程.

一步一步慢慢转同步

顺着这条路继续分析 getDfpMoreInfo 的调用过程,可以添加更多的调用细节,因此现在完善成如下代码:

其中约定标记为字母表 A,B,C的同步调用顺序,对应异步请求 C <- B <- A,深入分析其中的 A 时,依然采用 C,B,A的标记方式.

为了表现出层次性,第二层的 C,B,A 可以表示为 AC,AB,AA,以此类推.

这样最终技能通过层级调用关系形成调用树状图,最终效果大致如下:

调用栈树状图浏览异步函数调用顺序一目了然,仿照数据结构的栈结构进行设计,后进先出是最外层的 C,然后发现 C 还要依赖B,B 要依赖 A,执行完返回上一层继续执行,这个设计感觉很棒啊,为自己点赞!

然而实际分析过程中发现同级请求不总是三级 ABC的形式,这里可以根据实际情况按照这个思路自行分析研究再结合断点调试验证猜想.

  • step 1 : 获取基本信息并在获取加密字体后回调

var f = c.x64hash128(d.join("~~~"), 31); 涉及到一系列的加密操作,暂时不用管,最重要的下面这句 return a(f, b) 会执行回调函数继续下一个逻辑.

  • step 2 : 获取浏览器更多信息并在最后回调

b.cfp.get(function(c, d) {}) 函数就是上一步的 aa.prototype.get() 函数.

  • step 3 : 打包参数前先获取浏览器机器码

该函数来自于 initEc 函数中 c.getDfpMoreInfo( 回调函数里的 g = c.getpackStr(b),this.getMachineCode() 心机很深,暗藏玄机.

  • step 4 : 打包参数前再组合更多信息并重新排序

该函数同样来自于 initEc 函数中 c.getDfpMoreInfo( 回调函数里的 g = c.getpackStr(b),值得学习研究!

  • step 5 : 重新分类浏览器信息并加密生成请求参数

e = c.hashAlg(m, a, e); 加密过程比基本信息的加密更加复杂,但是总体来说难度也不大,主要涉及到字符串反转,分段组装,哈希算法以及 base64 编码等等.

遇到瓶颈则略过细节

通读全文并结合断点调试反复验证猜想后,我们发现为了最终请求https://kyfw.12306.cn/otn/HttpZF/logdevice 时的参数可真的为呕心沥血,费心尽力啊!

总体来说,获取浏览器各种信息并且还涉及兼容 IE 浏览器而处理的各种补丁,对于原始信息较长时采用独特的加密算法进行加密处理,仅此还不够,还存在判断是否伪造浏览器参数的相关逻辑判断,真的是大写的服!

关于获取浏览器的相关信息反而很简单,只要结合如何识别是否说谎的代码一起设置就能绕过这部分逻辑,比如说常见的设置浏览器用户代码如下:

不同对象对同一个用户代理的处理逻辑不同,类似上述例子还有很多,简单到处都是陷阱,不看源码根本就不知道,看完以后你还会吐槽 12306 技术垃圾吗?

接下来的是三个加密算法,一个是最初基本信息加密,一个字体信息加密,还有一个是最终分类信息加密.

这三个算法看起来比较吓人,实际上只要耐心调试是可以慢慢还原的,不过是字母的各种排列组合顺序而已,谁让他没有加密能让我们看到源码呢,仅仅的变量名称替换是难不倒聪明才智的开发者的,更何况这部分和业务逻辑关心不大,暂且略过吧.

算法复现

算法整体采用闭包设计面向对象的编程风格,基于原型链特性实现原始对象的加密逻辑,添加特有方法用于临时修改浏览器相关信息,最后将自定义对象 chromeHelper 直接挂载于 window 属性,方便外部调用.

step 1 : 获取基本信息

  • 用户代理

  • 语言

  • 颜色深度

  • 像素比例

  • 屏幕分辨率

  • 可用屏幕分辨率

  • 时区偏移量

  • Session存储

  • Local存储

  • IndexedDb存储

  • AddBehavior存储

  • websql存储

  • cpu类型

  • 平台类型

  • 反追踪

  • 插件

  • TODO 画布

  • webgl画布

  • adBlock广告拦截

  • 说谎语言

  • 说谎分辨率

  • 说谎操作系统

  • 说谎浏览器

  • 触摸支持

  • 字体

step 2 : 加密基本信息

  • x64hash128

  • x64Multiply

  • x64Rotl

  • x64Add

  • x64Xor

  • x64Fmix

  • x64LeftShift

step 3 : 获取更多信息

  • 获取画布代码

  • 获取Session存储代码

  • 获取Local存储代码

  • 获取IndexedDb存储代码

  • 获取Websql存储代码

  • 获取反追踪代码

  • 获取插件代码

  • 获取广告拦截代码

  • 获取说谎语言代码代码

  • 获取说谎分辨率代码代码

  • 获取说谎操作系统代码代码

  • 获取说谎浏览器代码代码

  • 获取说谎浏览器代码代码

  • 获取说谎字体代码

step 4 : 获取机器信息

  • 浏览器uuid代码

  • cookie 代码

  • 用户代理代码

  • 源高度代码

  • 源宽度代码

  • 可用宽度代码

  • 可用宽度代码

  • 颜色深度代码

  • 源设备XDPI代码

  • app代码名称代码

  • app名称代码

  • Java是否启用代码

  • 媒体类型代码

  • 平台代码

  • app次版本信息

  • 浏览器语言代码

  • cookie是否启用代码

  • cpu类型代码

  • 是否在线代码

  • 系统语言代码

  • 用户语言代码

  • 偏移时区代码

  • flash 版本代码

  • 历史记录条数代码

  • 自定义ID代码

  • 发送平台代码

step 5 : 合成指纹信息

step 6 : 重新分类指纹

step 7 : 加密分类指纹

  • hashAlg

  • sortArray

  • encryptedKeyInArray

  • reverse

  • split2partInOrder

  • split3partInOrder

  • covert2charCode

  • 加密算法核心代码

模拟伪装

现在已经还原了算法的实现逻辑,下一步就是如何更好地伪造自己,本文提供临时设置的实现方式,方便在不修改之前复现代码的基础上实现扩展,当然也可以直接在还原算法源码中写入伪造代码.

值得注意的是,这种 Object.defineProperty 方式只会临时生效而且仅仅针对使用 js 代码获取对象属性的值,并不会真正修改对象属性!

  • 设置用户代理

  • 设置浏览器语言

  • 设置浏览器语言

  • 设置屏幕颜色深度

  • 设置设备像素比率

  • 设置屏幕宽度

  • 设置屏幕高度

  • 设置屏幕可用宽度

  • 设置屏幕可用高度

  • 设置Session存储

  • 设置Local存储

  • 设置indexedDB存储

  • 设置addBehavior存储

  • 设置Cpu类型

  • 设置平台类型

  • 设置反追踪

  • 设置插件

  • 设置Canvas

  • 设置Webgl

  • 设置AdBlock

  • 设置AdBlock

  • 设置字体

  • 设置最多触控点

  • 设置ontouchstart事件

  • 设置app代码名称代码

  • 设置app代码名称代码

  • 设置Java是否启用

  • 设置媒体类型

  • 设置cookie是否启用

  • 设置是否在线

  • 添加历史记录

使用示例

亲测构造请求 /otn/HttpZF/logdevice时,关于参数 algID 经常性发生变化,因此无法提供静态的请求方法,建议根据实际情况实时改变.

通过翻阅源码实现,最终发现关于发送请求的代码是这样的:

其中,参数 a 表示的是加密后的浏览器指纹信息,(new Date).getTime() 是当前时间戳,而 algID\x3dmBxuYhGXYR\x26hashCode\x3d 这部分的 algID 算法参数是暂时性静态的,比如今天一段时间都是 mBxuYhGXYR 而第二天这个值就变成其他值了.

hashCode 参数的值就是程序运行结果的 value 值,最后面的变量 a 代表的是剩下的浏览器指纹信息,即运行结果的 key.

假设此时此刻为例,演示如何使用该 js 文件:

12306-algorithm-web-js-website-console-ajax.png

回顾展望

在还原算法实现过程中,充分复习了 web 前端开发的调试技巧,针对通篇无意义的变量命名方式,有效的应对方式就是采用正则表达式精确匹配进行查找.

同时,为了验证自己的猜想是否正确,还需要结合断点调试,如果存在反调试手段,那么只能靠自己硬啃压缩混淆代码了,我只能说:考验真正技术的时候,到了!

本文给我们留下了不少启发供后续工作学习使用,从开发者的角度上来讲:

  • 不完全依靠现成加密技术,哪怕真正加密时没自己实现也要在加密前后实现自己的混淆逻辑.

例如重新打乱字符串,将字符串分隔成三份,按照首尾中或者尾中首等反人类次序重新排序等.

  • 重复使用同一数据时也不一定要抽象成同一个方法,不同对象调用不同的处理逻辑,更是让人防不胜防,陷入思维惯性误区.

例如获取用户代理采用不同的正则表达式进行替换,获取浏览器语言时采用另外的途径验证上一步获取结果是否造假等.

  • 无序更胜似有序,看似规整优美的代码是给开发人员看的而不该给机器看,一定不能使用源码上线而是要加密处理或者其他处理.

只有要基本的开发经验很容易一叶知秋,进而判断相关技术栈,因为技术都是通用的方案,非常容易复制,打造独特的技术流会增大破解成本,进而吓退一部分菜鸟小白.

  • 前端和后端需要密切配合协同协作,缺少统一指挥难以避免一方或者多方偷懒进而暴露我方阵地.

重要的业务逻辑肯定是放在后端进行处理,哪怕前端已经处理过相同逻辑也不能偷懒,更要保证前后端处理逻辑的一致性.

攻防是矛与盾,作为攻克方要做的一点就是打铁还需自身硬,多了解不同的技术栈才能做到有的放矢而不至于临阵脱逃,望而却步!

最后祝大家抢票愉快,需要买票时人人都有票,再也不需要抢票回家,人生苦短何必浪费生命去抢票?

再次声明,本文仅供学习研究,一切用作它途的行为均与本人无关,如有侵权,烦请告知,谢谢合作.

参考资料

最后更新于

这有帮助吗?