Python, 仅凭base64编码能猜出文件类型吗?

Python, can someone guess the type of a file only by its base64 encoding?

假设我有以下内容:

image_data = """iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="""

这只是一个点图像(来自 https://en.wikipedia.org/wiki/Data_URI_scheme)。但我不知道它是图像还是文本等。是否可以理解它只有这个编码字符串是什么?我在 Python 中尝试过,但这也是一般性问题。因此,非常欢迎对这两者的任何见解。

你不能,至少不能不解码,因为有助于识别文件类型的字节分布在 base64 字符中,这些字符不直接与整个字节对齐。每个字符编码6,也就是说每4个字符,有3个字节编码。

识别文件类型需要访问不同块大小的那些字节。例如,JPEG 图像可以从字节 FF D8 或 FF D9 中识别出来,但那是 两个 字节;随后的第三个字节也必须编码为 4 字符块的一部分。

可以做的是解码刚好的base64字符串来进行文件类型指纹识别。所以你可以解码前4个字符得到3个字节,然后用前两个看对象是否是JPEG图像。可以仅从第一个或最后一个字节系列识别大量文件格式(PNG 图像可以通过前 8 个字节识别,GIF 可以通过前 6 个字节识别,等等)。从 base64 字符串中解码那些字节是微不足道的。

您的样本是PNG图片;您可以使用 imghdr module:

测试图像类型
>>> import imghdr
>>> image_data = """iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="""
>>> sample = image_data[:44].decode('base64')  # 33 bytes / 3 times 4 is 44 base64 chars
>>> for tf in imghdr.tests:
...     res = tf(sample, None)
...     if res:
...         break
...
>>> print res
png

我只使用了 base64 数据的前 33 个字节,以回显 imghdr.what() 函数将从您传递给它的文件中读取的内容(它读取 32 个字节,但该数字不会除以 3 ).

有一个等价物 soundhdr module, and there is also the python-magic project 可让您传入多个字节以确定文件类型。

它是 PNG 图片

import base64

encoded_string = 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='

decoded_string = base64.b64decode(encoded_string)
print 'Decoded :', decoded_string

输出:

python base_decode.py 
Decoded : �PNG

当然可以。我能想到的解决问题的方法很少:

部分解码

每个 base64 字符编码 6 位输入,因此您可以将它们关联如下:

Base64: AAAAAABBBBBBCCCCCCDDDDDDEEEEEEFFFFFFGGGGGGHHHHHH
Data:   xxxxxxxxyyyyyyyyzzzzzzzzqqqqqqqqwwwwwwwweeeeeeee

如果你想提取 4 个字节的数据,从偏移量 1 开始,像这样:

                ................................
Base64: AAAAAABBBBBBCCCCCCDDDDDDEEEEEEFFFFFFGGGGGGHHHHHH
Data:   xxxxxxxxyyyyyyyyzzzzzzzzqqqqqqqqwwwwwwwweeeeeeee
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

然后,要仅解码您想要的部分,您需要知道位距。它们很容易计算,只需将字节距离乘以 8。现在,在知道需要 32 位后,从第 8 位开始,您可以找到包含起始位的 base64 字符。为此,将您的 offsetoffset+length 除以 6:

start = bit  8 = char 1 + bit 2
end   = bit 40 = char 6 + bit 4

好吧,这映射到上面的方案 — 您的跨度在 1 个完整的 base64 字符和 2 位之后开始,并在 6 个完整的 base64 字符和 4 位之后结束。

现在,在您知道所需的确切 base64 字符后,您需要对它们进行解码。为此,利用现有的 base64 解码器是有意义的,因此我们不需要自己处理 base64 编码。为此,您应该知道 base64 代码的每 4 个字符对应 3 个字节的数据。所以,这就是诀窍——你可以在你提取的 base64 代码中添加和附加乱码,直到 base64 和字节边界对齐——并且知道 base64 解码器会产生多少无效输入,扔掉多余的。

因此,前置多少取决于位余数的值。如果起始位余数为0,则表示Ax对齐,所以不需要修改:

           |==========================
Base64: ...AAAAAABBBBBBCCCCCCDDDDDD...
Data:   ...xxxxxxxxyyyyyyyyzzzzzzzz...
           |==========================

如果位余数为2,需要在prepend 1 base64 char,解码后丢掉前导字节:

                 ##|==================
Base64: ...AAAAAABBBBBBCCCCCCDDDDDD...
Data:   ...xxxxxxxxyyyyyyyyzzzzzzzz...
                   |==================

如果位余数为4,则需要前置两个base64字符,解码后丢掉两个前导字节:

                       ####|==========
Base64: ...AAAAAABBBBBBCCCCCCDDDDDD...
Data:   ...xxxxxxxxyyyyyyyyzzzzzzzz...
                           |==========

尾随也是如此。如果结束位余数为零,则没有变化:

        ===|
Base64: ...AAAAAABBBBBBCCCCCCDDDDDD...
Data:   ...xxxxxxxxyyyyyyyyzzzzzzzz...
        ===|

如果结束位余数为2,则需要追加两个base64字符,并丢掉尾随的两个字节:

        =========##|
Base64: ...AAAAAABBBBBBCCCCCCDDDDDD...
Data:   ...xxxxxxxxyyyyyyyyzzzzzzzz...
        ===========|

如果结束位余数为4,则需要追加一个base64字符,并丢掉一个尾部字节:

        ===============####|
Base64: ...AAAAAABBBBBBCCCCCCDDDDDD...
Data:   ...xxxxxxxxyyyyyyyyzzzzzzzz...
        ===================|

因此,对于上面的合成示例,需要在前面加上一个字符(而不是 A),并附加一个字符(代替 H):

                ................................
Base64: ??????BBBBBBCCCCCCDDDDDDEEEEEEFFFFFFGGGGGG??????
Data:   ????????yyyyyyyyzzzzzzzzqqqqqqqqwwwwwwww????????
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

现在,在解码之后,从头部和尾部扔掉额外的字节,你就完成了。

实例

想象一下你有像 ?PNG\r\n??????IHDR 这样的魔法。然后,要检查 base64 编码的字符串是否与您的魔法匹配,您可以识别魔法中已知的字节,以及它们的位偏移量和长度:

"PNG\r\n"  ->  offset =  8, length = 40
"IHDR"     ->  offset = 96, length = 32

因此,使用我们上面的想法:

"PNG\r\n"  ->  start =  8 ( char  1, bits = 2 ), end = 48  ( char 8, bits = 0 )
"IHDR"     ->  start = 96 ( char 16, bits = 0 ), end = 128 ( char 21, bits = 2 )

要解码 "PNG\r\n" 部分,您需要获取 7 个完整的 base64 字符,从字符 1 开始,然后添加 1 个字符,解码,丢弃 1 个前导字节并进行比较。

要解码 "IHDR" 部分,您需要取 6 个 base64 字符,从字符 16 开始,然后附加 2 个字符,解码,丢弃 2 个尾随字节并进行比较。

翻译魔法

我上面描述的替代方法是不翻译数据,而是自己翻译魔法。

所以,如果你有魔法 ?PNG\r\n??????IHDR(出于演示目的,我已经替换了 \r\n),就像上面的示例一样,当编码为 base64 时,它看起来像这个:

Data:   [?PN]  [Grn]  [???]  [???]  [IHD]  [R??]
Base64: (?~BO) (Rw0K) (????) (????) (SUhE) (Ug==)

?~BO部分,~符号只是部分随机。让我们按位看一下该构造:

Data:   ????????PPPPPPPPNNNNNNNN
Base64: ??????~~~~~~BBBBBBOOOOOO

因此,只有 ~ 的低两位是真正未知的,这意味着您可以在针对数据测试魔术时使用该信息,以缩小魔术的范围。

对于这种特殊情况,这里是所有编码的详尽列表:

Data:   ??????00PPPPPPPPNNNNNNNN
Base64: ??????FFFFFFBBBBBBOOOOOO  => ?FBO

Data:   ??????01PPPPPPPPNNNNNNNN
Base64: ??????VVVVVVBBBBBBOOOOOO  => ?VBO

Data:   ??????10PPPPPPPPNNNNNNNN
Base64: ??????llllllBBBBBBOOOOOO  => ?lBO

Data:   ??????11PPPPPPPPNNNNNNNN
Base64: ??????111111BBBBBBOOOOOO  => ?1BO

同样适用于尾随 R?? 组,但因为有 4 个未定义位而不是 2 个,排列列表更长:

Ug??  <=  0000???? ????????
Uh??  <=  0001???? ????????
Ui??  <=  0010???? ????????
Uj??  <=  0011???? ????????
Uk??  <=  0100???? ????????
Ul??  <=  0101???? ????????
Um??  <=  0110???? ????????
Un??  <=  0111???? ????????
Uo??  <=  1000???? ????????
Up??  <=  1001???? ????????
Uq??  <=  1010???? ????????
Ur??  <=  1011???? ????????
Us??  <=  1100???? ????????
Ut??  <=  1101???? ????????
Uu??  <=  1110???? ????????
Uv??  <=  1111???? ????????

所以,在正则表达式中,?PNG\r\n??????IHDR 的 base64-magic 看起来像这样:

rx = re.compile(b'^.[FVl1]BORw0K........SUhEU[g-v]')
if rx.match(base64.b64encode(b'xPNG\r\n123456IHDR789foobar')):
    print('Yep, it works!')

我使用这个示例函数

def detect_mime_type(base64):
    signatures = {
    "JVBERi0": "application/pdf",
    "R0lGODdh": "image/gif",
    "R0lGODlh": "image/gif",
    "iVBORw0KGgo": "image/png",
    "/9j/": "image/jpg"
    }
    for s in signatures:
        if(base64.find(s)!=-1):
            return signatures[s]

如果你有base64字符串,那么扩展名写在数据的开头。您可以使用以下方法:

def detect_image_type(base64_data):
    extensions = {
        "data:image/png;": "png",
        "data:image/jpeg;": "jpeg",
        "data:image/jpg;": "jpeg",
    }
    for ext in extensions:
        if base64_data.startswith(ext):
            return extensions[ext]