Files
bl/common/utils/go-sensitive-word-1.3.3/docs/unicode.md

164 lines
12 KiB
Go
Raw Normal View History

# Unicode相似字符攻击
## 攻击原理
Unicode 相似字符攻击是一种利用 Unicode 字符集中存在的字符相似性来绕过文本过滤或识别系统的攻击手段攻击者通过使用看似相同但实际上具有不同 Unicode 编码的字符来欺骗系统以达到绕过过滤器或识别系统的目的这种攻击可以用于欺诈钓鱼恶意代码注入等各种恶意活动中
**原理**
1. **Unicode 字符集的复杂性**Unicode 字符集非常庞大包含了数千个字符其中很多字符在外观上看起来非常相似或者完全一样但实际上它们的 Unicode 编码是不同的这种复杂性使得攻击者可以利用这些相似字符来绕过文本过滤或识别系统的检测
2. **字符规范化的差异**Unicode 规范定义了多种字符规范化形式比如 NFCNFDNFKCNFKD 它们会对字符进行不同程度的归一化处理攻击者可以利用不同的字符规范化形式来生成看似相同但实际上具有不同 Unicode 编码的字符
**外形一致不同点位文字演示**
例如下面两个 [](https://symbl.cc/en/unicode-table/#cjk-compatibility-ideographs) 和 [“⾦”](https://symbl.cc/en/unicode-table/#cjk-radicals-supplement) 字的比较,虽然肉眼看起来外形一致,但是再 unicode 中确是不同的点位。
![](assets/unicode-word1.png)
![](assets/unicode-word2.png)
```go
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "金"
r, _ := utf8.DecodeRuneInString(s)
fmt.Printf("Unicode 十六进制格式码点编码:%U\n", r)
s2 := "⾦"
r2, _ := utf8.DecodeRuneInString(s2)
fmt.Printf("s2 Unicode 十六进制格式码点编码:%U\n", r2)
}
// 输出内容
// Unicode 十六进制格式码点编码U+F90A
// s2 Unicode 十六进制格式码点编码U+2FA6
```
**UTF-8中文字符Unicode码点范围**
Unicode CJK 的范围分布在多个区段中带有 CJK 的区块名中都拥有汉字但最常用的范围是 `U+4E00U+9FA5`即名为CJK Unified Ideographs 的区块 U+9FA6U+9FFF 之间的字符还属于空码暂时还未定义但不能保证以后不会被定义
包括来自中文日文韩文越南文壮文琉球文中的汉字还包括越南的喃字与儒字方块壮字未来还包括甲骨文金文简帛文陶文鸟虫书等
- 注1中文范围 4E00-9FBFCJK 统一表意符号 (CJK Unified Ideographs)
- 注2正则表达式[\u4e00-\u9fa5] 可匹配中文字符但这种方式并不能根据平台所提供的字符集范围不同而改变
- 注3Unicode U+4E00U+9FFF 的码表http://www.unicode.org/charts/PDF/U4E00.pdf
**汉字Unicode编码范围**
对于不属于 `U+4E00U+9FA5` 范围的字符则是需要我们防止unicode相似字符攻击思考的地方
| **字符集中文名** | **字数** | **Unicode 编码** |
|-------------------------------------------------------------------------| -------- | ---------------- |
| [基本汉字](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=jbhz) | 20902字 | 4E00-9FA5 |
| [基本汉字补充](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=jbhzbc) | 90字 | 9FA6-9FFF |
| [扩展A](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=kza) | 6592字 | 3400-4DBF |
| [扩展B](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=kzb) | 42720字 | 20000-2A6DF |
| [扩展C](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=kzc) | 4154字 | 2A700-2B739 |
| [扩展D](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=kzd) | 222字 | 2B740-2B81D |
| [扩展E](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=kze) | 5762字 | 2B820-2CEA1 |
| [扩展F](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=kzf) | 7473字 | 2CEB0-2EBE0 |
| [扩展G](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=kzg) | 4939字 | 30000-3134A |
| [扩展H](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=kzh) | 4192字 | 31350-323AF |
| [扩展I](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=kzi) | 622字 | 2EBF0-2EE5D |
| [康熙部首](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=kxbs) | 214字 | 2F00-2FD5 |
| [部首扩展](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=bskz) | 115字① | 2E80-2EF3 |
| [兼容汉字](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=jrhz) | 472字② | F900-FAD9 |
| [兼容扩展](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=jrkz) | 542字 | 2F800-2FA1D |
| [汉字笔画](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=hzbh) | 36字 | 31C0-31E3 |
| [汉字结构](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=hzjg) | 16字 | 2FF0-2FFF |
| [汉语注音](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=hyzy) | 43字 | 3105-312F |
| [注音扩展](https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php?zfj=zykz) | 32字 | 31A0-31BF |
| | 1 | 3007 |
Unicode 版本15.1
字数备注:
部首扩展2E9A 是空码位
兼容汉字FA6EFA6F 是空码位
此页面的字数按实际字数标示排除空码位编码范围则排除了首尾空码位另一个页面[世界文字大全](https://www.qqxiuzi.cn/zh/unicode-zifu.php)》的编码范围标注则与 Unicode 一致(包括空码位)。 另,原有 PUA 字符产生于上世纪九十年代,现已全部标准化拥有正式码位,故从表中删除。
**形成过程**
1. **拉丁字符和特殊符号的混合**攻击者可以将拉丁字符比如英文字母与一些看似相似的特殊符号比如希腊字母数学符号等混合在一起生成一个看起来和原始字符相似但实际上具有不同编码的字符例如使用希腊字母的小写 sigmaσ替代拉丁字母的小写 ss
2. **全角和半角字符的混合**攻击者可以利用全角字符全角空格全角标点等和半角字符英文字符标点符号等的外观相似性来混合使用生成绕过过滤器的字符例如使用全角字符的英文句号替代半角字符的英文句号.
3. **Unicode 组合字符的利用**Unicode 支持字符的组合表示攻击者可以利用组合字符来生成看似相同但实际上具有不同编码的字符例如使用拉丁字母和组合重音符号combining acute accent来生成特定语言的字符绕过过滤器检测
通过利用这些技巧攻击者可以成功地绕过文本过滤或识别系统的检测传播有害内容或实施其他恶意活动因此对于开发文本过滤或识别系统的人员来说需要认识到 Unicode 相似字符攻击的潜在风险并采取相应的防御措施来保护系统的安全
## 解决方案
防止 Unicode 相似字符攻击是敏感词检测中的一个重要问题因为攻击者可以利用这些字符来绕过过滤器传播有害内容或攻击目标以下是一些防范措施
1. **字符归一化Normalization**将输入的文本进行字符归一化处理将不同形式的 Unicode 字符统一成同一种形式从而减少相似字符的影响Unicode 规范定义了几种不同的归一化形式Normalization Forms比如 NFCNFDNFKCNFKD可以根据具体需求选择合适的归一化方式
2. **白名单验证**限制用户输入只能包含特定的字符集合而不是接受任意字符可以定义一个白名单只允许合法字符通过过滤器
3. **规则匹配**建立一套严格的规则来检测和匹配潜在的相似字符攻击这些规则可以基于字符的视觉相似性Unicode 编码的相似性等
4. **黑名单过滤**维护一个黑名单包含已知的相似字符及其对应的正常字符当检测到输入中包含黑名单中的字符时可以将其替换或过滤掉
5. **用户教育**向用户提供教育让他们意识到利用相似字符来规避过滤器是不道德的行为并鼓励他们使用正常的字符输入
6. **多种检测手段结合**使用多种不同的技术和方法结合起来提高检测的准确性和鲁棒性从而有效地防止相似字符攻击
综合使用以上策略可以在敏感词检测中有效地防止相似字符攻击然而需要注意的是安全性是一个持续的过程需要不断地更新和改进防御措施以适应不断变化的威胁和攻击手法
**NFKC和NFKD的区别**
NFKC NFKD Unicode 字符串的两种归一化形式它们之间的主要区别在于归一化的方式和处理范围
1. **NFKCNormalization Form KC**
- 这种形式是 Unicode 归一化的一种形式其中K表示兼容compatibility
- NFKC 形式会尽可能地将字符转换为兼容的形式以便更容易地比较和匹配字符串它执行的归一化操作包括将一些特殊字符转换为等效的普通字符比如把全角标点转换成半角标点把特殊的组合字符转换为对应的单一字符等等
- NFKC 形式会保留字符的兼容性因此在某些情况下可能会导致字符的损失或变化但通常会更适合用于搜索比较和匹配等操作
2. **NFKDNormalization Form KD**
- 这种形式也是 Unicode 归一化的一种形式其中K表示兼容compatibilityD表示分解decomposition
- NFKD 形式会将字符分解为其组成部分即将组合字符拆分为基字符和重音这种形式更进一步对于某些特殊字符会将其转换为更基本的形式
- NFKD 形式会尽量消除字符的兼容性即使在某些情况下会导致字符的丢失或变化这种形式适用于一些特定场景如文本标准化索引化等
总的来说NFKC NFKD 形式在处理 Unicode 字符串时NFKC 更注重于保留字符的兼容性和整体可读性 NFKD 则更注重于字符的分解和简化如果你需要进行搜索比较或匹配操作那么 NFKC 形式可能更适合而如果你需要进行文本标准化或分析等操作那么 NFKD 形式可能更为合适
## Go代码实现
**归一化处理**
```go
import (
"fmt"
"golang.org/x/text/unicode/norm"
)
func main() {
// 原始字符串
original := "㋅"
targetStr := "6月"
fmt.Println("原始字符串:", original)
fmt.Println("目标字符串:", targetStr)
// NFC 形式(由组合字符组成)
nfc := norm.NFC.String(original)
fmt.Println("NFC 形式:", nfc)
// NFD 形式(由基字符和重音分开)
nfd := norm.NFD.String(original)
fmt.Println("NFD 形式:", nfd)
// 【推荐】 NFKC 形式(合成后的形式,且兼容 ASCII
nfkc := norm.NFKC.String(original)
fmt.Println("NFKC 形式:", nfkc)
// NFKD 形式(分解后的形式,且兼容 ASCII
nfkd := norm.NFKD.String(original)
fmt.Println("NFKD 形式:", nfkd)
// 判断目标字符串
res0 := original == targetStr
fmt.Println("原始和目标字符串比较", res0)
res1 := nfkc == targetStr
fmt.Println("NFKC字符串比较", res1)
res2 := nfkd == targetStr
fmt.Println("NFKD字符串比较", res2)
}
```