流量劫持 —— GZIP 页面零开销注入 JS

这篇具有很好参考价值的文章主要介绍了流量劫持 —— GZIP 页面零开销注入 JS。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

HTTP 代理给页面注入 JS 是很常见的需求。由于上游服务器返回的页面可能是压缩状态的,因此需解压才能注入,同时为了节省流量,返回下游时还得再压缩。为了注入一小段代码,却将整个页面的流量解压再压缩,白白浪费大量性能。

是否有高效的解决方案?本文从注入位置、压缩格式、校验算法进行探讨。

注入位置

常见的注入方式,是对某个 HTML 标签进行替换,例如将 <head> 替换成 <head><script>...

字符匹配的方式虽然简单,但并不严谨。假如页面中没有出现 <head>,那么就不会注入了。若要考虑大小写、标签存在属性的情况,还得使用正则匹配。更极端的情况,例如匹配点出现在注释、字符串中,并且只替换一次的话,那么注入的代码根本不会运行:

<!doctype html>
<html>
  <!-- <head></head> -->
  <head></head>
  <body></body>
</html>

至于在网关上解析 HTML 这样的重量级操作,通常不会考虑。

现实中使用正则匹配足以支持大多数情况。不过正则匹配仍有一定的开销,是否有更轻量甚至零开销的注入方式?

其实可以有,直接将代码注入到页面最顶端!这种做法虽然不规范,但主流浏览器都支持。如果担心 doctype 失效,可以在注入的代码里补上:

<!doctype html><script src="inject.js"></script>
<!doctype html>
<html>
  <head></head>
  <body></body>
</html>

这样网关无需任何替换操作,只需转发时将注入的代码拼在第一个 chunk 之前即可。

不过这只是明文传输的情况。如果上游返回的是压缩流量,那么在其之前拼上「压缩后的注入代码」,是否仍有效?

我们以 gzip 为例接着探讨。

文件格式

gzip 使用 DEFLATE 算法压缩数据(下图 body 部分),并在前面加上 10 字节的文件头、不定长的可选头(记录文件名等),末尾加上 8 字节的文件尾:

struct field length
header magic number (1f 8b) 2
compression method (08) 1
flags 1
timestamp 4
compression flags 1
operating system ID 1
extra headers (optional) ... ...
... ...
body block1 ...
block2 ...
... ...
trailer CRC32 4
uncompressed data length 4

https://en.wikipedia.org/wiki/Gzip

由于我们的数据在最前面,因此需提供文件头,并删除上游返回的文件头。

此外,还需要确定如下问题:

  1. 文件尾的 CRC32 校验是否需要更新

  2. 压缩数据中每个 block 块是否独立

第一个问题即使不调研,大概也能猜到,在浏览器端肯定是不需要的。因为网页是流模式的,收到一些渲染一些。等渲染完成后才说数据有问题,那网页是留着还是不让显示?至少到目前还没见过网页提示 gzip 校验失败的错误。

第二个问题,在 RFC1951 中有讲解:

Each block is compressed using a combination of the LZ77 algorithm
and Huffman coding. The Huffman trees for each block are independent
of those for previous or subsequent blocks; the LZ77 algorithm may
use a reference to a duplicated string occurring in a previous block,
up to 32K input bytes before.

Each block consists of two parts: a pair of Huffman code trees that
describe the representation of the compressed data part, and a
compressed data part. (The Huffman trees themselves are compressed
using Huffman encoding.) The compressed data consists of a series of
elements of two types: literal bytes (of strings that have not been
detected as duplicated within the previous 32K input bytes), and
pointers to duplicated strings, where a pointer is represented as a
pair <length, backward distance>. The representation used in the
"deflate" format limits distances to 32K bytes and lengths to 258
bytes, but does not limit the size of a block, except for
uncompressible blocks, which are limited as noted above.

https://www.rfc-editor.org/rfc/rfc1951#page-4

每个块可能会引用之前块的数据,好在引用方式是从当前位置计算的(<长度, 反向距离>),因此是个相对值,不会因数据流开头插入我们的块而受到干扰。

此外还需注意的是,每个块的头部有个 BFINAL 字段标记当前是否为最后一块,因此我们的块中该字段不能被标记,否则后续块就不会解析了。

尝试

