如何通过Devtools协议拦截和修改Chrome响应数据

2021-04-15 10:51:21

参考地址  如何通过Devtools协议拦截和修改Chrome响应数据

一、前言

在日常研究中,我们经常碰到大量JavaScript代码,我们首先要深入分析才能了解这些代码的功能及具体逻辑。这些代码代码可能会被恶意注入到页面中,可能是客户送过来需要我们帮忙分析的脚本,也可能是我们的安全团队在网页上找到的引用了我们服务的某些资源。这些脚本通常代码量不大、经过混淆处理,并且我们总是需要经过多层修改才能继续深入分析。

到目前为止,最简单的分析方法就是使用支持手动编辑的本地缓存环境或者使用代理来动态重写内容。本地方案最为方便,但并不是所有的网站都能完美适应其他环境,并且有时候人们会在提高效率的路上越走越远。代理非常灵活,但通常配置起来非常麻烦且不便携,每个人都有自己定制的环境,并且大家对各种代理的熟悉程度也不相同。我个人使用的是Chrome及devtools协议,以便hook请求及响应数据、动态修改数据内容。这种方式可以移植到搭载Chrome的任意平台,规避一系列问题,也能与常见的JavaScript工具完美集成。在本文中,我将介绍如何使用Chrome的devtools协议动态拦截并修改JavaScript。

本文使用的是node环境,但其中许多内容适用于各种语言,以便让大家能够轻松使用devtools。

如果大家之前没尝试过脚本化的Chrome,可以先参考下Eric Bidelman写的关于headless Chrome的入门指南,其中提到的技巧适用于headless以及GUI版本的Chrome(其中有一个小技巧会在下文给出解决方案)。

 

二、启动Chrome

我们可以使用npm中的chrome-launcher库来启动Chrome:

npm install chrome-launcher

我们从名字上就能猜到chrome-launcher的功能,也可以在命令行中无缝使用Chromium的参数(具体参数可参考此列表),这里我们选择使用如下选项:

1、–window-size=1200,800

自动设置窗口大小。

2、–auto-open-devtools-for-tabs

自动打开devtools(因为我们基本上每次都会用到这些功能)

3、–user-data-dir=/tmp/chrome-testing

设置用户目录。理想情况下我们并不需要这个参数,但出于某些原因,如果不使用该标志我们就无法在Mac OS X上使用非headless模式来拦截请求。由于我已经找到了解决方案,因此就不去深究背后的具体原因。如果大家找到更好的解决办法,欢迎推特联系我。

const chromeLauncher = require('chrome-launcher');async function main() {  const chrome = await chromeLauncher.launch({    chromeFlags: [      '--window-size=1200,800',      '--user-data-dir=/tmp/chrome-testing',      '--auto-open-devtools-for-tabs'
    ]
  });
}

main()

大家可以尝试运行如上脚本,确保能正确打开Chrome,效果如下图所示:

 

三、使用Chrome Devtools协议

该协议也被称为“Chrome调试器协议”,并且Google的某些文档中也用过这个称呼。首先,我们需要通过npm安装chrome-remote-interface包,以便与devtools协议交互。如果我们想深入了解具体细节,可以参考协议相关文档

npm install chrome-remote-interface

为了使用CDP,我们需要连接到调试器端口,因为我们正在使用chrome-launcher库,只需要使用如下语句就能完成该任务:

const protocol = await CDP({ port: chrome.port });

首先我们需要启用协议中的许多域,这里我们先从Runtime域开始,这样我们才能hook控制台API,将浏览器对控制台的调用传递到命令行中:

const { Runtime } = protocol;await Promise.all([Runtime.enable()]);

Runtime.consoleAPICalled(   ({ args, type }) => console[type].apply(console, args.map(a => a.value))
);

现在当我们运行脚本时,将获得一个功能完整的Chrome窗口,也会将所有控制台消息输出到我们的终端,这对测试来说本身就是非常棒的一件事。

 

四、拦截请求

首先我们需要注册待拦截的目标,具体方法是向setRequestInterception提交待拦截的RequestPatterns。我们可以在Request阶段或者HeadersReceived阶段进行拦截,为了修改响应数据,我们需要等待HeadersReceived。resourceType与我们常在devtools网络面板中看到的类型一致。

