document.cookie的hook及引申

一、前言

对于逆向工程师来说,加密参数隐藏在cookie中的情况并不少见,比如某数,通常需要通过hook技术快速定位到加密点。常用的做法是直接利用Object.defineProperty覆写对象的相关方法实现,但这种方法并不能保证原方法的执行,可能会影响代码执行流程,且容易被检测出来。

本文基于此出发,做了以下工作:

  1. 为保证hook代码与原代码都能得到执行,从原型链角度出发对属性描述符进行修改;
  2. 为方便拓展到对其它对象的hook,对hook方法进行封装;
  3. 针对反hook检测,做了隐藏痕迹处理。
  4. 为支撑之后补环境框架的建设,对window对象进行了简单hook封装。

二、方法

2.1 重写对象属性set/get方法

let temp = document.cookie;
(function () {
        Object.defineProperty(document, 'cookie', {
            get: function () {
                console.log(`get: ${temp}`)
                return temp
            },
            set: function (val) {
                console.log(`set: ${val}`)
                temp = val;
            },
        });
    }
)();

上面是网上很常见的用于hookcookie的一段代码,主要是将document.cookie提前存放到一个变量中,用于在get/set方法中做暂存器。随后重写document对象的cookie属性,在get/set方法中插入自定义的hook代码。

让我们来看一下效果。随便打开一个网站,输入document.cookie="a=123"

从上图中可以看出,效果还是比较好的,再测试一下:

从上图中不难发现一些问题,按理来说,输出结果应该是a=123,b=456才对,这里只显示出来b=456,虽然大部分情况下,这并不影响我们调试,但少部分情况下这会导致原有逻辑的改变。

之所以出现这种情况是因为上面那段代码中的temp变量每次经过set方法都会被重新赋值,解决的方法有2个,第一种是将修改代码让其符合追加赋值的逻辑,第二种是保留原set/get方法的逻辑,即将原get/set方法的引用用2个全新的变量接收(只要引用还在,对象就不会被回收),下面来看看第二种方法具体如何实现。

2.2 从原型链上hook原型对象

首先我们来看document的cookie属性是哪里来的,输入dir(document)可以查看document对象的各种属性。

document对象属性

上图中,颜色较深的是document自己的属性,颜色浅的是document从原型链上继承的,而cookie属性存在于颜色浅的一类中,这也说明了第一种方法的问题所在,其是在document对象上定义了一个叫cookie的属性,如果documentd所以我们还需要寻找其原型对象,通过__proto__ 寻找。

document.__proto__自身并未定义cookie属性

继续往下寻找。

在Document对象上定义了cookie属性

最终确定了对象是Document.prototype,属性是cookie。

let oldProperities = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie')
let newProperities = {
    configurable: true,
    enumerable: true
}
let _get = oldProperities.get
let _set = oldProperities.set
newProperities.get = function () {
    result = _get.call(this)
    console.log(`get: ${result}`)
    return result
}
newProperities.set = function (val) {
    console.log(`set: ${val}`)
    _set.call(this, val)
}
Object.defineProperty(Document.prototype, 'cookie', newProperities)

上述代码首先用getOwnPropertyDescriptor提取出Document.prototype的cookie属性描述符,随后定义了一个新属性描述符,在新属性描述符中调用了旧属性描述符中的get/set方法(保证原逻辑不变),并添加了自定义的hook代码(打印日志),最后将新属性描述符通过Object.defineProperty覆写了Document.prototype的cookie属性。

让我们来看一下效果:

解决了直接重写了get/set方法导致更改了原逻辑更改造成的隐患,并且hook代码也可正常运行。然后我们可以引发一下思考,在这里hook的本质是什么?hook的本质其实就是修改对象属性的属性描述符,hook其它原型对象也是如此,既然如此,是不是可以封装成一个方法?

2.3 封装一下以hook任意对象

