此图像也是一个有效的 Javascript 文件 选择正确的图像类型 选择正确的图像尺寸 在其中获取我们自己的脚本 清理二进制文件 说服浏览器执行图像但是......为什么?

2025-05-26

此图像也是一个有效的 JavaScript 文件

选择正确的图像类型

选择正确的图像尺寸

在那里获取我们自己的脚本

清理二进制文件




说服浏览器执行图像

但为什么?

图像通常存储为二进制文件,而 JavaScript 文件基本上只是文本。两者都必须遵循各自的规则:图像具有具体的文件格式,以特定方式对数据进行编码。JavaScript 文件必须遵循特定的语法才能执行。我想知道:我能否创建一个具有有效 JavaScript 语法的图像文件,以便它也可以执行?

在继续阅读之前,我强烈建议您先查看一下这个代码沙箱,其中包含我的实验结果:

https://codesandbox.io/s/executable-gif-8yq0j?file=/index.html

如果您想查看图像并亲自检查,可以在此处下载:

https://executable-gif.glitch.me/image.gif

选择正确的图像类型

不幸的是,图像包含大量二进制数据,如果用 JavaScript 解释,会报错。所以我的第一个想法是:如果我们把所有图像数据都放在一个长注释里,就像这样:



/*ALL OF THE BINARY IMAGE DATA*/


Enter fullscreen mode Exit fullscreen mode

那将是一个有效的 JavaScript 文件。但是,图像文件需要以特定的字节序列开头;即特定于图像格式的文件头。例如,PNG 文件必须始终以字节序列89 50 4E 47 0D 0A 1A 0A开头。如果图像以 开头/*,它将不再是有效的图像文件。

这个文件头引发了下一个想法:如果我们可以使用这个字节序列作为变量名,并进行如下巨大的字符串分配,会怎么样?



PNG=`ALL OF THE BINARY IMAGE DATA`;


Enter fullscreen mode Exit fullscreen mode

我们使用模板字符串而不是普通"字符串',因为二进制数据可能包含换行符,而模板字符串更适合处理这些问题。

不幸的是,大多数图像文件头的字节序列都包含不可打印的字符,这些字符不允许出现在变量名中。不过,有一种图像格式我们可以使用:GIF。GIF 的头文件块是47 49 46 38 39 61,用 ASCII 码拼写就是GIF89a,这是一个完全合法的变量名!

选择正确的图像尺寸

现在我们找到了一个以有效变量名开头的图像格式,接下来需要添加等号和反引号。因此,该文件接下来的四个字节是:3D 09 60 04

图像的第一个字节

在 gif 格式中,标头后面的四个字节指定了图像的尺寸。我们必须在其中放入3D(等号)和60(字符串开头的反引号)。GIF 采用小端序,因此第二个和第四个字符对图像尺寸的影响很大。我们希望尽可能地保持它们的大小,以免最终得到一张宽度达数万像素的图像。因此,我们将较大的3D60字节存储在最低有效字节中。

图像宽度的第二个字节必须是有效的空格字符,因为它是等号和字符串开头之间的空格GIF89a= `...。请记住,字符的十六进制代码应尽可能小,否则图像会变得非常大。

最小的空白字符是09,即水平制表符。这给我们的图像宽度是3D 09,在小端模式下转换为 2365 ;比我想要的宽一点,但仍然合理。

对于第二个高度字节,我们可以选择一个能够产生良好纵横比的值。我选择了04,它的高度为60 04,也就是 1120 。

在那里获取我们自己的脚本

目前,我们的可执行 gif 文件实际上什么也没做。它只是将一个长字符串赋值给全局变量GIF89a。我们希望发生一些有趣的事情!GIF 中的大部分数据用于对图像进行编码,因此如果我们尝试在其中添加 JavaScript,最终可能会得到一张非常损坏的图像。但出于某种原因,GIF 格式包含一个叫做注释扩展 的东西。它用于存储一些不会被 GIF 解码器解释的元数据——非常适合我们的 JavaScript 逻辑。

这个注释扩展位于 GIF 颜色表之后。由于我们可以在其中放置任何内容,因此我们可以轻松地关闭GIF89a字符串,添加所有 JavaScript 代码,然后启动多行注释块,这样图像的其余部分就不会干扰 JavaScript 解析器。

总而言之,我们的文件看起来应该是这样的:



GIF89a= ` BINARY COLOR TABLE DATA ... COMMENT BLOCK:

`;alert("Javascript!");/*

REST OF THE IMAGE */