此外,别忘了启用Network域(与启用Runtime域的方法一样,将Network.enable()加入数组中)。

await Network.setRequestInterception(
  { patterns: [
    {
      urlPattern: '*.js*',
      resourceType: 'Script',
      interceptionStage: 'HeadersReceived'
    }
  ] }
);

事件处理程序(handler)的注册过程相对比较简单,每个待拦截的请求都带有一个interceptionId,该ID可以用来查询关于该请求的信息或者继续放行。这里我们只是将已拦截的所有请求信息输出到控制台界面。

Network.requestIntercepted(async ({ interceptionId, request}) => { console.log(    `Intercepted ${request.url} {interception id: ${interceptionId}}`
  );

 Network.continueInterceptedRequest({
    interceptionId,
  });
});

 

五、修改请求

为了修改请求,我们需要安装一些辅助库,用来编码及解码base64字符串。现在有许多库可以完成这项任务,我们可以选择自己惯用的工具。这里我们使用的是atob以及btoa,在使用这些库之前我们记得先得执行require操作,确保在脚本中能使用这些资源。

npm install btoa atob

这里用来处理响应的API略微有点麻烦。为了处理响应数据,我们需要在请求拦截上执行所有操作(而不是简单地拦截响应数据),然后通过interceptionId查询body数据。这是因为当handler被调用时body数据可能处于不可用状态,通过这种方式我们就能等待所需的数据。body可能经过base64编码处理,因此我们可能还需要检查编码状态,在继续传递之前执行解码操作。

const response = await Network.getResponseBodyForInterception({ interceptionId });const bodyData = response.base64Encoded ? atob(response.body) : response.body;

此时我们可以自由使用JavaScript:作为响应链的中间一环,我们可以访问所请求的完整JavaScript代码,也能返回任意内容。我们还可以稍微修改JS代码,在末尾添加一行console.log语句,这样当浏览器执行经过修改的代码时,我们的控制台就能收到相应消息。

const newBody = bodyData + `nconsole.log('Executed modified resource for ${request.url}');`;

我们不能直接简单地传递经过修改的body数据,因为修改后的内容可能会与来自原始资源的头部数据冲突。由于我们能自由测试并调整内容,我们可以从头开始构建最基本的头部数据,避免其它头部信息带来的潜在影响。如有必要,我们可以将responseHeaders传递给事件处理程序来访问响应头,但这里我们只需要使用数组创建所需的最小集合,以便将来进一步操作及编辑:

const newHeaders = [  'Date: ' + (new Date()).toUTCString(),  'Connection: closed',  'Content-Length: ' + newBody.length,  'Content-Type: text/javascript'];

为了发送新的响应,我们需要构造一个完整的、经过base64编码的HTTP响应包(包括HTTP状态码),然后通过对象的rawResponse属性配合continueInterceptedRequest执行发送操作。

Network.continueInterceptedRequest({
  interceptionId,
  rawResponse: btoa(    'HTTP/1.1 200 OKrn' +
    newHeaders.join('rn') +    'rnrn' +
    newBody
  )
});

此时,如果我们执行脚本,在互联网上随意冲浪时,就可以在终端界面看到如下类似内容,这是因为我们的脚本拦截了JavaScript,并且浏览器执行了经过我们修改的JavaScript,通过console.log()打印出这些信息。

完整的示例代码如下所示:

const chromeLauncher = require('chrome-launcher');const CDP = require('chrome-remote-interface');const atob = require('atob');const btoa = require('btoa');async function main() {  const chrome = await chromeLauncher.launch({    chromeFlags: [      '--window-size=1200,800',      '--user-data-dir=/tmp/chrome-testing',      '--auto-open-devtools-for-tabs'
    ]
  });  const protocol = await CDP({ port: chrome.port });  const { Runtime, Network } = protocol;  await Promise.all([Runtime.enable(), Network.enable()]);

  Runtime.consoleAPICalled(({ args, type }) => console[type].apply(console, args.map(a => a.value)));  await Network.setRequestInterception({ patterns: [{ urlPattern: '*.js*', resourceType: 'Script', interceptionStage: 'HeadersReceived' }] });

  Network.requestIntercepted(async ({ interceptionId, request}) => {    console.log(`Intercepted ${request.url} {interception id: ${interceptionId}}`);    const response = await Network.getResponseBodyForInterception({ interceptionId });    const bodyData = response.base64Encoded ? atob(response.body) : response.body;    const newBody = bodyData + `nconsole.log('Executed modified resource for ${request.url}');`;    const newHeaders = [      'Date: ' + (new Date()).toUTCString(),      'Connection: closed',      'Content-Length: ' + newBody.length,      'Content-Type: text/javascript'
    ];

    Network.continueInterceptedRequest({
      interceptionId,      rawResponse: btoa('HTTP/1.1 200 OK' + 'rn' + newHeaders.join('rn') + 'rnrn' + newBody)
    });
  });

}

