首页 > 其他分享 >前端黑魔法 —— 如何让自己的函数变成原生函数

前端黑魔法 —— 如何让自己的函数变成原生函数

时间:2023-08-07 11:25:26浏览次数:43  
标签:原生 function 黑魔法 console 函数 alert code log

前言

熟悉 JS 的都知道,原生函数转成字符串,显示的是 native code:

alert + ''    // "function alert() { [native code] }"

如果用自己的函数对其重写,显示的则是自己的代码:

alert = function(s) { console.log(s) }
alert + ''    // "function(s) { console.log(s) }"

有没有什么黑魔法,让自己的函数也变成 native code,从而掩盖这个破绽?

toString

最容易想到的办法,就是重写函数的 toString 方法:

alert = function(s) { console.log(s) }
alert.toString = () => "function alert() { [native code] }"

alert.toString()    // "function alert() { [native code] }"

不过这种办法破绽极多,例如调用原生 toString 即可将其原形毕露:

Function.prototype.toString.call(alert)   // "function(s) { console.log(s) }"

即便原生 toString 也被重写,还可创建 iframe 页面获取未被污染的原生 toString

事实上这个办法并没有改变函数本质,只是改变观察结果而已。

bind

《如何让 JS 代码不可断点》文中提到,通过 bind 方法创建的新函数,其实是原生的。

我们来验证对比下:

// 原生结果
console.log('before:', alert + '')    // "function alert() { [native code] }"

alert = (function() {
  function alert(s) {
    console.log('test:', s)
  }
  return alert.bind(window)
})()

// 重写结果
console.log('after:', alert + '')     // "function () { [native code] }"

alert('hello bind')

两者主体部分都为 [native code]。不过重写后的结果少了函数名,比原生更短。算是一个小破绽。

此外,除了字符串结果不同,函数的 namelength 属性也有差异:

// 原生值
console.log('before:', alert.name)    // "alert"
console.log('before:', alert.length)  // 0

alert = (function() {
  function alert(s) {
    console.log('test:', s)
  }
  return alert.bind(window)
})()

// 重写值
console.log('after:', alert.name)     // "bound alert"
console.log('after:', alert.length)   // 1

运行前刷新页面

可见通过 bind 创建的新函数,其 name 属性会多一个 bound 前缀(中间有个空格),也算是个小破绽。

由于我们的 alert 函数声明了一个 s 参数,导致其 length 属性变成了 1,而原生的则为 0。

当然函数的 length 属性可随意伪造,只需声明相应个数的形参即可。使用 ...rest 这种剩余参数声明的形参,不会增加 length 值。

function A(...args) {}
A.length    // 0

function B(i, ...args) {}
B.length    // 1

Proxy

提到重写、切面等概念时,不得不联想到一个功能强大的 API —— Proxy,不少黑魔法都借助它实现。

先来试下,被代理的函数转成字符串是什么结果:

new Proxy(function F() {}, {}) + ''    // "function () { [native code] }'

和上述 bind 结果类似,虽有 native code,但少了函数名。

下面完整对比下,包括 namelength 属性:

console.log('before:', alert + '')      // "function alert() { [native code] }"
console.log('before:', alert.name)      // "alert"
console.log('before:', alert.length)    // 0

alert = (function() {
  function alert(...args) {
    console.log('test:', ...args)
  }
  return new Proxy(alert, {})
})()

console.log('after:', alert + '')       // "function () { [native code] }"
console.log('after:', alert.name)       // "alert"
console.log('after:', alert.length)     // 0

alert('hello proxy')

相比 bind 方案,Proxy 不会修改 name 属性,因此破绽更少。

不过在 Safari 浏览器上有明显的破绽,函数转成字符串后变成:

function ProxyObject() {
    [native code]
}

WebAssembly

由于 JS 程序是文本格式的,因此函数 toString 的结果自然能包含相应的代码。如果是二进制程序,那么 toString 的结果又会是什么?这个问题,可以用 WebAssembly 的导出函数来验证。

MDN 文档 中提到,WebAssembly 导出的函数会显示成 native code

我们构造一个精简的 WebAssembly 程序,将导入的 x.y 函数导出成 z 函数:

(module
  (func (export "z") (import "x" "y") (param externref))
)

使用 wat2wasm 将其转成二进制数据,并进行封装:

function genNativeFunction(callback) {
  const buf = new Uint8Array([
    0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 1, 111, 0, 2, 7, 1, 1, 120, 1, 121, 0, 0, 7, 5, 1, 1, 122, 0, 0, 0, 10, 4, 110, 97, 109, 101, 2, 3, 1, 0, 0
  ])
  const mod = new WebAssembly.Module(buf)
  const obj = new WebAssembly.Instance(mod, {x: {y: callback}})
  return obj.exports.z
}

alert = (function() {
  function alert(...args) {
    console.log('test:', ...args)
  }
  return genNativeFunction(alert)
})()

console.log(alert + '')     // "function 0() { [native code] }"
console.log(alert.name)     // "0"
console.log(alert.length)   // 1

alert('hello wasm')

