无分支编码

2021-05-05 20:50:22

因此,我最近受到同事的设计启发,可以将JSON(JavaScript对象符号)数据包装成64位元素数组中的一堆位字段。二进制算术,比特包装和高性能对我来说很有趣,所以我想到了一些方法来改善它,以便学习目的。然后我有一个想法:也许我可以在不使用条件分支(如果语句)的情况下解压缩/包装比特。

性能和安全性。现代CPU经常会对达到条件跳跃进行分支预测,这基本上是猜测。如果CPU猜测错误,则需要展开其更改,然后执行IF语句的其他分支。错误的猜测是绩效命中和潜在的信息泄漏。在同一台机器上运行的攻击者可以花时间进行操作需要多长时间来推断CPU的猜测,例如幽灵。对于更高级的操作,例如加密和服务器端API,时序攻击可以显示个人信息。

我们在代码中的分支机构越少,CPU可以执行它的速度越快。猜测错误=)

打开包装的二进制格式通常需要一堆位操作。假设我们有一个8位二进制结构,其中包含8个布尔配置选项,这是我们如何解压缩它的方式:

测试:= make([] bool,8)... var输入uint8 = uint8(* num)//将每个布尔设置为输入IF输入&amp的位;(1&lt; 0)!= 0 {测试[0] = true}如果输入&amp;(1&lt; 1)!= 0 {tests [1] = true}如果输入&amp;(1&lt; 2)!= 0 {tests [2] =真实}如果输入&amp;(1 <3)!= 0 {tests [3] =真}如果输入&amp;(1&lt; 4)!= 0 {tests [4] = true}如果输入&amp;(1&lt; 5)!= 0 {tests [5] = true}如果输入&amp;(1&lt; 6)!= 0 {tests [6] = true}如果输入&amp;( 1&lt; 7)!= 0 {tests [7] = true} ...

我缩短了它有点可读性。基本上,我们一次检查每个输入位,并将结果存储到bool。现在,让我们看一下装配输出:

404 B.Go:19 Movq 86 + 96(SP),AX; AX = INPEP409 B.GO:19 NOP; 416 B.GO:19 TestB $ 1,Al;旗帜= al&amp; 0B0001418 B.Go:19 JEQ 862;如果标志{JMP} 424 B.Go:20 Movq 85 + 112(SP),CX; CX =测试[] 429 B.Go:20 Movb $ 1,(CX);测试[0] = TRUE432 B.GO:22 Testb $ 2,Al;旗帜= al&amp; 0B0010434 B.Go:22 JEQ 440;如果标志{JMP} 436 B.Go:23 Movb $ 1,1(CX);测试[1] = TRUE440 B.GO:25 TestB $ 4,AL442 B.Go:25 JEQ 448444 B.Go:26 Movb $ 1,2(CX)448 B.Go:28 TestB $ 8,Al450 B.Go:28 JEQ 456452 B.Go:29 Movb $ 1,3(CX)456 B.Go:31 TestB $ 16,Al458 B.Go:31 JEQ 464460 B.Go:32 Movb $ 1,4(CX)464 B.Go:34 TestB $ 32 ,Al466 B.Go:34 JEQ 472468 B.Go:35 Movb $ 1,5(CX)472 B.Go:37 TestB $ 64,Al474 B.Go:37 JEQ 480476 B.Go:38 Movb $ 1,6(CX) 480 B.Go:40 TestB $ -128,Al482 B.Co:40 JEQ 488484 B.Go:41 Movb $ 1,7(CX)488 B.Go:44 Movq CX,(SP)... 862 B.Go :23 MOVQ 85 + 112(SP),CX;相同的MODQ作为位置418867 B.Go:19 JMP 432; (阑尾A用于解释)

这是很多有条件的分支。在有条件分支的情况下,有CPU分支预测,并且存在分支预测的位置,有性损耗和潜在的矢量用于定时攻击等幽灵。偶尔读取配置文件,这可以忽略不计。如果这是频繁运行的热代码,则具有敏感的信息,也许我们应该删除分支机构。

我们可以在没有条件跳转的情况下解压缩这些布尔值吗?是的!我们甚至可以在Go,而没有三元算子=)

测试:= make([] bool,8)... var输入uint8 = uint8(* num)... //将每个布尔设置为输入测试[0] =(输入&amp;(1&lt; &lt; 0))!= 0tests [1] =(输入&amp;(1&lt; 1))!= 0tests [2] =(输入&amp;(1&lt; 2))!= 0tests [3 ] =(输入&amp;(1&lt; 3))!= 0tests [4] =(输入&amp;(1&lt; 4))!= 0tests [5] =(输入&amp;(1&lt; &lt; 5))!= 0tests [6] =(输入&amp;(1&lt; 6))!= 0tests [7] =(输入&amp;(1&lt; 7))!= 0