let hook = function hook(func, funcInfo, isDebug, onEnter, onLeave, isExec) {
    // func : 原函数,需要hook的函数
    // funcInfo: 是一个对象,objName,funcName属性
    // isDebug: 布尔类型, 是否进行调试,关键点定位,回溯调用栈
    // onEnter:函数, 原函数执行前执行的函数,改原函数入参,或者输出入参
    // onLeave: 函数,原函数执行完之后执行的函数,改原函数的返回值,或者输出原函数的返回值
    // isExec : 布尔, 是否执行原函数,比如无限debugger函数
    if (typeof func !== 'function') {
        return func;
    }
    if (funcInfo === undefined) {
        funcInfo = {};
        funcInfo.objName = "globalThis";
        funcInfo.funcName = func.name || '';
    }
    if (isDebug === undefined) {
        isDebug = false;
    }
    if (!onEnter) {
        onEnter = function (obj) {
            console.log(`{hook|${funcInfo.objName}[${funcInfo.funcName}]正在调用,参数是${JSON.stringify(obj.args)}}`);
        }
    }
    if (!onLeave) {
        onLeave = function (obj) {
            console.log(`{hook|${funcInfo.objName}[${funcInfo.funcName}]正在调用,返回值是[${obj.result}}]`);
        }
    }
    if (isExec === undefined) {
        isExec = true;
    }
    // 替换的函数
    let hookFunc = function () {
        if (isDebug) {
            debugger;
        }
        let obj = {};
        obj.args = [];
        for (let i = 0; i < arguments.length; i++) {
            obj.args[i] = arguments[i];
        }
        // 原函数执行前
        onEnter.call(this, obj); // onEnter(obj);
        // 原函数正在执行
        let result;
        if (isExec) {
            result = func.apply(this, obj.args);
        }
        obj.result = result;
        // 原函数执行后
        onLeave.call(this, obj); // onLeave(obj);
        // 返回结果
        return obj.result;
    }

    return hookFunc;
}

// hook 对象的属性,本质是替换属性描述符
let hookObj = function hookObj(obj, objName, propName, isDebug) {
    // obj : 对象,需要hook的对象
    // objName: 字符串,hook对象的名字
    // propName: 字符串,需要hook的对象属性名
    // isDebugger: 布尔值,是否需要debugger
    let oldDescriptor = Object.getOwnPropertyDescriptor(obj, propName);
    let newDescriptor = {};
    if (!oldDescriptor.configurable) { // 如果是不可配置的,直接返回
        return;
    }
    // 必须有的属性描述
    newDescriptor.configurable = true;
    newDescriptor.enumerable = oldDescriptor.enumerable;
    if (oldDescriptor.hasOwnProperty("writable")) {
        newDescriptor.writable = oldDescriptor.writable;
    }
    if (oldDescriptor.hasOwnProperty("value")) {
        let value = oldDescriptor.value;
        if (typeof value !== "function") {
            return;
        }
        let funcInfo = {
            "objName": objName,
            "funcName": propName
        }
        newDescriptor.value = hook(value, funcInfo, isDebug);
    }
    if (oldDescriptor.hasOwnProperty("get")) {
        let get = oldDescriptor.get;
        let funcInfo = {
            "objName": objName,
            "funcName": `get ${propName}`
        }
        newDescriptor.get = hook(get, funcInfo, isDebug);
    }
    if (oldDescriptor.hasOwnProperty("set")) {
        let set = oldDescriptor.set;
        let funcInfo = {
            "objName": objName,
            "funcName": `set ${propName}`
        }
        newDescriptor.set = hook(set, funcInfo, isDebug);
    }
    Object.defineProperty(obj, propName, newDescriptor);
}

上述代码定义了一个hookObj函数,传入需要hook的对象、对象属性名,以及是否需要debugger,随后更改属性描述符,属性描述符中configurableenumerable是一定会有的,但get/set属性确不一定有,有时是以valuewritable 形式存在,例如window下的btoa属性,因此需要加以判断,其中value有可能不是函数,而是字符串,这种情况下无需hook直接返回。

所以总的来说,只有value/get/set三种函数需要hook;在hook方法中,入参有原函数,函数信息(包括对象名和属性),是否需要debugger,原函数执行前hook函数,原函数执行后hook函数,原函数是否执行等6个参数,其中如果没有传,则执行默认表达,让我们来看一下效果:

结果显示hook代码与原函数均执行成功。不过如果反hook方想检测也很容易,例如Object.getOwnPropertyDescriptor(Document.prototype,'cookie').get.toString()

而原函数的输出结果是:

又或者这样检测Object.getOwnPropertyDescriptor(Document.prototype,'cookie').get.name

而原函数的输出结果是:

2.4 隐藏hook痕迹

关于toString型的检测,需要手动覆写Function.prototype.toString方法以实现模拟native化的返回,该方案在
hook与反hook之间的对抗
中描述过,有兴趣的可以点击跳转去查看。

至于第二种.name型的检测,可以用Object.defineProperty直接修改name属性的描述符即可。

