大家好,我是程序员牛肉。
今天我在微信群里遇到了一个怪事:群友发送了一个消息,我粘贴到自己的发送栏中却无法进行删除。
如果你想体验这个消息,可以复制我评论区的置顶消息,然后尝试进行删除。看看是否可以删除成功
我愿意将其称为是新一版本的赛博灯泡。那介绍完了这个比较好玩的事情之后,我们来探究一下其原理。
为了方便你更好的理解这个把戏的原理,我们要先来介绍一下计算机是怎么表示汉字的。
人类世界有海量的文字,这些文字可以被刻在石头上,墙壁上,竹筒上。这一个个符号都是字符。而我们把字符组成的集合就叫做字符集。
但很遗憾的是:计算机并不能读懂这些字符。计算机是0和1的世界,我们该如何将这些字符存储到计算机中成为一个难题
因此我们想出了一个办法:用一个特定的二进制数字代表一个特定的字符。这样就实现了字符在计算机中的表示,字符集编码应运而生。
而最知名的字符集编码就是ASCLL了。
有关字符集的内容就不详细介绍了,感兴趣的可以看一看我之前写的这一篇文章:
2024-07-21
而在这些字符集映射中,不是所有的二进制都会被映射成为一个实际存在的汉字,还有一部分特殊的成员——零宽字符。顾名思义,这些字符在视觉上不占用任何宽度,肉眼无法直接看到,但它们确实存在于文本中,并被计算机系统识别和处理。
常见的零宽字符有:
这些零宽字符的设计初衷是解决特定语言的排版问题。例如在阿拉伯文和波斯文中,字母的形状会根据其在单词中的位置而变化,零宽连字符和零宽不连字符可以控制这些形状变化。
那既然这些字符是实际存在且不可见的,如果有人把这些不可见的字符大量的粘贴到可见字符的后面。
这样当你删除的时候,实际上是一直在删除不可见字符。但不可见字符的特性又导致你对删除无感知,以为没有删除成功。
其实这就是刚才那个“叶”无法被删除的原理。当我们把那个不可被删除的消息粘贴到编译器中,就可以看到可见字符后面跟着海量的不可见字符:
由于实在是跟了太多的不可见字符,所以就算你长按删除键十几秒也删不完后面的不可见字符。这就让你误以为这条消息删除不掉。但只要你按的时间够长还是可以删完的,不信邪的朋友可以尝试长按删除七八分钟试一试。
[微信公众号的评论区的字数限制更严格,所以不可见字符的数量会少一些,可能长按个七八秒就删完了。]
这个小把戏的本质就是给你玩了一手字符集编码的小特性而已。只不过这个还没有被大规模传播。如果你上网冲浪频繁的话,就应该知道18年的小黑点表情导致IOS卡死事件。
这个就是利用了Unicode编码字符集中的不可见字符串,这个 “黑点” 表情看似普通,实际上包含了大量不可见的 Unicode 字符,如零宽空格(ZERO - WIDTH SPACE)等,还可能有从左到右标记(LRM)、从右向左标记(RLM)等隐形字符。这些字符在文本中不显示实际内容,但会影响文本的排版和解析规则。
当你查看了这个表情包的时候,你手机的文本编辑器会开始处理这些隐式字符串所设置的排版要求。
而这个文本中插入了大量的特殊 Unicode 字符时,需要进行大量的运算来处理字符的宽度、排列顺序等信息,这会使 CPU 处于高负载状态,占用大量系统资源,最终就导致了系统的卡死。
而基于不可见字符的特性,我们其实还可以玩一手隐藏水印。前面我们说过两个重要的特性:
那我们是不是可以用不同的隐式字符来代表 0 和 1,这样我们就可以通过在显式文本的后面添加不同的隐式文本来实现暗水印的能力。
感兴趣的朋友可以用下面的代码来玩一下:
public class InvisibleWatermark {
public static void main(String[] args) {
// 要隐藏的水印文本
String watermarkText = "程序员牛肉";
// 可见的普通文本
String visibleText = "你好我是李元鑫";
// 转换水印文本为零宽字符
String binaryWatermark = textToBinary(watermarkText);
String hiddenPart = binaryToZeroWidth(binaryWatermark);
// 将隐藏内容插入到可见文本的末尾
String result = visibleText + hiddenPart;
System.out.println("包含不可见水印的文本:");
System.out.println(result);
System.out.println("\n复制上方文本到支持显示控制字符的编辑器中查看隐藏内容");
// 验证提取结果
String extracted = extractWatermark(result);
if (extracted != null && !extracted.isEmpty()) {
System.out.printf("\n提取的水印: %s%n", extracted);
} else {
System.out.println("\n未提取到水印");
}
}
/**
* 确保二进制字符串为8位,不足则在前面补0
*/
private static String zeroPad(String numStr) {
if (numStr.length() >= ) {
return numStr; // 修复:避免substring负数索引
}
return"00000000".substring(numStr.length()) + numStr;
}
/**
* 将文本转换为带空格分隔的8位二进制字符串
*/
private static String textToBinary(String text) {
StringBuilder binaryBuilder = new StringBuilder();
for (char c : text.toCharArray()) {
binaryBuilder.append(zeroPad(Integer.toBinaryString(c))).append(" ");
}
return binaryBuilder.toString().trim();
}
/**
* 将二进制字符串转换为零宽字符序列
*/
private static String binaryToZeroWidth(String binary) {
StringBuilder result = new StringBuilder();
for (char c : binary.toCharArray()) {
switch (c) {
case'1':
result.append('\u200b'); // 零宽空格
break;
case'0':
result.append('\u200c'); // 零宽非连接符
break;
case' ':
result.append('\u200d'); // 零宽连接符
break;
default:
// 忽略其他字符
}
}
return result.toString();
}
/**
* 将零宽字符序列转换回二进制字符串
*/
private static String zeroWidthToBinary(String zeroWidth) {
StringBuilder binaryBuilder = new StringBuilder();
for (char c : zeroWidth.toCharArray()) {
switch (c) {
case'\u200b':
binaryBuilder.append('1');
break;
case'\u200c':
binaryBuilder.append('0');
break;
case'\u200d':
binaryBuilder.append(' ');
break;
default:
// 忽略其他字符
}
}
return binaryBuilder.toString();
}
/**
* 将二进制字符串转换回文本
*/
private static String binaryToText(String binary) {
StringBuilder textBuilder = new StringBuilder();
String[] bytes = binary.split(" ");
for (String byteStr : bytes) {
if (!byteStr.isEmpty()) {
textBuilder.append((char) Integer.parseInt(byteStr, ));
}
}
return textBuilder.toString();
}
/**
* 从文本中提取零宽字符并转换为原始文本
*/
private static String extractWatermark(String text) {
StringBuilder zeroWidthChars = new StringBuilder();
for (char c : text.toCharArray()) {
if (c == '\u200b' || c == '\u200c' || c == '\u200d') {
zeroWidthChars.append(c);
}
}
if (zeroWidthChars.length() > ) {
String binary = zeroWidthToBinary(zeroWidthChars.toString());
return binaryToText(binary);
}
returnnull;
}
}
像这种招数找出一些违规粘贴复制的内鬼还是很有用的,比如我们可以将上述的代码写成js形式的;当用户粘贴的时候就将当前用户的UID采用上述的方法添加隐式文本 到 显式文本后面。
那今天的内容就介绍到这里了,相信通过我的介绍,你已经大致了解了这个把戏是怎么实现的。