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 字符。为此,将您的 offset
和 offset+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,则表示A
和x
对齐,所以不需要修改:
|==========================
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]
假设我有以下内容:
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 字符。为此,将您的 offset
和 offset+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,则表示A
和x
对齐,所以不需要修改:
|==========================
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]