在 x86 体系结构中更改指令的操作码是否可能或有多难?
Is it possible, or how hard it is, to change the op code of an instruction in x86 architecture?
例如,PUSH imm32
的操作码为 68h。是否可以使用另一个数字,例如 69h 来 "represent" 这条指令(假设这个数字没有被其他指令使用)?
"represent",我的意思是在汇编中只要有 PUSH 指令,二进制可执行文件中就会出现 69h。当它最终被 CPU 获取并执行时,它将被传输回 68h。
我知道每个操作码都是根据CPU电路专门设计的,但是我有没有可能只是想用另一个十六进制数作为代理?
当然我不会在CPU上做任何改变,我仍然希望指令在x86架构上执行。
更新:我为什么要问这个问题?
你可能知道 Return Oriented Attack,它故意错误解释机器语言流,并利用标准库中有很多 C3(即 ret)。我最初的想法是,如果我们能够将 return 的操作码从 C3 更改为其他代码,最好是 2 个字节,那么 ROA 将不起作用。我不是建筑领域的专家,我只是发现我的想法在现实中行不通。感谢您的所有回复。
理论上是的...
你可以使用未定义的操作码例外,以防你找到多余的操作码(虽然没有很多空闲点)。异常处理程序将使用适当的操作码修改内存位置并重新执行处理。
但它会在这个内存位置留下 "good" 操作码。
您可以在 "good" 操作码执行后将存储在内存中的 "fix" 操作码设置为 "fake" 单步中断处理程序,然后将其禁用,以免影响性能。
此外,伪造的操作码必须与正确的操作码大小相同(或更长),否则您将不得不按照说明进行备份以防止损坏(被 "good" 操作码覆盖)。
如果 fake 比 true 长,可以用 NOP 填充额外的替换指令。
我不必说它是麻烦的 AF。对于现代操作系统来说,在 DOS 中它会非常简单,它几乎是不可行的解决方案。
My initial thought was, if we are able to change the opcode of return from C3 to some other code, preferably 2 bytes, then the ROA will not work.
不,x86 指令编码是固定的,并且大部分硬连线在 CPU 内部解码器的芯片中。 ( 重定向到微码 ROM 以获取指令的定义,但被识别为指令的操作码仍然是硬连接的。)
我认为即使来自 Intel 或 AMD 的微代码更新也无法将其现有的 CPU 更改为 而不是 将 C3
解码为 ret
. (尽管他们可能会使其他一些多字节序列也解码为非常慢的微编码ret
,但可能只能通过接管现有微编码指令的编码。)
没有将 C3
解码为 ret
的 CPU 将不再是 x86 CPU。或者我猜你可以把它变成一个新的模式,其中的指令编码是不同的。不过,它不再与 x86 二进制兼容。
不过,这是一个有趣的想法。 x86 上的单字节 RET 使得将小工具链接在一起变得非常容易 (https://en.wikipedia.org/wiki/Return-oriented_programming#On_the_x86-architecture)。 (或者意味着有更多的小工具可以链接,给你一个更大的工具箱。)
我不会屏住呼吸等待 CPU 供应商提供一种 ret
使用 2 字节操作码的新模式。不过,这是可能的(对于 CPU 供应商进行新设计, 而不是 让您破解现有的 CPU)。通过使它成为一个单独的模式(比如 64 位长模式与 64 位内核下的 32 位兼容模式,与 32 位内核下的 "legacy mode" 相比)操作系统仍然可以在这样的 CPUs,你可以在同一个内核下 mix/match user-space 进程,一些为 x86 编译,一些为 new86 编译。
如果供应商要引入一种新的不兼容模式,该模式不能 运行 现有的二进制文件,希望他们会对指令集进行其他清理。例如通过让它们始终写入 FLAGS,即使计数 = 0,也可以消除变量计数移位对 FLAGS 的错误依赖。或者完全重做操作码,不要在 1 字节 xchg eax, r32
上花费太多编码 space,以及缩短 SIMD 指令的编码。但是他们无法与常规 x86 解码器共享尽可能多的解码器 t运行sistors。任何像 EFLAGS 语义变化这样的变化都可能需要改变后端,而不仅仅是解码器。
他们还可以使 [rsp+disp8/32]
寻址模式缩短 1 个字节,也许使用不同的寄存器作为即使没有索引也始终需要 SIB 字节的寄存器。 (-fomit-frame-pointer
现在很典型,所以相对于堆栈指针的寻址会花费额外的字节。)
请参阅 Agner Fog 的 Stop the instruction set war 博客 post 了解有关 x86 指令编码有多混乱的更多详细信息。
至少需要对 CPU 电路设计进行多少更改才能使 c3
要求第二个字节为 c3
的 2 字节指令的开始=20=]?
Intel CPUs 多级解码:
指令长度预解码器找到指令边界,将指令字节放入队列中(每个周期最多处理 16 个字节或 6 条指令,以较低者为准)。请参阅 https://www.realworldtech.com/sandy-bridge/3/ 的框图。
解码器从该队列中抓取 4 条(或 Skylake 中的 5 条)指令,并将它们并行提供给实际的解码器。每个输出 1 个或多个微指令。 (请参阅 David Kanter 的 SnB 文章的下一页)。
一些CPUs在L1i缓存中标记指令边界,并在一行从L2到达时进行解码。 (AMD 比英特尔最近才这样做,但 IIRC Ryzen 没有,英特尔也没有在 P6 或 SnB 系列中。参见 Agner Fog's microarch guide。)
c3
是一个没有后续字节的单字节操作码这一事实已硬连接到指令长度解码器中,因此必须更改。
但是第二个字节怎么处理呢?您可以让获得 c3 xx
的解码器检查 xx == 00
,如果没有则引发 #UD
异常(未定义指令,也称为非法指令)。
或者它可以将其解码为 imm8
ope运行d,并让执行单元检查 ope运行d 是否为 0。
让解码器对下一个字节执行这种依赖于模式的检查可能更容易,因为它们无论如何都必须针对不同的模式以不同的方式解码其他 insn。
00
不是 "special"。常规解码器可能会在可能有 15 个字节长(最大 x86 指令长度)的宽输入中接收指令字节。但是没有理由假设他们会查看 bits/bytes 超过指令长度和错误,如果它不是零扩展的话。它可能是这样设计的,但是像 c3
这样的 1 字节操作码的处理很可能是硬连线的,并且没有任何更高的位与任何操作码位进行 ANDed、ORed 或 XORed。
操作码或整个 insn 不是必须进行零扩展的整数。您不能假设存在 "instruction register".
之类的东西
使 c3 xx
不解码为 ret
for xx!=0 基本上仍然会破坏所有现有二进制文件,并且如果您正在制作 CPU 可能仍然需要新模式那样操作。
在 L1i 缓存中标记指令边界的 CPUs 上,始终将 ret
视为 2 字节指令(不包括前缀)是行不通的。 ret
之后的字节成为跳转目标或不同函数的情况并不少见。跳转到另一条指令的 "middle" 会强制这样的 CPU 重做指令边界标记,从缓存行中的那个点开始,然后当你 运行 再次 ret
。
此外,页面最后一个字节中的 c3
后跟未映射的页面不能出现页面错误。但是,如果指令长度解码阶段在让它解码之前总是在 c3
之后获取另一个字节,就会发生这种情况。 (来自不可缓存内存的 运行 代码也会将此计为可观察到的更改。UC 是 CPU 等同于 volatile
)
我想如果 运行 在 ret
的模式下,您可以将长度解码阶段添加到解码器的假 00
字节上是单字节。 ret
是无条件跳转,但如果 [rsp]
不可读,它可能会出错。但我认为异常帧只有指令的起始地址,而不是长度。因此,流水线的其余部分可能认为它是一条 2 字节指令,但实际上只有 1 个字节。
但它仍然必须以某种方式进入 uop 缓存,uop 缓存需要关心 insn start/end 地址,即使是无条件跳转。对于跨越 64 字节高速缓存行边界的指令,如果其中任何一个发生更改,则需要使指令无效。
我的理解是,现实生活中的 CPU 设计总是比您从 David Kanter 的文章中看到的框图所想象的更难、更复杂。
顺便说一句,需要对解码器进行多小的改动并不是特别重要。 只有 CPU 供应商可以在新设计中进行此更改这一事实使您的想法完全无法启动,超出了指令集设计想法。它比完全重新组织 x86 机器代码更合理一些,因为它仍然可以与现有模式共享几乎所有的解码器 t运行sistors。
对此支持全新模式意义重大,需要更改 CPU 的代码段描述符(GDT 条目)解码。
创建 总是 需要 c3
后跟 00
的 CPU 会容易得多,但是它不会是 x86,也不会 运行 绝大部分代码。英特尔或 AMD 出售这样的 CPU 的可能性为零。
例如,PUSH imm32
的操作码为 68h。是否可以使用另一个数字,例如 69h 来 "represent" 这条指令(假设这个数字没有被其他指令使用)?
"represent",我的意思是在汇编中只要有 PUSH 指令,二进制可执行文件中就会出现 69h。当它最终被 CPU 获取并执行时,它将被传输回 68h。
我知道每个操作码都是根据CPU电路专门设计的,但是我有没有可能只是想用另一个十六进制数作为代理?
当然我不会在CPU上做任何改变,我仍然希望指令在x86架构上执行。
更新:我为什么要问这个问题?
你可能知道 Return Oriented Attack,它故意错误解释机器语言流,并利用标准库中有很多 C3(即 ret)。我最初的想法是,如果我们能够将 return 的操作码从 C3 更改为其他代码,最好是 2 个字节,那么 ROA 将不起作用。我不是建筑领域的专家,我只是发现我的想法在现实中行不通。感谢您的所有回复。
理论上是的...
你可以使用未定义的操作码例外,以防你找到多余的操作码(虽然没有很多空闲点)。异常处理程序将使用适当的操作码修改内存位置并重新执行处理。
但它会在这个内存位置留下 "good" 操作码。 您可以在 "good" 操作码执行后将存储在内存中的 "fix" 操作码设置为 "fake" 单步中断处理程序,然后将其禁用,以免影响性能。
此外,伪造的操作码必须与正确的操作码大小相同(或更长),否则您将不得不按照说明进行备份以防止损坏(被 "good" 操作码覆盖)。 如果 fake 比 true 长,可以用 NOP 填充额外的替换指令。
我不必说它是麻烦的 AF。对于现代操作系统来说,在 DOS 中它会非常简单,它几乎是不可行的解决方案。
My initial thought was, if we are able to change the opcode of return from C3 to some other code, preferably 2 bytes, then the ROA will not work.
不,x86 指令编码是固定的,并且大部分硬连线在 CPU 内部解码器的芯片中。 (
我认为即使来自 Intel 或 AMD 的微代码更新也无法将其现有的 CPU 更改为 而不是 将 C3
解码为 ret
. (尽管他们可能会使其他一些多字节序列也解码为非常慢的微编码ret
,但可能只能通过接管现有微编码指令的编码。)
没有将 C3
解码为 ret
的 CPU 将不再是 x86 CPU。或者我猜你可以把它变成一个新的模式,其中的指令编码是不同的。不过,它不再与 x86 二进制兼容。
不过,这是一个有趣的想法。 x86 上的单字节 RET 使得将小工具链接在一起变得非常容易 (https://en.wikipedia.org/wiki/Return-oriented_programming#On_the_x86-architecture)。 (或者意味着有更多的小工具可以链接,给你一个更大的工具箱。)
我不会屏住呼吸等待 CPU 供应商提供一种 ret
使用 2 字节操作码的新模式。不过,这是可能的(对于 CPU 供应商进行新设计, 而不是 让您破解现有的 CPU)。通过使它成为一个单独的模式(比如 64 位长模式与 64 位内核下的 32 位兼容模式,与 32 位内核下的 "legacy mode" 相比)操作系统仍然可以在这样的 CPUs,你可以在同一个内核下 mix/match user-space 进程,一些为 x86 编译,一些为 new86 编译。
如果供应商要引入一种新的不兼容模式,该模式不能 运行 现有的二进制文件,希望他们会对指令集进行其他清理。例如通过让它们始终写入 FLAGS,即使计数 = 0,也可以消除变量计数移位对 FLAGS 的错误依赖。或者完全重做操作码,不要在 1 字节 xchg eax, r32
上花费太多编码 space,以及缩短 SIMD 指令的编码。但是他们无法与常规 x86 解码器共享尽可能多的解码器 t运行sistors。任何像 EFLAGS 语义变化这样的变化都可能需要改变后端,而不仅仅是解码器。
他们还可以使 [rsp+disp8/32]
寻址模式缩短 1 个字节,也许使用不同的寄存器作为即使没有索引也始终需要 SIB 字节的寄存器。 (-fomit-frame-pointer
现在很典型,所以相对于堆栈指针的寻址会花费额外的字节。)
请参阅 Agner Fog 的 Stop the instruction set war 博客 post 了解有关 x86 指令编码有多混乱的更多详细信息。
至少需要对 CPU 电路设计进行多少更改才能使 c3
要求第二个字节为 c3
的 2 字节指令的开始=20=]?
Intel CPUs 多级解码:
指令长度预解码器找到指令边界,将指令字节放入队列中(每个周期最多处理 16 个字节或 6 条指令,以较低者为准)。请参阅 https://www.realworldtech.com/sandy-bridge/3/ 的框图。
解码器从该队列中抓取 4 条(或 Skylake 中的 5 条)指令,并将它们并行提供给实际的解码器。每个输出 1 个或多个微指令。 (请参阅 David Kanter 的 SnB 文章的下一页)。
一些CPUs在L1i缓存中标记指令边界,并在一行从L2到达时进行解码。 (AMD 比英特尔最近才这样做,但 IIRC Ryzen 没有,英特尔也没有在 P6 或 SnB 系列中。参见 Agner Fog's microarch guide。)
c3
是一个没有后续字节的单字节操作码这一事实已硬连接到指令长度解码器中,因此必须更改。
但是第二个字节怎么处理呢?您可以让获得 c3 xx
的解码器检查 xx == 00
,如果没有则引发 #UD
异常(未定义指令,也称为非法指令)。
或者它可以将其解码为 imm8
ope运行d,并让执行单元检查 ope运行d 是否为 0。
让解码器对下一个字节执行这种依赖于模式的检查可能更容易,因为它们无论如何都必须针对不同的模式以不同的方式解码其他 insn。
00
不是 "special"。常规解码器可能会在可能有 15 个字节长(最大 x86 指令长度)的宽输入中接收指令字节。但是没有理由假设他们会查看 bits/bytes 超过指令长度和错误,如果它不是零扩展的话。它可能是这样设计的,但是像 c3
这样的 1 字节操作码的处理很可能是硬连线的,并且没有任何更高的位与任何操作码位进行 ANDed、ORed 或 XORed。
操作码或整个 insn 不是必须进行零扩展的整数。您不能假设存在 "instruction register".
之类的东西使 c3 xx
不解码为 ret
for xx!=0 基本上仍然会破坏所有现有二进制文件,并且如果您正在制作 CPU 可能仍然需要新模式那样操作。
在 L1i 缓存中标记指令边界的 CPUs 上,始终将 ret
视为 2 字节指令(不包括前缀)是行不通的。 ret
之后的字节成为跳转目标或不同函数的情况并不少见。跳转到另一条指令的 "middle" 会强制这样的 CPU 重做指令边界标记,从缓存行中的那个点开始,然后当你 运行 再次 ret
。
此外,页面最后一个字节中的 c3
后跟未映射的页面不能出现页面错误。但是,如果指令长度解码阶段在让它解码之前总是在 c3
之后获取另一个字节,就会发生这种情况。 (来自不可缓存内存的 运行 代码也会将此计为可观察到的更改。UC 是 CPU 等同于 volatile
)
我想如果 运行 在 ret
的模式下,您可以将长度解码阶段添加到解码器的假 00
字节上是单字节。 ret
是无条件跳转,但如果 [rsp]
不可读,它可能会出错。但我认为异常帧只有指令的起始地址,而不是长度。因此,流水线的其余部分可能认为它是一条 2 字节指令,但实际上只有 1 个字节。
但它仍然必须以某种方式进入 uop 缓存,uop 缓存需要关心 insn start/end 地址,即使是无条件跳转。对于跨越 64 字节高速缓存行边界的指令,如果其中任何一个发生更改,则需要使指令无效。
我的理解是,现实生活中的 CPU 设计总是比您从 David Kanter 的文章中看到的框图所想象的更难、更复杂。
顺便说一句,需要对解码器进行多小的改动并不是特别重要。 只有 CPU 供应商可以在新设计中进行此更改这一事实使您的想法完全无法启动,超出了指令集设计想法。它比完全重新组织 x86 机器代码更合理一些,因为它仍然可以与现有模式共享几乎所有的解码器 t运行sistors。
对此支持全新模式意义重大,需要更改 CPU 的代码段描述符(GDT 条目)解码。
创建 总是 需要 c3
后跟 00
的 CPU 会容易得多,但是它不会是 x86,也不会 运行 绝大部分代码。英特尔或 AMD 出售这样的 CPU 的可能性为零。