reNameFunc = function reNameFunc(func, name) {
    Object.defineProperty(func, "name", {
        configurable: true,
        enumerable: false,
        writable: false,
        value: name
    });
}

name参数传入重命名名称,该案例中get/set的名称分别是get cookieset cookie,为防止变量污染,把函数都定义在一个新对象中,完整代码见文末附录,测试结果如下:

上图测试表明,我们成功躲过了这2种检测,伪装成功。那么再发散一步,window对象如何hook?

2.5 window全局对象的hook

仔细观察window对象的构成,主要由三部分构成,第一部分是普通函数,例如atob,第二部分是原型函数,例如Animation,这两者主要区别是普通函数没有原型对象,而原型函数有原型对象。如下图所示:

第三部分是非函数属性,例如window.closedwindow.chrome,该类属性要么是一个值,要么是一个对象,如下图所示:

这部分一般不用hook,对于第一类原型函数的hook,首先用.prototype得到其原型对象,然后用Object.getOwnPropertyDescriptors得到其所有属性,然后遍历属性,对每个属性调用上文提到过的hook原型对象属性的方法,这里有一点要注意的是,在浏览器环境中只有属性描述符中configurabletrue才可以被hook,configurable表示是否可配置,具体解释可见
https://developer.mozilla.org/

对于普通函数的hook,由于没有原型对象,直接修改其属性描述符即可,以下是具体代码:

yueqian.hookGlobal = function hookGlobal(isDebugger) {
    for (const key in Object.getOwnPropertyDescriptors(window)) {
        if (typeof window[key] === "function") {
            if (typeof window[key].prototype === "object") {
                // 函数原型
                let protoObj = window[key].prototype;
                let name = window[key].name;
                for (const prop in Object.getOwnPropertyDescriptors(protoObj)) {
                    yueqian.hookObj(protoObj, `${name}.prototype`, prop, isDebugger);
                }
                console.log(`hook ${name}.prototype`);
            } else if (typeof window[key].prototype === 'undefined') {
                // 普通函数
                yueqian.hookObj(window, "window", key, isDebugger);
            }
        }
    }
}

测试一下结果:

测试结果表明,window对象已成功被我们hook。

三、总结

hook技术在js逆向中是很重要的一门技术,hook写的好,加密位置找的快,补环境也能更顺利些,本文指出了以下几个改进方向:

  1. 尽量通过原型链修改属性描述符,以保证hook代码与原函数都能得到执行;
  2. 基于语法共性将hook方法封装成函数以便于对其它对象进行快速hook
  3. hook痕迹进行了隐藏,一是隐藏toString痕迹,二是隐藏.name痕迹,避免为反hook检测识别出来;

最后对window对象进行了hook,为之后建立补环境框架打下良好的地基。

四、附录

yueqian = {};

//函数native化
!function () {
    const $toString = Function.prototype.toString;
    const symbol = Symbol()
    const myToString = function () {
        return typeof this === 'function' && this[symbol] || $toString.call(this);
    }

    function set_native(func, key, value) {
        Object.defineProperty(func, key, {
            enumerable: false,
            configurable: true,
            writable: true,
            value: value
        })
    }

    delete Function.prototype.toString;
    set_native(Function.prototype, 'toString', myToString);
    set_native(Function.prototype.toString, symbol, "function () { [native code] }");
    yueqian.setNative = function (func, funcname) {
        set_native(func, symbol, `function ${funcname || func.name || ''}() { [native code] }`);
    }
}()

// 函数重命名
yueqian.reNameFunc = function reNameFunc(func, name) {
    Object.defineProperty(func, 'name', {
        configurable: true,
        enumerable: false,
        writable: false,
        value: name
    })
}

