khbox 的项目已放gitee, https://gitee.com/sugarysp/khbox_pro ,欢迎各位大佬使用测试。
目前完成的有
- 调用链追踪
- document.all
- 原型保护以及toString
- c层调用链日志保存
- illegal 保护机制
- ps:可能有bug
待做:
- 异步
- console.log 业务代码保存日志
目录
- 核心挑战
- 原型链继承体系
- Function.prototype.toString 保护
- Illegal Invocation 保护机制
- 测试验证
- 总结
核心挑战
1. 原型链继承
浏览器中的 DOM 对象有着复杂的继承关系:
Document → Node → EventTarget → Object Navigator → Object Window → EventTarget → Object检测手段:
// 网站检测代码Object.getPrototypeOf(Document.prototype).constructor.name==='Node'// 必须为 true'addEventListener'inEventTarget.prototype// 必须为 true2. Native Code 伪装
真实浏览器的原生方法调用toString()会返回[native code]:
// 真实浏览器navigator.javaEnabled.toString()// "function javaEnabled() { [native code] }"// 普通 JS 实现functionjavaEnabled(){returnfalse;}javaEnabled.toString()// "function javaEnabled() { return false; }" ❌ 暴露了3. Illegal Invocation 保护
浏览器原生方法强制检查this的合法性:
// 真实浏览器constuaGetter=Object.getOwnPropertyDescriptor(Navigator.prototype,'userAgent').get;uaGetter.call(window);// TypeError: Illegal invocation// 普通 JS 实现functionget_userAgent(){returnthis._ua;}get_userAgent.call(window);// 可以执行 ❌ 行为不一致原型链继承体系
实现方式
KhBox 通过在 sandbox 中暴露构造函数,结合 JSDOM 的原型链实现正确的继承关系:
// main.js - 构建 sandboxconstsandbox={// 暴露实例(经过 KhBox Proxy 包装)document:proxyDocument,navigator:proxyNavigator,window:proxyWindow,// 暴露构造函数(直接引用 JSDOM 的)Document:dom.window.Document,Navigator:dom.window.Navigator,Window:dom.window.Window,Node:dom.window.Node,EventTarget:dom.window.EventTarget,// ...};验证结果
// 测试原型链Document.prototype.constructor.name// "Document"Object.getPrototypeOf(Document.prototype).constructor.name// "Node"Object.getPrototypeOf(Node.prototype).constructor.name// "EventTarget"// 测试属性定义位置'cookie'inDocument.prototype// true'addEventListener'inEventTarget.prototype// trueFunction.prototype.toString 保护
技术原理
利用 V8 的Private Symbol机制,为函数对象存储自定义的 toString 返回值:
// native_protection.ccLocal<Function>CreateNativeWrapper(...){// 创建原生函数包装器Local<FunctionTemplate>tpl=FunctionTemplate::New(isolate,callback);Local<Function>nativeFunc=tpl->GetFunction(context).ToLocalChecked();// 生成 [native code] 字符串std::string native_code;if(strcmp(method_type,"get")==0){native_code="function get "+name+"() { [native code] }";}else{native_code="function "+name+"() { [native code] }";}// 🔥 关键:用 Private Symbol 存储nativeFunc->SetPrivate(context,GetNativeStringSymbol(isolate),String::NewFromUtf8(isolate,native_code.c_str()).ToLocalChecked());returnnativeFunc;}Private Symbol 的优势
- 不可枚举:
Object.keys()无法获取 - 不可访问:JavaScript 层无法读取或修改
- V8 内部支持:
Function.prototype.toString会优先读取此 Symbol
验证效果
navigator.javaEnabled.toString()// "function javaEnabled() { [native code] }" ✅String(navigator.javaEnabled)// "function javaEnabled() { [native code] }" ✅Illegal Invocation 保护机制
什么是 Illegal Invocation?
当调用浏览器原生方法时,如果this不是预期的对象类型,会抛出TypeError: Illegal invocation:
// 合法调用navigator.userAgent// ✅// 非法调用constuaGetter=Object.getOwnPropertyDescriptor(Navigator.prototype,'userAgent').get;uaGetter.call(window)// ❌ TypeError: Illegal invocation实现架构
KhBox 通过三层检查机制实现 Illegal Invocation 保护:
第一层:焊死到 Prototype 上
// toolFuncs.jslazyLoader:function(receiver,target,prop,classNameFromC){// ...// 🔥 焊死到 Constructor.prototype 而不是实例上constwindow=khBox.memory.jsdom.window;constprotoTarget=window[protoOwner]?.prototype||receiver;return{impl:khBox.envFuncs[implKey],target:protoTarget,// Navigator.prototypeclassName:protoOwner,// "Navigator"// ...};}为什么要焊死到 Prototype?
// 如果焊死到实例上:navigator.userAgent// ✅ 可以访问Navigator.prototype.userAgent// ❌ 拿不到我们的 wrapper// 焊死到 Prototype 上:navigator.userAgent// ✅ 继承自 prototypeNavigator.prototype.userAgent// ✅ 直接获取到我们的 wrapperObject.getOwnPropertyDescriptor(Navigator.prototype,'userAgent').get// ✅ 受保护第二层:className 权威来源
使用 JSON 配置中的类名而不是 JSDOM 的constructor.name,因为jsdom某些可能不够全,优先用自己脱下来的环境:
// watcher.cc - Watch 函数voidWatch(constv8::FunctionCallbackInfo<Value>&args){// 优先使用传入的 className(来自 JSON 配置)constchar*class_name=nullptr;if(args.Length()>=2&&args[1]->IsString()){String::Utf8Valuecn(isolate,args[1]);class_name_str=*cn;class_name=class_name_str.c_str();}// 存储到 Proxy 的 InternalFieldLocal<Object>proxy=CreateWatcherProxy(isolate,target,source_path,class_name);// ...}// main.js - 调用时明确指定类名constproxyNavigator=addon.watch(navigator,'Navigator');constproxyWindow=addon.watch(window,'Window');降级策略:
// toolFuncs.jsconstclassName=classNameFromC// 优先:JSON 配置||target[Symbol.toStringTag]// 降级 1||target.constructor?.name// 降级 2:JSDOM||"Unknown";测试验证
测试脚本
完整的测试代码位于khbox_vm/pages/demo_proto/input.js:
// Part 1: 原型链测试console.log('Document.prototype.constructor.name:',Document.prototype.constructor.name);console.log('Document 的父类:',Object.getPrototypeOf(Document.prototype).constructor.name);// Part 2: toString 保护console.log(navigator.javaEnabled.toString());// "function javaEnabled() { [native code] }"// Part 3: Illegal InvocationconstuaGetter=Object.getOwnPropertyDescriptor(Navigator.prototype,'userAgent').get;// 正常调用console.log(navigator.userAgent);// ✅ 成功// 非法调用uaGetter.call(window);// ❌ TypeError: Illegal invocation运行结果
$nodemain.js## Part 1: 原型链继承测试✅ Document.prototype.constructor.name: Document ✅ Document.prototype 的父类: Node ✅ Node.prototype 的父类: EventTarget## Part 2: toString 保护机制测试✅ navigator.javaEnabled.toString():functionjavaEnabled(){[native code]}## Part 3: Illegal Invocation 保护测试---3.1正常调用 --- ✅ navigator.userAgent: Mozilla/5.0(Windows NT10.0;Win64;x64)... ✅ navigator.javaEnabled():false---3.2非法调用 ---[Illegal Invocation Check]Expected:'Navigator', Actual:'Window'✅ 抛出 Illegal Invocation: Illegal invocation总结
KhBox 框架通过以下机制实现了完整的浏览器行为模拟:
1. 原型链继承
- 在 sandbox 中暴露 JSDOM 的构造函数
- 保持正确的
prototype链关系 - 属性定义位置符合浏览器标准
2. Native Code 伪装
- 使用 V8 Private Symbol 存储自定义 toString
Function.prototype.toString返回[native code]- JavaScript 层无法篡改或检测
3. Illegal Invocation 保护
- 焊死到
Constructor.prototype而不是实例 - className 优先使用 JSON 配置(权威来源)
- 运行时严格检查
this的类型 - 类型不匹配时抛出
TypeError: Illegal invocation
更多文章,敬请关注gzh:零基础爬虫第一天天