网址就是csdn官网

今日在csdn浏览文章时候发现:无法复制文字内容,总是弹出一个登录窗口(我没登录);devtool的console.log的功能不可用

console.log不可用解决

先在 源代码->事件侦听器断点->勾选脚本事件->f5刷新页面

此时会断住,然后在控制台输入:ori_log=console.log并回车,接着取消勾选脚本事件断点,等待页面加载完成,即可用ori_log替代

这是因为bot-score-v1.js劫持了console对象下的["log", "warn", "info", "error", "exception", "table", "trace"]方法,并绑定到了一个空函数上

这个可以通过在控制台输入console.dir(console.log),会发现:

console.log被劫持

正常的应该是:

正常的console.log

顺着[[BoundThis]]下的方法点进去,这是一个典型的ob混淆,可以在 https://obf-io.deobfuscate.io/ 里面解混淆

让我们来看看做了什么(AI生成):

这是一个经过高度混淆的脚本,主要用于设备指纹采集、机器人检测以及数据上报。从请求路径 /cdn_cgi_bs_bot/ 可以判断,它属于 Cloudflare Bot Management(Cloudflare 的机器人管理服务)的一部分。

下面按执行顺序和功能模块,详细解释代码做了什么。


1. 自保护与反调试机制(开头两个闭包)

const _0x260d53 = function () { ... }();
const _0x4703da = _0x260d53(this, function () {return _0x4703da.toString().search("(((.+)+)+)+$") ...
});
_0x4703da();
  • _0x260d53 是一个单次执行封装器,它会返回一个函数,但内部函数只能被调用一次(通过闭包标志位 _0x18bbbe 控制)。这通常用于防止外部重复调用或调试器干扰。
  • _0x4703da 调用自身 toString() 并执行正则 (((.+)+)+)+$ 搜索。这个正则是一个经典的ReDoS(正则拒绝服务)攻击模式,同时也可以用于检测代码是否被格式化或篡改——如果函数体被改动,search 结果会不同,可能直接抛出异常,起到防逆向作用。

类似的第二个闭包 _0x228bed_0x4c84d5 功能更具体,见下一条。


