JavaScript Hook 的使用

发布于:2024-08-09 ⋅ 阅读:(62) ⋅ 点赞:(0)

Hook 技术又叫钩子技术,指在程序运行过程中,对其中某个方法进行重写,在原先某个方法前后加入我们自定义的代码。相当于在系统没有调用该函数执之前,钩子程序就先捕获该消息,得到控制权,这时钩子程序既可以加工处理(改变)该函数的执行行为,也可以强制结束消息传递

插件安装

Tampermonkey : 中文名也叫油猴,支持 Chrome , Edge 等

下载地址:首页 | Tampermonkey

安装方法:打开扩展程序-----开启开发者模式--将下好的插件拖入即可

实用脚本下载: 用户脚本 (greasyfork.org)

脚本介绍

除了下载脚本以外,还可以自己编写一些想要的脚本

查看已有脚本:  点击 Tampermonkey ----管理面板

就可以看到已有的脚本,点击上方的 + 就可以创建一个新脚本

最前面的一些注释,它们非常有用,这部分内容叫作 UserScript Header,我们可以在里面配置一些脚本信息,如名称,版本,描述,生效站点等

下面简单介绍 UserScript Header 的一些参数定义

@name: 脚本名称,就是在控制面板显示的脚本名称

@namespace:  脚本的命令空间

@version: 脚本的版本,主要是做版本的更新时用

@author: 作者

@descrition : 脚本描述

@homepage, @homepageUTL, @website, @source : 作者主页,用于在 Tampermonkey  选项页面上从脚本名称点击跳转。请注意,如果 @namespace 标记以 http:// 开头,此处也要一样

@icon, @iconURL , @defaulticon: 低分辨率图标

@icon64, @icon64URL : 64 x 64 高分辨率图标

@updateURL:  检查更新网址, 需要定义 @version

@downloadURL: 更新下载脚本的网址,如果定义为 none , 就不会检查更新

@supportURL :  报告问题的网址

@include: 生效页面,可以配置 多个,但注意这里并不支持 URL Hash 例如:

        // @include http://* 

@match : 和 @include 差不多,可以配置多个

@exclude:  不生效页面,可以配置多个

@require:  附加脚本网址,相当于引入外部脚本,这些脚本会在自定义脚本执行之前执行,比如引入一些必要的库, 如 jQuery 等, 这里可以支持多个 @require 参数

例如:

// @require https://code.jquery.com/jquery-2.1.4.min.js

// @require https://code.jquery.com/jquery-2.1.3.min.js#sha256=23456....

@resource: 预加载资源,可通过 GM_getResourceURL 和 GM_getResourceText 读取

@connect: 允许被 GM_xmlhttpRequest 访问的域名, 每行一个

@run-at: 脚本注入时刻,如页面刚加载时,某个事件发生后等

document-sart : 尽可能早的执行脚本

document-body: DOM 的body 出现时执行

document-end: DOMcontentLoaded 事件发生时或发生后执行

document-idle: DOMcontentLoaded事件发生后执行,即DOM 加载完成之后执行,这是默认选项

context-menu: 如果在浏览器上下文 菜单(仅限桌面版)中点击该脚本,则会注入该脚本。注意: 如果使用此值,则忽略所有 @include 和 @exclude

@grant: 用于添加 GM 函数到白名单,相当于授权某些 GM 函数的使用权限

例如:  // @grant GM_setValue

如果没有定义过 @grant 选项, Tampermonkey 会猜测所需要的函数使用情况

@noframes: 此标记使脚本在主页上运行,但不会在 iframe 上运行

@nocompat:由于部分代码可能是为专门的浏览器所写,通过此标记, Tampermonkey 会知道脚本可运行的浏览器

例如: //  @nocompat Chrome

除此之外, Tampermonkey 还定义了一些 API ,使得我们可以方便的执行某个操作

GM_log : 将日志输出到控制台

GM_setValue: 将参数保存到浏览器存储中

GM_addValueChangeListener:为某个变量添加监听,当这个变量值改变时,就会触发回调

GM_xmlhttpRequest: 发起 Ajax 请求

GM_download: 下载某个文件到磁盘

GM_setClipboard: 将某个内容保存到粘贴板

其他 API 查阅: https://www.tampermonkey.net/documentation.php

在 UserScript Header 下方, 是 JavaScript 函数和调用的代码,其中 'use strict’ 标明代码使用 JS 的严格模式。在严格模式下,可以消除 JS 语法的一些不合理,减少一些怪异行为,如 不能直接使用未声明的变量。这样可以保证代码安全运行。同时提高编译器的效率,在下方 //  Your code here 处就可以编写自己的代码了

实战分析

下面我们通过一个简单的 JS 逆向案例来演示如何实现 JS  的 Hook 操作,轻松找到某个方法的执行位置,从而快速定位到逆向入口

案例网站:https://login1.scrape.center/

账户密码都是: admin

在输入账户密码点击登录后,提交 POST 的内容是一个加密后的 token

需要解决的问题: 

1. 没有类似 username 和 password 的内容 怎么登录

2. token 和用户名 ,密码的关系

初步观察: token 很像一个 Base64 编码。 那么这里大概猜测为: 用户名和密码输入后,网站先将这些混为一个新字符串,然后经过一次 Base64 编码,最后赋值给 token 来提交

解决方法有两种:1. Ajax 断点  2. Hook 

Ajax 断点

这里登录的时候发送的是一个 Ajax 请求,所以我们可以尝试使用 Ajax 断点来查看编码规则

步骤:

1. 请求的 URL 地址是  

https://login1.scrape.center/

我们可以用 login1.scrape.center 作为断点