因此,这几乎是相同的,除了我们将位字段转换为布尔值而不是使用if语句。 C会让我们自动将int转换为Booleans,但Go不允许将布尔值转换为任何东西。但是,如果在某种情况下,可以使用的任何表达式也可以作为布尔变量的值来使用;-)

404 N.GO:19 MOVIQ 86 + 96(SP),AX; Ax = Input409 N.Go:19 TestB $ 1,Al;旗帜= al&amp; 0B0001411 N.GO:19 MOVIQ 85 + 112(SP),CX; cx =测试[] 416 n.go:19 setne(cx);测试[0] = FLAB419 N.GO:20 TestB $ 2,Al;旗帜= al&amp; 0B0010421 N.Go:20 Setne 1(CX);测试[1] = FLAB425 N.GO:21 TestB $ 4,Al427 N.Go:21 Setne 2(CX)431 N.Go:22 TestB $ 8,Al433 N.Go:22 Setne 3(CX)437 N.Go: 23 TESTB $ 16,AL439 N.GO:23 SETNE 4(CX)443 N.GO:24 TESTB $ 32,AL445 N.GO:24 SETNE 5(CX)449 N.GO:25 TESTB $ 64,AL451 N.GO:25 Setne 6(CX)455 N.Go:26 TestB $ -128,Al457 N.Go:26 Setne 7(CX)461 N.Go:28 Movq CX,(SP)

检查一下,没有分支!没有分支,没有分支预测 - 至少在此代码中=)。基本上,Go利用setne x86指令,以便在有条件地设置布尔变量而不是使用条件跳转。

现在我们有一个二进制解包器在不断的时间内运行,我们如何写作反向? Go不允许我们直接将二进制值转换为位。我们不能只像我们在C中的整数一样比赛:

#include&lt; stdio.h&gt; #include&lt; stdbool.h&gt; // c99 bool支持int main(){bool a = true; bool b = true; bool c = false; bool d = true; printf(&#34;%x%x%x%x \ n&#34;,d,c,b,a); //只有一行= d unsigned int = a | (b <1)| (C 2)| (D <第3条); Printf(&#34;结果:0x%x \ n&#34;出);返回0;}

旁边:在代码高尔夫中击败它很少见;这是一些成就吗?此外,这段代码只能使用C99风格的BOOL,而不是任何其他非零整数。

在Go中,我们必须使用(如果将BOOL转换为位)。我只是为了清晰起见,这里只是在这里做了4位。

var out 32pa:=标志。 BOOL(&#34; A&#34;假,&#34; A&#34;)PB:=标志。 BOOL(&#34; B&#34;假,&#34; B&#34;)PC:=标志。 BOOL(&#34; C&#34;假,&#34; C&#34;)PD:=标志。 BOOL(&#34; D&#34;假,&#34; D&#34;)旗帜。 parse()a:= * pab:= * pbc:= * pcd:= * pdfmt。 printf(&#34;%t%t%t%t%t \ n&#34;,a,b,c,d)//如果一个{out | = 0x1}如果b { out | = 0x2}如果c {out | = 0x4}如果d {out | = 0x8} fmt。 Printf(&#34;结果:%b \ n&#34;出)

659 U.Go:29 Movblzx&#34;&#34; .a + 87(sp),斧头; AX = A664 U.GO:29 MovL AX,CX; CX = AX666 U.GO:29 ORL $ 2,AX;斧头| 0B0010669 U.Go:35 Movblzx&#34;&#34; .b + 86(sp),dx; dx = b674 u.go:35 testq dx,dx;标志= DX!= 0677 U.GO:35 CMOVLNE AX,CX;如果标志{cx = ax} 680 u.go:32 movl cx,ax; AX = CX682 U.GO:32 Orl $ 4,CX; CX | 0b0100685 u.go:35 movblzx&#34;&#34; .c + 85(sp),dx; dx = c690 u.go:35 testq dx,dx;标志= DX!= 0693 U.GO:35 CMOVLNE CX,AX;如果标志{AX = CX} 696 U.GO:35 MovL斧,CX; CX = AX698 U.GO:35 ORL $ 8,AX;斧头| 0B1000701 U.Go:38 Movblzx&#34;&#34; .d + 84(sp),dx; dx = d706 u.go:38 testq dx,dx;标志= DX!= 0709 U.GO:38 CMOVLNE AX,CX;如果标志{cx = ax} 712 u.go:38 movl cx,(sp);出= CX.

好的,这一个更难挑选。去聪明。要了解发生了什么,知道GO将BOOL类型视为0或1.如C99 BOOL,但更严格。

然后我们在AX上设置0B0010位。是的,在检查B是真的之前,我们将其设置为此。 RL $ 2,斧头

如果B是非零,请将CX设置为AX。请记住,AX具有“如果是真实”的价值。 CMOVLNE AX,CX

然后我们重复这一点,直到我们拥有所有的布尔斯。请注意,AX和CX备用角色。

似乎Go编译器比我预期的更聪明。它看到布局,否定出我们想要完成的内容,然后用条件移动指令替换源代码中的分支机构!再次,再次,恒定时间代码执行,没有分支预测。聪明的go =)

