爬虫——夜幕团队JS逆向系列课
// chrome dev-tools console
console.count() console.table()
Copy(var)复制到粘贴板
自带jQuery中操作符: $、$$、$x(xpath)
js语法-函数
变量定义:var在函数内部起作用,const、let 块级作用域
变量提升:扫描整个函数体语句,将所有声明提升到函数顶部
全局作用域:如不使用 var、const、let 关键字声明,变量会绑定到全局 window 上
块级作用域:for、while 等语句内
js进阶
- 事件循环
- 宏任务(Macrotasks)与微任务(Microtasks)概述
- Node.js 事件循环
- 原型链
ES6引入class关键字后用的就不多,但现有项目中很多都没用es6,或用bable转为es5![]()
访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 会自动在其原型对象上查找。如果原型对象也没有,就会继续在原型对象的原型上查找,这样一直向上查找直到找到该属性或方法或者到达原型链的顶端(Object.prototype) - 异步编程
回调函数:简单、回调地狱
异步操作对象:resolve reject then catch,可读性不高
async await,多个并行操作要用 Promise.all 执行 - 浏览器存储
//cookie 通过document.cookie属性来设置,格式为name=value; expires=expiration_time; path=domain_path; domain=domain_name; secure。 例如:document.cookie = "username=John Doe; expires=Thu, 18 Dec 2024 12:00:00 UTC; path=/"; 同样使用document.cookie,它会返回一个包含所有当前页面可用 cookies 的字符串,需要手动解析。 将expires设置为过去的时间即可删除。 //Local Storage localStorage.setItem('key', 'value') const theme = localStorage.getItem('theme') //如果不存在则返回null localStorage.removeItem('key') //Session Storage 数据在页面会话期间(浏览器打开直到关闭)有效,关闭浏览器后数据丢失,大小限制与localStorage类似 sessionStorage.setItem('key', 'value') sessionStorage.getItem('key') sessionStorage.removeItem('key') // IndexedDB const request = indexedDB.open('myDatabase', 1); request.onupgradeneeded = function(event) { const db = event.target.result; const objectStore = db.createObjectStore('customers', { keyPath: 'id' }); };![]()
- 跨域
- Webpack 打包
python 调用 JS
库:PyV8 Js2Py PyExecJS(特殊编码可能导致报错,可通过删除或Base64编码处理) PyminiRacer、 Selenium Pyppeteer、使用nodejs开放一个执行js文件的微服务
无限 debugger
- 禁用所有断点,或禁用条件断点
![]()
- 使用devtools的override功能
- 使用代理修改页面内容,如 Fiddler Script, mitmProxy
- 函数调用前,在控制台重写函数
block = function () {} - reres 插件
快速定位
明文搜索、sources查看、xhr请求追溯、hook追溯、调用栈跟踪、全局断点、dom断点、主动debugger、warning error 追溯、内存优化分析、事件监听等
搜索
dev-tools:CTRL-F、CTRL-SHIFT-F、Network 界面 Filter
Fiddler:Ctrl-F
断点
xhr、DOM、EVENT、自定义


事件监听器