打开开发者模式---切换到 Sources 选项卡---在 XHR/fetch Breakpoints 右边的 + 输入断点--然后点击登录就会停在断点位置,进入调试模式

然后我们从 Call Stack 里面一步步找到入口, 发现是在 onSubmit 方法那里,这里断点的栈顶还包括了一些 Promise 相关的内容,而我们真正想找的是用户名和密码经过处理,再进行 Base64 编码的地方,这些请求的调用实际上和我们找寻的入口没有多大关系。

另外。如果我们想找的入口位置并不伴随这一次 Ajax 请求, 这个方法就没用了

Hook

第二种可以快速定位入口的方法,就是使用 Tampermonkey 自定义 JS , 实现某个 JS 方法的 Hook ,Hook 哪里呢? 很明显, Hook Base64 编码的位置就好了

这里涉及一个小知识点,JS 里面的 Base64 编码是怎么实现的呢?

没错,就是 btoa 方法,在 JS 中该方法用于将字符串编码成 Base64 字符串,因此我们来 Hook btoa 方法就好了

这里我们建立一个 Tampermonkey 脚本, 其内容如下

// ==UserScript==
// @name         HookBase64
// @namespace    http://login1.scrape.center/
// @version      0.1
// @description  Hook Base64 encode function
// @author       Germey
// @match        https://login1.scrape.center/
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    function hook(object, attr){
        var func = object[attr]
        object[attr] = function(){
            console.log('hooked', object. attr)
            var ret = func.apply(object, arguments)
            debugger
            return ret
        }
    }
    hook(window, 'btoa')
})();

首先我们定义了一个 UserScript Header , 包括 @name 和 @match 等,这里比较重要的就是 @name ,表示脚本的名称,另外一个就是 @match , 它代表的是脚本生效的网址

接着,我们定义了 hook 方法, 这里给其传入 object 和 attr 参数, 意思就是 Hook object 对象的 attr 参数。 例如, 如果我们想 hook alert 方法, 那就把 object 设置为 window , 把 attr 设置为字符串 alert。 这里我们要 Hook Base64 的编码方法, 而在 JS 中, Base64 编码用的是 btoa 方法实现的,所以这里我们只需要 Hook window 对象的 btoa 方法就好了

那么, Hook 怎么实现呢? 我们来看一下, var func = object[attr] , 相当于我们先把它赋值给一个变量, 即我们调用 func 方法就可以实现和原来相同的功能。 接着,我们直接改写这个方法的定义,将 object[attr] 改成一个新方法。 在新的方法中, 通过 func.apply 方法又重新调用了原来的方法。 这样我们就可以保证前后方法的执行效果不受影响, 之前那个方法该干啥还干啥

但是和之前不同的是, 现在我们自定义方法之后, 可以在 func 方法执行后加入自己的代码,如果通过 console.log 将信息输出到控制台, 通过 debugger 进入断点等。 在这个过程中,我们先零食保存下来 func 方法, 然后定义一个新方法, 接管程序的控制权,在其中定义我们想要的实现, 同时新方法里面重新调回 func 方法, 保证前后结果不受影响。 所以,我们达到了在不影响原方法效果的前提下, 实现方法前后自定义的功能, 这就是 Hook 的过程。

最后,我们调用 hook 方法, 传入 window 对象和 btoa 字符串, 保存

接下来刷新页面,这时我们看到这个脚本在当前页面生效了, Tampermonkey 插件面板提示了已经启用。 同时, 在 Sources 面板下的 Page 选项卡中, 可以观察到我们定义的 JS 脚本被执行了

输入用户名和密码, 然后点击 “登录” 按钮, 成功进入断点模式并停下来, 代码就卡在我们自定的 debugger 这行代码的位置

成功 Hook 住了,这说明 JS 代码执行过程中调用到了 btoa 方法

这时看一下控制台, 这里也输出了 window 对象和 btoa 方法,验证正确

这样我们顺利找到了 Base64 编码操作这个路口,然后看一下栈信息,已经不会出现 Promise 相关的信息了,其中清晰的呈现了 btoa 方法的逐层调用过程。

另外。观察下 Local 面板, 看看 arguments 变量

可以说是一幕了然, arguments 就是指传给 btoa 方法的参数, ret 就是 btoa 方法返回的结果。可以看到, arguments 就是 username 和 password 通过 JSON 序列化之后的字符串, 经过 Base64 编码之后得到的值恰好就是 Ajax 请求参数 token 的值

我们还可以通过调用栈找到 onSubmit 方法的处理源码

     onSubmit: function() {
                    var e = c.encode(JSON.stringify(this.form));
                    this.$http.post(a["a"].state.url.root, {
                        token: e
                    }).then((function(e) {
                        console.log("data", e)
                    }
                    ))
                }

仔细看看,encode 方法其实就是调用了 btoa 方法,就是一个 Base64 编码的过程,答案其实已经很明了了

当然,我们还可以进一步添加断点验证一下流程, 比如在调用 encode 方法那行添加断点,

添加断点之后,可以点击 Resume script execution 按钮恢复 JS 的执行 跳过当前 Tampermonkey 定义的断点位置

然后重新点击登录按钮, 可以看到这时候代码就停在了当前添加断点的位置

这时候可以在 Watch 面板下输入 this.form ,验证此处是否为在表单输入的用户名和密码

没问题,然后逐步调试。我们还可以观察到,下一步就跳到了我们 Hook 的位置,这说明调用了 btoa 方法

验证到这里, 已经非常清晰了, 整体逻辑就是对登录表单的用户和密码进行 JSON 序列化, 然后调用 encode (也就是 btoa ) 方法, 并把 encode 方法的结果赋值为 token 发起登录的 Ajax 请求,逆向完成。