WebAssembly 导出函数转成字符串后确实包含 native code,不过破绽也非常明显,其中的函数名居然是一个数字!正常的 JS 函数显然不可能以数字命名。破绽 +1。

同样 name 属性也变成了数字。破绽 +2。

并且 WebAssembly 函数的形参数量是固定的,因此 length 属性也难以伪造。破绽 +3。

因此这个方案虽然有趣,但并不隐蔽。

当然在奇葩的 Safari 上,返回结果并不包含函数名,并且 name 属性是个空字符串。不过这依然是个大破绽。

此外在 Chrome 上,调试器单步断点(F11)无法进入 WebAssembly 的导出函数:

debugger
alert(123)

看来之前的《如何让 JS 代码不可断点》又可以新增一个黑魔法了。

总结

想要完美伪造一个原生函数还是非常困难的,多多少少总有一些破绽。

如果有更好的方案,欢迎补充~

标签:原生,function,黑魔法,console,函数,alert,code,log
From: https://www.cnblogs.com/index-html/p/masquerading-custom-functions-as-native-functions.htm

相关文章

  • 7-3 两个有序序列的中位数 (25分) 已知有两个等长的非降序序列S1, S2, 设计函数求S1与
    7-3 两个有序序列的中位数 (25分)已知有两个等长的非降序序列S1,S2,设计函数求S1与S2并集的中位数。有序序列A0,A1,⋯,AN−1的中位数指A(N−1)/2的值,即第⌊(N+1)/2⌋个数(A0为第1个数)。输入格式:输入分三行。第一行给出序列的公共长度N(0<N≤100000),随后每行输入一个序列的信息,即......
  • 【我和openGauss的故事】带有out参数的存储过程及自定义函数的重载测试
    【我和openGauss的故事】带有out参数的存储过程及自定义函数的重载测试DarkAthenaopenGauss2023-08-0418:01发表于四川背景先说下数据库里说的函数重载是什么。比如我们知道数据库可能会有同名的函数,但是函数的参数声明不一样selectto_char(sysdate,'yyyymmdd')fromdual;se......
  • 用 Prometheus 打造云原生监控
    数字化转型背景下,随着轻量化的容器化技术和微服务应用的深度融合,业务复杂度随之上升。基于Prometheus的容器云监控体系成为目前主流容器监控事实标准,本文主要介绍Prometheus云原生监控体系,涵盖指标采集、数据存储、可视化展示,告警入库等功能,结合生产实践供大家参考。一、监控对象Pr......
  • 前端原型和原型链构造函数的使用
     目录前言导语原型的构造器指向构造函数 原型上添加方法注意的地方构造器指向构造函数本身总结前言我是歌谣我有个兄弟巅峰的时候排名c站总榜19叫前端小歌谣曾经我花了三年的时间创作了他现在我要用五年的时间超越他今天又是接近兄弟的一天人生难免坎坷大不了从头再来歌......
  • angular组件的生命周期钩子函数
    ​ 上图[4]展示了angular生命周期钩子函数的执行顺序,在此之前,angular会先执行constructor函数。一、基本说明1.constructor用途:初始化组件,设定属性,注入依赖。说明:构造函数中能读取到本组件内部定义的基本变量和函数的值,但是读不到@ContentChildren、@ContentChild、@V......
  • 【230806-4】三角形ABC中,内角ABC的对边为abc,已知b=2,角B=45度。求:三角形ABC面积的最大
    ......
  • 【补充】箭头函数
    【补充】箭头函数函数写法变简单箭头函数没有自己的this,在箭头函数中使用this,就是它上一层的【1】简解箭头函数是ES6中的语法特性,它提供了一种更简洁的函数定义方式。相比传统函数,箭头函数具有以下特点:简化的语法:箭头函数的语法非常简洁,可以帮助我们更快速地编写函数......
  • 【6.0】Vue之生命周期函数
    【一】Vue的生命周期【1】详解Vue.js生命周期是指在Vue实例从创建到销毁的过程中,会经历一系列的钩子函数,这些钩子函数可以让我们在不同的阶段插入自定义的代码。Vue的生命周期分为三个主要阶段:创建阶段更新阶段销毁阶段。(1)创建阶段beforeCreate:在实例初始化之后,......
  • 欧拉函数与积性函数
    \(Update\:\:on\:\:2023.8.3\):增加了积性函数的内容,修改了内容排版Part1:欧拉函数及其性质定义:欧拉函数\(φ(n)\)表示小于等于\(n\),且与\(n\)互质的正整数的个数。公式:若在算数基本定理中,\(n=p_1^{c_1}p_2^{c_2}...p_m^{c_m}\)(\(p\)为质数),则由容斥原理:\[φ(n)=n*\d......
  • C与C++之间的相互调用及函数区别
    最近项目需要使用googletest(以下简称为gtest)作为单元测试框架,但是项目本身过于庞大,main函数无从找起,需要将gtest框架编译成静态库使用。因为项目本身是通过纯c语言编写,而gtest则是一个c++编写的测试框架,其中必然涉及c与c++之间的相互调用。注意,本文的前提是,c代码采用gcc等c语言编......