1

例如,PUSH imm32具有操作码 68h。是否可以使用另一个数字,例如 69h,来“表示”这条指令(假设这个数字没有被其他指令使用)?

“表示”是指程序集中有 PUSH 指令的任何地方,69h 都会出现在二进制可执行文件中。当它最终被 CPU 取出并执行时,它将被转移回 68h。

我知道每个操作码都是根据 CPU 电路专门设计的,但是我是否可能只想使用另一个十六进制数字作为代理?

当然我不会对 CPU 做任何改变,我仍然希望指令在 x86 架构上执行。

更新:我为什么要问这个问题?

可能你知道 Return Oriented Attack,它故意曲解机器语言流,并利用标准库中有很多 C3(即 ret)。我最初的想法是,如果我们能够将返回的操作码从 C3 更改为其他代码,最好是 2 个字节,那么 ROA 将无法工作。我不是建筑领域的专家,我只是发现我的想法在现实中行不通。感谢您的所有回复。

4

2 回答 2

4

我最初的想法是,如果我们能够将返回的操作码从 C3 更改为其他代码,最好是 2 个字节,那么 ROA 将无法工作。

不,x86 指令编码是固定的,并且大部分是硬连线在 CPU 内部解码器的硅片中。(微码指令重定向到微码 ROM 以定义指令,但被识别为指令的操作码仍然是硬连线的。)

我认为即使是英特尔或 AMD 的微码更新也无法将其现有 CPU 更改为解码C3ret. (尽管他们可能会使其他一些多字节序列也解码为非常慢的微编码ret,但可能只能通过接管现有微编码指令的编码。)


没有解码的 CPUC3ret不再是 x86 CPU。或者我猜你可以让它成为一种新模式,其中指令编码不同。不过,它不再与 x86 二进制兼容。