hook
- json
var my_stringify = JSON.stringify; JSON.stringify = function (params) { console.log("yemu", params); return my_stringify(params); }; var my_parse = JSON.parse; JSON.parse = function (params) { console.log("yemu", params); return my_parse(params); }; - cookie
var cookie_cache = document.cookie; Object.defineProperty(document, 'cookie', { get: function () { }, set: function (val) { console.log('setting cookie', val); var cookie = val.split(";")[0]; var ncookie = cookie.split("="); var flag = false; var cache = cookie_cache.split(";"); cache = cache.map(function (a) { if (a.split("=")[0] === ncookie[0]) { this.value = val; return cookie_cache; } return a; flag = true; return cookie; }); cookie_cache = cache.join(";"); if (!flag) { cookie_cache += cookie + ";"; } } }); - window attr
- eval / Function
window._cr_eval = window.eval; var myeval = function(src) { console.log(src); console.log("======================= eval end ======================="); return window._cr_eval(src); }; var myeval = myeval.bind(null); myeval.toString = window._cr_eval.toString; Object.defineProperty(window, 'eval', { value: myeval }); // 尝试执行 eval('1+2') window._cr_fun = window.Function; var myfun = function() { var args = Array.prototype.slice.call(arguments, 0, -1).join(","); var src = arguments[arguments.length - 1]; console.log(src); console.log("======================= Func end ======================="); return window._cr_fun.apply(this, arguments); }; myfun.toString = function() { return window._cr_fun + ""; }; Object.defineProperty(window, 'Function', { value: myfun }); // 尝试执行 let a = new Function("return 1 + 2"); - websocket
Websocket.prototype.sendA = Websocket.prototype.send; Websocket.prototype.send = function(data) { console.info('Hook websocket', data); return this.sendA(data); }; - tampermonkey hook https://blog.csdn.net/Yy_Rose/article/details/124216720
hook
// ==UserScript== // @name HookBase64 // @namespace https://login1.scrape.center/ // @version 0.1 // @description Hook Base64 encode function // @match https://login1.scrape.center/ // @grant none // ==/UserScript== (function() { 'use strict'; function hook(object, attr){ var func = object[attr] object[attr] = function(){ console.log('hooked', object, attr) var ret = func.apply(object, arguments) debugger return ret } } hook(window, 'btoa') })();hook
// ==UserScript== // @name Hook global // @namespace http://tampermonkey.net/ // @include * // @grant none // @run-at document-start // ==/UserScript== (function() { 'use strict'; //全局变量 监控 var t = window._t; var window_flag = '_t'; // 要监控的值 var window_value = window[window_flag]; Object.defineProperty(window, window_flag, { // window 对象上的值 get: function() { console.log('Getting window._t',window_value); return t; }, set: function(val) { console.log('Setting window._t', val); debugger; t = val; return t; } }); })();检测是否存在可疑的加密函数
// ==UserScript== // @name HOOK 遍历 // @namespace http://tampermonkey.net/ // @version 0.1 // @description day day up // @author FY // @include * // @grant none // @run-at document-end // ==/UserScript== (function() { 'use strict'; !function () { 'use strict'; var source = ['DeCode','EnCode','decodeData','base64decode','md5','decode','btoa','MD5','RSA','AES','CryptoJS','encrypt','strdecode',"encode",'decodeURIComponent','_t','JSON.stringify','String.fromCharCode','fromCharCode']; console.log("开始测试是否有解密函数"); let realCtx, realName; function getRealCtx(ctx, funcName) { let parts = funcName.split("."); let realCtx = ctx; for(let i = 0; i < parts.length - 1; i++) { realCtx = realCtx[parts[i]]; } return realCtx; } function getRealName(funcName) { let parts = funcName.split("."); return parts[parts.length - 1]; } function test(ctx) { for(let i = 0; i < source.length; i++) { let f = source[i]; let realCtx = getRealCtx(ctx, f); let realName = getRealName(f); let chars = realCtx[realName]; if (chars != undefined){ console.log("发现可疑函数:", f); console.log(chars); console.log("---------------------"); }else{ console.log("未发现:", f); } } } test(window); }(); })();获取返回值、内部调用函数名
// ==UserScript== // @name HOOK ALL end // @namespace http://tampermonkey.net/ // @include * // @grant none // @run-at document-end // ==/UserScript== (function() { 'use strict'; var source = ['DeCode','EnCode','decodeData','base64decode','md5','decode','btoa','MD5','RSA','AES','CryptoJS','encrypt','strdecode',"encode",'decodeURIComponent','_t','JSON.stringify','String.fromCharCode','fromCharCode']; console.log("开始测试是否有解密函数"); let realCtx, realName; function getRealCtx(ctx, funcName) { let parts = funcName.split("."); let realCtx = ctx; for(let i = 0; i < parts.length - 1; i++) { realCtx = realCtx[parts[i]]; } return realCtx; } function getRealName(funcName) { let parts = funcName.split("."); return parts[parts.length - 1]; } function hook(ctx, funcName, level, originFunc) { ctx[funcName] = function(a){ console.log("level:" + level + " function:" + funcName,a); console.log(originFunc.toString()); console.log(originFunc.toString); debugger; return originFunc(a); }; } function test(ctx, level) { for(let i = 0; i < source.length; i++) { let f = source[i]; let realCtx = getRealCtx(ctx, f); let realName = getRealName(f); let chars = realCtx[realName]; hook(realCtx, realName, level, chars); } } test(window, 1); })(); /////////////////////////// // ==UserScript== // @name HOOK 二层函数名 end // @namespace http://tampermonkey.net/ // @include * // @grant none // @run-at document-end // ==/UserScript== (function() { 'use strict'; var source = ['decodeData','base64decode','md5','decode','btoa','MD5','RSA','AES','CryptoJS','encrypt','strdecode',"encode",'decodeURIComponent','_t','JSON.stringify','String.fromCharCode','fromCharCode']; console.log("开始测试是否有解密函数"); let realCtx, realName; function getRealCtx(ctx, funcName) { let parts = funcName.split("."); let realCtx = ctx; for(let i = 0; i < parts.length - 1; i++) { realCtx = realCtx[parts[i]]; } return realCtx; } function getRealName(funcName) { let parts = funcName.split("."); return parts[parts.length - 1]; } function hook(ctx, funcName, level, originFunc) { ctx[funcName] = function(a){ console.log("level:" + level + " function:" + funcName,a); let regexp = / [\S]*\(.*\)\;/g; let match = originFunc.toString().match(regexp) console.log(match); debugger; return originFunc(a); }; } function test(ctx, level) { for(let i = 0; i < source.length; i++) { let f = source[i]; let realCtx = getRealCtx(ctx, f); let realName = getRealName(f); let chars = realCtx[realName]; hook(realCtx, realName, level, chars); } } test(window, 1); })();
可通过tampermonkey、执行js前打断点的方式进行注入
分析
Event、网络、XMLHttpRequest 调用栈
nodejs模拟chrome环境
// npm install jsdom
const jsdom = require('jsdom');
const { JSDOM } = jsdom;
const dom = new JSDOM(`<!DOCTYPE html>`);
global.window = dom.window;
global.document = dom.window.document;
// 定义 window.atob
global.window.atob = function (encoded) {
// 根据 atob 的实际功能实现逻辑,如果只是模拟一个固定值返回,可以这样:
return 'decoded_value';
};
// 定义 screen 对象
global.screen = { width: 1920, height: 1080 };
// 定义 navigator 对象
global.navigator = {
userAgent: 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/[your_version] Safari/537.36',
};
处理js代码混淆
查找加密参数:
- 通过抓包找到加密参数
- 全局搜索参数
- 查看网络面板 Initiator
- xhr 断点调试
- hook
- 分析加密逻辑
颜文字、符号加密 JSFuck 等原理:

