用侧面频道攻击PROGCOMP

2021-05-07 05:36:10

就像这种撰写一样,我在这个代码游戏中获得了最佳分数。我欺骗了它。就是这样。

代码游戏是一种编程竞争,遵循典型的progcomp格式。有10个问题,每个问题都有两个或三个测试用例。当您提交代码时,它通过STDIN输入输入,并将解决方案写入STDOUT。每个正确的答案都会让你成为一个观点;排行榜首先按积分排列所提交的程序,其次是您的程序完成测试用例的最低经过时间。

最后一点很重要,因为这意味着不允许程序员看到测试用例。如果他们可以,它将普遍普通代码每个测试输入的正确答案。这不是代码中的Progcomps中的问题,因为他们没有根据执行时间得分,只有提交时间。但是,当速度很重要时,获得测试输入的知识允许您为给定的问题编写理论上最佳程序:读取输入,请咨询答案表,写入输出。两个syscalls和几微秒的数组查找。

现在,代码的游戏很难隐藏测试输入。他们不允许您查看程序的输出;只有它是否成功或失败了。这可以防止您只需将测试输入打印到STDOUT。此外,您的程序在受限制的环境中运行:文件系统是只读的,并且阻止了网络调用。因此,您无法通过HTTP请求或其他基于网络的方式潜入输入。

但是一个漏洞熄灭:排行榜列出了每个提交的执行时间。那是一个侧面频道。具体地,我们可以使用时序攻击来恢复有关测试输入的信息,仅使用程序的记录的执行时间。这是我用于在上面链接的“入侵”问题上实现(近)最佳时间的攻击。但首先,要了解方法,让我们来看看一个更简单的问题。

这是“岛屿”的问题描述,这是一个简单的时间攻击的程序。进行攻击时,我们希望最大限度地减少我们必须传输的信息量。请注意,虽然示例输入相当​​冗长,但示例输出是一组一小段少数。因此,在这种情况下,我们将泄漏输出而不是输入。区别并不重要;无论哪种方式,我们都可以构建我们的最佳解决方案。

首先,我们需要解决问题,因为代码服务器的游戏只能告诉您成功测试的执行时间。一旦我们有一个工作计划,我们就可以为每个测试用例建立基线时序。我的程序运行了大约30ms的每个测试用例。然后,我们通过添加睡眠呼叫来介绍我们的泄漏代码。我们将睡觉为n秒,其中n是我们想要泄漏的值,从第一个输出开始:

当我们将此代码推到代码服务器的游戏时,我们得到一个如此如此的响应(实际值已更改):

编译...> Go Build -o Runcompilation成功在267.644988MSRunning示例中

繁荣,我们刚刚泄露了我们的第一个输出!测试1在4秒内运行,因此其第一个输出为4;同样,测试2的第一个输出为7.示例在2秒内运行,因此第一个输出应该是2 - 并且我们可以从问题描述中确认它是。

现在,这只是重复输出[1],输出[2]等的此过程的问题。我们会知道当测试开始失败时我们发现所有这些都有 - 这意味着由于访问不存在的输出索引而导致的程序崩溃。我们还可以通过在将泄漏值写入STDOUT之前通过覆盖输出的内容来查看我们的进度。如果我们的泄漏值不正确,则覆盖实际值会导致测试失败。

一旦我们知道所有输出,我们需要解决另一个问题:将每个输入与其输出匹配。换句话说,我们需要一种方法来为我们的程序确定哪个输入它正在处理。不出所料,我们可以与以前相同的方式泄漏此信息。在“岛屿”的情况下,第一个输入的输入足以区分测试用例。但由于输入数字很大,我们不能将它们乘以一个完整的第二次,就像我们为输出所做的那样 - 测试需要太长而运行。相反,我们将睡觉为输入[0] * time.millisecond。但是当我们这样做时,我们遇到了一个新的并发症:方差。如果执行时间与我们的基线相比有超过1ms,则我们泄露的值将是错误的。恢复正确的输入可能需要很多尝试。

幸运的是,我们可以通过使用两阶段方法来改善这一点。首先,我们将使用1ms乘数来获得输入值的“初始猜测”。然后我们将再次运行该程序,但此时间从输入中减去我们的猜测,并将其余乘以1s。例如,假设我们的基线执行时间为26ms。在我们第一次运行时,我们乘以1ms,执行时间为854ms。这为我们提供了初步猜测828.在第二个运行中,我们从输入中减去828,乘以1S,导致执行时间为4.024s。这告诉我们(具有高置信度),实际值为828 + 4 = 832。

一旦我们将每个测试输入与其输出匹配,我们就可以编写“最佳程序”(改变的实际值):

