零宽度字符是隐藏不显示的,也是不可打印的,也就是说这种字符用大多数程序或编辑器是看不到的。最常见的是零宽度空格,它是Unicode字符空格,就像如果在两个字母间加一个零宽度空格,该空格是不可见的,表面上两个字母还是挨在一起的。就比如说,我在这句话中插入了10个零宽度空格,你能看到吗?(For example, I’ve inserted 10 zero-width spaces into this sentence, can you tell?)
请你复制括号中的英文句子到 Diff Checker中找找不同就一目了然了。前有NSA通过打印机水印追踪到了泄密者的案例,而某种程度上来说,我们这里所说的零宽度字符可作为识别某些用户身份的“指纹”数据,也可非常方便地追溯到某些秘密数据的泄露源。
为什么零宽度字符可以起到这种作用呢?
这还要从多年前的一个事件说起,当时,我还是一支视频游戏比赛竞技团队的队员,在团队网站中有一个私人留言板,用来发布团队内部重要的通知信息。但最终,这些仅可内部知晓的消息却被泄露到了其它网络论坛中,团队比赛策略也随之被泄密。看来,这种用来共享内部信息和策略的留言板机制防不住“内鬼”。
我们的团队网站安全做的还算到位,唯一可能的原因就是某位内部登录用户通过复制粘贴方式把私人留言板信息泄露到其它论坛中。于是,我为网站后台维护团队写了一个脚本嵌入到留言板中,该脚本能隐蔽标识浏览了留言板通告信息的每位内部用户。
最近看到 Zach Aysan 发表的零宽度字符的很多用处,所以我也打算把该方法给大家分享出来,最终效果可以通过这个交互式demo来测试,其中的代码实例为JavaScript,但逻辑效果大致相同。
实现原理
具体步骤和实现原理如下正向逆向Fingerprint过程所示,其用户名字符串会被转换为二进制形式,然后这些二进制形式会被转换为一系列用二进制位表示的零宽度字符,零宽度字符会被隐蔽地插入到文本内容中。如果这种插入了零宽度字符的文本内容被复制粘贴到网络其它论坛中后,通过提取其中隐蔽的零宽度字符就能标识出复制了这些信息的登录用户身份。
向文本内容中加入指纹识别(正向Fingerprint)
1 提取登录用户的用户名,把其转换为二进制形式。这里,我们把用户名中的每个字母都转换为它对应的二进制形式:
const zeroPad = num => ‘00000000’.slice(String(num).length) + num;
const textToBinary = username => (
username.split('').map(char =>
zeroPad(char.charCodeAt(0).toString(2))).join(' ')
);
2 把这些经过二进制转换的用户名字母转换为零宽度字符。该过程中,二进制串中的每个1转换为零宽度空格,每个0转换为零宽不连字符(zero-width-non-joiner),每个字母完成转换后再在其后面加上一个零宽连字符(zero-width joiner ),然后再对下一个字母进行类似转换。
const binaryToZeroWidth = binary => (
binary.split('').map((binaryNum) => {
const num = parseInt(binaryNum, 10);
if (num === 1) {
return ''; // zero-width space
} else if (num === 0) {
return ''; // zero-width non-joiner
}
return ''; // zero-width joiner
}).join('') // zero-width no-break space
);
3 将最终转换过的“用户名”插入到需要保密的信息文本内容中。只需向信息文本内容中插入零宽度字符块即可。
向加入指纹识别的文本内容中提取用户名信息(逆向提取Fingerprint)
1 从加入指纹识别的文本内容中提取出零宽度形式的用户名。也就是删除那些我们看得到的文本信息,只留下零宽度字符。
2 把零宽度形式的用户名转换为二进制形式。这里我们根据之前添加的零宽度不连字符空格来分割整个字符串,我们会得到每个字母对应的与其零宽度字符等效的二进制字符,对所有零宽度字符进行迭代之后,我们就得到了二进制形式的1和0数字串。如果没找到对应的1或0,有可能是你忘了正向Fingerprint中步骤2中每个字母转换后还加了一个零宽连字符。
const zeroWidthToBinary = string => (
string.split('').map((char) => { // zero-width no-break space
if (char === '') { // zero-width space
return '1';
} else if (char === '') { // zero-width non-joiner
return '0';
}
return ' '; // add single space
}).join('')
);
3 将二进制形式的用户名转换为实际可看的用户名文本。我们可以通过解析把1和0组成的二进制串转换为实际可看的用户名文本。
const binaryToText = string => (
string.split(' ').map(num =>
String.fromCharCode(parseInt(num, 2))).join('')
);
总结
很多公司采取了多种措施防止“内鬼”向外进行信息泄密,这种零宽度字符的方法不失为一种值得一用的技巧。当然,这得根据公司的工作和信息存储模式来看,但从风险上来说,无疑一个普通的复制粘贴举动就可能会暴露自己泄密者的身份。目前,很少的的应用程序会解析到零宽度字符。
再回到之前提到的留言板场景下,我们把零宽度字符指纹脚本嵌入之后,按计划进行部署,然后在留言板上发布了一条内部信息通告,仅在几个小时之后,这些夹杂了零宽度字符的信息通告内容就被泄露到了其它论坛,好在,其中加入的指纹识别功能发挥效应,最终我们发现了泄密者,从此注销了他的账号,及时封堵了信息泄露源。
*参考来源:medium,FreeBuf小编clouds编译,转载请注明来自FreeBuf.COM