中国商标网JS调试 - 动态代码注入
前言
中国商标网地址:http://wcjs.sbj.cnipa.gov.cn/txnT01.do
本文的主要目的并不是对中国商标网的爬虫实现,而是对其反爬机制的核心突破。由于反爬策略的更新频繁,不保证本文中的代码的时效性。
本文不会对JS脚本从头开始逆向分析,而是有选择性的挑选一些有趣反爬机制进行描述其解决思路。
本文主要是记录中国商标网的调试过程,不会提供任何关于爬虫实现的代码。!!!!!!!!!别问我要商标网的爬虫代码!!!!!!!,因为我也没有。
背景
中国商标网
在反爬安全上是属于标杆的存在。其反爬技术提供者是瑞数信息
,反爬机制是属于动态安全
。对于动态反爬机制最核心的地方不是token加密算法,而是其反调试策略。
任何动态的事物都是基于静态产生的,所以只要找到静态点,那么动态就会坍缩为静态。
工具
Fiddler: 尽可能使用新版本,如果必要,至少要带Fiddler Script功能。
谷歌浏览器:字面意思,也是尽可能使用新版本。
知识点
JavaScript
Fiddler Script
谷歌浏览器开发者工具
正文
在背景里面我有说过中国商标网的反爬机制是动态反爬,那么根据定理:
必须用魔法打败魔法。
我们必须要用动态打破动态。
本文章的核心思路是代码注入。代码注入是字面意思,其作用就是在脚本执行之前插入自己的代码,以实现对代码的查看修改替换等各种操作。通过 Fiddler
可以实现代码注入,下面会对其实现原理进行简单的介绍。如对该知识点了解请跳过。
了解 Fiddler Script
众所周知Fiddler
是一个很强大的抓包工具,但是大多数人对它的印象主要是抓包,但其实其最强大的功能实属FiddlerScript
,下面引用《Fiddler调试权威指南 - Debugging with Fiddler》
对FiddlerScript
的描述。
Fiddler
在处理每个Session
时,脚本文件CostomRules.js
中的方法都会运行,该脚本使得你可以隐藏、表示或任意修改复杂的Session
。规则脚本在运行状态下就可以修改并重新编译,不需要重新启动Fiddler
。
上面一句话简单的来说就是Fiddler
作为一个中间者(代{过}{滤}理服务器)
,其FiddlerScript
可以实现在客户端与服务器之间的数据请求转发过程中进行修改。当然作为规则脚本还可以对Fiddler
的业务功能进行拓展。
所以,根据上面所说,我们就可以理解代码注入就是在服务器
返回请求响应之后,Fiddler
作为中间者根据预先编写的FiddlerScript
来将代码注入到响应中,之后再将响应转发给 客户端(本文所说的客户端主要是浏览器)
。以上的描述的过程体现在下面的流程图中。
1-0.png
1-0
Session 处理函数
编译
FiddlerScript
时,Fiddler
保留了某些关键静态函数的引用,这些函数都能在Handlers
类中找到。
除了OnReturningError
外,以下的处理函数Handlers
按照了其执行顺序进行描述:
OnPeekAtRequestHeaders: 在客户端发送请求头
(Request Header)
之后,中间者接收到其请求头后进入该处理函数。OnBeforeRequest: 在客户端发送请求体
(Request Body)
之后,中间者接收到请求体后进入该处理函数。之后将新的请求头和请求体转发给服务器。OnPeekAtResponseHeaders: 在服务器返回响应头
(Response Headers)
之后,中间者接收到响应头后进入该处理函数,之后将请求头转发给客户端。OnBeforeResponse: 在服务器返回响应体
(Response Body)
之后,中间者接收到响应体后进入该处理函数,之后将请求体转发给客户端。OnReturningError: 在
Fiddler
生成的错误信息(如“DNS Lookup Failed”
)返回给客户端时被调用。通过这个函数可以定制客户端应用看到的错误信息。
下图描述了Session
处理函数的流程
1-1.png
1-1
在本文中,代码注入是在OnBeforeResponse
处理函数中进行。
为了加深对代码注入方法的理解,我们不妨拿中国商标网来练练手。
反调试策略
浏览器打开商标网,刚打开开发者工具(F12),发现调试工具触发了中断,无一例外都停在了debugger处。
debugger指令是调试器断点,在未进入调试器的情况下,JS引擎会忽略任何debugger指令。
有很多人其实看到这种情况就已经束手无策,这反调试机制是建立在动态脚本的前提下的,这在传统方法来看确实是没有任何招架之力。如非要逆向这些脚本,就必须要硬啃代码。
1-2.png
1-2
问题分析
事实上,存在两处debugger
指令,一处是鼠标事件(MouseEvent)
触发的,另一处是定时器(setInterval)
触发的,在debugger
指令的前后进行了时间差new Date().getTime()
方式判断,通常只要时间差大于几百毫秒或者更低时间差内就会触发反调试防御,在这几百毫秒内一般来说无法当然只要你不继续执行代码,就不会被检测到。
既然这是为调试器准备的反调试策略,那么我们直接把他删掉或者注释掉就能避免调试器中断。透过Fiddler
这个中间者,我们就能动态注入代码。
有一个问题是,要注入什么地方,要修改什么地方?没错,只要删掉debugger
指令就能卸除debugger
反调试防御。但是debugger
指令在哪里?
我们可以注意到上图中的标签页的名称是VM+数字
,这种组合的名称通常这是为了区别原网页的JS脚本,是由eval()
方法产生的或者是ajax
方法获取的。商标网的debugger
所处的脚本是通过eval()
动态执行的脚本,在本文我们不讨论如何一步一步找eval()
执行的位置<sup>1</sup>。这里直接说结果,eval()
在下图中的下红框中执行。
1-3.png
1-3
解决思路
__如果debugger
是在原网页的JS脚本中,即使是动态脚本,也可以直接修改脚本代码以删除debugger
。但是这里经过了在脚本中eval(string)
来代{过}{滤}理执行的代码,debugger
存在于string
中,所以只能通过将代码注入到string
才能修改删除debugger
。所以我们需要在脚本中的eval(string)
的执行之前通过注入修改string
的代码来达到修改删除debugger
,之后执行的eval(new_string)
就是经过了去debugger
的代码,这样反调试防御就能卸下来了。__
这里面的一个技巧是,在eval(string)
执行的刚好之前,string
必然是已经经过了解密了的代码字符串,这样在这恰好之前注入代码可以直接跳过脚本解密这个过程,完全可以不用在意解密算法。
如下代码,既然这是动态的脚本,那么怎么定位呢?一个最简单的办法其实就是找特征信息,然后使用正则匹配就行了。
... } else if (_$71 < 75) { // 注意,这里的75和除了ret外的所有变量名也是动态生成的,不应作为特性值 ret = _$Az.call(_$j1, _$8t); } else { ... } ...
既然有这么多特征值,那么/ret\s*=\s*[\w\$]+\.call\([\w\$]+,\s*([\w\$]+)\)/
,这一正则就能定位。
注入代码
既然是通过Fiddler
来实现代码注入,我们肯定要先搞清楚如何进行编写FiddlerScript
来实现代码注入,关于FiddlerScript
的介绍可以参考书《Fiddler调试权威指南 - Debugging with Fiddler》和ModifyRequestOrResponse。
通过
Rule - Customize Rules
进入Fiddler脚本编辑器Fiddler ScriptEditor
FiddlerScript
的脚本编写语言是JScript.NET
,这是微软自己开发的IE
脚本执行引擎,也是按照ECMAScript
标准的,所以一般我直接把它当JavaScript
来用,当然其中存在一定的使用差异。
1-4.png
1-4
好了,我们进入Fiddler ScriptEditor
,找到OnBeforeResponse
处理函数。加入如下代码。
static function OnBeforeResponse(oSession: Session) { if (m_Hide304s && oSession.responseCode == 304) { oSession["ui-hide"] = "true"; } // 以下为商标网的代码注入 // 注意的是,对Fiddler来说,所有经过Fiddler的请求都会经过该处理函数,这就要求你自己对请求进行筛选,否则将会对所有请求都执行对应的操作。我这里仅仅是对域名为 "wcjs.sbj.cnipa.gov.cn" 和其请求头中"Content-Type" 包含 "html"的请求进行响应的处理。关于oSession对象,可以参见参考处的链接或书籍。 if (oSession.HostnameIs("wcjs.sbj.cnipa.gov.cn") && oSession.oResponse.headers.ExistsAndContains("Content-Type", "html")){ // 这一步我们将响应将Body作为字符串解析。 var oBody = oSession.GetResponseBodyAsString(); var newBody = oBody; var oRegEx = /\bret\s*=\s*[\w\$]+\.call\([\w\$]+,\s*([\w\$]+)\)/; var oEvalLineRes = oBody.match(oRegEx); if(oEvalLineRes !== null){ var oLine = oEvalLineRes[0]; var oCodeVar = oEvalLineRes[1]; // 创建被注入的代码,通过修改脚本代码string来实现取出debugger指令。 var rmDbg1Code = "var rmDbg1Res = " + oCodeVar + ".replace(/\\bdebugger\\s*;/, '') ;"; var rmDbg2Code = "var dbg2RegEx = /\\{\\s*\\bvar\\s*([\\w\\$]+)\\s*=\\s*[\\w\\$]+\\[[\\w\\$]+\\[\\d+\\]\\]\\([\\w\\$]+\\(.*?\\)\\);\\s*\\}/;" + "var dbg2assignVarName = rmDbg1Res.match(dbg2RegEx)[1];" + "var rmDbg2Res = rmDbg1Res.replace(dbg2RegEx, '{var ' + dbg2assignVarName + '=false;}');"; // 替换成经过处理后的代码所在的变量 var newEvalCode = oLine.replace(oCodeVar, "rmDbg2Res"); // 注入代码到脚本。 newBody = newBody.replace(oLine, rmDbg1Code + rmDbg2Code + newEvalCode); } // 替换响应体 oSession.utilSetResponseBody(newBody); }}