func main(){//读取4个字节以确定我们'重新运行fst:= make([],4)os.stdin.read(fst [:])开关(fst){case&#34 ; 3000&#34 ;: //示例os.stdout.writeString(" 2 \ n1 \ n0 \ n4 \ n3 \ n5 \ n")案例" 1234"://测试1 os.stdout.writeString(" 1 \ n2 \ n3 \ n4 \ n5 \ n6 \ n \ n \ n \ n \ n \ n \ n \ n \ n \ n \ n \ n \ n \ n \ n \ n \ n \ n \ n \ n \ n7;)案例" 5678&#34 ;: // test2 os.stdout.writestring(& #34; 1 \ n2 \ n3 \ n4 \ n5 \ n6 \ n7 \ n")}}

这表现得非常好 - 但令人惊讶的是,它并不足够快,以获得记分牌。我不确定为什么,但不知何故,顶部程序比这个代码快5毫秒。幸运的是,......的不属于......

这个问题存在一些新的挑战,但如果我解决了它,我知道我可以得到顶级排行榜。当我开始时,最快的程序在大约500ms中运行。关于这些最佳程序的好处是它们的速度与实际问题的计算复杂性无关;代码始终只是查找表。所以我期望我可以提交一个解决方案,即迅速作为我的岛屿解决方案:30ms。

问题描述表明,此问题归结为计算一堆哈希和随后将消息与哈希匹配,以便重建原始的“消息链”。我写了一个直接的(未优化的)解决方案,在前两个测试输入中运行了大约25米,并为第三个测试输入1.5s。然后我设置恢复输出。

此问题的预期输出是一堆连接的消息字符串。恢复所有字符串数据将是乏味的,但幸运的是,有一种简单的方法来压缩它。由于每个消息来自输入阵列中的一个元素,因此我们可以简单地泄露每个消息的索引,而不是消息本身。要检查这是否合理,我泄露的第一件事是每个测试输出中的消息数。第一次测试有4条消息;第二个有19个;第三个有501.我可以手动恢复前两位,但没有办法我会用手泄漏501个价值。为了复制问题,我打算泄漏的值很大,这意味着我无法将它们乘以1s;我需要使用前面提到的猜测和检查策略。这呼吁某些自动化。

我写了一个小型程序,重复生成的新代码,将其推到代码服务器的游戏,解析响应,并使用时间信息来生成下一次迭代。生成的代码包括一个检查,以便如果猜测错误值,它会失败;然后,观察者可以注意到此故障,还原最后的提交,并尝试再次猜测该值。这意味着我最能让程序运行无监督。

但是当我解雇该计划时,很快就会变得显而易见的是,这些“重试”的频率是不可接受的。我的基线执行时间为1.5s具有显着的方差,即使使用两阶段猜测和检查方法,也会导致许多不正确的猜测。测试也花了很长时间才能运行 - 有时只要50秒 - 这意味着错误的猜测可能会导致延迟超过2分钟。我需要改进我的方法。

经过大量的头部划伤,我有一个昙花一现,回想起来似乎令人尴尬的明显。通过填充基线执行时间,我可以减少方差,让我减少睡眠倍增器。在代码:

func main(){start:= time.now()// ...解决方案代码进入这里...时间。leep(5 * time.second - time.since(start))time.sleep(sleepMultiplier * liftedValue) }

我测量了,实际上,这种方法将方差夹在几毫秒。因此,我能够将睡眠倍增器放入10毫秒并使用猜测和检查;大多数时候,第一次猜测足够准确,即不需要第二次通过。我荣耀地部署了这段新代码并观看了它的跳闸。这是一个缓慢的过程,因为代码服务器的游戏是限制性的,因为在每个推动之间强制实施10s延迟。但在每价值15岁时,只需两个小时即可计算所有501。

一旦计算所有值,我构建了一个问题的近最优解(实际值改变):

var示例= [] {2,1,3} var test1 = [] {1,2,3,4} var test2 = [] {1,2,3,...} // 16值省略var test3 = [] {1,2,3,...} // 498值省略func main(){var js [] [2] json.newdecoder(os.stdin).decode(& js)var indices []开关len(js){case 6:indices =示例=示例案例1:indices = test1 case 2:indices = test2 case 3:indices = test3}消息:= make([],len(indices))用于i:=范围索引{消息[i] = js [indices [i]] [1]}} os.stdout.writeString(strings.join(消息,""))}

> Go Build -o Runcompilation成功在210.389187MSRunning示例中取得了成功......示例1在15.855545MsRunning测试中取得成功......测试1成功14.883978MSTEST 2成功21.048268MST最多的成功3次成功在16.664773MS1中取得了成功,其中26.597019ms成功了。

成功!由于json解析慢,我们在这里丢失了一段时间,但这种解决方案仍然比其他任何人都快10倍!

拉开这个黑客很有趣。人们似乎是拆分它是否被视为作弊。确实,我只使用公开可用的信息 - 我没有利用服务器代码中的缓冲区溢出或类似的东西 - 但这显然不是如何解决问题。我觉得有点愧疚地从花费大量时间优化代码的人那里偷走顶级排行榜的罪名。但是,嘿,我也在我的方法中投入了相当多的时间,所以......这是一个洗。对于它的价值,我不打算对任何其他代码游戏执行这次攻击,我劝阻其他人这样做。我希望Codes团队的比赛发现我的滑稽动态有趣(而不是威胁),我希望这会发现您对时序攻击和其他侧渠道的兴趣。

更新:代码游戏团队从排行榜中删除了我的提交,但此处仍然可见。他们还发布了一项公告,宣布了解硬编码的解决方案将没有资格赢得奖品。 isupport他们的决定。