编码转换

0x01 常见编码类型

常见编码:

  • ASCII
  • GBK
  • GB2312
  • UNICODE
  • UTF-8
  • UTF-16
  • url编码
  • hex编码
  • base64编码

这篇文章不涉及url编码、hex编码、base64编码,后面有时间再补充。

0x02 ASCII

所有信息最终都是一个二进制值。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从00000000到11111111。

上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今。

ASCII 码一共规定了128个字符的编码,这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的一位统一规定为0。

ASCII 码表:

编码转换/ASCII_Table.png

python 显示 ASCII 码表(10进制 对应 字符表):

1
2
>>> for i in range(0,128):
... print i,' == ',chr(i)

0x03 GBK

GB类的汉字编码与后文的 Unicode 和 UTF-8 是毫无关系的。

GBK是国家标准编码,它是对gb2312的扩展,定义了包含简体中文、繁体中文、日文、韩文等所用的字符。

在编码上,GBK采用了单双字节混合的方式:

  • 兼容ASCII,1个字节,范围为0x00~0x7F
  • 对其它字符使用2个字节表示,但第一个字符最高位必须是1,即必须是0x80~0xFF。

GB2312字符集包含了6763个简体汉字,和682个标准中文符号。每个汉字用2个字节表示,每个字节的ASCII码为161-254(16进制A1-FE),第一个字节对应于区码的1-94区,第二哥字节对应于位码的1-94位。

GB2312简体中文码表

0x04 乱码

世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。

如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是 Unicode,就像它的名字都表示的,这是一种所有符号的编码。

0x05 UNICODE

可以容纳100多万个符号。每个符号的编码都不一样。

  • 比如,汉字严的 Unicode 是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说,这个符号的表示至少需要2个字节。

  • 表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。

中日韩汉字unicode编码表

Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储

存储问题

变化长度存储 or 固定长度存储

  • 如果使用变化长度进行存储,怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?

  • 如果规定每个符号用三个或四个字节表示,英文字母存储是极大的浪费,文本文件的大小会因此大出二三倍。

多种存储方式

为了实现unicode码在计算机上的二进制存储,出现了 Unicode 的多种存储方式

  • UTF-8 是 Unicode 的实现方式之一(字符使用1~4个字节)
  • UTF-16(字符用两个字节或四个字节表示)
  • UTF-32(字符用四个字节表示)

说明

1
2
UTF-16和UTF-32在互联网上基本不用。
UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。

0x06 UTF-8 编码规则

UTF-8 的编码规则很简单,只有二条:

  • 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
  • 对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。

下表总结了编码规则,字母x表示可用编码的位。

1
2
3
4
5
6
7
Unicode符号范围      |         UTF-8编码方式
(十六进制) | (二进制)
--------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

0x07 BOM(Byte Order Mask)

  • 如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头(大端法)方式;

  • 如果一个文本文件的头两个字节是FF FE,就表示该文件采用小头(小端法)方式。

UTF-8文件的BOM“EF BB BF”,它实际上就是FE FF(大端法)用UTF-8编码而得到的。

  • FEFF 大端法,UTF-8表示为:
1
2
3
4
5
6
7
8
FEFF在unicode与utf-8转化表中查询在第三行,使用3个字节表示
1110 XXXX 10XX XXXX 10XX XXXX

将FEFF填充进去后为:
1110 1111 1011 1011 1011 1111

十六进制表示即为:
EF BB BF

0x08 编码测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#'胜'的unicode编码为u'\u80dc'
>>> a=u'胜'
>>> a
u'\u80dc'

#'胜'的gb2312编码为'\xca\xa4'
>>> b=a.encode('gb2312')
>>> b
'\xca\xa4'

#'胜'的gbk编码为'\xca\xa4'
>>> c=a.encode('gbk')
>>> c
'\xca\xa4'

#'胜'的utf-8编码为'\xe8\x83\x9c'
>>> d=a.encode('utf-8')
>>> d
'\xe8\x83\x9c'

#使用gb2312编码的'胜',使用utf-8解码时为ʤ,本测试使用MAC系统,默认print解码方式为utf-8
#之所以没有报错,是因为编码内容也符合utf-8的解读格式。
#二进制为[11001010 10100100],utf-8解码认为,第1个字节110开头且后面的字节10开头,表明是2个字节存储的,实际解读的unicode编码为 【1010100100】,为u'\u02a4',所以utf-8解码后为ʤ
#>>> e=u'\u02a4'
#>>> print e

>>> print b
ʤ

#使用gbk编码的'胜',使用utf-8解码时为ʤ
>>> print c
ʤ

#使用utf-8编码的'胜',使用utf-8解码时为'胜'
>>> print d

>>>

0x09 编码绕过-SQL宽字节注入

宽字节注入 gbk 解码

php程序在开启magic_quotes_gpc或使用了addslashes()函数、或mysql_[real_]escape_string()函数对输入的特殊符号进行转义的情况下,针对字符型的输入,还有可能发生SQL注入。

主要原因是用户输入的单引号(')会被自动加上反斜杠(\)进行转义,防止单引号的闭合导致SQL注入。但如果php程序中设置指定mysql连接使用gbk解码,则会导致宽字节注入。

比如,用户恶意输入%df',则php程序接收参数后,转义后为%df\',而%df\gbk解码后为字符,导致单引号'转义失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> a=urllib.unquote('%df')+'\\'
>>> a
'\xdf\\'
>>> b=a.decode('utf-8')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/encodings/utf_8.py", line 16, in decode
return codecs.utf_8_decode(input, errors, True)
UnicodeDecodeError: 'utf8' codec can't decode byte 0xdf in position 0: invalid continuation byte
>>> b=a.decode('gbk')
>>> b
u'\u904b'
>>> c=b.encode('utf-8')
>>> c
'\xe9\x81\x8b'
>>> print c

>>>
>>>

0x0A iconv()转换字符集造成截断

php程序中iconv()函数说明:

string iconv ( string $in_charset , string $out_charset , string $str )

  • in_charset:输入的字符集
  • out_charset:输出的字符集
  • str:要转换的字符串

iconv在字符编码转换时可能导致字符串截断。当$str中有一个字符不能被目标字符集所表示时,$str从第一个无效字符开始截断并导致一个 E_NOTICE。

例如:$d = iconv(“UTF-8”, “gb2312”, $c);该代码是将变量$c从UTF-8编码转换为gb2312。那么当$c中存在一个不能被gb2312表示的字符时,那么就会截断。

1
2
3
4
5
6
7
8
<?php
$a = "1.php";
$b = ".jpg";
for($i=0; $i<200; $i++){
    $c = $a.chr($i).$b;
    $d = iconv("UTF-8", "gb2312", $c);
    echo "$i ==> ".$d."\n";
}

可以发现当$i为128(0x80)时输出的字符串截断为1.php。

为什么从0x80开始截断,可以看utf8和gbk的编码范围。

漏洞案例:

建站之星模糊测试实战之任意文件上传漏洞

0x0B 参考链接

http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html

http://www.docin.com/p-178554948.html