x86上的意外指令

2021-05-11 10:14:32

永久链接当前是一个草稿。它可能是任意错误的。反馈非常欢迎。本文件(旨在最终)概述用于处理意外指令的技术,我的希望是这对他人有所帮助,但主要的目标是帮助我组织自己的思想并在文学中包装我的脑袋话题。我一直在为客户做这个话题的工作,并将在不久的将来在上游LLVM中进行一些相关的工作。完成后,此写作将作为该项目的背景。

x86和x86-64使用可变长度指令编码。有一些指令只采用一个字节,其他指令可以消耗最多15个字节(架构限制)。这导致有效指令可以在指令流中的任何字节中启动的情况。硬件不强制对分支目标的任何对齐限制,因此每个字节可能是某些跳转的目标。

在描述X86组件时,通常提供单个指令列表。然而,由于解码可以在任何偏移中开始,并且有效地通过一串可执行字节 - 一个预期的一个,并且14个不合预先的未对准流,因此有效地实现了15个并行指令流。许多次并行流将是纯垃圾,但不幸的是,并不总是如此。完全可以在未对准的流中发生有效的指令。这些被称为#34;意外指令"

考虑作为示例,由十六进制字符串&#34表示的字节序列; 89 50 04 d0 c3"以下清单显示了如何使用offset = 0的解码,offset = 1.注意两者都是有效的(但相当不同的)指令序列。对于该特定示例,这些是唯一有趣的偏移,因为所有其他人都会产生其中一个列出的子序列。通常,我们可能必须查看15个不同的偏移,以查看来自相同字节字符串的所有可能的指令序列。

$ yaxdis" 895004d0c3" 0x00000000:895004:mov [rax + 0x4],edx0x00000003:d0c3:rol bl,0x1 $ yaxdis" 5004d0c3" 0x00000000:50:push rax0x00000001:04d0:添加al ,-0x300x00000003:C3:RET

值得注意的是,由于编码是可变的长度,因此许多意外指令序列倾向于最终与预期流中的边界对齐。在实践中,由于x86具有许多有效的一个字节指令和一个通常不是语义的一个字节前缀字节,因此找到一系列错位字节并在原始预期流中的边界处结束并不罕见。这导致仅在序列的前缀未对准的情况下,因此大大增加了攻击者在执行其意想不到的感兴趣的指导之后运动员可以实现有趣的控制流的容易性。 (这在不同的上下文中观察到"对可执行代码的混淆来改善电阻较令人抵抗"(第3.1节)。)

最后一点复杂性出现了(未对准的)流中的字节的解释,其中DON' t对任何已知指令进行了解码。不幸的是,该陈述的关键部分是单词"已知"不幸的是,它在文献中已经很好地建立了它,只要一个字节序列ISN' t被记录为具有含义并不意味着它不会有效果。事实证明,真实的处理器行为可以与文档不同。例如:

各个代代的英特尔处理器在处理指令上处理冗余或重复的前缀字节。结果,在不知道执行字节流的确切处理器,它&#39是不可能准确地解码这种情况。对于这种特殊的情况,谢天谢地,所有已知的行为都忽略冗余前缀或生成非法指令故障。

在某些通孔处理器上,字节序列0f3f将控制到高度特权的协处理器,尽管不是作为文档的有效指令。

虽然最后一个案例是一个极端的例子,但它不是不合理的,希望在执行垃圾字节时具有意外行为。处理器充满了无证指示,正如桑德斯这样的工具所记录的那样。 Sandsifter的另一个不舒服结果是AMD和英特尔偶尔为相同的指令实施不同的语义(例如,近呼叫附近的大小前缀)。值得注意的另一个案例是,随着ISA的扩展,之前"垃圾"字节突然有意义(例如,AVX512使用先前为空的编码空间)。因此,不可能正确地解码一些指令而不知道代码正在运行的CPU。

因此,根据我们的威胁模型,当处理未对准的流中出现的垃圾字节时,我们可能需要非常注意。至少建议一个适当的偏执型工程师不要假设执行垃圾字节将是确定的故障。允许疏忽可能已经足够,但原则上有没有任何阻止这些未知的效果包括控制流或其他任意处理器副作用。

在我们潜入我们如何避免或呈现无害的意外指令之前,请让'花点时片刻并涵盖一些用例。如果没有别的话,这有助于构建我们的想法。

对于逆向工程,调试和利用分析,通常需要拆卸二进制文件。对于这种用例,意识到意外指令的存在是主要目标。据我所知,没有工具呈现并行执行流的良好工作。相反,典型的流动需要人们通过在不同的偏移中尝试拆卸而迭代。

在轻质(即用户模式)沙箱技术的领域中,它'很需要禁止特定指令发生在沙箱代码内。可能不允许的操作码的示例包括:SYSCALL,VMCALL,用户模式中断,PKEY操纵,段状态操作或设置方向标志。我们' LL稍后返回此应用程序更深入。

