【web安全】js逆向之Hook篇

JavaScript 中的 Hook 是一种常见的编程技术,用于在代码执行某些特定操作时执行其他代码。具体来说,Hook 方法是一些预定义的函数,它们会在特定的事件发生时被调用。

在程序中将其理解为“劫持”可能会更好理解,我们可以通过 Hook 技术来劫持某个对象,把某个对象的程序拉出来替换成我们自己改写的代码片段,修改参数或替换返回值,从而控制它与其他对象的交互。

Hook方式

直接覆盖

在 JavaScript 逆向中,替换原函数的过程都可以被称为 Hook,如将window.alert函数替换:

window.alert = function(s){
    console.log('Alert: ' + s);
}

这种直接替换的方式只能覆盖具体变量的成员函数,通常只在临时调试的时候用

使用Object.defineProperty()

语法:Object.defineProperty(obj, prop, descriptor)​​,它的作用是在一个对象上定义一个新的属性,或者修改一个对象上已有的属性,并返回该对象,接收三个参数含义如下:

  • obj:需要定义属性的对象
  • prop:字符串或Symbol​对象,指定要修改或定义的属性名
  • descriptor:要修改或定义的属性描述符,可以取以下值:
属性名 默认值 含义
get undefined 存取描述符,目标属性获取值的方法
set undefined 存取描述符,目标属性设置值的方法
value undefined 数据描述符,设置属性的值
writable false 数据描述符,目标属性的值是否可以被重写
enumerable false 目标属性是否可以被枚举
configurable false 目标属性是否可以被删除或是否可以再次修改特性

使用 Object.defineProperty()​ 方法定义对象属性(可以在细粒度下控制对象属性):

const object1 = {};

Object.defineProperty(object1, 'property1', {
  value: 42,
  writable: false,		// 不可写
  configurable: true,	// 可以再次修改
  enumerable: true,		// 不可以被枚举
});

object1.property1 = 77;
// Throws an error in strict mode

console.log(object1.property1);
// Expected output: 42

Hook中最常用的是存取描述符,即get​、set​属性

get​:属性的 getter 函数,默认为undefined,当访问该属性时,会调用此函数,调用时不传入参数,但会传入this对象(由于继承关系,这个对象不一定是定义该属性的对象),返回值会作为属性的值。

set​:属性的setter函数,默认undefined,当属性的值被修改时,会调用此函数,传入一个参数,并将其值赋给属性值,同样会传入this对象。

一个例子演示:

function Archiver() {
  let temperature = null;
  const archive = [];

// hook temperature属性
  Object.defineProperty(this, "temperature", {
    get() {
	 // 添加了一行输出代码,其它不变
      console.log("get!");
      return temperature;
    },
    set(value) {
      temperature = value;
      archive.push({ val: temperature });
    },
  });

  this.getArchive = () => archive;
}

const arc = new Archiver();
arc.temperature; // 'get!'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{ val: 11 }, { val: 13 }]

通过以上的思路,可以想到对某些关键的元素进行hook,在属性被访问/修改/生成时执行自定义的一些操作(如debugger),以快速定位到关键代码

使用Proxy

Proxy具有拦截函数执行功能,可用于运算符重载、对象模拟等功能,这种方法比较适合需要Hook某个对象中大部分属性、函数的场景。

拦截Array.prototype.push​方法的例子

const handler = {
  get: function(target, prop) {
    if (prop === 'push') {
      return function(...args) {
        console.log(`调用了Array.prototype.push方法,参数为:${args}`);
        return target[prop].apply(target, args);
      };
    } else {
      return target[prop];
    }
  }
};

const arr = [1, 2, 3];
const arrProxy = new Proxy(arr, handler);

arrProxy.push(4);	\\"调用了Array.prototype.push方法,参数为:4"

Hook实践

以某花顺财经登录接口请求头的hexin-v​参数为例,演示如何Hook

登录接口发现每次请求头包含一个hexin-v​参数,值随机,全局搜索无结果,尝试定位到该值生成的代码

image-20230303153243572

Fidder插件注入