不过,这是一个有趣的想法。x86 上的单字节 RET 使得将小工具链接在一起变得更加容易(https://en.wikipedia.org/wiki/Return-orienting_programming#On_the_x86-architecture)。(或者意味着可以链接更多的小工具,为您提供更大的工具箱。)

我不会屏住呼吸等待 CPU 供应商提供ret使用 2 字节操作码的新模式。不过,这是有可能的(CPU 供应商可以进行新设计,而不是让您破解现有的 CPU)。通过使其成为单独的模式(如 64 位长模式与 64 位内核下的 32 位兼容模式,与 32 位内核的“传统模式”)操作系统仍然可以在此类 CPU 上运行,并且您可以在同一个内核下混合/匹配用户空间进程,一些为 x86 编译,一些为 new86。

如果供应商要引入无法运行现有二进制文件的新不兼容模式,希望他们会对指令集进行其他清理。例如,即使计数 = 0,也可以通过让它们始终写入 FLAGS 来消除变量计数移位对 FLAGS 的错误依赖。或者完全重做操作码以不在 1-byte 上花费太多编码空间xchg eax, r32,并缩短 SIMD 指令的编码。但随后他们无法与常规 x86 解码器共享尽可能多的解码器晶体管。任何像 EFLAGS 语义的变化都可能需要后端的变化,而不仅仅是解码器。

他们还可以使[rsp+disp8/32]寻址模式缩短 1 个字节,可能使用不同的寄存器作为即使没有索引也总是需要 SIB 字节的寄存器。(-fomit-frame-pointer现在很典型,所以相对于堆栈指针的寻址会花费额外的字节。)

有关x86 指令编码的混乱程度的更多详细信息,请参阅 Agner Fog 的停止指令集战争博客文章。


至少需要对 CPU 电路设计进行多少更改才能c3启动需要第 2 个字节的 2 字节指令00

Intel CPU 分多个阶段解码:

  • 指令长度预解码器找到指令边界,将指令字节放入队列中(每个周期最多处理 16 字节或 6 条指令,以较低者为准)。有关框图,请参见https://www.realworldtech.com/sandy-bridge/3/ 。

  • 解码器从该队列中抓取 4 个(或 Skylake 中的 5 个)指令,并将它们并行馈送到实际的解码器。每个输出 1 个或多个微指令。(请参阅 David Kanter 的 SnB 文章的下一页)。

一些 CPU 在 L1i 高速缓存中标记指令边界,并在一行从 L2 到达时进行解码。(AMD 比英特尔最近做到了这一点,但 IIRC Ryzen 没有,而英特尔在 P6 或 SnB 系列中没有。请参阅Agner Fog 的微架构指南。)

没有后续字节的单字节操作码这一事实c3被硬连接到指令长度解码器中,因此必须改变。

但是那么如何处理第二个字节呢?您可以让解码器对其进行c3 xx检查,如果没有则xx == 00引发#UD异常(未定义指令,即非法指令)。

或者它可以将其解码为imm8操作数,并让执行单元检查操作数是否为 0。

让解码器对下一个字节进行这种与模式相关的检查可能更容易,因为无论如何它们都必须针对不同的模式对其他 insn 进行不同的解码。


00不是“特别”。常规解码器可能会在可能有 15 个字节长(最大 x86 指令长度)的宽输入中接收指令字节。但是没有理由假设他们会查看超过指令长度的位/字节,如果它不是零扩展的,则会出现错误。它可能是这样设计的,但是对于 1 字节操作码的处理也可能c3是硬连线的,并且没有任何高位与任何操作码位进行与、或或异或运算。

操作码或整个 insn 不是必须零扩展的整数。你不能假设有像“指令寄存器”这样的东西。


c3 xx像 xx!=0 那样不解码ret仍然会破坏所有现有的二进制文件,如果你正在制作一个可以以这种方式运行的 CPU,仍然需要一个新模式。

在 L1i 高速缓存中标记指令边界的 CPU 上,始终将ret其视为 2 字节指令(不包括前缀)是行不通的。ret紧跟在 a 之后的字节成为跳转目标或不同的函数并不罕见。跳转到另一条指令的“中间”会迫使这样的 CPU 从缓存行中的那个点开始重做指令边界标记,然后当你ret再次运行时你会遇到另一个问题。

此外,c3页的最后一个字节中的 a,后跟一个未映射的页,不得出现 pagefault。但是,如果指令长度解码阶段总是c3在让它解码之前获取另一个字节,就会发生这种情况。(从不可缓存的内存中运行代码也会使这被视为可观察到的变化。UC 相当于 CPU volatile

我想如果在单字节模式下运行,您可能会在00解码器的假字节ret上添加长度解码阶段。 ret是无条件跳转,但如果[rsp]不可读,它可能会出错。但我认为异常帧只有指令的起始地址,而不是长度。因此,当它实际上只有 1 时,管道的其余部分可能认为它是 2 字节指令。

但它仍然必须以某种方式进入 uop-cache 中,并且 uop 缓存需要关心 insn 开始/结束地址,即使是无条件跳转也是如此。对于跨越 64 字节高速缓存行边界的指令,如果其中任何一个发生更改,则需要使指令无效。

我的理解是,现实生活中的 CPU 设计总是比您从 David Kanter 的文章等框图中想象的更难、更复杂。


顺便说一句,解码器需要多小的变化并不是特别重要。 只有 CPU 供应商才能在新设计中进行这种更改,这一事实使您的想法在指令集设计想法之外完全无法启动。这比完全重组 x86 机器代码更合理一些,因为它仍然可以与现有模式共享几乎所有的解码器晶体管。

为此支持一种全新的模式将非常重要,需要更改 CPU 的代码段描述符(GDT 条目)解码。

创建一个总是需要c3跟随的 CPU 将是一个更容易的更改00,但它不会是 x86 并且无法运行绝大多数代码。英特尔或 AMD 销售这样的 CPU 的可能性为零。

于 2018-06-19T14:30:06.457 回答
3

理论上是的...

你可以使用未定义的操作码异常,以防你发现多余的操作码(虽然不是很多空闲点)。异常处理程序将使用正确的操作码修改内存位置并重新执行处理。

但它会在这个内存位置留下“好”的操作码。您可以将单步中断处理程序设置为“修复”存储在内存中的操作码,以便在执行“好”操作码后“伪造”一个,然后将其禁用,以免影响性能。

此外,假操作码的大小必须相同(或更长),然后才是正确的,否则您必须按照说明进行备份,以免损坏(被“好”操作码覆盖)。如果 fake 比 true 替换指令长,额外的 spced 可以被 NOP 填充。

我不必提到它是繁琐的AF。在 DOS 中这将非常简单,因为对于现代操作系统来说,这几乎是不可行的解决方案。

于 2018-06-15T14:13:38.467 回答