解决方案:
- 直接粘贴到console执行即可获得原js,通过VM查看
- 或删除最后一个表情
('_');/括号(),添加toString()方法在控制台执行 - 删除最后的
();删除,执行得到代码 - 如果不是以
()结尾,那么将最后一对括号内部的符号复制并执行,得到原生代码。剩余代码大概率为 eval 函数的编 ![]()
- 以
eval开头的代码,将 eval 改为alertconsole.log
平坦化混淆
- 全局观察
是否有 dom操作、纯计算循环体、try-catch异常捕获 - 整体分析与载入
断点定于 while开头、try代码第一行、while整体取出构造原始函数 - 构造函数
通过报错补充函数或数据
通过 AST 增强代码可读性 https://astexplorer.net/ ,npm的 recast 包将js转为AST语法树。平坦流将代码转换为 while-switch-case 格式,通过 consolelog 打印加AST语法分析。
CSS反爬
- 字体
通过font-family指定特殊字体,在页面中不可见的unicode在指定字体中映射到可读字符。爬虫只能爬取到unicode而不是可读字体
应对:下载woff字体转为tff字体,用字体编辑器确定其字符与unicode间映射关系,替代映射得到正确数据
有些网站动态生成woff,很难自动化绕开 - 背景
数据(通常是数字)通过雪碧图(Sprite)通过背景偏移展示,抓取时看不到实际值而是图片背景
下载图片,手动检查获取 background-position 偏移量与实际值间映射关系,爬虫获取偏移值并转化为实际值 - 伪类
不直接展示内容,而是通过伪类content属性展示值。难在获取指定元素的伪类属性
利用puppeteer或Selenium获取伪类,解析css
/* .valuable-content::before { content: "hiding content" } */ const el = document.querySelector('.valuable-content') const styles = getComputedStyle(el, 'before') console.log(styles.content) - 元素定位
利用绝对定位(position: absolute)将某个数字或字符将原字符通过一定偏移量替换。替换的字符是随机的,直接抓取获得错误信息。
计算出替换的元素的偏移量,与被替换元素对比,还原实际值![]()
const elPr = document.querySelector('.mb-10.b-airfly:nth-child(1).fixprice.prc_wp'); let strArr = Array.from(elPr.querySelectorAll('b:first-child > i')).map(el => el.innerText); // 替换元素 elPr.querySelectorAll('b:not(:first-child)').forEach(el => { // 偏移 const left = Number(getComputedStyle(el).left.replace('px', '')); // 替换 strArr[strArr.length + left / 16] = el.innerText; }); console.log(strArr.join('')); - 字符切割
将字符串用标签分割,内联块级(inline-block)可一行展示,还可混有不显示标签(display:none)
拼接 innerText 并忽略 display:none 标签![]()
const elIp = document.querySelector('.ip'); let str = ''; const elList = elIp.querySelectorAll('*:not([style="display: none;"])'); elList.forEach((el, i) => { if (i === elList.length - 1) return; str += el.innerText; }); console.log(str);
应对策略:
- 通过调试工具人工查看CSS样式
- 判断CSS反爬类型
- 思考应对措施,css反爬非常多变,没有固定套路







浙公网安备 33010602011771号