这 10 行比较字符串相等的代码给我整懵了,不信你也来看看,源代码特洛伊木马攻击
计时攻击 Timing Attacks | 酷 壳 - CoolShell
抱歉用这种标题吸引你点进来了,不过你不妨看完,看看能否让你有所收获。(有收获,请评论区留个言,没收获,下周末我直播吃**,哈哈,这你也信)
补充说明:微信公众号改版,对各个号主影响还挺大的。目前从后台数据来看,对我影响不大,因为我这反正都是小号
阅读量本身就少的可怜,真相了,
(刚从交流群学会的表情)。
先直接上代码:
boolean safeEqual(String a, String b) {
   if (a.length() != b.length()) {
       return false;
   }
   int equal = 0;
   for (int i = 0; i < a.length(); i++) {
       equal |= a.charAt(i) ^ b.charAt(i);
   }
   return equal == 0;
}
上面的代码是我根据原版(Scala)翻译成 Java的,Scala 版本(最开始吸引程序猿石头注意力的代码)如下:
def safeEqual(a: String, b: String) = {
  if (a.length != b.length) {
    false
  } else {
    var equal = 0
    for (i <- Array.range(0, a.length)) {
      equal |= a(i) ^ b(i)
    }
    equal == 0
  }
}
刚开始看到这段源码感觉挺奇怪的,这个函数的功能是比较两个字符串是否相等,首先“长度不等结果肯定不等,立即返回”这个很好理解。
再看看后面的,稍微动下脑筋,转弯下也能明白这其中的门道:通过异或操作1^1=0, 1^0=1, 0^0=0,来比较每一位,如果每一位都相等的话,两个字符串肯定相等,最后存储累计异或值的变量equal必定为 0,否则为 1。
再细想一下呢?
for (i <- Array.range(0, a.length)) {
  if (a(i) ^ b(i) != 0) // or a(i) != b[i]
    return false
}
我们常常讲性能优化,从效率角度上讲,难道不是应该只要中途发现某一位的结果不同了(即为1)就可以立即返回两个字符串不相等了吗?(如上所示)。
这其中肯定有……

再再细想一下呢?
结合方法名称 safeEquals 可能知道些眉目,与安全有关。
本文开篇的代码来自playframewok 里用来验证cookie(session)中的数据是否合法(包含签名的验证),也是石头写这篇文章的由来。
以前知道通过延迟计算等手段来提高效率的手段,但这种已经算出结果却延迟返回的,还是头一回!
我们来看看,JDK 中也有类似的方法,如下代码摘自 java.security.MessageDigest:
public static boolean isEqual(byte[] digesta, byte[] digestb) {
   if (digesta == digestb) return true;
   if (digesta == null || digestb == null) {
       return false;
   }
   if (digesta.length != digestb.length) {
       return false;
   }
   int result = 0;
   // time-constant comparison
   for (int i = 0; i < digesta.length; i++) {
       result |= digesta[i] ^ digestb[i];
   }
   return result == 0;
}
看注释知道了,目的是为了用常量时间复杂度进行比较。
但这个计算过程耗费的时间不是常量有啥风险?(脑海里响起了背景音乐:“小朋友,你是否有很多问号?”)

真相大白
再深入探索和了解了一下,原来这么做是为了防止计时攻击(Timing Attack)。(也有人翻译成时序攻击)