Enter fullscreen mode Exit fullscreen mode

有一个小小的限制:虽然注释块本身可以是任意大小,但它由多个子块组成,每个子块的最大大小为 255。子块之间有一个字节,指示下一个子块的长度。因此,为了在其中容纳更大的脚本,必须将其分成更小的块,如下所示:



alert('Javascript');/*0x4A*/console.log('another subblock');/*0x1F*/...


Enter fullscreen mode Exit fullscreen mode

注释中的十六进制代码是表示下一个子块大小的字节。它们与 JavaScript 无关,但对于 GIF 文件格式是必需的。为了防止它们干扰其余代码,它们必须放在注释中。我编写了一个小脚本来处理脚本块并将其添加到图像文件中:https://gist.github.com/SebastianStamm/c2433819cb9e2e5af84df0904aa43cb8

清理二进制文件

现在我们已经确定了基本结构,接下来需要确保二进制图像数据不会破坏我们的语法。如上一节所述,该文件包含三个部分:第一部分是对变量GIF89a 的赋值,第二部分是 JavaScript 代码,第三部分是多行注释。

我们先来看第一部分,变量赋值:



GIF89a= ` BINARY DATA `;


Enter fullscreen mode Exit fullscreen mode

如果二进制数据包含字符`或字符组合,${我们就会遇到麻烦,因为这会结束模板字符串或生成无效的表达式。解决方法很简单:只需更改二进制数据!例如,我们可以用`字符 (十六进制码61 ) 代替字符a(十六进制码60 )。由于文件的这一部分包含颜色表,因此某些颜色可能会略有偏差,例如使用颜色#286148而不是#286048。不太可能有人会注意到这种差异。

打击腐败

在 Javascript 代码的末尾,我们打开了多行注释,以确保二进制图像数据不会干扰 Javascript 解析:



alert("Script done");/*BINARY IMAGE DATA ...


Enter fullscreen mode Exit fullscreen mode

如果图像数据包含字符序列*/,注释就会提前结束,从而导致 JavaScript 文件无效。在这里,我们可以手动更改其中一个字符,使其不再作为注释的结束符。但是,由于我们现在位于编码图像部分,这将导致图像损坏,如下所示:

图像损坏

在极端情况下,图像根本无法显示。通过仔细选择要翻转的位,我能够最大限度地减少损坏。幸运的是,只有少数有害组合*/需要处理。最终图像中仍然可以看到一些损坏,例如在“有效的 Javascript 文件”字符串的底部,但总的来说,我对结果非常满意。

结束文件

我们要执行的最后一个操作是在文件末尾。文件必须以字节00 3B结尾。因此,我们必须提前结束注释。由于这是文件的末尾,任何潜在的损坏都不会很明显,所以我只结束了多块注释,并添加了一行注释,这样文件末尾就不会在解析时造成任何问题:



/* BINARY DATA*/// 00 3B

Enter fullscreen mode Exit fullscreen mode




说服浏览器执行图像

现在,经过所有这些,我们终于得到了一个既是图像又是有效 JavaScript 文件的文件。但还有最后一个挑战需要克服:如果我们将图像上传到服务器并尝试在脚本标签中使用它,我们很可能会看到如下错误:

拒绝从“ http://localhost:8080/image.gif ”执行脚本,因为其 MIME 类型(“image/gif”)不可执行。

所以浏览器理所当然地说:“那是一张图片!我不会执行它!”。在大多数情况下,这是一种很好的心态。但我们无论如何都想执行它。我们的解决方案是不告诉浏览器它是一张图片。为此,我编写了一个小型服务器,它在不提供任何标头信息的情况下提供图片。

如果没有来自标题的 MIME 类型信息,浏览器就不知道它是一张图片,而只能做最适合上下文的事情:在<img>标签中将其显示为图片,或在标签中将其作为 Javascript 执行<script>

但为什么?

这是我还没搞清楚的事情。让这些东西发挥作用是一项不错的智力挑战,但如果你能想到任何可能真正有用的场景,请告诉我!

文章来源:https://dev.to/sebastianstamm/this-image-is-also-a-valid-javascript-file-5fol
PREV
初学者密码学
NEXT
设计百万美元仪表盘的技巧