上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖

[油猴脚本开发指南]监控对象初始化属性分析

[复制链接]
  • TA的每日心情

    2022-6-4 20:51
  • 签到天数: 32 天

    [LV.5]常住居民I

    321

    主题

    2838

    帖子

    2846

    积分

    荣誉开发者

    非物质文化遗产社会摇传承人

    Rank: 10Rank: 10Rank: 10

    积分
    2846

    猫咪币纪念章

    发表于 2021-10-3 00:44:30 | 显示全部楼层 | 阅读模式

    前言

    这里依然算是一个番外篇,主要还是想领大家一起学习如何分析一段代码,养成良好的阅读习惯,为以后做铺垫

    适合学有余力的人。

    请注意这个代码是对方随手写的,不要拿着当正经函数用,可能存在多多少少的错误。

    代码来自:cxxjackie

    代码

    function waitForProperty(obj, ...props) {
        let _obj = obj;
        let prop;
        const realDP = Object.defineProperty;
        const waitForValue = () => {
            return new Promise(resolve => {
                Object.defineProperty = function() {
                    if (arguments[0] === _obj && arguments[1] === prop) {
                        const value = 'value' in arguments[2] ? arguments[2].value : arguments[2].get.call(_obj);
                        Object.defineProperty = realDP;
                        resolve(value);
                    }
                    return realDP.apply(Object, arguments);
                };
                realDP.call(Object, _obj, prop, {
                    configurable: true,
                    enumerable: false,
                    get: () => undefined,
                    set: value => {
                        realDP.call(Object, _obj, prop, {
                            configurable: true,
                            enumerable: true,
                            writable: true,
                            value: value
                        });
                        Object.defineProperty = realDP;
                        resolve(value);
                    }
                });
            });
        };
        return new Promise(async (resolve, reject) => {
            while (props.length > 0) {
                prop = props.shift();
                if (prop in _obj) {
                    _obj = _obj[prop];
                } else {
                    _obj = await waitForValue();
                }
                if (props.length > 0 && typeof _obj !== 'function' && typeof _obj !== 'object') {
                    return reject(`The property '${prop}' is not a function or object.`);
                }
            }
            resolve(_obj);
        });
    }
    

    希望大家跟着我的思路一步一步读。

    function waitForProperty(obj, ...props)

    首先传入了两个参数,一个obj,一个props,收集了除了第一个参数以外的所有参数。

    调用方式await waitForProperty(unsafeWindow, 'a', 'b', 'c')

    接下来我们继续看

        let _obj = obj;
        let prop;

    这里创建了一个_obj作为obj的引用,为接下来做铺垫,prop根据名字和下文可以知道是单个属性的变量。

    const realDP = Object.defineProperty;

    保存defineProperty的引用。为接下来劫持做铺垫。

    const waitForValue = xxxx

    这里是一个函数,我们因为还没有调用到,所以先不看。继续往下走

        return new Promise(async (resolve, reject) => {
            while (props.length > 0) {
                prop = props.shift();
                if (prop in _obj) {
                    _obj = _obj[prop];
                } else {
                    _obj = await waitForValue();
                }
                if (props.length > 0 && typeof _obj !== 'function' && typeof _obj !== 'object') {
                    return reject(`The property '${prop}' is not a function or object.`);
                }
            }
            resolve(_obj);
        });

    这里函数最后返回了一个promise,这个promise内是一个async函数,是async函数是因为其内部有await阻塞promise。

    通过while进行循环,判断props.length是否大于0,如果大于0则一直循环

    prop接收props弹出的一个参数,每次shift函数调用都会弹出一个,并且props中少一个参数。

    判断prop是否存在于_obj中,如果存在,则将_obj变为_obj[属性]。

    这里是为了逐步往下递归,比如我之前传入了a,b,c和unsafewindow,是为了找到unsafewindow.a.b.c

    逐步往下走,这里我们把unsafewindow.a变为了_obj,接下来继续判断b存不存在_obj上就可以了。

    如果存在则进行了_obj的赋值。然后对其进行判断

                if (props.length > 0 && typeof _obj !== 'function' && typeof _obj !== 'object') {
                    return reject(`The property '${prop}' is not a function or object.`);
                }

    判断参数是否大于0,并且_obj不为函数以及对象,则直接返回一个错误。

    这里是因为function和object都是一个对象,function函数也可以存储属性。

    这里只对这两种进行处理,以外的属性会出现报错的情况。

    接下来我们继续分析。

    之前我们分析了存在的情况,现在我们来分析不存在的情况。

        const waitForValue = () => {
            return new Promise(resolve => {
                Object.defineProperty = function() {
                    if (arguments[0] === _obj && arguments[1] === prop) {
                        const value = 'value' in arguments[2] ? arguments[2].value : arguments[2].get.call(_obj);
                        Object.defineProperty = realDP;
                        resolve(value);
                    }
                    return realDP.apply(Object, arguments);
                };
                realDP.call(Object, _obj, prop, {
                    configurable: true,
                    enumerable: false,
                    get: () => undefined,
                    set: value => {
                        realDP.call(Object, _obj, prop, {
                            configurable: true,
                            enumerable: true,
                            writable: true,
                            value: value
                        });
                        Object.defineProperty = realDP;
                        resolve(value);
                    }
                });
            });
        };

    waitForValue是一个箭头函数,返回一个promise,方便我们进行阻塞并等待。

                Object.defineProperty = function() {
                    if (arguments[0] === _obj && arguments[1] === prop) {
                        const value = 'value' in arguments[2] ? arguments[2].value : arguments[2].get.call(_obj);
                        Object.defineProperty = realDP;
                        resolve(value);
                    }
                    return realDP.apply(Object, arguments);
                };

    这里是对defineProperty进行了一个劫持,因为我们接下来也用了defineProperty监控了属性的修改,但是这有一个致命的弊病,就是没法对抗对方直接使用define进行赋值,所以我们干脆也对其进行劫持,并且判断属性,如果属性符合,则结束这个函数,如果属性不符合,则正常调用define函数

                   if (arguments[0] === _obj && arguments[1] === prop) {
                        const value = 'value' in arguments[2] ? arguments[2].value : arguments[2].get.call(_obj);
                        Object.defineProperty = realDP;
                        resolve(value);
                    }

    这里第一个参数判断对象是否相等,第二个参数判断属性,如果都相等证明正在赋值这个属性的操作。

    然后判断value这个属性是否存在于第三个对象中,我们都知道define是需要传入三个对象的,并且存在两种,一种是{value:xxx}另外一中是{get:function,set:function}

    这里通过判断value属性存在,如果存在则返回参数的value属性返回给const value,如果不存在则调用第二个参数内的get函数,并将其this指向到__obj上,来获取函数返回的value内容,然后恢复define劫持,并通过resolve结束这个函数。

    接下来是

              realDP.call(Object, _obj, prop, {
                    configurable: true,
                    enumerable: false,
                    get: () => undefined,
                    set: value => {
                        realDP.call(Object, _obj, prop, {
                            configurable: true,
                            enumerable: true,
                            writable: true,
                            value: value
                        });
                        Object.defineProperty = realDP;
                        resolve(value);
                    }
                });

    刚才那种是为了防止调用define来进行属性的设置,如果直接设置属性如window.a=666,则不会触发define的劫持函数。为了监控这种直接设置属性,我们使用了define进行监控,由于刚才对define进行了劫持,所以这里我们用define的原函数进行设置。因为我们获取的是defineProperty的函数引用,之前我们调用的是Object.defineProperty,他的this是有.前边的对象来决定的,也就是Object,如果执行使用函数,则是defineProperty(),并没有点,这时候的this对象则是window,所以我们需要通过call传入Object来设置this。

    接下来设置对象,属性,来进行监控。

    设置了configurable和enumerable,前者代表可删除,后者代表可以被枚举到。

    因为属性没有被初始化,所以一旦读取这个属性要返回undefined。

    如果属性被set触发了,说明某个地方在设置这个属性的值,这时候我们再继续处理。

                        realDP.call(Object, _obj, prop, {
                            configurable: true,
                            enumerable: true,
                            writable: true,
                            value: value
                        });
                        Object.defineProperty = realDP;
                        resolve(value);

    这里依然与之前差不多,不同的是这里我们设置了很多属性

    configurable设置了可删除,enumrable设置了可枚举,writable设置了可写,然后value将我们set的值设置到对应的属性上,接下来我们已经完成了操作并且得到了value,我们恢复define的劫持,并且返回这个值。

    那么到这里我们已经拆解完了这个函数的所有内容,并且了解了大概的思想

    相信你对如何学习他人代码以及劫持的思路都进一步加深了。

    祝你写出越来越好的代码!

    结语

    撒花~

    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    10

    主题

    347

    帖子

    427

    积分

    荣誉开发者

    Rank: 10Rank: 10Rank: 10

    积分
    427

    活跃会员热心会员突出贡献三好学生猫咪币纪念章中秋纪念章国庆纪念章

    发表于 2021-10-3 10:04:07 | 显示全部楼层
    这个其实是我随手写的,最好不要直接拿来用,还有Object.defineProperties的情况没有考虑。
    回复

    使用道具 举报

  • TA的每日心情

    2022-6-4 20:51
  • 签到天数: 32 天

    [LV.5]常住居民I

    321

    主题

    2838

    帖子

    2846

    积分

    荣誉开发者

    非物质文化遗产社会摇传承人

    Rank: 10Rank: 10Rank: 10

    积分
    2846

    猫咪币纪念章

    发表于 2021-10-3 11:03:52 | 显示全部楼层
    cxxjackie 发表于 2021-10-3 10:04
    这个其实是我随手写的,最好不要直接拿来用,还有Object.defineProperties的情况没有考虑。 ...

    标注了哥哥
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

    发表回复

    本版积分规则

    快速回复 返回顶部 返回列表