通过这种新的知识,我写了一些服务器端JWT验证代码,这些验证代码主要是常数时间。由于JWT验证是初始认证的一部分,因此可以使用定时攻击来确定针对JWT运行的检查,以搜索潜在的弱点。代码我编写的代码是验证检查并将每个错误条件存储在错误状态变量中。毕竟检查完成后,如果valError!= 0检查是否已运行,以查看是否发生了任何错误,那么成功或错误响应返回给呼叫者。这也具有整洁的副作用,使其更容易编写检查验证错误组合的单元测试。

典型的解决对身份攻击的典型解决方案是设置最小等待时间。这样,任何短路行为都将被屏蔽,并且定时是相同的是否运行所有检查。如果为身份验证完成数据库查找,则最低等待时间更为重要,例如在检查用户帐户时是否存在时。定时差异通常会更大,因此在网络上将更可检测到。据我所知,MySQL和Postgres没有任何机制来防止通过定时泄露信息。

当您的代码取决于不在恒定时间不运行的另一个服务或数据库时,可以使用

要求您在时间上执行代码,以便您可以将计时器设置为比max运行时间一点时间

如果您的代码定时更改,则可能需要更新最低等待时间,因为代码需要超过MWT的代码将脱颖而出

如果他们通过时序公开信息,不会保护服务或数据库

您仍然需要测试和时间您的代码以确保时间信息没有泄漏

防止侧渠攻击是一项挑战。找到关键的安全路径(例如登录,身份验证,验证等)并硬化这些。对于加密学,这是一个正确的。其他一切,正常进行,并希望幽灵错误在您可能使用的共享云主机上咬人。

为了性能,请记住,只有热点真的需要优化,通常会有一些低悬挂的水果,它将比分支编码更大的性能升压。示例:使用非垃圾收集的语言= p

所以...在写这篇文章时,我发现IF版本中发现了一个有趣的组装输出。

404 B.Go:19 Modq 86 + 96(SP),AX409 B.Go:19 NOP416 B.Go:19 TestB $ 1,Al418 B.Go:19 JEQ 862;跳至862424 B.Go:20 Movq 85 + 112(SP),CX;这条线也在862429 B.Go:20 Movb $ 1,(CX);我们需要Skip432 B.Go:22 Testb $ 2,Al;返回此处867434 B.Go:22 JEQ 440436 B.Go:23 Movb $ 1,1(CX)... 862 B.Go:23 Movq 85 + 112(SP),CX;相同的MODQ作为位置418867 B.Go:19 JMP 432;这可能是jeq

等等,为什么424和862相同的指令?为什么我们跳过这个只是重复它,然后跳回来?为什么跳到代码块的末尾?我们只需要在429上跳过作业,那是什么?

我最初认为这是一些聪明的编译器技巧,或者也许是一个错误,但我想不出它应该这样做的原因。

2021-03-22 18:08&lt; fizzie&gt;我没有看过go装配输出,但如果它是类似的编译器,我认为问题是假设需要成为编译器做某事的原因,而不是“与编译器相关的实现细节内部数据结构导致它“。

&lt; fizzie&gt; FWIW,我得到的输出基本相似,除了没有额外的nop。只是跳到最后,“重复”操作,跳回来。如果我不得不猜测,它就像是在做出那样的时候,它没有意识到这两个分支机构最终会以等效的代码结束,因此它必须生成一个单独的“else”分支它位于其他代码块的末尾。

&lt; gnum4n&gt;但是该MOV在分支之前发生。他们自己搬到了分支后

&lt; fizzie&gt;这种情况真的在任何一点中真的“发生”吗?它不像在源代码中有任何明确的东西,说“将其放入寄存器”。

&lt; gnum4n&gt;哦!我明白你的意思了。仅仅因为我在本地函数范围中的第20行之前创建了阵列并不意味着转到第19行之前应该将其加载到寄存器中,因此它假定它需要在第20行或23之前加载它

&lt; fizzie&gt; 类似的东西,是的。 FWIW,它并没有完全解释它如何实现它可以在所有其他IF(而不是以同样的方式翻译它们,但这就是我认为往下归结为“它制作的部分 通过许多中级陈述的大量变革,这就是筹码的落下。 所以,这不是编译器所做的(奇怪的复制),但编译器没有做的是什么,这是为了重复MOV并跳转到我们期望的地方。 您可能已经注意到通过命令行标志提供输入值。 我最初是硬编码的价值观,但Go编译器将它们转换为一个大常数并优化所有决策。 聪明的编译器。