unicode详解以及有关的安全问题

unicode详解以及有关的安全问题

关于编码的几个关键问题

1.文本编辑器是怎么将二进制形式翻译成字符的?

2.字节是怎样分组的?

3.一个或多个字节是怎么映射到字符上的?

维基百科中这样解释:Character Encoding

大致说来,编码定义了两件事:

1,字节是怎么分组的,如8 bits或16 bits一组,这也被称作编码单元。

2,编码单元和字符之间的映射关系。例如,在ASCII码中,十进制65映射到字母A上

字符编码和字符集之间有微小的区别。不过通常它和你无关,除非你在设计一个底层的库。

(准确地说,utf-8是字符编码,unicode是字符集)

区别在哪呢?来看看Python中将一个字符以unicode和utf-8输出有什么区别吧(linux上)

image-20210420183639069

我自己本地没复现成功,Windows和Linux都没,合理猜测是python版本问题

image-20210420183811069

这里unicode和utf-8体现出地区别在哪儿呢?

区别在于,一个字符在unicode里,和ascii一样,只是由一个数字代表,而utf8则可以看到它的编码方式,一个中文的’我’字,由\xe6\x88\x91组成,这就是它在计算机内存里的表达方式,而在unicode里面,它仅仅只是由“6211”这个数字代表的字符。

这就是字符集(unicode)和字符编码/编码规则(utf-8)的区别

字符集:为每个字符分配一个’ID’,具体怎么实现不管(code point)

编码规则:将(code point)转换为实体字符的规则(类似加密解密)

Unicode并不涉及字符是怎么在字节中表示的,它仅仅指定了字符对应的数字,仅此而已。

关于Unicode的其它误解包括:Unicode支持的字符上限是65536个,Unicode字符必须占两个字节。告诉你这些的人应该去换换脑子了。

记住,Unicode只是一个用来映射字符和数字的标准。它对支持字符的数量没有限制,也不要求字符必须占两个、三个或者其它任意数量的字节。

Unicode并不告诉你字符是怎么编码成字节的。这是被编码方案决定的,通过UTF来指定。

计算机编码的发展历史

转自 https://www.freebuf.com/articles/web/25623.html

很久以前,计算机制造商有自己的表示字符的方式。他们并不需要担心如何和其它计算机交流,并提出了各自的方式来将字形渲染到屏幕上。随着计算机越来越流行,厂商之间的竞争更加激烈,在不同的计算机体系间转换数据变得十分蛋疼,人们厌烦了这种自定义造成的混乱。

最终,计算机制造商一起制定了一个标准的方法来描述字符。他们定义使用一个字节的低7位来表示字符,并且制作了如上图所示的对照表来映射七个比特的值到一个字符上。例如,字母A是65,c是99,~是126等等, ASCII码就这样诞生了。原始的ASCII标准定义了从0到127 的字符,这样正好能用七个比特表示。不过好景不长。。。

为什么选择了7个比特而不是8个来表示一个字符呢?我并不关心。但是一个字节是8个比特,这意味着1个比特并没有被使用,也就是从128到255的编码并没有被制定ASCII标准的人所规定,这些美国人对世界的其它地方一无所知甚至完全不关心。

其它国家的人趁这个机会开始使用128到255范围内的编码来表达自己语言中的字符。例如,144在阿拉伯人的ASCII码中是گ,而在俄罗斯的ASCII码中是ђ。即使在美国,对于未使用区域也有各种各样的利用。IBM PC就出现了“OEM 字体”或”扩展ASCII码”,为用户提供漂亮的图形文字来绘制文本框并支持一些欧洲字符,例如英镑(£)符号。

lpcec9qu3zw3u5uldh_bnawejbesszsmhiv2jm-pyemhgj2ky0vkvn0ousiaqvmv4kmsg_gmhhlhwcbem-uem4wcxkm5hcungr3r7bibhm1ievimyks2cxidbg

用IBM扩展字符集绘制的很酷的DOS启动画面

再强调一遍,ASCII码的问题在于尽管所有人都在0-127号字符的使用上达成了一致,但对于128-255号字符却有很多很多不同的解释。你必须告诉计算机使用哪种风格的ASCII码才能正确显示128-255号的字符。

这对于北美人和不列颠群岛的人来说不算什么问题,因为无论使用哪种风格的ASCII码,拉丁字母的显示都是一样的。英国人还需要面对的问题是原始的ASCII码中不包含英镑符号,但是这个已经无关紧要了。

与此同时,在亚洲有更让人头疼的问题。亚洲语言有更多的字符和字形需要被存储,一个字节已经不够用了。所以他们开始使用两个字节来存储字符,这被称作DBCS(双字节编码方案)。在DBCS中,字符串操作变得很蛋疼,你应该怎么做str++或str–?

这些问题成为了系统开发者的噩梦。例如,MS DOS必须支持所有风格的ASCII码,因为他们想把软件卖到其他国家去。他们提出了「内码表」这一概念。例如,你需要告诉DOS(通过使用”chcp”命令)你想使用保加利亚语的内码表,它才能显示保加利亚字母。内码表的更换会应用到整个系统。这对使用多种语言工作的人来说是一个问题,因为他们必须频繁的在几个内码表之间来回切换。

