中文字符形近字的研究

本文是中文字符形近字,或者说是中文的同形异义词的研究。

发现问题

前几天对象和我抱怨,有一道数据安全 CTF 题,本意是给一个 csv,然后需要对其中的数据进行脱敏,里面有一列数据的列名就是 “银行卡”。她用 excel 打开这个表格,可以看到“银行卡”这个列。但是她在写 Python 代码提取数据的时候,通过类似 if col == "银行卡" 或者 col["银行卡"] 来进行筛选这个行却拿不到数据,但是打印所有列名的时候却又能看到“银行卡”这个列。

这个现象抽象出来就是这样:

b 是手打的。

经过比对,发现这两个字符串的长度是一样的,这就排除了是多了不可见字符的问题。

研究问题

这个时候就有趣了,猜测大概率是形近字,但我的确没遇到过中文本身有如此相似的形近字,中文里的多音字也是音不同,但字是完全一个码。

打印出 unicode 值来看看:

可以看出 “行” 这个字是不一样的,手打的 Unicode 值是 34892

那么问题来了,我们知道即使是多音字,这个字也是一模一样的 Unicode 值,不会出现不一样的情况,如果 34892 是真正的 “行”,那 12175 又是什么字呢?

经过一番搜索,答案是康熙部首。“康熙部首”是指《康熙字典》中所采用的汉字部首分类系统,是清朝康熙年间编纂的一部权威汉字字典,它将汉字按照部首进行分类,共分为 214 个部首。这种分类方法主要依据汉字的字形和字义,具有较强的系统性,方便人们检索和排版汉字。按照大模型的说法,这些部首都是没有读音的。

在 Unicode 字符集中,康熙部首符号被分配在 U+2F00U+2FDF 的范围内,共包含 214 个字符。这些符号主要用于汉字字典的编排和检索。

测试脚本

为了方便测试,我写了一段 Python 脚本,如果遇到在这个范围里的汉字,会自动修改成对应部首的字:

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
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")

return f'{Style.BRIGHT if bold else ""}{COLOR}{str(string)}{Style.RESET_ALL}'


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"\n\n[*] 修改了 {count} 个字")

稍微改改就能用到其他地方,比如这种攻击手法的检测。

一些想法

由于这种部首并没有拼音,因此我推测出题人是五笔打字打出来的,不过我稍微研究了下五笔打字,tfh 打印出来的就是普通的 “行”,也不是康熙部首,不过其他部首的确有些可以打出来。有点奇怪,不知道这是咋打出来的,可能是有康熙部首的字库吧。

那么这个有什么用呢?会造成人眼阅读的结果,与计算机的识别出现差异,从而引发其他安全问题:

  1. 对抗文字内容的检测策略,例如钓鱼邮件的关键字检测绕过,或者是黄赌毒暴恐政文字过滤策略
  2. 对于一些软件的用户名称重复检测,可以通过这样的方式绕过,或许用来做一些欺诈,或者假装靓号装逼
  3. 作为出题人用来折磨参赛选手
  4. ... 待挖掘

对于防御方,可以非常简单地基于 Unicode 范围,快速检测文本中可能混入的康熙部首字符,直接干掉。

之前也有类似的 unicode 的研究,见 从一个绕过长度限制的 XSS 中,我们能学到什么?

整个研究过程非常短,差不多就 4 小时,但非常有趣。


不得不说
我对象的确是吸引各种各样 bug 的体质


中文字符形近字的研究
https://www.tr0y.wang/2025/01/23/中文同型异义词/
作者
Tr0y
发布于
2025年1月23日
更新于
2025年3月11日
许可协议