对于返回面向返回的编程(ROP)风格攻击,意外指令经常用于形成"小工具"这将被关在一起攻击者的期望执行。减轻这种攻击损坏的一种方法是减少可用小工具的数量。我单独列出了Sanboxing,强调缓解可能采取简单地减少可用小工具的数量,而不是其彻底消除。除了RET指导之外,减轻的减少对减少了许多相同的指导家庭的人数,也可能有兴趣的人在沙箱时出现。 (出于同样的原因!)

一种值得突出显示的特定形式的沙箱​​是使用沙箱来优化不受信任的代码的执行。与其他沙箱技术的关键差异是假设存在回退安全执行机制,但是该机制意味着可以在常见情况下避免的开销。示例可以包括用于JVM的优化JNI调度,陷阱和步骤系统(见下文),或者用户为查询引擎提供优化二进制文件。此用例中的关键差异是,未能完全沙箱一段代码是可接受的(如果不是理想的)结果,因为始终可以拍摄慢路径。

我确实想突出这些类别之间的线条有点模糊,并且可以进行解释。是一个尝试沙箱用户代码,但未能考虑未记录的指令问题(上文所述)或侧侧频道攻击沙箱或缓解的映射?我不在回答这个问题时看到了很多价值。这种写作侧重于它们之间的共性,而不是区别。我更像最强烈减轻最强烈的频谱。重要的是要承认,我们发现我们对新问题的力量变化的看法。

有三个主要的方法I' m意识到:陷阱和检查,避免生成和控制可达性。让&#39或者依次通过每个人。

通过在加载时间识别所有有问题的字节序列(无论是意图还是意外),然后使用一些断点 - 类似机制的组合来陷阱围绕感兴趣的字节序列的代码执行。机制I' m意识到涉及硬件断点,页面保护技巧,中断处理程序中的单步,或动态二进制转换。总而言之,某种故障处理程序是合理的,用于确保未经预期的指令areN' t执行(例如,程序计数器从未指向意想到的指令的开始,而是通过预期指令流的步骤)。最坏的情况表现这种系统趋于差(捕获热路径可能非常昂贵),但是当意外指令不在热路径中时以天然速度执行。它们也倾向于在运行时更简单,因为它们不需要工具链更改。

涉及在(硬件)控制流程图中禁止边缘的机制。核心思想是防止控制流程指令将控制转移到意想到的指令的偏移量。这最终是一组控制流程的子集,其中有几百个方法具有不同的权衡。对我来说,核心外卖是实现合理的实现复杂性,全面并发支持,低性能开销极为挑战。我们' LL稍后回来讨论两种这种方法有点深度。

涉及用于工具链的一些调整,用于生成二进制(并且可能是动态加载器),以避免将意外指令引入二进制文件以开始。这是我们的技术系列' LL在下面讨论的时间很多。

我以似乎最简单到最复杂的顺序列出了这些。不幸的是,这两个前者都很难解决挑战,所以我们'如果我们最终会花费大部分时间谈论第三个。

陷阱和检查方法的挑战是,对于具有大量意外指令的并发程序非常努力地实现。使用硬件断点处理井的少数(例如< 4)井 - 这足以让某些用例。当意外指令的数量超过调试寄存器的数量时,并发结果将成为核心挑战。关键竞赛涉及一个无法保护页面的一个线程,以允许它在单步模式下进行进度,然后再访问同一页面,从而绕过检查。最终需要确保如果任何线程必须通过单个线程的页面单步,即单个踏步或停滞不前。值得注意的是,避免发出大多数(但不是全部)意外指令的工具链将与陷阱和检查回退搭配很好。

可用的其他主要方法是动态二进制翻译。建立这种系统的复杂性大多超出了本文档的范围。我将简要提到,在页面中每种可能的偏移量拦截执行的需要显着使劫持性复杂化。它可以完成(例如,通过用Int3修补来源),但复杂性与性能权衡有挑战性。

"本机客户端:用于便携式的沙箱,不受信任的x86本机代码"是我看到的最强大的方法之一。 NACL通过确保所有分支目标是32字节对齐,并且没有指令交叉32字节边界,防止执行意外指令。 NACL' S指令捆绑支持已经在LLVM' S汇编程序中实现,并且捆绑的运行时成本非常低。

NACL的主要挑战是返回保护的性能开销。返回结合了三个操作:从堆栈中的返回地址的加载,堆栈指针的调整和间接分支。有效仪器的问题在于,在并发环境中,我们需要在负载后仪器,但在分支之前。这可以' t完成。相反,我们必须使用备用指令序列。这样做的主要效果是有效地禁用了返回预测。这是相当昂贵的 - 虽然我已经and#39;它能够在准确地找到良好的数字。

Intel'即即将到来的控制流行实施技术(CET)技术在本次讨论中具有高度相关性。 CET包含两个关键件:分支终止指令和单独的硬件托管返回堆栈。 CET肯定是一个有趣的一步,但它是一个完整的解决方案。 ENDBR64(新分支机构指令)本身可以在意外指令发生!因此,当CET确实很大程度上减少了可用的小工具的数量,而且它不会完全消除它们。我们' D仍然需要一些处理意外腹部的机制,以成为一个完整的沙箱解决方案。

在本文件的末尾,我们' LL更详细地讨论CET。 TLDR结果是CET不完整的同时,在实践中建立一个完整的解决方案是一个相当良好的起点。