main();

 

六、后续工作

后面我们还可以较为规整地打印出源代码,这对逆向工程来说通常是非常有用的第一步。当然现在许多浏览器都支持这个操作,但我们还是想自己控制数据修改的每个步骤,以便在各个浏览器版本之间保持兼容性,也能在分析源码时将各个环节连接起来。在分析外来的、经过混淆处理的代码时,如果我理解了变量以及函数的具体作用后,我就喜欢重命名这些对象。想要安全地修改JavaScript代码并非易事,本文只是一篇入门文章,后续大家还可以使用类似unminify的工具还原经过缩略或者混淆的代码。

const unminify = require('unminify');

[...]const newBody = unminify(bodyData + `nconsole.log('Intercepted and modified ${request.url}');`);


  • 2020-12-16 22:46:44

    基于coturn项目的stun/turn服务器搭建

    webrtc是google推出的基于浏览器的实时语音-视频通讯架构。其典型的应用场景为:浏览器之间端到端(p2p)实时视频对话,但由于网络环境的复杂性(比如:路由器/交换机/防火墙等),浏览器与浏览器很多时候无法建立p2p连接,只能通过公网上的中继服务器(也就是所谓的turn服务器)中转。示例图如下:

  • 2020-12-16 23:06:05

    Rocket.Chat推送信息

    Rocket.Chat推送消息 Rocket.Chat是一个开源实时通讯平台, 支持Windows, Mac OS, Linux. 支持聊天, 文件上传, 视频通话, 语音通话功能. 向Rocket.Chat推送消息 以下示例可以转为别的语言的版本, 本示例使用Linux平台的curl测试, curl非常强大. 登陆 首先需要登陆Rocket.Chat服务器

  • 2020-12-17 09:01:23

    对BitTorrent Tracker源码分析

    tracker服务器是BT下载中必须的角色。一个BT client 在下载开始以及下载进行的过程中,要不停的与 tracker 服务器进行通信,以报告自己的信息,并获取其它下载client的信息。这种通信是通过 HTTP 协议进行的,又被称为 tracker HTTP 协议,它的过程是这样的: client 向 tracker 发一个HTTP 的GET请求,并把它自己的信息放在GET的参数中;这个请求的大致意思是:我是xxx(一个唯一的id),我想下载yyy文件,我的ip是aaa,我用的端口是bbb。。。

  • 2020-12-17 10:55:48

    html5 video p2p research

    节约带宽,减少缓冲时间,提升服务质量,处理峰值流量, 视频观看的人越多,播放越流畅。

  • 2020-12-17 10:57:34

    使用 MediaSource 搭建流式播放器

    Media Source Extensions(媒体源扩展)大大地扩展了浏览器的媒体播放功能,提供允许JavaScript 生成媒体流。这可以用于自适应流(adaptive streaming,也是我毕设的研究方向)及随时间变化的视频直播流(live streaming)等应用场景。

  • 2020-12-17 11:00:37

    H5流式播放(FMP4转封装与mediaSource)

    W3C上有明确关于mediaSource 扩展接口的文档。mediaSource 扩展文档中是这么定义的, 它允许JS脚本动态构建媒体流用于和,允许JS传送媒体块到H5媒体元素。这种接口的应用可以让h5播放器实现持续添加数据进行播放。做as的朋友都知道as中的appendBytes方法,一种添加二进制数据进行播放的方式。这两种接口在概念上是类似的。只是里面的定义和对媒体文件的要求有所不同。对于mediaSource扩展接口我只介绍我们主要应用的几个。