//hook对象
let hook = function (func, funcInfo, isDebug, onEnter, onLeave, isExec) {
    //func:原函数,需要hook的函数
    //funcInfo:是一个对象,objName,funcName属性
    //isDebug:布尔类型,是否进行调试,关键点定位,回溯调用栈
    //onEnter:函数,原函数执行前执行的函数,改原函数入参,或者输出入参
    //onLeave:函数,原函数执行完之后执行的函数,改原函数的返回值,或者输出原函数的返回值
    //isExec:布尔,是否执行原函数,比如无限debugger函数
    if (typeof func !== 'function') {
        return func; //如果传入的第一个参数不是类型不是function,就直接返回,例如Object
    }
    if (funcInfo === undefined) {
        //如果没有传入funcInfo,则定义一个对象,其内objName是全局对象,funcName是当前函数名称,如果func.name取不到值,则传空
        funcInfo = {};
        funcInfo.objName = 'globalThis'
        funcInfo.funcName = func.name || '';
    }
    if (isDebug === undefined) {
        //如果没有传isDebug,则默认为false,即不注入debugger
        isDebug = false
    }
    if (!onEnter) {
        //若未传入onEnter参数,也就是正式函数执行前的函数,则默认是输入日志,说明当前是哪个对象下的函数正在调用,其参数是什么
        onEnter = function (obj) {
            console.log(`{hook|${funcInfo.objName}[${funcInfo.funcName}]正在调用,参数是${JSON.stringify(obj.args)}}`);
        }
    }
    if (!onLeave) {
        //若未传入onLeave参数,也就是正式函数执行后的函数,则默认是输入日志,说明当前是哪个对象下的函数正在调用,其结果是什么
        onLeave = function (obj) {
            console.log(`{hook|${funcInfo.objName}[${funcInfo.funcName}]正在调用,返回值是[${obj.result}}]`);

        }
    }
    if (isExec === undefined) {
        //默认原函数是执行的,如果是hook debugger函数,则这里可以传False.
        isExec = true
    }
    //替换的函数
    hookFunc = function () {
        if (isDebug) {
            debugger;
        }
        let obj = {};
        obj.args = []; //用来承载原函数的入参
        for (let i = 0; i < arguments.length; i++) {
            obj.args[i] = arguments[i];
        }
        //原函数执行前
        onEnter.call(this, obj);
        //原函数正在执行
        let result;
        if (isExec) {
            result = func.apply(this, obj.args);
        }
        obj.result = result;
        //原函数执行后
        onLeave.call(this, obj);
        //返回结果
        return obj.result
    }
    yueqian.setNative(hookFunc, funcInfo.funcName)
    yueqian.reNameFunc(hookFunc, funcInfo.funcName)
    return hookFunc;
}

// hook 对象的属性,本质是替换属性描述符
yueqian.hookObj = function hookObj(obj, objName, propName, isDebug) {
    // obj : 对象,需要hook的对象
    // objName: 字符串,hook对象的名字
    // propName: 字符串,需要hook的对象属性名
    // isDebugger: 布尔值,是否需要debugger
    let oldDescriptor = Object.getOwnPropertyDescriptor(obj, propName);
    let newDescriptor = {};
    if (!oldDescriptor.configurable) { // 如果是不可配置的,直接返回
        return;
    }
    // 必须有的属性描述
    newDescriptor.configurable = true;
    newDescriptor.enumerable = oldDescriptor.enumerable;
    if (oldDescriptor.hasOwnProperty("writable")) {
        newDescriptor.writable = oldDescriptor.writable;
    }
    if (oldDescriptor.hasOwnProperty("value")) {
        let value = oldDescriptor.value;
        if (typeof value !== "function") {
            return;
        }
        let funcInfo = {
            "objName": objName,
            "funcName": propName
        }
        newDescriptor.value = hook(value, funcInfo, isDebug);
    }
    if (oldDescriptor.hasOwnProperty("get")) {
        let get = oldDescriptor.get;
        let funcInfo = {
            "objName": objName,
            "funcName": `get ${propName}`
        }
        newDescriptor.get = hook(get, funcInfo, isDebug);
    }
    if (oldDescriptor.hasOwnProperty("set")) {
        let set = oldDescriptor.set;
        let funcInfo = {
            "objName": objName,
            "funcName": `set ${propName}`
        }
        newDescriptor.set = hook(set, funcInfo, isDebug);
    }
    Object.defineProperty(obj, propName, newDescriptor);
}

// hook 全局对象
yueqian.hookGlobal = function hookGlobal(isDebugger) {
    for (const key in Object.getOwnPropertyDescriptors(window)) {
        if (typeof window[key] === "function") {
            if (typeof window[key].prototype === "object") {
                // 函数原型
                let protoObj = window[key].prototype;
                let name = window[key].name;
                for (const prop in Object.getOwnPropertyDescriptors(protoObj)) {
                    yueqian.hookObj(protoObj, `${name}.prototype`, prop, isDebugger);
                }
                console.log(`hook ${name}.prototype`);
            } else if (typeof window[key].prototype === 'undefined') {
                // 普通函数
                yueqian.hookObj(window, "window", key, isDebugger);
            }
        }
    }
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,如有问题请邮件至2454612285@qq.com。
跃迁主页