计时攻击(Timing Attack)
计时攻击是边信道攻击(或称"侧信道攻击", Side Channel Attack, 简称SCA) 的一种,边信道攻击是一种针对软件或硬件设计缺陷,走“歪门邪道”的一种攻击方式。
这种攻击方式是通过功耗、时序、电磁泄漏等方式达到破解目的。在很多物理隔绝的环境中,往往也能出奇制胜,这类新型攻击的有效性远高于传统的密码分析的数学方法(某百科上说的)。
这种手段可以让调用 safeEquals("abcdefghijklmn", "xbcdefghijklmn") (只有首位不相同)和调用 safeEquals("abcdefghijklmn", "abcdefghijklmn") (两个完全相同的字符串)的所耗费的时间一样。防止通过大量的改变输入并通过统计运行时间来暴力破解出要比较的字符串。
举个🌰,如果用之前说的“高效”的方式来实现的话。假设某个用户设置了密码为 password,通过从a到z(实际范围可能更广)不断枚举第一位,最终统计发现 p0000000 的运行时间比其他从任意a~z的都长(因为要到第二位才能发现不同,其他非 p 开头的字符串第一位不同就直接返回了),这样就能猜测出用户密码的第一位很可能是p了,然后再不断一位一位迭代下去最终破解出用户的密码。
当然,以上是从理论角度分析,确实容易理解。但实际上好像通过统计运行时间总感觉不太靠谱,这个运行时间对环境太敏感了,比如网络,内存,CPU负载等等都会影响。
但安全问题感觉更像是 “宁可信其有,不可信其无”。为了防止(特别是与签名/密码验证等相关的操作)被 timing attack,目前各大语言都提供了相应的安全比较函数。各种软件系统(例如 OpenSSL)、框架(例如 Play)的实现也都采用了这种方式。
例如 “世界上最好的编程语言”(粉丝较少,评论区应该打不起架来)—— php中的:
// Compares two strings using the same time whether they're equal or not.
// This function should be used to mitigate timing attacks; 
// for instance, when testing crypt() password hashes.
bool hash_equals ( string $known_string , string $user_string )
//This function is safe against timing attacks.
boolean password_verify ( string $password , string $hash )
其实各种语言版本的实现方式都与上面的版本差不多,将两个字符串每一位取出来异或(^)并用或(|)保存,最后通过判断结果是否为 0 来确定两个字符串是否相等。
如果刚开始没有用 safeEquals 去实现,后续的版本还会通过打补丁的方式去修复这样的安全隐患。
例如 JDK 1.6.0_17 中的Release Notes[1]中就提到了MessageDigest.isEqual 中的bug的修复,如下图所示:
MessageDigest timing attack vulnerabilities
大家可以看看这次变更的的详细信息openjdk中的 bug fix diff[2]为:

MessageDigest.isEqual计时攻击
Timing Attack 真的可行吗?
我觉得各大语言的 API 都用这种实现,肯定还是有道理的,理论上应该可以被利用的。这不,学术界的这篇论文就宣称用这种计时攻击的方法破解了 OpenSSL 0.9.7 的RSA加密算法了。关于 RSA 算法的介绍可以看看之前本人写的这篇文章。
这篇Remote Timing Attacks are Practical[3] 论文中指出(我大致翻译下摘要,感兴趣的同学可以通过文末链接去看原文):
计时攻击往往用于攻击一些性能较弱的计算设备,例如一些智能卡。我们通过实验发现,也能用于攻击普通的软件系统。本文通过实验证明,通过这种计时攻击方式能够攻破一个基于 OpenSSL 的 web 服务器的私钥。结果证明计时攻击用于进行网络攻击在实践中可行的,因此各大安全系统需要抵御这种风险。
最后,本人毕竟不是专研安全方向,以上描述是基于本人的理解,如果有不对的地方,还请大家留言指出来,感谢。
RSA算法及一种"旁门左道"的攻击方式
RSA 算法一种常见的非对称加密算法, 常用来对一些在网络上传输的敏感信息进行加密, 本文将概述RSA算法的流程以及一种意想不到的”旁门左道”的攻击方式.
RSA
RSA 算法流程如下,
找到互质的两个数,
p和q, 计算N = p*q确定一个数
e, 使得e与(p-1)(q-1)互质, 此时公钥为(N, e), 告诉给对方确定私钥
d, 使得e*d-1能够被(p-1)(q-1)整除消息传输方传输消息
M, 加密密文C为:消息接受方通过收到密文消息
C, 解密消息M:RSA算法依赖于欧拉定理, 一个简化版本为大致为
a和p互质, 那么有,
,
a的p-1次方 对p取余为1, (a的p-1次方减去1可以整除p).欧拉定理的证明比较复杂,本来有一个绝妙的证明方式的, 但由于微信公众号字数有限, 这里就省略了(什么? 这跟费马有什么关系? 实在要看的可以看文末参考资料)
举个例子
N = pq, 取俩素数p=11, q = 3, N = p * q = 33, 取e与(p-1)(q-1) = 20互质的数e = 3, 然后通过确定私钥,
即取一个d使得3*d -1能 20 被整除, 假设取d=7或者d=67. (3*7-1=20当然能被20整除,3*67-1=200也能被20整除)因此 public key 为
(N=33, e=3), private key 为d=7或者d=67,
假设加密消息M=8,通过加密算法得到密文
C=8^3 % 33 = 17再来看解密, 由
, 得到明文
M = 17^7 % 33 = 8或者M=17^67 % 33=8, 是不是很神奇? (这里^表示多少次方, 后文中的有的表示异或)(来, 安利一个计算器的工具,
bc命令, 支持任意精度的计算, 其实 Mac简单的计算就可以通过前面介绍的 Alfred 可以方便得完成)
RSA 破解
如果需要破解 RSA 的话, 就是需要找到
p和q, 使得pq=33, 如果知道了p和q就能通过公钥N和e反推出私钥d了. 然而大数分解在历史以来就一直是数学上的难题.
当然上面所述的案例较简单, 当 N 很大时, 就特别困难了. 曾经有人花了五个月时间分解了这个数39505874583265144526419767800614481996020776460304936454139376051579355626529450683609727842468219535093544305870490251995655335710209799226484977949442955603(159位数), RSA-155 (512 bits) [from wikipedia].这条路走不通, 就有人走了”旁门左道”了, Stanford 的几个研究者用了两个小时破解了 OpenSSL 0.9.7 的 1024-bit 的 RSA 私钥 (感兴趣的同学可以看他们的论文Remote Timing Attacks are Practical),
用到的方法就是后面提到的时序攻击(或译为”计时攻击”), 主要思想是因为在进行加密时所进行的模指数运算是一个bit一个bit进行的, 而bit为1所花的运算比bit为0的运算要多很多(耗时久),
因此可以通过得到大量消息与其加密时间, 然后基于统计的方法就可以大致反推出私钥的内容.计时攻击(Timing Attack)
计时攻击是边信道攻击(或称”侧信道攻击”, Side Channel Attack, 简称SCA) 的一种, 主要是一种利用不同的输入会有不同的执行时间这个特点.
举个具体的例子, 这个来自playframewok 里用来验证cookie(session)中的数据是否合法(包含签名的验证), 也是我写这篇文章的由来.def safeEquals(a: String, b: String) = {
if (a.length != b.length) {
false } else { var equal = 0 for (i <- Array.range(0, a.length)) { equal |= a(i) ^ b(i) } equal == 0 } }刚开始看到这段源码感觉挺奇怪的, 这个函数的功能是比较两个字符串是否相等, 首先长度不等肯定不等, 立即返回这个是可以理解的,
可是后面的代码得发挥下想象力了, 当然这个逻辑还是好懂: 通过异或操作1^1=0, 1^0=1, 0^0=0, 如果每一 bit 都相等的话, 两个字符串肯定相等, 最后的equal肯定为0, 否则为1.
但从效率角度上讲, 不是应该只要中途发现某一位的结果为1了就可以立即返回 false 了吗? (如下所示)for (i <- Array.range(0, a.length)) {
if (a(i) ^ b(i) != 0) // or a(i) != b[i] return false
}结合方法名称
safeEquals可能知道些眉目, 与安全有关, 延迟计算等提高效率的手段见过不少, 但这种延迟返回的还是很少见.
这种手段可以让调用safeEquals("abcdefghijklmn", "xbcdefghijklmn")和调用safeEquals("abcdefghijklmn", "abcdefghijklmn")的所耗费的时间一样,
防止通过大量的改变输入并通过统计运行时间来暴力破解出要比较的字符串, 这里其实都忽略了对比较字符串长度的attack问题.举个例子, 假设某个用户设置了密码为
password, 通过从a到z(实际范围可能更广)不断枚举第一位, 最终统计发现p0000000的运行时间比其他从任意a~z的都短,
这样就能猜测出用户密码的第一位很可能是p, 然后再不断一位一位迭代下去最终破解出用户的密码. 如果密码通过hash加密后也能通过这种攻击方式得到hash后的密文.当然, 从理论角度上讲这个确实容易理解, 如上文所提到的学术界已经有论文发表指出用这种计时攻击的方法破解了 OpenSSL 0.9.7 的RSA加密算法了.
然而在实际中是否存在这样的攻击问题呢?
因为好像通过统计运行时间总感觉不太靠谱, 这个运行时间对环境太敏感了, 比如网络, 内存, CPU负载等等都会影响.
尽管如此, 各个软件的实现也都采用了这种safeEquals的方法.
JDK 1.6.0_17中的Release Notes中就提到了MessageDigest.isEqual中的bug的修复 (JDK官网隐藏了这个bug详细的信息, 链接进不去, 只有一个概述)
这次变更的diff详细信息[来源](http://hg.openjdk.java.net/jdk6/jdk6/jdk/rev/562da0baf70b)为:
为了防止(特别是与签名/密码验证等相关的操作)被 timing attack, 目前各大语言都提供了响应的安全比较函数, 例如 “世界上最好的编程语言” — php中的:
// Compares two strings using the same time whether they're equal or not.
// This function should be used to mitigate timing attacks; for instance, when testing crypt() password hashes.
bool hash_equals ( string $known_string , string $user_string )
//This function is safe against timing attacks.
boolean password_verify ( string $password , string $hash )各种语言版本的实现方式都与上面的版本差不多, 将两个字符串每一位取出来异或(
^)并用或(|)保存, 最后通过判断结果是否为0来确定两个字符串是否相等.源代码特洛伊木马攻击
最近,我们在 Github 的 Code Review 中看到 Github 开始出现下面这个 Warning 信息—— “This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below.”也就是说我们的代码中有一些 bidirectional unicode 的文本,中文直译作 “双向文本”,意思是一些语言是从左到右的,而另一些则是是从右到左的(如:阿拉伯语),如果同一个文件里,即有从左向右的文本也有从右向左文本两种的混搭,那么,就叫bi-direction。术语通常缩写为“ BiDi ”或“ bidi ”。使用双向文本对于中国人来说并不陌生,因为中文又可以从左到右,也可以从右到左,还可以从上到下。

早期的计算机仅设计为基于拉丁字母的从左到右的方式。添加新的字符集和字符编码使许多其他从左到右的脚本能够得到支持,但不容易支持从右到左的脚本,例如阿拉伯语或希伯来语,并且将两者混合使用更是不可能。从右到左的脚本是通过ISO/IEC 8859-6和ISO/IEC 8859-8等编码引入的,通常以书写和阅读顺序存储字母。可以简单地将从左到右的显示顺序翻转为从右到左的显示顺序,但这样做会牺牲正确显示从左到右脚本的能力。通过双向文本支持,可以在同一页面上混合来自不同脚本的字符,而不管书写方向如何。
双向文本支持是计算机系统正确显示双向文本的能力。对于Unicode来说,其标准为完整的 BiDi 支持提供了基础,其中包含有关如何编码和显示从左到右和从右到左脚本的混合的详细规则。你可以使用一些控制字符来帮助你完成双向文本的编排。
好的,科普完“双向文本”后,我们正式进入正题,为什么Github 会出这个警告?Github的官方博客“关于双向Unicode的警告”中说,使用一些Unicode中的用于控制的隐藏字符,可以让你代码有着跟看上去完全不一样的行为。
我们先来看一个示例,下面这段 Go 的代码就会把 “Hello, World”的每个字符转成整型,然后计算其中多少个为 1 的 bit。
这个代码你看上去没有什么 奇怪的地方,但是你在执行的时候(可以直接上Go Playground上执行 – https://play.golang.org/p/e2BDZvFlet0),你会发现,结果是 0,也就是说“Hello, World”中没有值为 1 的 bit 位。这究竟发生了什么事?
如果你把上面这段代码拷贝粘贴到字符界面上的 vim 编辑器里,你就可以看到下面这一幕。

其中有两个浅蓝色的尖括号的东西—— <202e> 和 <202d> 。这两个字符是两个Unicode的控制字符(注:完整的双向文本控制字符参看 Unicode Bidirectional Classes):
- U+202E – Right-to-Left Override [RLO] 
表示,开始从右到左显示,于是,接下来的文本10x", 0变成了0 ,"x01 - U+202D – Left-to-Right Override [LRO]
表示,开始从左到右显示,于是,0,"x01中的前4个字符0 ,"反转成", 0,于是整个文本成了", 0x01 
所以,你在视觉上看到的是结果是—— "Hello, World!”, 0x01, 但是实际上是完全是另外一码事。
然后,Github官方博客中还给了一个安全问题 CVE-2021-42574 ——
在 Unicode 规范到 14.0 的双向算法中发现了一个问题。它允许通过控制序列对字符进行视觉重新排序,可用于制作源代码,呈现与编译器和解释器执行逻辑完全不同的逻辑。攻击者可以利用这一点对接受 Unicode 的编译器的源代码进行编码,从而将目标漏洞引入人类审查者不可见的地方。
这个安全问题在剑桥大学的这篇论文“Some Vulnerabilities are Invisible”中有详细的描述。其中PDF版的文章中也给了这么一个示例:
通过双向文本可以把下面这段代码:

伪装成下面的这个样子:

在图 2 中'alice'被定义为价值 100,然后是一个从 Alice 中减去资金的函数。最后一行以 50 的值调用该函数,因此该小程序在执行时应该给我们 50 的结果。
然而,图 1 向我们展示了如何使用双向字符来破坏程序的意图:通过插入RLI (Right To Left Isolate) – U+2067,我们将文本方向从传统英语更改为从右到左。尽管我们使用了减去资金功能,但图 1 的输出变为 100。
除此之外,支持Unicode还可以出现很多其它的攻击,尤其是通过一些“不可见字符”,或是通过“同形字符”在源代码里面埋坑。比如文章“The Invisible Javascript Backdoor”里的这个示例:
上面这个代码实现了一个非常简单的网络健康检查,HTTP会执行 ping -c 1 google.com 以及 curl -s http://example.com 这两个命令来查看网络是否正常。其中,可选输入 HTTP 参数timeout限制命令执行时间。
然后,上面这个代码是有不可见的Unicode 字符,如果你使用VSCode,把编码从 Unicode 改成 DOS (CP437) 后你就可以看到这个Unicode了

于是,一个你看不见的 πàñ 变量就这样生成了,你再仔细看一下整个逻辑,这个看不见的变量,可以让你的代码执行他想要的命令。因为,http 的请求中有第二个参数,这个参数可奖在后面被执行。于是我们可以构造如下的的 HTTP 请求:
http://host:port/network_health?%E3%85%A4=<any command>
其中的,%E3%85%A4 就是 \u3164 这个不可见Unicode 的编码,于是,一个后门代码就这样在神不知鬼不觉的情况下注入了。
另外,还可以使用“同形字符”,看看下面这个示例:
如何你以为 ǃ 是 惊叹号,其实不是,它是一个Unicode ╟â。这种东西就算你把你的源码转成 DOS(CP437) 也没用,因为用肉眼在一大堆正常的字符中找不正常的,我觉得是基本不可能的事。
现在,是时候检查一下你的代码有没有上述的这些情况了……
https://www.compart.com/en/unicode/bidiclass
                    
                
阅读量本身就少的可怜,真相了,
(刚从交流群学会的表情)。

, 
确定私钥,


                
            
        
浙公网安备 33010602011771号