Fiddler是一种流行的免费的跨平台Web调试代理工具(抓包软件),配合插件注入js代码(注意:Hook代码在网站代码加载之前执行

Hook一下XMLHttpRequest​的setRequestHeader​方法,设置当请求头为hyxin-v​值时断下

(function () {
    var org = window.XMLHttpRequest.prototype.setRequestHeader;
    window.XMLHttpRequest.prototype.setRequestHeader = function (key, value) {
        if (key == 'hexin-v') {
            debugger;
        }
        return org.apply(this, arguments);
    };
})();

开启Hook

image-20230303154406213

刷新网站,发现直接被断下

image-20230303154502745

跟堆栈,可以看到值是由W()函数生成

image-20230303154730643

接着再进入W函数接着分析即可

TamperMonkey 注入

TamperMonkey 俗称油猴插件,网上搜索会有很多教程,此处略

浏览器插件注入

浏览器插件/扩展实质是在加载网页时执行插件的代码,所以也是可以用来注入Hook代码的,如何编写一个插件也是有相当多教程的,此处略。

部分hook模板

js语法不熟悉时,可以快速套用以下模板,当然必须根据具体场景,进行修改。

Hook Header

header 钩子用于定位 header 中关键参数生成位置,以下代码演示了当 header 中包含 Authorization​ 时,则插入断点

Chrome扩展为例:

manifest.json

{
   "name": "Injection",
    "version": "1.0",
    "description": "RequestHeader钩子",
    "manifest_version": 1,
    "content_scripts": [
        {
            "matches": [
                "<all_urls>"
            ],
            "js": [
                "inject.js"
            ],
            "all_frames": true,
            "permissions": [
                "tabs"
            ],
            "run_at": "document_start"
        }
    ]
}

inject.js

(function () {
    var org = window.XMLHttpRequest.prototype.setRequestHeader;
    window.XMLHttpRequest.prototype.setRequestHeader = function (key, value) {
        if (key == 'Authorization') {
            debugger;
        }
        return org.apply(this, arguments);
    };
})();

cookie 钩子用于定位 cookie 中关键参数生成位置,以下代码演示了当 cookie 中匹配到了 __dfp​关键字, 则插入断点:

// 第一种方式
(function () {
  'use strict';
  var cookieTemp = '';
  Object.defineProperty(document, 'cookie', {
    set: function (val) {
      if (val.indexOf('__dfp') != -1) {
        debugger;
      }
      console.log('Hook捕获到cookie设置->', val);
      cookieTemp = val;
      return val;
    },
    get: function () {
      return cookieTemp;
    },
  });
})();
// 第二种方式
(function () {
    'use strict';
    var org = document.cookie.__lookupSetter__('cookie');
    document.__defineSetter__('cookie', function (cookie) {
        if (cookie.indexOf('__dfp') != -1) {
            debugger;
        }
        org = cookie;
    });
    document.__defineGetter__('cookie', function () {
        return org;
    });
})();

Hook url

请求钩子用于定位请求中关键参数生成位置,以下代码演示了当请求的 url 里包含 AbCdE​ 时,则插入断点:

var code = function(){
var open = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function (method, url, async){
    if (url.indexOf("AbCdE")>-1){
        debugger;
    }
    return open.apply(this, arguments);
};
}
var script = document.createElement('script');
script.textContent = '(' + code + ')()';
(document.head||document.documentElement).appendChild(script);
script.parentNode.removeChild(script);

Hook JSON.stringify

JSON.stringify()​ 方法用于将 JavaScript 值转换为 JSON 字符串,在某些站点的加密过程中可能会遇到,以下代码演示了遇到 JSON.stringify()​ 时,则插入断点:

(function() {
    var stringify = JSON.stringify;
    JSON.stringify = function(params) {
        console.log("Hook JSON.stringify ——> ", params);
        debugger;
        return stringify(params);
    }
})();

Hook JSON.parse

JSON.parse()​ 方法用于将一个 JSON 字符串转换为对象,在某些站点的加密过程中可能会遇到,以下代码演示了遇到 JSON.parse()​ 时,则插入断点:

(function() {
    var parse = JSON.parse;
    JSON.parse = function(params) {
        console.log("Hook JSON.parse ——> ", params);
        debugger;
        return parse(params);
    }
})();

Hook eval

eval经常被用来动态执行 JS。以下代码执行后,之后所有的 eval()​ 操作都会在控制台打印输出将要执行的 JS 源码:

(function() {
    // 保存原始方法
    window.__cr_eval = window.eval;
    // 重写 eval
    var myeval = function(src) {
        console.log(src);
        console.log("=============== eval end ===============");
        debugger;
        return window.__cr_eval(src);
    }
    // 屏蔽 JS 中对原生函数 native 属性的检测
    var _myeval = myeval.bind(null);
    _myeval.toString = window.__cr_eval.toString;
    Object.defineProperty(window, 'eval', {
        value: _myeval
    });
})();

Hook Function

以下代码执行后,所有的函数操作都会在控制台打印输出将要执行的 JS 源码:

(function() {
    // 保存原始方法
    window.__cr_fun = window.Function;
    // 重写 function
    var myfun = function() {
        var args = Array.prototype.slice.call(arguments, 0, -1).join(","),
            src = arguments[arguments.length - 1];
        console.log(src);
        console.log("=============== Function end ===============");
        debugger;
        return window.__cr_fun.apply(this, arguments);
    }
    // 屏蔽js中对原生函数native属性的检测
    myfun.toString = function() {
        return window.__cr_fun + ""
    }
    Object.defineProperty(window, 'Function', {
        value: myfun
    });
})();

参考:

https://mp.weixin.qq.com/s/IYFyjVrVkHtUdCzn9arkJQ

https://cloud.tencent.com/developer/article/1639578