V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a JavaScript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
JavaScript 权威指南第 5 版
Closure: The Definitive Guide
xieqiqiang00
V2EX  ›  JavaScript

JS 有什么手段可以判断一个函数是不是原生代码吗?

  •  1
     
  •   xieqiqiang00 · 35 天前 · 3792 次点击
    这是一个创建于 35 天前的主题,其中的信息可能已经有所发展或是发生改变。

    比如 XMLHttpRequest,有没有什么手段可以知道这个东西有没有被人为重写?
    网站找了不少方法都做不到
    下面是我做的一些尝试,这些检测方法都能被绕过

    {
        function isNative(api) {
            return /native code/.test(api.toString()) && typeof api !== 'undefined'
        }
    
        let test = function (input, fake) {
            console.log("------------------------")
            console.log("是否是伪造:", fake)
            console.log("toString:", input.toString())
            console.log("toString.toString:", input.toString)
            console.log("prototype 方法", input.hasOwnProperty("prototype"))
            console.log("toString.call","方法",Function.prototype.toString.call(input))
            console.log("网传最不靠谱方法:isNative", isNative(input))
        }
        test(XMLHttpRequest, false)
        {
            let XMLHttpRequest = function () {
                "[native code]"
            }
            XMLHttpRequest.toString = function () {
                return "function XMLHttpRequest() { [native code] }"
            }
            let toString = function () {
                return "function toString() { [native code] }"
            }
            toString.toString = toString
            XMLHttpRequest.toString.toString = toString
            Function.prototype.toString = toString
            delete XMLHttpRequest.prototype
            test(XMLHttpRequest, true)
            // XMLHttpRequest.prototype = undefined
            // test(XMLHttpRequest, true)
        }
    }
    
    80 条回复    2021-05-10 18:19:54 +08:00
    JK9993
        1
    JK9993   35 天前
    Function.prototype.toString.call
    JK9993
        2
    JK9993   35 天前
    哦,不行
    JK9993
        3
    JK9993   35 天前
    如果原型链上的 toString 被修改了,就只能检测到 toString 被修改这一步了
    xieqiqiang00
        4
    xieqiqiang00   35 天前
    @JK9993 怎么检测 tostring 被修改了?
    JK9993
        5
    JK9993   35 天前
    你可以构造一个函数,然后 toString 输出
    xieqiqiang00
        6
    xieqiqiang00   35 天前
    @JK9993 不行,我试过
    7075
        7
    7075   35 天前
    首先你先想想怎么给“原生 /非原生”下一个明确的、可量化、可操作的定义。
    有了定义,才有分类的判断标准。
    如果能拿到目标函数原始的代码,那么可以用代码做指纹...
    mopig
        8
    mopig   35 天前
    @xieqiqiang00 Function.prototype.toString.call(Function.prototype.toString) 这样检测🤔
    xieqiqiang00
        9
    xieqiqiang00   35 天前
    @7075 我指的原生就是看不到代码,可能都不是 JS 实现的,浏览器内置的这些东西<br>
    fetch 、XMLHTTPRequest 这种
    daysv
        10
    daysv   35 天前
    XMLHttpRequest.toString()

    "function XMLHttpRequest() { [native code] }"
    xiangwan
        11
    xiangwan   35 天前 via Android
    如果你信任你的运行时,这个判断才有意义
    daysv
        12
    daysv   35 天前
    再加一个 XMLHttpRequest.toString.toString() ?
    xieqiqiang00
        13
    xieqiqiang00   35 天前
    @JK9993 懂了
    xieqiqiang00
        14
    xieqiqiang00   35 天前
    @daysv 我最上面的代码就能绕开这个了
    xieqiqiang00
        15
    xieqiqiang00   35 天前
    @xiangwan 提高一下成本,打开控制台随便改改就能操作,门槛也太低了
    chogath
        16
    chogath   35 天前
    没懂需要判断的需求是什么,可否描述下场景和需求啊
    7075
        17
    7075   35 天前   ❤️ 2
    这个问题往大了说是一个哲学问题。怎么证明你是你。
    还不如从原始需求出发,解决最直接的需求问题。
    xieqiqiang00
        18
    xieqiqiang00   35 天前
    @chogath 想法是用 electron 写一个程序,代码用 vm 预编译,网络用 electron 的证书绑定,但如果直接改 js 文件重写了 XHR,通信的具体细节还是会一览无余。希望提高破解者的破解成本
    chogath
        19
    chogath   35 天前
    @xieqiqiang00 js 有转移二进制码,或者可执行文件的第三方库,或许你可以从这个角度去考虑?
    maichael
        20
    maichael   35 天前
    如果你想沿着现有思路走基本是走不通的,毕竟你的检测工具都可能被篡改。
    还不如回到你的需求本身,考虑下其它的方向。
    3dwelcome
        21
    3dwelcome   35 天前
    用 websocket+自定义协议就可以避免被简单抓包。

    类似一个主流网络游戏的自定义加密通讯协议,绝对没那么容易破解。

    或者学微信,自己造 HTTPS/SSL 轮子,也没那么容易被破解。直接用标准的 http/https 确实很不安全,客户端注入 JS,解开后就是明文了。
    renmu123
        22
    renmu123   35 天前 via Android
    如果可以直接改你的代码,你代码底裤都被看到了,那么你防止修改 xhr 又有什么用。
    代码混淆再找找有什么好的加壳工具吧
    xieqiqiang00
        23
    xieqiqiang00   35 天前
    @chogath 代码保护这块我打算用 vm 编译成字节码,但如果运行环境事先被修改了一样会上钩
    xieqiqiang00
        24
    xieqiqiang00   35 天前
    @renmu123 不想被看到请求了什么接口,代码加密现在有 JS 预编译,够了
    xieqiqiang00
        25
    xieqiqiang00   35 天前
    @maichael 没有绝对的安全嘛,提高一下门槛
    BoringTu
        26
    BoringTu   35 天前
    emmmm,我来给你个正确答案吧~ 思路其实很简单,给你个小 demo~

    window.ABC = function() {
    console.log('origin ABC');
    };

    window.tempABC = window.ABC;

    window.ABC = function() {
    window.tempABC.call(this, arguments);
    console.log('new ABC');
    }

    有思路没~
    我只是模拟了一下被人为覆盖浏览器内置函数的逻辑

    那现在公布答案~

    在 HTML head 标签里所有 script 标签的最前面加上一个 script,里面:
    window.originXMLHttpRequest = window.XMLHttpRequest;

    然后在你想判断的时候:
    originXMLHttpRequest === XMLHttpRequest
    其实换句话说,window.originXMLHttpRequest 这东西就是浏览器原装内置函数,因为人为覆盖的逻辑肯定在后面执行的

    是不是豁然开朗?(手动抠鼻
    xieqiqiang00
        27
    xieqiqiang00   35 天前
    @BoringTu 在这之前执行了替换的话不就 GG
    BoringTu
        28
    BoringTu   35 天前
    @xieqiqiang00 都在所有脚本执行之前了,你这疑问不成立啊
    ch2
        29
    ch2   35 天前
    你首先得保证你的检测代码不能被修改
    des
        30
    des   35 天前
    既然你是 electron 程序,建议在 c++层面动手脚
    js 层面加东西不说白费功夫,但起码能力有限
    carlclone
        31
    carlclone   35 天前
    运行的时候下断点, 在 console 输入函数名, 会输出一个可以点击跳转的函数定义位置
    luofeii
        32
    luofeii   35 天前
    建立沙箱比如 iframe,需要用到哪个函数直接从 iframe 生成的 window 对象调用就可以
    xieqiqiang00
        33
    xieqiqiang00   35 天前
    要是这么简单就能解决我的顾虑,我就不提这个问题了
    xieqiqiang00
        34
    xieqiqiang00   35 天前
    @BoringTu 要是这么简单就能解决我的顾虑,我就不提这个问题了
    xieqiqiang00
        35
    xieqiqiang00   35 天前
    @ch2 检测部分打算预编译成字节码,就当他是改不了的
    xieqiqiang00
        36
    xieqiqiang00   35 天前
    @des 魔改 electron 哈哈哈
    xieqiqiang00
        37
    xieqiqiang00   35 天前
    @carlclone 这个是可以改的,vm 就可以
    BoringTu
        38
    BoringTu   35 天前
    楼上的各位都想啥呢。。
    也就 @luofeii 这位大佬的靠谱点,跟我的逻辑很像,但你的逻辑是行不通的,因为并不是同一个对象

    let iframe = document.createElement('iframe');
    iframe.id = 'iframe';
    document.body.append(iframe);
    iframe = document.getElementById('iframe');
    iframe.contentWindow.XMLHttpRequest === window.XMLHttpRequest // false
    BoringTu
        39
    BoringTu   35 天前
    @xieqiqiang00 ? 哪里有问题可以提出来,不提出来咋给你解决。。
    BoringTu
        40
    BoringTu   35 天前
    只有一种情况是我说的方案失效的,就是如果是说 electron 是在加载 html 之前就覆盖了内置函数
    luofeii
        41
    luofeii   35 天前 via Android
    @BoringTu 正是因为它俩不等于,所以 window.XMLHttpRequest 无论做任何更改,iframe.contentWindow.XMLHttpRequest 对象还是原生的
    xiangwan
        42
    xiangwan   35 天前
    直接不用 XMLHttpRequest 。用其他语言的 http client 编译成 wasm 调用。
    gamexg
        43
    gamexg   35 天前
    @BoringTu #38

    以前实现过 js 的浏览器伪装,iframe 也会被处理。
    记不清具体细节,印象是 createElement 、getElementById 函数都替换为自己的函数。
    浏览器插件来可以实现页面代码之前执行替换代码。

    当时考虑过网站检测对抗,只能考虑寻找各个未处理好的细节。

    楼主也许可以考虑故意用落后几个版本的 electron(或魔改版) ,然后去依赖老版本不支持的 js 新特征来检查。
    例如,看似正常的功能,依赖新特征,浏览器不支持这个功能时 js 回退使用兼容实现。
    但是应该故意让 electron 不支持这个特征,那个客户端支持就证明是破解版。
    daysv
        44
    daysv   35 天前
    @xieqiqiang00
    用这种绕开呢?
    String.toString.call(XMLHttpRequest)
    ochatokori
        45
    ochatokori   35 天前 via Android
    js 层面别费劲了,不管怎么绕都只能过滤一些通用 hook 工具,针对你的程序的话你没办法
    就算是 native 层也不是不可能的
    KuroNekoFan
        46
    KuroNekoFan   35 天前 via iPhone
    这种问题跟“如何避免 https 抓包”是一个性质的吧
    ochatokori
        47
    ochatokori   35 天前 via Android
    @daysv #44 直接重写 String.toString.call
    no1xsyzy
        48
    no1xsyzy   35 天前
    如果替换方法是 js,其实是可以用 js 检测的,之前 V2 上面有人做过不记得是 JS 还是 Python 的思考题的,如何判断一个对象是不是 Proxy
    hint:递归
    但如果对面直接掉替换你的 V8 那也是白搭。
    xieqiqiang00
        49
    xieqiqiang00   35 天前 via Android
    @no1xsyzy 这还能找到传送门吗?
    @KuroNekoFan 搞证书绑定的话就可以极大程度上避免被抓包了吧,抬高破解成本
    ychost
        50
    ychost   35 天前
    很难防御,之前用类似的方式破过很多竞赛网站,比如 10fastfingers.com 之类的,前端没法防御只能通过服务端来校验提交有没有异常
    rekulas
        51
    rekulas   35 天前
    抛砖引玉
    ```
       var iframe = document.createElement('iframe')
       document.body.appendChild(iframe)
       XMLHttpRequest === iframe.contentWindow.XMLHttpRequest // true or false?
    ```
    no1xsyzy
        52
    no1xsyzy   35 天前
    @xieqiqiang00 我尝试找了一下,但没找到……
    假设一个期望中调用层数为 k 的待测函数
    就是去撞递归最大层数
    撞到了退回来 k-1 层,调用待测的函数 f,应当调用失败( InternalError: too much recursion )
    再多退一层到 k 层,调用待测函数 f,应当调用成功
    但是似乎最近浏览器都随机化了最大调用层(防 fingerprinting 吧),不确定 Electron 如何。
    (其实最方便的、最坚固的可能是干脆换 Qt 商业授权)
    musi
        53
    musi   35 天前
    @rekulas 这不是 false 么,因为都不在一个 realm 或者说浏览器上下文里

    @luofeii 这位大佬的意识是不需要判断是否是原生的代码,直接用 iframe 里的函数,因为每次创建 iframe 的时候,iframe 自己的浏览器上下文是重新创建的,基本上都能得到原生的代码
    JerryCha
        54
    JerryCha   35 天前
    那你不如直接用 C++写个 addon 专门负责网络请求
    learningman
        55
    learningman   34 天前
    js 预编译不好使的,建议 wasm
    jones2000
        56
    jones2000   34 天前
    直接后台渲染, 不就行了。 后台生成静态页面,显示。什么 js 都没有。
    aaronlam
        57
    aaronlam   34 天前
    @musi 我试过貌似直接用 iframe 里的 XMLHttpRequest 发送请求是可行的
    binux
        58
    binux   34 天前
    @BoringTu
    > 在 HTML head 标签里所有 script 标签的最前面加上一个 script
    这一条做不到
    musi
        59
    musi   34 天前 via iPhone
    @aaronlam 本来就是可行的,现在微前端里的沙箱基本都离不开 iframe,比如比较流行的 qiankun 框架,比如阿里云的 console os,都是拿 iframe 来做的沙箱隔离,因为目前的 realm api 还在草案阶段
    rekulas
        60
    rekulas   34 天前
    @musi 正常是可以判断的,你可以运行下

    不过...
    @luofeii @BoringTu
    正如 @gamexg 提到的,iframe 仍然不完美,客户端仍然能进行攻击,举个栗子
    ```
    document.createElement = () => {
    return {contentWindow: {XMLHttpRequest: XMLHttpRequest}}
    };
    document.body.appendChild = () => {};
    var iframe = document.createElement('iframe')
    document.body.appendChild(iframe)
    console.log(XMLHttpRequest === iframe.contentWindow.XMLHttpRequest)
    ```
    最后判断始终是 true,除非你继续证明 document.createElement 也是原生,这。。。俄罗斯套娃
    我觉得可以考虑从某些无法被覆盖的对象入手,例如 navigator 之类
    rekulas
        61
    rekulas   34 天前
    我想到一种结合 valueof 的判断方式 ,大家考虑下如何可以绕过
    ```
    function isNative(api) {
    if (typeof XMLHttpRequest != 'function') return false;
    if (typeof XMLHttpRequest.valueOf != 'function' ||
    XMLHttpRequest.valueOf().toString() != 'function XMLHttpRequest() { [native code] }') {
    return false;
    }
    return /native code/.test(api.toString()) && typeof api !== 'undefined'
    }

    console.error(isNative(XMLHttpRequest))
    ```
    不过最终始终有疑问,如果客户端可以针对性劫持的话,那无论怎么写代码都无法判断的,客户端只需要在判断函数 return true 就行了
    musi
        62
    musi   34 天前
    @rekulas 我直接在这个帖子开启 console,把你的代码粘贴运行,结果为 false 。
    我重新建了个 html 页面,把你的代码粘贴进去运行还是 false,这种没修改都是 false 的要怎么判断?
    rekulas
        63
    rekulas   34 天前
    @musi
    应该是这句 != 'function XMLHttpRequest() { [native code] }'
    这个在不同环境下有一点差异所以需要尝试下多种环境,我使用的 chrome90,其他版本或 firefox 的话需要针对性判断下
    你可以先 console.log(XMLHttpRequest.valueOf()) 拿到真实值替换进去再测试
    musi
        64
    musi   34 天前
    @rekulas 可能我表达有问题,我说的是 51 楼的代码
    rekulas
        65
    rekulas   34 天前
    @musi 测试好像是不行了,奇怪昨天我测试还可以,我待会再试试
    不过不用纠结这个,因为这个最终还是有漏洞的,你可以看看我最新的代码如何绕过
    Huelse
        66
    Huelse   34 天前
    幸好楼主解释了,不然又是一个 x-y 问题了
    musi
        67
    musi   34 天前
    @rekulas #63 也不是很难
    ```
    let tem = XMLHttpRequest;
    function test() {
    console.log(111)
    return tem
    }
    XMLHttpRequest = test
    isNative(XMLHttpRequest) // false
    test.toString = () => "function XMLHttpRequest() { [native code] }"
    isNative(XMLHttpRequest) // true
    ```
    gzzhanghao
        68
    gzzhanghao   34 天前
    function guard(value) {
    function toString() {
    try {
    null.a
    } catch (error) {
    // TODO 判断 error.stack,在 chrome 下 send 和 Object.toString 应该是相邻的
    // 注意:不能用 prototype,只能访问 str.length 和 str[index],逐个字符判断
    console.log(error.stack)
    return value
    }
    }
    return { toString }
    }

    function send(method, url, body = undefined) {
    const xhr = new XMLHttpRequest()
    xhr.open(guard(method), guard(url))
    xhr.send(guard(body))
    }

    send('POST', '/', JSON.stringify({ foo: 'bar' }))

    /**
    * 测试劫持
    */

    XHR = XMLHttpRequest
    XMLHttpRequest = class {
    constructor() {
    this.xhr = new XHR()
    }
    open(method, url) {
    return this.xhr.open(method, url)
    }
    send(body) {
    return this.xhr.send(body)
    }
    }

    send('POST', '/', JSON.stringify({ foo: 'bar' }))
    gzzhanghao
        69
    gzzhanghao   34 天前
    https://codepen.io/gzzhanghao/full/PopqWZr
    v2 回复居然没有缩进…
    gzzhanghao
        70
    gzzhanghao   34 天前
    我错了,不用判断 stack 那么麻烦,直接 fn.caller 就能秒解

    ```js
    function guard(value) {
    function toString() {
    if (toString.caller === send) {
    return value
    }
    }
    return { toString }
    }

    function send(method, url, body = undefined) {
    const xhr = new XMLHttpRequest()
    xhr.open(guard(method), guard(url), false)
    xhr.send(guard(body))
    console.log(xhr.status, xhr.responseText)
    }

    send('GET', '/')
    ```
    rekulas
        71
    rekulas   34 天前
    @musi 确实又绕过了。。又升级了下,再看看呢

    ```
    ```
    function isNative(api) {
    if (XMLHttpRequest.hasOwnProperty('toString')) return false;
    if (typeof XMLHttpRequest != 'function') return false;
    if (typeof XMLHttpRequest.valueOf != 'function' ||
    XMLHttpRequest.valueOf().toString() != 'function XMLHttpRequest() { [native code] }') {
    return false;
    }
    return /native code/.test(api.toString()) && typeof api !== 'undefined'
    }

    console.error(isNative(XMLHttpRequest))
    ```
    ```
    BoringTu
        72
    BoringTu   33 天前
    @luofeii #41 但是你怎么做对比呢?
    如果不考虑是否是 electron 环境,只是浏览器环境的话,你可以参考我提供的思路,这是最简单也最有效的解决方案
    要的就是同一个对象的引用,这样才能判断出是否是同一个内存地址

    @binux 为啥做不到呢?在.html 文件里直接写死就好啊,你是考虑会有动态插入 script 标签么?这不需要考虑啊,HTML 在浏览器内核上的渲染逻辑没有那么玄幻,都是自上而下的,就算有动态插入也都是执行到了具体脚本才会有的动作,但早在这一步之前,我想要执行的那句脚本就已经执行完毕了

    @rekulas 嗯,我不推荐 iframe 的这种做法哇,虽然按我最早发的那个思路,一样是可以避免你提的这个可能的,我拿到内置 document.createElement 的原生函数引用不就好了嘛。而且你的思路最后的那个判断本来就不是应该的,这个判断木有意义:“XMLHttpRequest === iframe.contentWindow.XMLHttpRequest”,如果不执行你的第一句那个重写 createElement,前面这个判断是永远是 false 的,因为并不是同一个环境,所以也就不是同一个对象

    @aaronlam
    @musi
    以及楼上某些木有点名到的童鞋,你们都没注意听讲啊。。
    我给各位总结然后回答一下楼主这个问题(两种情况):

    1. 如果只是判断前端环境中是否有脚本重写了 XMLHttpRequest,那最简单也最靠谱的就是我提供的那个方案:
    “在 HTML head 标签里所有 script 标签的最前面加上一个 script,里面:
    window.originXMLHttpRequest = window.XMLHttpRequest;
    然后在你想判断的时候:
    originXMLHttpRequest === XMLHttpRequest”
    (顺便聊一下 iframe 的这个方案,用来判断的话是没戏的,上下文环境不同,就算没被重写,也没法判断,因为不可能是同一个对象。iframe 的方案,你只能是 iframe 环境里的 XMLHttpRequest 拿来直接当做原生内置函数来用

    2. 如果是说 electron 直接修改了浏览器内核里的 XMLHttpRequest,那就无解了,人家都不是在前端环境修改的,你怎么判断?你就算 toString 了,一样拿到的是这个:'function XMLHttpRequest() { [native code] }'。这种情况你想要知道是否被重写了,只能肉眼去看 electron 的源码,没有其他方式了
    binux
        73
    binux   33 天前 via Android
    @BoringTu 如果我控制了浏览器呢?甚至不需要是浏览器,一个插件,mitm 修改 HTML 都行。
    BoringTu
        74
    BoringTu   33 天前
    @binux 你怎样控制用户的浏览器呢?你说插件,你只能在自己电脑上安装插件,你怎样让用户也安装?如果你随意的就能控制他人浏览器,那浏览器这个行业也不用干了。。
    你只能在你电脑的浏览器上装你想装的插件,然后你在自己浏览器上通过插件篡改了原生函数,这都可以啊,但能说明啥问题。。
    这个其实没啥好杠的,而且都不需要去查什么资料去验证
    这就好像之前我碰到有问这么个事儿的,说我通过自己浏览器开发者工具拿到了我登录后的 token,然后发给别人,不就把我的登录信息暴露出去了么。。我。。当时是真不知道该回啥。。
    SakuraKuma
        75
    SakuraKuma   33 天前
    囧, 楼主都说是 electron, js 怎么弄都么得用, 还是上面说的走 c++写 dll 用 ffi 吧.
    binux
        76
    binux   33 天前 via Android   ❤️ 1
    @BoringTu 请认真阅读 #18
    gzzhanghao
        77
    gzzhanghao   33 天前 via iPhone
    @BoringTu 楼主好像说过不能控制 script 标签的位置,另外这里应该不用讨论改内核的场景,那是无解的
    musi
        78
    musi   33 天前
    @BoringTu #72 你没仔细看我说的,我说的就是直接用 iframe 的 xhr 去请求,就不判断了。至于修改内核,能修改内核了你只靠 js 是无解的。
    还有,你的方案也不是最简单的,我要是多页面那我每个页面都要修改这侵入性也太大了,这也能叫最简单的?
    BoringTu
        79
    BoringTu   33 天前
    @musi 就算再多页面,你肯定也是按模板来的,难道你这多页面要每个页面都单独完整写一份 html ?
    而且你谈侵入性,我的方案只是暴露原对象引用出来而已,并没有做哪怕一丁点的修改,这叫有侵入性?
    至于你前半段说的,我都已经说过一遍了。。


    @gzzhanghao 为啥不能控制?详见 #72
    gzzhanghao
        80
    gzzhanghao   33 天前 via iPhone
    @BoringTu 见#27,另外我之前也遇到过类似的问题,作为第三方库提供出去,这种情况是控制不了的
    关于   ·   帮助文档   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1985 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 15:14 · PVG 23:14 · LAX 08:14 · JFK 11:14
    ♥ Do have faith in what you're doing.