我们用 Node.js 实现一个初步演示:

import zlib from 'node:zlib'
import http from 'node:http'

// 上游返回的 gzip 数据(出于演示,未使用流模式)
const htmlGzipBuf = zlib.gzipSync('<h1>Hello World</h1>')

// 注入代码的 gzip 数据(部分压缩,防止被标记成最后一个 block)
let injectGzipBuf = Buffer.alloc(0)

const tmp = zlib.createGzip()
tmp.on('data', buf => {
  injectGzipBuf = Buffer.concat([injectGzipBuf, buf])
})
tmp.write('<!doctype html><script>console.log("Hi Jack")</script>')
tmp.flush()

http.createServer((req, res) => {
  res.setHeader('content-type', 'text/html')
  res.setHeader('content-encoding', 'gzip')
  // 输出压缩态的注入代码
  res.write(injectGzipBuf)
  // 跳过上游的 gzip 文件头(默认 10 字节)
  res.end(htmlGzipBuf.subarray(10))
}).listen(8080)

这个案例中,我们两次输出的都是压缩态数据,最终被浏览器成功解析。

流量劫持 —— GZIP 页面零开销注入 JS

经测试所有主流浏览器都没问题,curl 也没问题。但也有一些库会校验 CRC,例如 Node.js 的 fetch:

const res = await fetch('http://127.0.0.1:8080/')
const reader = res.body.getReader()
for (;;) {
  const {done, value} = await reader.read()
  if (done) {
    break
  }
  console.log(value)
}

读取最后块时报错:

Uncaught TypeError: terminated
    at Fetch.onAborted ...
  [cause]: Error: incorrect data check
      at Zlib.zlibOnError [as onerror] ...
    code: 'Z_DATA_ERROR'

导致读取的数据比预期少。

校验算法

如何更新校验值?最笨的办法,就是把上游流量全都解开,重新计算一次 CRC。毕竟解压的开销比压缩小很多,还是可以接受的。

不过本文追求的是低开销甚至零开销,因此这个方案很不完美。记得曾经开发防火墙时,如果数据包只修改很小一部分,那么 checksum 是不用重新计算的,只需稍加修正即可。这个思路是否可用在 CRC 上?毕竟 CRC 又不是什么密码学 hash 算法,就几个简单的 xor 运算,大概是可以玩出一些花招的。

一查文档,发现不仅可以,甚至这个奇技淫巧还被 zlib 库收录了,提供了一个 crc32_combine 函数,用于合并两个 CRC32 值:

crc32_combine(crc1, crc2, len2)

  Combine two CRC-32 check values into one.  For two sequences of bytes,
seq1 and seq2 with lengths len1 and len2, CRC-32 check values were
calculated for each, crc1 and crc2.  crc32_combine() returns the CRC-32
check value of seq1 and seq2 concatenated, requiring only crc1, crc2, and
len2.

至于原理细节,可参考:

https://stackoverflow.com/questions/23122312/crc-calculation-of-a-mostly-static-data-stream/23126768

https://github.com/stbrumme/crc32/blob/master/Crc32.cpp#L560

我们预先计算出注入 JS 的 CRC 作为 crc1,而上游返回的 gzip 文件尾正好携带了 crc2 和 len2,因此可直接算出新的 CRC。

使用这个方案,即可兼容所有 HTTP 客户端。

完整演示

前面的演示出于简单,未考虑 gzip 扩展文件头,并且直接使用 Buffer 代替数据流。下面分享一个更完整的演示:

https://github.com/EtherDream/gzip-js-injector

流量劫持 —— GZIP 页面零开销注入 JS

后记

几年前研究流量劫持时写的文章,不过一直没发布,前段时间翻新了下并补了个 demo。由于那时还没 brotli 压缩,因此也没调研。之后有时间再补充。文章来源地址https://www.toymoban.com/news/detail-473522.html

