🍊 中文字符形近字的研究
这是橘子杀手的第 59 篇文章
题图摄于:斐济·罗马尼岛
发现问题
前几天对象和我抱怨,有一道数据安全 CTF 题,本意是给一个 csv,然后需要对其中的数据进行脱敏,里面有一列数据的列名就是 “银行卡”。她用 excel 打开这个表格,可以看到“银行卡”这个列。但是她在写 Python 代码提取数据的时候,通过类似 if col == "银行卡"
或者 col["银行卡"]
来进行筛选这个行却拿不到数据,但是打印所有列名的时候却又能看到“银行卡”这个列。
这个现象抽象出来就是这样:
b 是手打的。
经过比对,发现这两个字符串的长度是一样的,这就排除了是多了不可见字符的问题。
研究问题
这个时候就有趣了,猜测大概率是形近字,但我的确没遇到过中文本身有如此相似的形近字,中文里的多音字也是音不同,但字是完全一个码。
打印出 unicode 值来看看:
可以看出 “行” 这个字是不一样的,手打的 Unicode 值是 34892
。
那么问题来了,我们知道即使是多音字,这个字也是一模一样的 Unicode 值,不会出现不一样的情况,如果 34892 是真正的 “行”,那 12175 又是什么字呢?
经过一番搜索,答案是康熙部首。“康熙部首”是指《康熙字典》中所采用的汉字部首分类系统,是清朝康熙年间编纂的一部权威汉字字典,它将汉字按照部首进行分类,共分为 214 个部首。这种分类方法主要依据汉字的字形和字义,具有较强的系统性,方便人们检索和排版汉字。按照大模型的说法,这些部首都是没有读音的。
在 Unicode 字符集中,康熙部首符号被分配在 U+2F00
到 U+2FDF
的范围内,共包含 214 个字符。这些符号主要用于汉字字典的编排和检索。
测试脚本
为了方便测试,我写了一段 Python 脚本,如果遇到在这个范围里的汉字,会自动修改成对应部首的字:
import argparse
from colorama import Fore, Style
def put_color(string, color, bold=True):
if color == "gray":
COLOR = Style.DIM + Fore.WHITE
else:
COLOR = getattr(Fore, color.upper(), "WHITE")
returnf'{Style.BRIGHT if bold else ""}{COLOR}{str(string)}{Style.RESET_ALL}'
trans_map = trans_map = { "一": "⼀", "|": "⼁", None: [ "⼃", "⼅", "⼇", "⼌", "⼍", "⼎", "⼐", "⼓", "⼕", "⼖", "⼙", "⼛", "⼞", "⼡", "⼢", "⼣", "⼧", "⼪", "⼬", "⼮", "⼵", "⼶", "⼹", "⼺", "⼻", "⼾", "⽁", "⽎", "⽏", "⽙", "⽦", "⽧", "⽨", "⽰", "⽱", "⽷", "⽾", "⾇", "⾋", "⾌", "⾑", "⾒", "⾓", "⾘", "⾙", "⾞", "⾡", "⾤", "⾧", "⾨", "⾫", "⾭", "⾱", "⾴", "⾵", "⾶", "⾺", "⾻", "⾽", "⾾", "⾿", "⿂", "⿃", "⿄", "⿆", "⿇", "⿈", "⿋", "⿌", "⿍", "⿑", "⿒", "⿓", "⿔", "⿕", ], "乙": "⼄", "二": "⼆", "人": "⼈", "儿": "⼉", "入": "⼊", "八": "⼋", "几": "⼏", "刀": "⼑", "力": "⼒", "匕": "⼔", " 十": "⼗", "卜": "⼘", "厂": "⼚", "又": "⼜", "口": "⼝", "土": "⼟", "士": "⼠", "大": "⼤", "女": "⼥", "子": "⼦", "寸": "⼨", "小": "⼩", "尸": "⼫", "山": "⼭", "工": "⼯", "己": "⼰", "巾": "⼱", "干": "⼲", "幺": "⼳", "广": "⼴", "弋": "⼷", "弓": "⼸", "心": "⼼", "戈": "⼽", "手": "⼿", "支": "⽀", "文": "⽂", "斗": "⽃", "斤": "⽄", "方": "⽅", "无": "⽆", "日": "⽇", "曰": "⽈", "月": "⽉", "木": "⽊", "欠": "⽋", "止": "⽌", "歹": "⽍", "比": "⽐", "毛": "⽑", "氏": "⽒", "气": "⽓", "水": "⽔", "火": "⽕", "爪": "⽖", "父": "⽗", "爻": "⽘", "片": "⽚", "牙": "⽛", "牛": "⽜", "犬": "⽝", "玄": "⽞", "玉": "⽟", "瓜": "⽠", "瓦": "⽡", "甘": "⽢", "生": "⽣", "用": "⽤", "田": "⽥", "白": "⽩", "皮": "⽪", "皿": "⽫", "目": "⽬", "矛": "⽭", "矢": "⽮", "石": "⽯", "禾": "⽲", "穴": "⽳", "立": "⽴", "竹": "⽵", "米": "⽶", "缶": "⽸", "网": "⽹", "羊": "⽺", "羽": "⽻", "老": "⽼", "而": "⽽", "耳": "⽿", "聿": "⾀", "肉": "⾁", "臣": "⾂", "自": "⾃", "至": "⾄", "臼": "⾅", "舌": "⾆", "舟": "⾈", "艮": "⾉", "色": "⾊", "虫": "⾍", "血": "⾎", "行": "⾏", "衣": "⾐", "言": "⾔", "谷": "⾕", "豆": "⾖", "豕": "⾗", "赤": "⾚", "走": "⾛", "足": "⾜", "身": "⾝", "辛": "⾟", "辰": "⾠", "邑": "⾢", "酉": "⾣", "里": "⾥", "金": "⾦", "阜": "⾩", "隶": "⾪", "雨": "⾬", "非": "⾮", "面": "⾯", "革": "⾰", "韭": "⾲", "音": "⾳", "食": "⾷", "首": "⾸", "香": "⾹", "高": "⾼", "鬲": "⿀", "鬼": "⿁", "鹿": "⿅", "黍": "⿉", "黑": "⿊", "鼓": "⿎", "鼠": "⿏", "鼻": "⿐", }
parser = argparse.ArgumentParser()
parser.add_argument("-c", "--content", type=str, help="输入要转换的文字", required=True)
args = parser.parse_args()
raw_content = args.content
print(
f"-> {raw_content}",
)
count = 0
print("<- ", end="")
for c in raw_content:
tc = trans_map.get(c, c)
if tc != c:
tc = put_color(tc, "red")
count += 1
print(tc, end="")
print(f"nn[*] 修改了 {count} 个字")
稍微改改就能用到其他地方,比如这种攻击手法的检测。
一些想法
由于这种部首并没有拼音,因此我推测出题人是五笔打字打出来的,不过我稍微研究了下五笔打字,tfh
打印出来的就是普通的 “行”,也不是康熙部首,不过其他部首的确有些可以打出来。有点奇怪,不知道这是咋打出来的,可能是有康熙部首的字库吧。
那么这个有什么用呢?会造成人眼阅读的结果,与计算机的识别出现差异,从而引发其他安全问题:
-
对抗文字内容的检测策略,例如钓鱼邮件的关键字检测绕过,或者是黄赌毒暴恐政文字过滤策略 -
对于一些软件的用户名称重复检测,可以通过这样的方式绕过,或许用来做一些欺诈, 或者假装靓号装逼 -
作为出题人用来折磨参赛选手 -
... 待挖掘
对于防御方,可以非常简单地基于 Unicode 范围,快速检测文本中可能混入的康熙部首字符,直接干掉。
之前也有类似的 unicode 的研究,见 从一个绕过长度限制的 XSS 中,我们能学到什么?( https://www.tr0y.wang/2020/08/18/IDN/ )
整个研究过程很短,差不多就 4 小时,但非常有趣
不得不说
我对象的确是吸引各种各样 bug 的体质
原文始发于微信公众号(橘子杀手):中文字符形近字的研究
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论