2. 劫持控制台方法(_0x4c84d5

const _0x228bed = function () { ... }();
const _0x4c84d5 = _0x228bed(this, function () {const _0x17b582 = function () {// 获取全局对象 (window)};const _0x23f78c = _0x17b582().console;const _0x37fa34 = ["log", "warn", "info", "error", "exception", "table", "trace"];for (...) {const _0x219b17 = _0x228bed.constructor.prototype.bind(_0x228bed);_0x219b17.__proto__ = _0x228bed.bind(_0x228bed);_0x219b17.toString = _0x16e0f1.toString.bind(_0x16e0f1);_0x23f78c[methodName] = _0x219b17;}
});
_0x4c84d5();
  • 它通过 Function("return (function() {}.constructor(\"return this\")( ));")() 来获取全局对象 window(此方式可避开严格模式的限制)。
  • 遍历 console.logwarninfoerror 等方法,将它们全部替换成一个绑定了_0x228bed的空白函数。
  • 同时把替换函数的 __proto__toString 也做了重定向,让它在开发者工具中看起来像是原生的 console 方法,但实际上禁止了所有控制台输出,防止调试信息泄露或被人从控制台观察脚本行为。

3. Cookie 读取函数(_0x4a7a78

function _0x4a7a78(cookieName) {var cookies = document.cookie.split("; ");for (var i = 0; i < cookies.length; i++) {var parts = cookies[i].split('=');if (cookieName == parts[0]) return unescape(parts[1]);}return '';
}

简单地从 document.cookie 中解析并返回指定名称的 cookie 值。在后续流程中用于读取名为 "bc_bot_session" 的 cookie。


4. 加密函数(_0x26f755

function _0x26f755(plaintext, keyStr) {var keyMd5 = CryptoJS.MD5(keyStr).toString();var key = CryptoJS.enc.Utf8.parse(keyMd5.substring(0, 16));var iv  = CryptoJS.enc.Utf8.parse(keyMd5.substring(16));return CryptoJS.AES.encrypt(plaintext, key, {iv: iv,mode: CryptoJS.mode.CBC,padding: CryptoJS.pad.Pkcs7}).toString();
}
  • 依赖全局的 CryptoJS 库(AES 加密,CBC 模式,Pkcs7 填充)。
  • 先将密钥字符串进行 MD5 哈希,取结果的前 16 个字符作为 AES 密钥,后 16 个字符作为 IV(初始化向量)。
  • 返回加密后的密文(通常是 Base64 编码字符串)。

5. 指纹数据整理(_0x44786b

function _0x44786b(fingerprintResult) {let data = {visitorId: fingerprintResult.visitorId,confidence: fingerprintResult.confidence.score};// 提取 FingerprintJS 组件信息let components = fingerprintResult.components;if (components) {data.fontsLength = ...;data.audio = ...;data.colorGamut = ...;data.canvas = ...; // winding 等data.touchSupport = ...;data.cookiesEnabled = ...;data.videoCard = ...;data.pdfViewerEnabled = ...;data.platform = ...;// 等等}// 提取 Botd 机器人检测信息let botd = fingerprintResult.botd;let botdComps = botd.components;if (botdComps) {data.userAgent = ...;data.windowSize = ...;data.pluginsLength = ...;data.webGL = ...;data.webDriver = ...;     // 是否启用自动化工具(如 Selenium)data.android = ...;data.distinctiveProps = ...;data.browserKind = ...;// ...}data.bot = botd.bot;// 如果 botd 组件不存在且 bot 标志不为 true,则强制标记为机器人if (botd.bot != true && (botdComps == undefined)) {data.bot = true;}return data;
}
  • 将 FingerprintJS v3 生成的庞大指纹对象精简成一个只包含关键字段的平面对象。
  • 同时合并 Botd (Bot Detection) 模块的检测结果。
  • 特别关注 webDriver(自动化驱动检测)、bot 标记等字段,用于判断访问者是否为机器人。

6. 加密并上报数据(_0x4f90d3

function _0x4f90d3(processedFp) {let sessionCookie = _0x4a7a78("bc_bot_session");let visitorId = processedFp.visitorId;let encrypted = _0x26f755(JSON.stringify(processedFp), sessionCookie + visitorId);const payload = { fp: visitorId, enc: encrypted };fetch("/cdn_cgi_bs_bot/api", {method: "POST",headers: { "Content-Type": "application/json" },body: JSON.stringify(payload)}).then(res => res.json()).then(data => data.status == 1).catch(err => console.error('Error:', err));
}
  • 从 cookie 读取 bc_bot_session 作为会话标识。
  • bc_bot_session + visitorId 拼接后作为加密密钥,对精简后的指纹数据进行 AES 加密。
  • visitorId(明文)和密文一起通过 POST 发送到 /cdn_cgi_bs_bot/api
  • 期望响应 JSON 中 status 为 1 表示成功。

7. 主执行流程

let botdResult;
const botdPromise = import("/cdn_cgi_bs_bot/static/fingerprintjs-botd-v1.js").then(module => module.load());botdPromise.then(loaded => {let result = loaded.detect();result.components = loaded.components;result.detections = loaded.detections;return result;}).then(res => { botdResult = res; }).catch(err => console.error(err));const fpPromise = import("/cdn_cgi_bs_bot/static/fingerprintjs-v3.js").then(module => module.load());fpPromise.then(fp => fp.get()).then(fpData => {fpData.botd = botdResult;            // 将 botd 检测结果挂到指纹对象上let processed = _0x44786b(fpData);  // 精简数据_0x4f90d3(processed);               // 加密并上报});
  • 使用动态 import() 加载两个外部脚本:
    • fingerprintjs-botd-v1.js:机器人检测库,提供浏览器特征检测(如 WebDriver、headless chrome 痕迹等)。
    • fingerprintjs-v3.js:FingerprintJS v3,生成浏览器指纹(Visitor ID)。
  • Botd 先执行 load()detect(),结果暂存到变量 botdResult
  • FingerprintJS 调用 get() 获取完整的浏览器指纹对象。
  • 将 Botd 结果合并进指纹对象,再经过 _0x44786b 提取关键字段,最后调用 _0x4f90d3 加密上报。

整体目的

这段脚本是 Cloudflare 的反机器人/安全防护系统 在客户端埋下的检测代码,其主要功能为:

  1. 收集多维度的浏览器指纹(Canvas、WebGL、字体、音频、硬件并发、插件、屏幕尺寸等)。
  2. 识别自动化工具与机器人(如 Selenium WebDriver、Headless Chrome、Android 模拟器等)。
  3. 将收集到的数据用动态密钥加密(密钥来自 Cookie 和指纹 ID)后回传到 Cloudflare 的接口。
  4. 抑制开发者工具的控制台输出,增加逆向分析的难度。
  5. 采用混淆和反调试技巧,防止篡改和静态分析。

最终,服务端根据这些加密的指纹数据判断访问者是否为正常用户,从而决定放行、拦截或提出人机验证挑战。


再让我们看看fingerprintjs-botd-v1.jsfingerprintjs-v3.js是干什么的(AI生成):

这两个脚本是Cloudflare机器人管理系统(Bot Management)中使用的客户端检测模块。fingerprintjs-v3.js 负责生成浏览器指纹,而 fingerprintjs-botd-v1.js 则用于检测自动化工具和机器人。

fingerprintjs-v3.js:浏览器指纹识别

脚本 fingerprintjs-v3.js 是FingerprintJS v3.4.2开源库,主要功能是生成一个独特的"访问者ID"(Visitor ID)

  • 技术原理:它通过收集浏览器的多种"信号"(如Canvas渲染、WebGL、音频处理、字体列表、屏幕色深等),利用MurmurHash3哈希算法生成一个紧凑的标识符。后续分析中提到的"Picasso"技术,也是其指纹采集的一部分,被用于生成信号。该ID在隐身模式下也能保持稳定,但纯客户端版本准确率有限,约为40%-60%

fingerprintjs-botd-v1.js:机器人检测

脚本 fingerprintjs-botd-v1.js 是Fingerprint BotD v1.9.1库,专门用于识别访问者是否为自动化程序

  • 检测机制:它不仅检测通用的无头浏览器和自动化框架(如Selenium、Playwright),还会根据navigator.userAgent来判断浏览器内核(如Chromium/Blink、WebKit、Gecko)。
  • 高级检测:它还会检查设备是否开启WebDriver模式,并检查navigator.plugins等属性,以区分真实浏览器和模拟浏览器的机器人。

与Cloudflare的协同工作

这两个脚本会被Cloudflare动态注入到HTML页面中。其生成的信号(如JA3/JA4指纹)会被纳入后台的机器人评分(Bot Score)系统,用于区分人类与自动流量。默认情况下,脚本每30分钟才执行一次。

总结

总的来说,这两个脚本共同构建了一个双层检测体系:第一个脚本用于识别"谁在访问"(即生成设备指纹),第二个脚本用于判断"是不是真人"(即检测机器人)。它们是Cloudflare机器人管理系统中不可或缺的一环。

未登录无法复制解决

这个就需要下断点跟踪了,我们现在元素中找到所有的事件绑定,这里面没有copy的绑定,所以就只好看keydown了,其实也可以全局搜索copy 的,但用前者却无意间发现了一个关键区别,这个等会再说。

事件绑定

进去下断点后,刚把鼠标挪到页面内就断住了,这不是我们想要的,我们先看看断点的情况:

断点情况

这是鼠标移动触发的,说明多个事件都被绑到这个'代理'上了,我们要的是copy时间,故改为条件断点:a.type=='copy',放开执行,进行复制操作,果然断住了。

接下来分析断点处的三元表达式得出下一步会走n.event.dispatch.apply(k.elem, arguments),让我们跟进:

跟进
  • a = n.event.fix(a);a进行了细微的改动,不用管
  • i = e.call(arguments) eslice,将arguments复制给i
  • j = (n._data(this, "events") || {})[a.type] || []这个就是取出对应事件的绑定,目前的a.type='copy'

让我们执行完j的赋值:

j的赋值

先埋一个伏笔:当前文件是detail-b15e6c982b.min.js

return n(),行处下一个断点,然后直接让程序走到这里,跟进到n函数,发现又调用的window.csdn.loginBox.show函数,继续跟进去:

loginBox.show

我把这个e函数拿过来了:

function e(e) {for (var t = document.cookie.split("; "), i = 0; i < t.length; i++) {var o = t[i].split("=");if (o[0] == e) return decodeURIComponent(o[1])}
}

这就是一个从cookie中取值的函数,传入了'UserName',在控制台执行e("UserName")返回undefined

当时我的猜测是登录了的肯定会返回有值,可能登录可以复制的原因就是执行了return void void 0;,所以我直接一把梭哈,把show的函数体换为了return void void 0;然后保存,刷新,再次复制的时候发现没有弹出窗口了,但是依然无法复制。。。

于是我直接另开一个无痕窗口登录了csdn,重复上面的路程,直到执行完那个j的赋值然后进入到copy的handler:

登录状态的handler

这里的文件名变为了edit_copy_code-43c2d050a0.min.js

原来检测了登录状态,执行不同的脚本

然后我回到了未登录的窗口继续观察,最后发现这是检测未登录才对copy等事件进行重新绑定的,调用getCookie("UserName")

getCookie调用

进入到getCookie函数发现是另一个文件的,这个大概率挂载到全局了,切换到登录的窗口中,控制台输入getCookie发现返回函数,点进去到了相同文件的地方

于是我反手写代理getCookie的代码,接着在if (copyPopSwitch && !getCookie("UserName"))处打一个断点:

断点
ori_getcookie = getCookie
getCookie = new Proxy(ori_getcookie, {apply(target, thisArg, argumentsList) {const [key] = argumentsList;if (key == 'UserName') {return true;}const result = Reflect.apply(target, thisArg, [key, value]);return result;}
});

然后刷新网页,断住后粘贴代理,继续运行,发现并没有重新绑定,继续运行等待页面加载完成,再次复制,发现可以正常复制

但是这样也有弊端,就是需要找到特定代码,然后下断点,刷新,注入代码,流程麻烦复杂

那么就直接将getCookie更改(替代文件)也可以直接进行复制

免责声明:本文仅用于技术学习和交流目的,请勿将本文内容用于任何非法用途。任何因使用本文内容而产生的后果,作者不承担任何责任。
--- 文章结束 ---