到了这里,关于流量劫持 —— GZIP 页面零开销注入 JS的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 驱动开发:内核RIP劫持实现DLL注入

    本章将探索内核级DLL模块注入实现原理,DLL模块注入在应用层中通常会使用 CreateRemoteThread 直接开启远程线程执行即可,驱动级别的注入有多种实现原理,而其中最简单的一种实现方式则是通过劫持EIP的方式实现,其实现原理可总结为,挂起目标进程,停止目标进程EIP的变换

    2024年02月09日
    浏览(44)
  • 13. 从零用Rust编写正反向代理, HTTP中的压缩gzip,deflate,brotli算法

    wmproxy 是由 Rust 编写,已实现 http/https 代理, socks5 代理, 反向代理,静态文件服务器,内网穿透,配置热更新等, 后续将实现 websocket 代理等,同时会将实现过程分享出来, 感兴趣的可以一起造个轮子法 gite: https://gitee.com/tickbh/wmproxy github: https://github.com/tickbh/wmproxy HTTP中压

    2024年02月02日
    浏览(43)
  • Node.js | 使用 zlib 内置模块进行 gzip 压缩

    🖥️ NodeJS专栏:Node.js从入门到精通 🖥️ 博主的前端之路:前端之行,任重道远(来自大三学长的万字自述) 🧧 加入社区领红包:海底烧烤店ai(从前端到全栈) 🧑‍💼个人简介:即将大三的学生,一个不甘平庸的平凡人🍬 👉 你的一键三连是我更新的最大动力❤️!

    2024年02月02日
    浏览(51)
  • SQL注入告警流量特征

             查看url / Referer字段/User-Agent字段/cookie字段 1、出现一些特殊字符(eg:单引号【‘】、双引号【“”】、括号【()】、单引号括号【‘(】、双引号括号【“(】等一些常见的特殊的字符);       eg:http://localhost/index.php/ ?id=1\\\'and+1=1--+       eg:http://loc

    2024年02月12日
    浏览(38)
  • SQL注入的流量特征

    Sql 注入攻击是通过将恶意的 Sql 查询或添加语句插入到应用的输入参数中,再在后台 Sql 服务器上解析执行进行的攻击,它目前黑客对数据库进行攻击的最常用手段之一。 Sql 注入带来的威胁主要有如下几点 猜解后台数据库,这是利用最多的方式,盗取网站的敏感信息。 绕过

    2024年02月08日
    浏览(36)
  • Go编写流量代理工具

    代理本地HTTP服务 代理局域网SSH服务 其他的TCP服务没测试了 main -》 主函数,包含Server、Client函数和goroutine函数 utils -》 工具函数,包含发包和读包函数,现在使用的是json序列化没有加密(内置AES256加密,可以把粘包问题处理后进行使用) 返回数据呈现给用户: 本地Conn -》

    2024年02月11日
    浏览(31)
  • 了解HTTP代理日志:解读请求流量和响应信息

      嗨,爬虫程序员们!你们是否在了解爬虫发送的请求流量和接收的响应信息上有过困扰?今天,我们一起来了解一下。 首先,我们需要理解HTTP代理日志的基本结构和内容。HTTP代理日志是对爬虫发送的请求和接收的响应进行记录的文件。在日志中,我们可以看到每一次请求

    2024年02月14日
    浏览(40)
  • 【工具篇】情侣大杀器,使用postman的代理,抓取手机流量

    目录 1. postman的版本 2. 代理的原理 3. 设置postman代理 4. 查看本机ip地址 5. 手机端设置 6. postman火力全开吧 在postman没有开启代理之前,手机访问直接访问互联网上的服务器,postman开启了代理,则手机对网站服务器发送的请求,会先经过postman,再由postman转发到服务器,同样,服

    2024年02月09日
    浏览(36)
  • 【SQL代理中转注入】对DVWA登录界面username字段实施注入

      以DVWA为攻击目标,将login.php中第21、22行注释掉 如此可知,首先需要通过token验证,然后能进行查询。 1.token验证通过。 这里token用到的函数是dvwaPage.inc.php中调用的 通过查看函数被调用的部分和逻辑,可知该检测是如此检测或者说避免CSRF攻击的: ①访问login.php,页面生成

    2024年04月25日
    浏览(34)
  • spring的aop动态代理对象注入时机

    bean生命周期: bean实例化 populateBean填充属性 invokeAwareMethods调用aware方法 postProcessBeforeInitialization后置处理器before方法 initializeBean初始化bean postProcessAfterAfterInitialization后置处理器after方法 代理对象注入有两种情况:提前和非提前生成代理对象 1. 非提前生成代理对象 依赖于bea

    2024年02月12日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包