Python3 中的默认编码是 UTF-8,这给大家写 Python 代码带来了很大的便利,不用再像 Python2.x 那样为数据编码操碎了心。但是,由于全面转向 UTF-8 编码,Python3 里面会有一些小细节,稍有不慎容易栽坑。本文就对二进制数据 XOR 编码这一种操作,浅析 Py2/Py3 中默认编码相关的一个细节小差异而引起的小 Bug。
XOR 编码是最简单有效的编码方法之一,虽然简单,但仍然应用广泛。在分析恶意样本时,经常会遇到样本内置的隐秘数据或者网络通信数据,用到了 XOR 编码。比如,一个典型就是 XOR.DDoS 家族,它样本内部关键字符串全用 XOR 编码过,而且其网络通信中 Bot 发给 C2 的上线数据包和 C2 给 Bot 下发的控制指令数据包中均涉及 XOR 编码/解码操作。
对于这类样本,分析的时候我们不免要写一些自动化的解析脚本,把其中的编码数据还原成名文以便分析。在其他开发场景中也偶尔会用 Python 写一些 XOR 编码/解码的程序。网上一搜 「Python XOR 编码 加密」或者「Python XOR encoding crypt」,都会搜出很多别人发出来的 Python XOR 编解码脚本,大多数情况下拿来直接用就行。比如我搜来的几个中文帖子中的相关脚本(本人不保证下面截图里代码的正确性):
这些脚本,在 Python2 环境下都没有问题,都可以正确进行 XOR 编解码,然而如果直接拿到 Python3 环境下去运行,却会发生一个不容易发现的小 Bug。来看一段在 ipthon3 里的操作记录:
|
|
可以看到,仿照 Python2 环境下那些常用的 XOR 编码操作写的函数,在 Python3 环境下,偶尔会出现意料之外的结果:上面的操作记录中,对于 sam1 和 sam2 两个全都是可打印字符的字节串进行 XOR 编解码是没有问题的;但是对于 sam3 ,一个内含大量 HEX 值大于 0x7F 的非可打印字符字节串,原本是 18 个字节,进行两次 XOR 操作之后竟然变成了 69 个字节。
这就十分蹊跷了。问题出在哪个环节?是函数内部的字节列表 cipher_data 的问题,还是最后 bytearray() 操作出了问题,还是进行 XOR 计算的时候,chr() 函数的问题?
经过一番排查,发现这是 chr() 函数的问题。先看这个函数在 Python2 和 Python3 中各有什么表现:
在 Python2 版本中,除了 chr() 还有一个 unichr() ,可以看到 Py2 中的 unichr() 与Py3 中的 chr() 行为是一致的:对于 HEX 值大于 0x7F 的字符,返回值占 2 Bytes;对于 HEX 值小于或等于 0x7F 的字符,返回值占 1 Byte。
为什么会出现这么个差异?刚开始一直以为 chr() 函数只会返回 1 Byte 的结果,对此感到很是不解。
查阅一下 Py2 中 chr() 和 unichr() 的文档如下:
而 Py3 中 chr() 函数的文档说明如下:
从文档来看, Py3 中的 chr() 函数确实对应到了 Py2 中的 unichr() 函数,只返回 Unicode 编码的结果。在点破最后的一层窗户纸之前,我们再去 CPython 的源码里瞅一眼,以便把这个结论锤结实了。
Py3 中的 chr() 函数,源码中是这样实现的:
至于其中的 unicode_char() 函数如何实现,我们就不深究了,知道它就是返回一个 Unicode 编码的字符即可。再看 Py2 中 unichr() 函数:
如出一辙有木有。
那最后一层窗户纸到底是什么?就是 Py3 默认的 UTF-8 编码了。在 http://www.utf-8.com 网站上有这么一段话:
UTF-8 encodes each Unicode character as a variable number of 1 to 4 octets, where the number of octets depends on the integer value assigned to the Unicode character. It is an efficient encoding of Unicode documents that use mostly US-ASCII characters because it represents each character in the range U+0000 through U+007F as a single octet.
注意上面加粗部分的重点:
- UTF-8 编码的字符占 1~4 个字节;
- 字符 U+0000 到 U+007F 都用一个字节来表示,其它字符 1 个字节不够,就用 2~4 个字节来表示。
这样就明确上面问题的原因了:Py3 中的 chr() 函数,只有在参数的 HEX 值位于 [0x00, 0x7F] 区间内的时候才返回 1 Byte 的结果,这个结果同于 Py2 中的 chr() 函数;当 HEX 值大于 0x7F ,其返回值占 2 Bytes,行为同于 Py2 中的 unichr() 函数。
那么 Py3 中正确的 XOR 编解码姿势是什么?上面 ipython3 操作记录中的函数稍加改动即可:
|
|
当然,还有更简洁的写法:
|
|