尽管内码表是一个好主意,但是它不是一个简洁的解决方案,它只是一个hack技术或者说是简单的修正来让编码系统可以工作。

进入Unicode的世界

最终,美国人意识到他们应该提出一种标准方案来展示世界上所有语言中的所有字符,以便缓解程序员的痛苦和避免字符编码引发的第三次世界大战。出于这个目的,Unicode诞生了。

Unicode背后的想法非常简单,然而却被普遍的误解了。Unicode就像一个电话本,标记着字符和数字之间的映射关系。Joel称之为「神奇数字」,因为它们可能是随机指定的,而且不会给出任何解释。官方术语是码位(Code Point),总是用U+开头。理论上每种语言中的每种字符都被Unicode协会指定了一个神奇数字。例如希伯来文中的第一个字母א,是U+2135,字母A是U+0061。

Unicode并不涉及字符是怎么在字节中表示的,它仅仅指定了字符对应的数字,仅此而已。

关于Unicode的其它误解包括:Unicode支持的字符上限是65536个,Unicode字符必须占两个字节。告诉你这些的人应该去换换脑子了。

记住,Unicode只是一个用来映射字符和数字的标准。它对支持字符的数量没有限制,也不要求字符必须占两个、三个或者其它任意数量的字节。

Unicode字符是怎样被编码成内存中的字节这是另外的话题,它是被UTF(Unicode Transformation Formats)定义的。

这里有个有趣的知识:没有纯文本这回事

既然没有「纯文本」文件这回事,那你的文本编辑器和浏览器为什么每次都能正确的显示内容呢?答案是,那些软件欺骗了你,这也是为什么那么多人对编码一无所知。当软件不能确定编码的时候,它会猜测。大部分时候,它会猜测是否是涵盖了ASCII码的UTF-8,还是ISO-8859-1,也有可能猜其他能想到的任意字符集。因为英文中使用的拉丁字母表在几乎所有的字符集中都能显示,包括UTF-8,所以即使编码猜错了,英文字母看起来也是正确的。

但是,如果你在浏览网页时看到�符号,这意味着这个网页的编码不是你的浏览器猜测的那个。这时你可以点开浏览器的查看->字符编码菜单来尝试不同的编码。

Unicode欺骗

上面已经将Unicode的意义讲透了,接下来就是实际技术了。

这里有篇讲Unicode同形字引起的安全问题的文章:https://paper.seebug.org/77/

写过一些ctf题的应该都见过用外型类似的unicode字符代替原字符进行攻击的的题目。

这里的问题是:

Unicode字符无非也是一种字符而已,只要程序支持,应该不会有任何问题。为什么这个unicode字符在处理过程中变成了另外一个字符,而且是看起来很相似的字符,难道程序也跟人眼看一样,被相同的外表迷惑了?这显然是不可能的。

这里有一篇很古老的文章提到了这个问题:https://www.peterbe.com/plog/unicode-to-ascii

大致原理其实是有专门的unicode转ascii函数,对unicode与同形的ascii之间有对应的map。会将一些类似的unicode字符转换成ascii字符。

如上面这篇文章中的unicodedata.normalize函数。

这里有一个大佬写的脚本,用于遍历这个函数中可用来替换ascii码的unicode同型字符。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import json
from unicodedata import normalize


def main():
    debug = False
    tables = {}
    for i in range(1, 0x10000):
        src = unichr(i)
        dst = normalize('NFKC', src)[0]
        try:
            if ord(dst) < 128 and dst != src:
                if debug:
                    print("%s (\\u%s) -- normalize --> %s (\\x%s)" % (
                        src, hex(i)[2:].rjust(4, '0'),
                        dst, hex(dst.charAt(0))[2:]
                    ))
                if dst in tables:
                    tables[dst].append(src)
                else:
                    tables[dst] = [src]
        except Exception as e:
            print(repr(e))
    with open("nfctable.txt", "wb") as fh:
        json.dump(tables, fh)


if __name__ == '__main__':
    main()

可以用于Bypass某些WAF或过滤

其它技巧

字符删除

例如 \x3c\x73\x63\x72\xc2\x69\x70\x74\x3e 这个字符串中,而 \xc2 并不是任何一个有效字符的子串,在一些处理逻辑中,可能会删除 \xc2 这个字符,从而导致问题。

字符替换

一些情况下,字符会被替换为其他的字符 如U+FFFF会被替换成 ? 这在一些 ? 有明确语义的情况下就会出现问题。不过关于这种利用只是存在理论上利用的可能性

缓冲区溢出

在一些大小写转换时,字符会变多,例如 'ß'.toUpperCase() 的运行结果是 SS。如果字符串的长度检查在大小写转换之前,就可能会存在缓冲区溢出问题。

这里有个buu的题目

[ASIS 2019]Unicorn shop https://buuoj.cn/challenges#[ASIS%202019]Unicorn%20shop

题目就是后台有个unicodedata.numeric()函数,会将unicode字符转换成等价的数字。

把一个值大于1337的unicode字符输进价格栏就行。

wp各种花里胡哨的unicode字符,实际上只要用中文的”万”就行了,这也是unicode字符。

post传参

id=4&price=万

  目录