在本节中,我们'重新讨论在重写组件时常用的一些策略,以避免嵌入意外指令。这些在组装语义方面描述,但本节是实施中立的。这些可以由编译器,汇编程序,运行时二进制重写器实现,甚至是手写组件中的仔细人员实现。可能需要对X86指令编码进行基本的理解来进行意义。

当意外指令交叉两个或更多个预期指令之间的边界时,可以通过在两个预期的指令之间插入填充字节来破坏序列。根据所消除的指令类,可以使用冗余前缀字节,单个字节NOP指令(0x90)或MOVL%EAX,%EAX的语义NOP。通过填充指令中的字节是否可以形成有效的后缀(或前缀)来控制填充的选择,其具有形成另一个有问题的意外指令的有效后缀(或前缀)。根据有问题指令的类,所选择的填充序列必须不同。

从性能角度来看,前缀字节优先于单个字节NOPS,这是其他指令的优选。

这是迄今为止最复杂的案例。我'请参阅读者对erim和免费文件的细节感兴趣,并将自己限制在这里的一些评论。这很远进入杂草;除非实施这样的工具,否则大多数读者可能最佳撇去撇去此方法。

我发现很难说服自己的纸张的完整性'重写规则。他们似乎严重依赖于X86解码规则的完整分类,事先经验让我对此非常犹豫。很容易认为你有完全覆盖,而实际上缺少重要情况。

作为一个特定的示例,似乎没有ERIM或G无,似乎考虑前缀字节形成非预期指令的一部分的情况。从先前的X86经验中,这似乎是值得怀疑的。一个目标模糊可以快速找到示例指令VPalignR $ 239,(%rcx),%xmm0,%xmm8,它们编码为c463790f01ef,从而将WRPKRU指令嵌入其后缀。此示例使用三字节的vex前缀来更改操作码字段的解释。

有时需要提到的每个技术需要重新分配寄存器。这通常很难完成,因为可能没有用于清除的寄存器。描述了这一点的技术都使用了后期编译后的重写通行证并倒回堆栈溢出(这是Abi Breaking!)。

旁边:为什么溢出abi打破?如果二进制重写工具插入推/流量对以释放寄存器,并且不调整与函数关联的所有元数据(例如,.ehframe,.stacksize,.dbg。*部分)运行时机械的各种比特(例如分析者,垃圾收集器,例外展开)可能会混淆。这是否在技术上是ABI问题,而不是我' LL作为读者锻炼;无论如何,我认为这是有问题的。

有一点我没有看到任何纸张,我们可以愿意通过愿意将计算进行清单来清除寄存器。作为示例,如果帧大小是常数,但代码保留帧指针,则可以在本地重写后可靠地清除和复制RBP。 (假设帧大小并不是其本身至少形成问题立即。)

在离线讨论中提出的另一个想法是通过将内容移动到空闲向量寄存器(XMM,YMM或ZMM)中来清除一般寄存器。这将有效,但仍然注册了扫除,找到了免费矢量寄存器加上一些新的寄存器操作代码。它可能在实践中减少了较少,但不遵守概念洞。

它'很诱人,使这个编译器(专门注册分配)责任,但由于它需要了解编码,因此需要打破编译器VS汇编抽象。我们可能能够通过调整指令成本来欺骗编译器,但它并不清楚,这在现有的寄存器分配基础架构中会良好。

另一种方法是保留免费注册(即保证清除可以成功),但这听起来非常昂贵的表现明智。也许你可以免费保留一个矢量寄存器吗?也许我们有寄存器分配器对待潜在的有问题的指示,好像它们堵塞了额外的寄存器?这将迫使自由寄存器至少具有更高的本地化损坏。它需要打破编译器/汇编程序抽象一点。

相对分支是一个共同的重要情况,因为我们的许多意外指令发生了编码小整数常量,而短分支非常常见。这里的技术还可用于PC相对数据负载(例如恒定池等)。

如文件中所述,我们可以将NOPS插入到遇到遇到非预期指令的erurburmplatey字节中的erurburb位移字节。鉴于小endian编码,我们可以通过在包含的预期指令之前或之后添加单个NOP来调整第一个字节。 (如果匹配一组相邻的编码,我们可能需要多个。)

另一个字节是棘手的。使用填充的其他字节快速调整到真正昂贵的代码明智。我们有三种主要技术向我们开放:

如果意外指令在预期的指令' s位移字段的末尾结束,并且我们可以合法使用后对齐和检查模式,我们可以简单地添加后检查。 (此与上面的NOP案例重叠,并且当存在还需要更改的其他字节或对最后一个字节的多个有问题的编码时,最有用。)

如果我们可以清除寄存器,我们可以使用LEA形成地址的一部分,然后在指令上使用较小的偏移量。

我们可以用分支替换到蹦床的指令,然后将分支回到实际目标(对于分支),或者执行原始指令,然后执行到下一个指令(用于其他PC相对地址)。新的相对位移不太可能仍然编码有问题的指令。在编译器或汇编程序中,这是一种直接的方法。对于二进制重写工具,请参阅指令的注释

......