我们专注于一种这样的技术——像素化——并将向您展示为什么它是一种让您的敏感数据泄露的不好、坏、不安全、万无一失的方法。为了向您展示原因,我编写了一个名为Unredacter的工具,它采用经过编辑的像素化文本并将其反转回未编辑的形式。在野外有很多这样的真实例子来编辑敏感信息,但我不会在这里命名。
我们专注于一种这样的技术——像素化——并将向您展示为什么它是一种让您的敏感数据泄露的不好、坏、不安全、万无一失的方法。为了向您展示原因,我编写了一个名为Unredacter的工具,它采用经过编辑的像素化文本并将其反转回未编辑的形式。在野外有很多这样的真实例子来编辑敏感信息,但我不会在这里命名。
因此,有一个名为 Depix 的现有工具试图通过一个非常聪明的过程来做到这一点,即在给定 正确字体的De Bruijn 序列的情况下,查找哪些像素排列可能导致某些像素化块。我非常喜欢这个工具的理论,但Jumpsec 的一位研究人员指出,它在实践中可能并不如你所愿。在现实世界的示例中,您可能会得到微小的变化和噪音,这会将扳手扔进齿轮中。然后他们向任何人发出挑战,如果您可以取消编辑以下图像,则提供奖品:
我怎么能拒绝这样的挑战?!
像素化看起来像这样:
该算法非常简单;您将图像划分为给定块大小的网格(上面的示例是 8×8)。然后对于每个块,您将编辑图像的颜色设置为相同区域的原始图像的平均颜色。就是这样——只是每个块的滚动像素平均值。
这种效果会在每个块上“涂抹”图像的信息。但是,虽然在此过程中丢失了一些信息,但它绝对会泄漏很多。我们将利用这些泄露的信息来发挥自己的优势。
值得注意的是,这个算法被广泛标准化,因为它非常简单。因此,无论您是在 GiMP、Photoshop 还是基本上任何其他工具中进行此编辑,编辑结果都是一样的。
对于我们的概念验证,让我们假设攻击者知道:
我会断言,这些都是相当合理的假设,因为在现实场景中的攻击者可能会收到一份完整的报告,其中只有一个被删节。在我们的挑战文本中,您可以在像素化文本上方看到几个单词,这些单词为我们提供了这些信息。
我们关注的关键是编辑过程本质上是本地的。在密码学术语中,我们会说它没有扩散。原始图像中某个像素的变化只会影响它所属的编辑块,这意味着我们可以(大部分)逐个字符地猜测图像。我们将对每个字符进行递归深度优先搜索,根据与编辑文本的边缘匹配程度对每个猜测进行评分。
基本上,我们猜测字母“a”,将该字母像素化,然后查看它与我们的编辑图像的匹配程度。然后我们猜测字母“b”,依此类推。听起来没那么难,对吧?好吧,还有一堆后勤问题需要克服,一开始可能并不那么明显!让我们进一步深入研究。
角色溢出问题
我们立即遇到的第一个问题是我们文本的字符没有与编辑块 1:1 对齐。这意味着给定的正确猜测实际上可能在最右边有一些错误的块。要了解我的意思,请查看以下示例:
您可以看到字母“t”和“h”共享一列块。因此,如果我们尝试猜测字母“t”,最左边的块列结果是正确的,但最右边的块有点错误。
正确的像素与猜测“t”与差异
第二列错误的原因是字母“h”把事情搞砸了。如果我们只看这个,你可能会得出结论,字母“t”是不正确的第一个字母,因为它几乎有一半的块完全错误。
我们尝试的第一件事是避免计算任何猜测的最右边的块。它是流血最多的列,并且可能有相当多的错误。这样做的问题是,在实践中,它大大减少了我们猜测的总规模,以至于你开始收到误报。你的信总是有可能会意外地排成一行,并且纯粹是偶然地产生匹配,而当需要考虑的块更少时,这个机会就会大大增加。
因此,我们所做的是在字母本身的边界处切断比较块。因此,我们的差异看起来像这样:
您可以看到我们的比赛质量大大提高,因为我们在右侧包含的错误区域减少了。这是因为我们在“t”结束的边缘切断了比较:
这样做的好处是我们的猜测字符越多地延伸到块中,块就越有可能是一个好的猜测,所以我们保留了更多的块。因此,它会在猜测错误时自动切断大部分区块,并在猜测正确时保留大部分区块。
字符溢出问题的一个特定子集是空格往往会破坏我们关于字符猜测如何工作的一些假设。整个问题的内在假设是,当我们猜测一个正确的字符时,我们期望得到的像素化版本与挑战图像最相似。
然而,当我们猜测的字符是空格时,这并不总是正确的。发生这种情况时,像素化块将被下一个字符完全取代。以这个例子为例,猜测“这是”(尾随空格):
然后像下面这样像素化,尾随空白列,如您所料:
问题是在解决方案图像中,空格后面还有另一个字符。它流血得如此严重,以至于我们的正确猜测看起来完全错误!
解决这个问题的方法不止一种。最明显的是永远不要自己猜测空格,而是将其与其他一些非空格字符配对。这样我们就可以控制流血的角色。虽然这“有效”,但它有效地使可用字符集加倍。这将整个过程减慢到爬行。
相反,我们可以做的是对空白猜测做一个特殊的分割,让他们在被认为是“好的”猜测时更加宽容。在测试中,似乎溢出从未如此糟糕以至于超过了较低的阈值。这有点笨拙,我会同意你的,但它似乎有效。
人们书写的大多数字体都是可变宽度的。这意味着每个字母占用的水平空间量取决于字母本身。例如,“w”比“i”占用更多空间。这与等宽字体形成鲜明对比,等宽字体故意分隔字母,使每个字母占据相同数量的水平空间。
可变宽度:
iiiiii
万维网
等宽:
iiiiii 万维网
这对我们的攻击(假设是可变宽度字体)意味着每个猜测的字母在其右侧都有级联效应。如果你猜:
这是supww
那么所有未来的字母都将被关闭,即使这些字母在其他方面是“正确的”。
这听起来很重要,但实际上并没有那么糟糕。这只是意味着我们必须坚持递归深度优先搜索,而不是将字母视为单独和独立的工件。递归深度优先搜索在这里效果很好,因为它自然会考虑到这种排序。它的工作方式如下:
假设我们目前的猜测是:
这是苏
我们所做的是尝试下一个字母的每个字符,看看哪些字符与编辑后的图像匹配得相当好。我们将得到一些“好”猜测的子集,可能是“p”和“q”,因为 p 是正确的,而 q 与它非常相似。然后,我们将重新开始整个过程,在链上重新猜测“this is sup”的新字符串,直到我们遇到没有好的猜测的死胡同。此时,函数调用堆栈自然会备份到尝试我们的其他猜测 q。
以此类推,直到我们用尽所有“好的”猜测。
碰巧的是,即使对于应该是完全相同的字体,不同的渲染引擎也会产生略有不同的图像。看看这两个相同文本的捕获。顶部是 GiMP 在 Sans Serif 中的渲染,底部是 FireFox:
它们几乎相同,但并不完全相同。有两件事很突出;一是长度。您可以看到顶部图像只是稍微长一点。对于足够长的字符串,这可能会产生级联效应,从而将整个事情搞砸。另一个区别是文本的光栅化方式。底线只是比顶线更大胆一点。我们可以通过调整亮度来处理这个问题,但这完全是一种痛苦。
对于 Unredacter,我们使用 Electron 截取本地无头 HTML 窗口的屏幕截图。因此,渲染器本质上是 Chrome。大多数时候,这不是问题。但是,如果您的编辑文本是使用一些不符合标准的非常不稳定的程序呈现的,那么它可能最终会偏离轨道。记住这一点。
如果有人想为 Unredacter 编写一个包装器,该包装器使用 MS Word 通过一些包装器和宏的 Rube Goldberg 机器生成猜测,欢迎您试一试。
在对图像进行像素化时,必须考虑两个自由度:x 和 y 偏移坐标。但这些到底是什么?
考虑我们猜测文本的图像被分成 8×8 块:
如果您认为这是一个静态网格,那么您可以在 64 个不同的位置将文本放置在该网格上。我们称其为 x 和 y “偏移量”。根据您选择的偏移量,它可以产生截然不同的图像:
相同文本的不同偏移值
此外,攻击者无法知道这些偏移量是什么。(与字体和字体大小不同)。在大多数编辑器(如 GiMP)中,偏移量是由用户在制作边界框时碰巧点击的大部分随机过程确定的。如果他们向上或向下单击单个像素,像素化将产生完全不同的图像!
这里的好消息是抵消的可能性并不多。有块大小2排列。对于 8 的块大小,需要尝试 64 个偏移量。在我们的挑战文本中,块大小为 5,这意味着只有 25 个偏移量需要测试。
因此,Unredacter 的第一步是发现使用了什么偏移量。我们通过在循环中尝试每个偏移量来做到这一点,看看是否有任何字母能很好地猜出首字母。我们获取所有具有良好首字母猜测的偏移量,并将它们添加到列表中,然后尝试正确的猜测。
好的!有了这些知识和利用它的工具,让我们再次看看 Jumpsec 的挑战图像:
您可能会注意到的第一件事是它有一些奇怪的颜色。是什么赋予了?由于文本是黑色的,它不应该只是黑白的吗?他们是在用彩色字母来欺骗我们吗?
我实际上不是 100% 确定为什么会发生这种情况(有时不会),但它是文本渲染到屏幕时光栅化过程的产物。看看当你放大到在记事本中输入的文本时会发生什么:
当 Unredacter 将字母渲染到无头 Chrome 窗口时,不会出现彩色伪影,因此我们需要将图像转换为灰度。这会丢失一些信息,但没关系。Unredacter 不需要完全匹配,只是为了“大部分正确”的猜测。转换下来后,我们的挑战图像如下所示:
我必须进行最后一项调整,它位于底行:
太小了!其余的块是 5×5,但底行是 5×3。经过几个小时的反复试验,我还注意到这些块太暗了。查看字母“g”的猜测与挑战图像的对比:
挑战形象与猜测
看看最后一行是怎么太黑了?这是因为当图像被像素化时,他们必须选择一个大小不是 5 倍数的边界框。所以,当算法确定平均值时,它是一个较小区域的平均值。(因此变暗)没关系,我们可以通过点亮最后一行来修复它。这给了我们最终的挑战形象:
接下来是找出正确的字体和字体大小。幸运的是,这并不太难,图像是在 MS 记事本中使用默认字体 Consolas 拍摄的。经过一番反复试验,我发现字体大小为 24px。(我通过反复尝试字体大小直到大写 M 的高度匹配来做到这一点。)唯一棘手的部分是记事本显然有一个默认的字母间距 -0.2px。如果您尝试在 Consolas 中的 Chrome 中呈现文本,则时间太长了。但是 -0.2px letter-spacing完全匹配。
顶部:原始挑战图像底部:Unredacter 的 Headless Chrome 渲染
如果您仔细观察,“s”、“e”和“c”在记事本的渲染中会有更多的曲线。但没关系。同样,我们不需要 100% 准确。这很接近!
Unredacter 很快就会在 [3, 1] 的偏移量上找到目标,所以让我们看看它是怎么做的!
运行了几分钟后,Unredacter 吐出了最终的猜测:
您甚至可以用肉眼看到我们的猜测非常接近!
顶部:原始挑战图像(灰度和底行固定)底部:Unredacter 的猜测
所以我联系了 Jumpsec 的 Caleb Herbert,他们证实我的猜测是正确的!
Caleb 还要求我不要透露解决方案,所以你可以自己尝试一下。(上面是模糊的,你无法阅读模糊的文字,对吧?)对 Jumpsec 发出这个挑战的巨大呼喊,这很有趣。也是测试新工具的好方法!