了解CTF的朋友们,肯定对一种题型不陌生——Misc,Misc里面有各种各样的编码,眼花缭乱的文本通过层层解码后得到一个令人满意的结果是一件及其感人的事件。
其实除了解题,这些编码本身在计算机领域也有重要的应用。所以当我们在CTF里看到一种新的编码时,不要简单丢到解码器里得出结果就认为万事大吉了,了解它——“知其然”和“所以然”,这才能在CTF里学到更多的东西。
01 字符编码的历史
一、诞生——ASCII码
我们都知道计算机只能处理数字,所以如果你要处理文本,就必须把文本转换为数字。最早的计算机在设计之初,采用8个比特(bit)作为一个字节(byte),所以一个字节最大能表示的整数就是255
,如果要表示更大等整数,就必须用更多的字节。
由于计算机是美国人发明的,因此,最早只有127
个字符被编码到计算机里,也就是大小写英文字母、数字和一些符号,这个编码表被称为ASCII编码,比如大写字母A
的编码是65
,小写字母z
的编码是122
。
ASCII 码使用指定的7 位或8 位二进制数组合来表示128
或256
种可能的字符。标准ASCII 码也叫基础ASCII码,使用7 位二进制数(剩下的1位二进制为0)来表示所有的大写和小写字母,数字0 到9、标点符号, 以及在美式英语中使用的特殊控制字符。
二、战国春秋——世界人民的各种编码
但是要处理中文显然一个字节是不够的,至少需要两个字节,而且还不能和ASCII编码冲突,所以,中国制定了GB2312编码,用来把中文编进去。
你可以想得到的是,全世界有上百种语言,日本把日文编到Shift_JIS里,韩国把韩文编到Euc-kr里,各国有各国的标准,就会不可避免地出现冲突,结果就是,在多语言混合的文本中,显示出来会有乱码。
三、秦王扫六合——为统一而生的Unicode
为了解决乱码,人们决定把所有语言都统一到一套编码里。于是,Unicode应运而生。
Unicode标准也在不断发展,但最常用的是用两个字节表示一个字符(如果要用到非常偏僻的字符,就需要4个字节)。现代操作系统和大多数编程语言都直接支持Unicode。
现在,我们不妨将ASCII码和Unicode码进行一个比较:
ASCII码是一个字节,Unicode码是两个字节
字符 | ASCII | Unicode |
---|---|---|
A | 01000001 | 00000000 01000001 |
中 | X | 01001110 00101101 |
因为汉字中
已经超出ASCII码的范围,所以无法表示。
由上表可知,如果把ASCII编码的A
用Unicode编码,只需要在前面补0就可以,因此,A
的Unicode编码是00000000 01000001
。
四、更上一层楼——UTF-8
如果统一成Unicode编码,乱码问题从此消失了。但是,如果你写的文本基本上全部是英文的话,用Unicode编码比ASCII编码需要多一倍的存储空间,在存储和传输上就十分不划算。
所以,本着节约的精神,又出现了把Unicode编码转化为“可变长编码”的UTF-8
编码。UTF-8编码把一个Unicode字符根据不同的数字大小编码成1-6个字节,常用的英文字母被编码成1个字节,汉字通常是3个字节,只有很生僻的字符才会被编码成4-6个字节。如果你要传输的文本包含大量英文字符,用UTF-8编码就能节省空间:
字符 | ASCII | Unicode | UTF-8 |
---|---|---|---|
A | 01000001 | 00000000 01000001 | 01000001 |
中 | X | 01001110 00101101 | 11100100 10111000 10101101 |
五、计算机里的实际表现
搞清楚了ASCII、Unicode和UTF-8的关系,我们就可以总结一下现在计算机系统通用的字符编码工作方式:
在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。
用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件:所以你看到很多网页的源码上会有类似<meta charset="UTF-8" />
的信息,表示该网页正是用的UTF-8编码。
六、总结
以上内容主要源自阮老师的博文《字符串和编码》
简单了解了字符编码的历史后,要想在比赛中多得一些分,我们还要判断给定的文本采用的是哪种编码方式。
ASCII
源文本:I love you 1314
编码后:73 32 108 111 118 101 32 121 111 117 32 49 51 49 52
对应可以转换成二进制,八进制,十六进制等
Unicode
源文本:love
&#x [Hex]:love
&# [Decimal]:love
\U [Hex]:\U006C\U006F\U0076\U0065
\U+ [Hex]:\U+006C\U+006F\U+0076\U+0065
Unicode在线编解码
UTF-8
对于英文字符来说UTF-8与ASCII的编码结果无异
源文本:爱
编码后:爱
此外,UTF-8
、UTF-16
、UTF-32
都是针对Unicode的再一次编码,(Unicode Transformation Formats, 或UTF,即Unicode格式转换的缩写)
02 深不见底的编码
有了上面的基础,接下来我们探究点更深入的问题。
字符编码:“o(>﹏<)o不要啊”
字符或者说信息,在进行编码后除了存储在硬盘里,还要进行传输以便人们可以进行信息交流。
而在进行数据传输时,为了满足传输环境的限制或者对数据进行压缩节省资源,我们还要再次对字符进行编码。但是为了更好理解这一内容,我们需要回到原点,看看究竟什么是编码。
一、现代编码模型
基本概念
现代编码模型自底向上分为五个层次:
层次 | 名称 |
---|---|
1.练气 | 抽象字符表(Abstract Character Repertoire) |
2.筑基 | 编码字符集(Coded Character Set) |
3.金丹 | 字符编码表(Character Encoding Form) |
4.元婴 | 字符编码方案(Character Encoding Schema) |
5.化神 | 传输编码语法(Transfer Encoding Syntax) |
练气:抽象字符表(ACR)
抽象字符表是现代编码模型的最底层,它是一个集合,通过枚举指明了所属的所有抽象字符。但是要了解抽象字符集是什么,我们首先需要了解什么是字符与抽象字符
字符
字符是指字母、数字、标点、表意文字(如汉字)、符号。 例: a
,啊
,あ
,α
等,都是抽象的字符。
抽象字符
抽象字符就是抽象的字符。像a
这样的字符是有形的,但在计算机中,有许多的字符是空白的,甚至是不可打印的。比如ASCII字符集中的NULL,就是一个抽象字符。
抽象字符表
抽象字符表顾名思义,指的是抽象字符的集合。 已经有了很多标准的字符集定义。 比如US-ASCII, UCS(Unicode), GBK。
需要注意一点的是,字符集也是有开放与封闭的区分的。 ASCII抽象字符集定义了128个抽象字符,再也不会增加,这就是一个封闭字符集。 Unicode尝试收纳所有的字符,一直在不断地扩张之中。
筑基:编码字符集(CCS)
编码字符集是一个每个所属字符都分配了码位的字符集。 编码字符集也经常简单叫做字符集。
抽象字符集(Character set)是抽象字符的集合,而集合是无序的。 无序的抽象字符集并没有什么卵用,因为我们只能判断某个字符是否属于某个字符集,却无法方便地引用,指称这个集合中的某个特定元素。
所以为了更好的描述,操作字符,我们可以为抽象字符集中的每个字符关联一个数字编号,这个数字编号称之为码位(Code Point)。
举个栗子,为amber抽象字符集进行编码,就可以得到amber编码字符集。
amber={'a':0x0, '啊':0x1, '①':0x2, 'α':0x3}
在这里a
不再是一个简单的抽象字符了,而是一个编码字符(Coded Chacter),且拥有码位(Code Point) 0x0
。
金丹:字符编码表(CEF)
在讲抽象字符表ACR的时候曾经提起,Unicode是一个开放字符集,未来可能有更多的符号加入到这个字符集中来。也就是说UCS需要的码位,理论上是无限的。 但是计算机的整形能表示的整数范围是有限的。譬如,一个字节的无符号整形(unsigned char, uint8)能够表示的码位只有0~0xFF,共256个;而一个无符号短整形(unsigned short, uint16)的可用码位只有0~0xFFFF,共65536个;一个整形(unsigned int, uint32)能表示的码位有0~0xFFFFFFFF,共4294967295个。
一个无限大的整数,一一映射为指定字宽的码元序列。就是CEF要解决的问题, 这个问题可以通过变长编码来解决。 无论是UTF-8还是UTF-16,本质思想都是通过预留标记位来指示码元序列的长度。从而实现变长编码的。
码元 Code unit: The minimal bit combination that can represent a unit of encoded text for processing or interchange. 码元是能用于处理或交换编码文本的最小比特组合。通常计算机处理字符的码元为一字节,即8bit。同时因为计算机中char其实是一种整形,而整形的计算往往以计算机的字长作为一个基础单元,通常来讲,也就是4字节。Unicode定义了三种不同的CEF,分别采用了1字节,2字节,4字节的码元,正好对应了计算机中最常见的三种整形长度。
元婴:字符编码方案(CES)
通过CEF,我们已经可以将字符转为码元(Code Unit)。无论是哪种UTF-X的码元,都可以找到计算机中与之对应的整形存放。那么现在我们能说存储处理交换字符这个问题解决了吗? 还不行。 因为从码元落实到底层的存储,还有一些问题需要解决。 假设一个字符按照UTF16拆成了A,B两个码元,那实际存储的时候究竟应该把A放在前面呢还是B放在前面呢?而另一个程序又如何知道当前这份文件是按照什么样的端序存储码元的呢? 无论是大端法与小端法的选择,还是用于决定编码字节序的标记,都是CES需要操心的方案。
我们通常所说的动词编码(Encode)就是指使用CES,将CCS中字符组成的字符串转变为字节序列。而解码(Decode)就是反过来,将字节序列通过CES的一一映射还原为CCS中字符组成的序列。
``
化神:传输编码语法(TES)
通过CES,我们已经可以将一个字符表示为一个字节序列。 但是有时候,字节序列表示还不够。比如在HTTP协议中,在URL里,一些字符是不允许出现的。这时候就需要再次对字节流进行编码。
著名的Base64编码,就是把字节流映射成了一个由64个安全字符组成字符集所表示的字符流。从而使字节流能够安全地在Web中传输。
至此,我们已经有了对编码有了一个大致而不再是简单的了解。本文这部分的绝大部分内容源自VONNG的一篇博文
二、CTF中的传输编码语法
常常好奇CTF里为什么会有base64、UTF-7等等这些奇怪的东西,现在我们知道了这些奇怪的编码并不是出题人用来整你的,而是在实际生活中发挥了作用的呢!
1.Base64
维基百科:Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节可表示4个可打印字符。它可用来作为电子邮件的传输编码。在Base64中的可打印字符包括字母A-Z、a-z、数字0-9,这样共有62个字符,此外两个可打印符号在不同的系统中而不同。一些如uuencode的其他编码方法,和之后binhex的版本使用不同的64字符集来代表6个二进制数字,但是它们不叫Base64。
为什么要进行Base64编码
其实BASE64编码的初衷是为了满足电子邮件中不能直接使用非ASCII码字符的规定,但是也有其他重要意义:所有的二进制文件,都可以因此转化为可打印的文本编码,使用文本软件进行编辑,并且对于数据流来说是一种简单的加密。
编码原理
Base64编码要求把3个8位字节转化为4个6位的字节,之后在6位的前面补两个0,形成8位一个字节的形式,6位2进制能表示的最大数是2的6次方是64,这也是为什么是64个字符(A-Z,a-z,0-9,+,/这64个编码字符,=号不属于编码字符,而是填充字符)的原因,这样就需要一张映射表,如下:
可以看下面几个例子:
其实我们都一样
有很多编码其实都属于Base64系列
Base家族:在Base家族中还有Base32和Base16,其实Base32/Base16和Base64目的是一样的,只是具体的编码规则的不一样罢了。Base32编码将二进制文件转换成由32个ASCII字符组成的文本;Base16编码则将二进制文件转换成由16个字符组成的文本。
UTF-7:UTF-7是一个修改版Base64(Modified Base64)。主要是将UTF-16的数据,用Base64的方法编码为可打印的字符序列。目的是传输Unicode数据。主要的区别在于不用等号
=
补余,因为该字符通常需要大量的转译。现在UTF-7已经走入历史,很少被人使用。
2.Quoted-printable
维基百科:Quoted-printable或QP encoding,没有规范的中文译名,可译为可打印字符引用编码或使用可打印字符的编码。Quoted-printable是使用可打印的ASCII字符 (如字母、数字与"=")表示各种编码格式下的字符,以便能在7-bit数据通路上传输8-bit数据, 或者更一般地说在非8-bit clean媒体上正确处理数据
Quoted-printable与Base64是两种基本的MIME内容传输编码, 如果通常的"8bit"编码不适用. 如果文本不含很多非ASCII字符,quoted-printable编码的结果的可读性相当好而且紧凑. 但是,如果输入的大多数是非ASCII字符,那么quoted-printable编码将变得既不可读又非常低效. Base64并不是人可读的,但对于所有数据其成本均匀,适用于二进制数据与非拉丁字母语言文本。
编码原理
任何8-bit字节值可编码为3个字符:一个等号"="后跟随两个十六进制]数字(0–9或A–F)表示该字节的数值. 例如,ASCII码换页符(十进制值为12)可以表示为"=0C", 等号"="(十进制值为61)必须表示为"=3D". 除了可打印ASCII字符与换行符以外,所有字符必须表示为这种格式.
所有可打印ASCII字符(十进制值的范围为33到126)可用ASCII字符编码来直接表示, 但是等号"="(十进制值为61)不可以这样直接表示.
ASCII的水平制表符(tab)与空格符, 十进制为9和32, 如果不出现在行尾则可以用其ASCII字符编码直接表示。如果这两个字符出现在行尾,必须QP编码表示为"=09" (tab)或"=20" (space).
如果数据中包含有意义的行结束标志,必须转换为ASCII回车(CR)换行(LF)序列,既不能用原来的ASCII字符也不能用QP编码的"="转义字符序列。 相反,如果字节值13与10有其它的不是行结束的含义,它们必须QP编码为=0D与=0A.
例子
If you believe that truth=3Dbeauty, then surely=20= mathematics is the most beautiful branch of philosophy.
解码后:
If you believe that truth=beauty, then surely mathematics is the most beautiful branch of philosophy.
3.被MIME打败的编码
还有几种编码在CTF中也经常见到,但是在实际应用中很少遇到,主要是因为同为传输编码语法,MIME中的两种编码base64和QP encoding已经足够强大,很好地满足了人们的使用要求。
Uuencode
维基百科:uuencode这个名字是衍生自"Unix-to-Unix encoding",原先是Unix系统下将二进制的资料借由uucp邮件系统传输的一个编码程式,是一种二进制到文字的编码。
XXencoding
维基百科:Xxencode是一种类似于uuencode的一种二进制到文字的编码,它只使用字母数字字符,以及加号和减号。也是一种用于传输文件的编码格式。
XXencode 将输入文本以每三个字节为单位进行编码。如果最后剩下的资料少于三个字节,不够的部份用零补齐。这三个字节共有 24 个 Bit,以 6bit 为单位分为 4 个组,每个组以十进制来表示所出现的数值只会落在 0 到 63 之间。以所对应值的位置字符代替。
三、总结
到了这里我们对传输编码语法进行一个对比总结
Base64
源文本:I love you 1314!
编码后:SSBsb3ZlIHlvdSAxMzE0IQ==
Base32
源文本:I love you 1314!
编码后:JEQGY33WMUQHS33VEAYTGMJUEE======
Base16
源文本:I love you 1314!
编码后:49206C6F766520796F75203133313421
Base全家桶 可以看到Base16是不用=填充的
UTF-7
源文本:我爱amber1314啊
编码后:+YhFyMQBhAG0AYg-e+AHIAMQAzADEANFVK-
这是一个神器 UTF-7的编码特点在于首有+
,尾有-
。
Quoted-printable
源文本:我爱amber1314啊
编码后:=E6=88=91=E7=88=B1amber1314=E5=95=8A
W3C不只于QP 特点是非ASCII码表中的字符前有=
UUencode
源文本:amber.txt
编码后:
begin 644 amber.txt
%86UB97(
`
end
begin 644
一般为UUencode的标识
XXencode
源文本:我爱amber1314啊
编码后:Enh8kfa3hMaJmAHAlB90V0U++
只有数字,大小写字母,+
,-
UU+XX都在这里
03 Web世界里的字符编码
看到这里确实很不容易,感谢能坚持到现在的读者。
但对于字符编码还有一部分内容十分重要,不论是CTF还是实际生活,Web都扮演了举足轻重的角色,所以我们不得不看看Web世界里的字符编码。而这一部分的内容也与大名鼎鼎的XSS漏洞息息相关。
一、浏览器解析过程
Web世界离不开浏览器,浏览器是显示信息,进行交互的地方。而信息的展示又离不开一个个HTML文档,所以我们先要看看浏览器是如何解析HTML文档的。
浏览器在解析HTML文档时无论按照什么顺序,主要有三个过程:HTML解析、JS解析和URL解析,每个解析器负责HTML文档中各自对应部分的解析工作。
首先浏览器接收到一个HTML文档时,会触发HTML解析器对HTML文档进行词法解析,这一过程完成HTML解码并创建DOM树,接下来JavaScript解析器会介入对内联脚本进行解析,这一过程完成JS的解码工作,如果浏览器遇到需要URL的上下文环境,这时URL解析器也会介入完成URL的解码工作,URL解析器的解码顺序会根据URL所在位置不同,可能在JavaScript解析器之前或之后解析。
二、三个火枪手
1.URL编码
一般来说,URL只能使用英文字母、阿拉伯数字和某些标点符号,不能使用其他文字和符号。这意味着,如果URL中有汉字和其他符号,就必须编码后使用。我们在进行 URL 请求时,浏览器会自动帮我们把部分符号转换成 %
+ 十六进制数字
的形式
http://127.0.0.1/flag.php?x=1{)wo我
转化为
http://127.0.0.1/flag.php?x=1{)wo%E6%88%91
2.HTML实体编码
有些时候,我们想在页面上显示的东西会跟 HTML 本身的标记冲突。
比如我们只想在页面中显示 <script>alert();</script>
而不是执行这串代码的时候,我们如下这样写是不行的:
<html>
<body>
<script>alert();</script>
</body>
</html>
进行HTML实体编码后,我们的代码就成了
<html>
<body>
<script>alert();</script>
</body>
</html>
上面用到的是'&' + '约定名称' + ';'
的形式,其实还有
'&#' + '十进制数字' + ';'
'&#x' + '十六进制数字' + ';'
比如上文的 <
这个字符可以表示为 <
和 <
3.JavaScript编码
JavaScript编码主要是为了解决URL编码留下的坑进行的。(具体参见这篇文章)
简单来讲,就是不同的操作系统、不同的浏览器、不同的网页字符集导致URL编码的结果差异悬殊,为了保证客户端只用一种编码方法向服务器发出请求,人们使用Javascript先对URL编码,然后再向服务器提交。
JavaScript编码一般有以下几种形式
- escape()函数编码:形式为
%uXXXX
、%XX
,字符范围0-F
,本身就是URL编码的一种实现方式 - Unicode 编码:
'\u' + '四位十六进制数字'
不够四位前面补0 - JSFuck: 一种基于JavaScript原子部分的晦涩的编程风格,它只有6种字符:
[
]
(
)
!
+
,也可以来编写程序并执行,可以做到所有JavaScript能做的事情
三、复合编码
<a href="javascript:alert(1)">test</a>
针对上述a标签我们分析一下该环境中浏览器的解析顺序,首先HTML解析器开始工作,并对href中的字符做HTML解码,接下来URL解析器对href值进行解码,正常情况下URL值为一个正常的URL链接,如:“https://www.xxx.com“,那么URL解析器工作完成后是不需要其他解码的,但是该环境中URL资源类型为JavaScript,因此该环境中最后一步JavaScript解析器还会进行解码操作,最后解析的脚本被执行。
整个解析顺序为3个环节:HTML解码>URL解码>JS解码
下面我们对其进行做JS编码>URL编码>HTML编码共3层,进行验证。
<a href="javascript:alert(1)">test</a>
JS编码:<a href="javascript:\u0061\u006c\u0065\u0072\u0074(1)">test</a>
URL编码:<a href="javascript:%5c%75%30%30%36%31%5c%75%30%30%36%63%5c%75%30%30%36%35%5c%75%30%30%37%32%5c%75%30%30%37%34(1)">test</a>
HTML编码:<a href="javascript:%5c%75%30%30%36%31%5c%75%30%30%36%63%5c%75%30%30%36%35%5c%75%30%30%37%32%5c%75%30%30%37%34(1)">test</a>
如下图,将编码后的内容可以正常执行。
四、总结
这就是Web世界里的一些编码内容,这些编码在XSS攻击中尤为重要,可以说是XSS的编码基础。
这部分内容主要源自两篇文章
XSS 编码的一些基础知识
浅谈XSS—字符编码和浏览器解析原理
URL编码
源文本:https://www.baidu.com/s?wd=https://www.baidu.com/s?wd=我的贴身校花
编码后:https://www.baidu.com/s?wd=%E6%88%91%E7%9A%84%E8%B4%B4%E8%BA%AB%E6%A0%A1%E8%8A%B1
HTML编码
源文本:<>
编码后:<>
或<>
或<>
JS编码
源文本:alert(1)
编码后:alert%281%29
或\u0061\u006c\u0065\u0072\u0074\u0028\u0031\u0029
04 终于写完了
一开始没有想到,会写这么多内容。不过我一直觉得CTF不是终点,CTF里遇到的知识往往比题目本身更有挑战、也更具趣味。
最后不得不说,本文的作者其实也是一个刚刚入门的小白,如果文中有错误的地方,请各位大神不吝赐教。