如果 x86 jmp 跳转到其他两个连续有效地址之间的地址会发生什么?
What happens if x86 jmp jumps to an address between two other consecutive valid ones?
在 x86 程序集的例程中,如果代码包含指向两个有效地址之间的有效地址的跳转,会发生什么情况?这是一个人为的例子:
0x0001: mov ...
0x0005: add ...
0x0009: jmp 0x0003
此外,我怎样才能在本地机器或在线上试验这样的东西?我检查了像https://defuse.ca/online-x86-assembler.htm#disassembly这样的在线x86编辑器,但它不允许我输入像“0x0001”这样的指令地址。
没有所谓的“有效”或“无效”地址。每一个地址都可以跳转到,如果对应的页被映射,则执行。
那么当你在指令“之间”跳转时会发生什么?好吧,处理器不知道你打算从哪里开始和结束指令。它只是执行它看到的字节。此代码将与您预期的不同,因为 CPU 试图将其他一些指令的中间部分解析为操作码。
您的具体示例不够具体,我无法说明指令的结果。也许你可以提供一个完整的示例(包括机器代码),以便我给出更好的解释。
CPU会在目标地址开始解码指令。
您在反汇编中看到的指令流(当使用像 objdump
这样的工具时)只是对程序可执行字节的一种解释,假定给定的起点。
碰巧“跳入指令的中间”是一种混淆技术,有时恶意软件会使用它来对线性扫描反汇编程序(如 objdump
)隐藏程序语义。更复杂的反汇编程序将尝试遵循这些“未对齐”的跳转,但这可能是不可能的,具体取决于 can/can未确定的内容 statically/dynamically。
论文 "Obfuscation of executable code to improve resistance to static disassembly" by Linn and Debray 对此进行了更详细的讨论。
参见第 3.2 节“垃圾插入”。您描述的场景是他们所说的“部分或完全重叠指令”,即字节流的不同解释可以为重叠地址范围提供不同的汇编指令。
我最近在 codegolf 的“使用 x86/x64 机器代码打高尔夫球的技巧”中添加了一个关于 skipping instructions 的技巧。你会发现那些是跳入先前指令的一部分的有意应用。而且不仅仅是为了混淆。这是该答案的完整文本:
跳过指令
跳过指令是与一个或多个后续操作码组合的操作码片段。后续操作码可以与不同的入口点一起使用,而不是预先设置的跳过指令。使用跳过指令代替无条件短跳转可以节省代码space,速度更快,并设置附带状态如NC
(No Carry)。
我的示例都是针对 16 位 Real/Virtual 86 模式的,但是很多这些技术可以类似地用于 16 位保护模式,或者 32 位或 64 位模式。
11: Skipping instructions
The constants __TEST_IMM8, __TEST_IMM16, and __TEST_OFS16_IMM8 are defined to the respective byte strings for these instructions. They can be used to skip subsequent instructions that fit into the following 1, 2, or 3 bytes. However, note that they modify the flags register, including always setting NC. The 16-bit offset plus 16-bit immediate test instruction is not included for these purposes because it might access a word at offset 0FFFFh in a segment. Also, the __TEST_OFS16_IMM8 as provided should only be used in 86M, to avoid accessing data beyond a segment limit. After the db instruction using one of these constants, a parenthetical remark should list which instructions are skipped.
86 模式定义 in lmacros1.mac 323cc150061e (2021-08-29 21:45:54 +0200):
%define __TEST_IMM8 0A8h ; changes flags, NC
%define __TEST_IMM16 0A9h ; changes flags, NC
; Longer NOPs require two bytes, like a short jump does.
; However they execute faster than unconditional jumps.
; This one reads random data in the stack segment.
; (Search for better ones.)
%define __TEST_OFS16_IMM8 0F6h,86h ; changes flags, NC
16 位模式下的 0F6h,86h
操作码是 test byte [bp + disp16], imm8
指令。我相信我实际上并没有在任何地方使用这个。 (事实上,堆栈内存访问实际上可能比无条件短跳转慢。)
0A8h
是任何模式下 test al, imm8
的操作码。 0A9h
操作码在 32 位和 64 位模式下变为 test eax, imm32
形式的指令。
两个用例in ldosboot boot32.asm 07f4ba0ef8cd (2021-09-10 22:45:32 +0200):
首先,为一个公共函数链接两个不同的入口点,这两个入口点都需要初始化一个字节大小的寄存器。 mov al, X
指令每条占用 2 个字节,因此 __TEST_IMM16
可用于跳过一条这样的指令。 (如果有两个以上的入口点,可以重复此模式。)
error_fsiboot:
mov al,'I'
db __TEST_IMM16 ; (skip mov)
read_sector.err:
mov al, 'R' ; Disk 'R'ead error
error:
其次,某个入口点需要两个字节的额外拆解,但可以与后面代码部分的 fallthrough case 共享。
mov bx, [VAR(para_per_sector)]
sub word [VAR(paras_left)], bx
jbe @F ; read enough -->
loop @BB
pop bx
pop cx
call clust_next
jnc next_load_cluster
inc ax
inc ax
test al, 8 ; set in 0FFF_FFF8h--0FFF_FFFFh,
; clear in 0, 1, and 0FFF_FFF7h
jz fsiboot_error_badchain
db __TEST_IMM16
@@:
pop bx
pop cx
call check_enough
jmp near word [VAR(fsiboot_table.success)]
这是一个用例 in inicomp lz4.asm 4d568330924c (2021-09-03 16:59:42 +0200),我们依赖 test al, X
指令清除进位标志:
.success:
db __TEST_IMM8 ; (NC)
.error:
stc
retn
最后,这里是 DOSLFN Version 0.41c (11/2012) 中跳转指令的非常相似的用法。他们使用 mov cx, imm16
而不是 test ax, imm16
,它对状态标志没有影响,但会破坏 cx
寄存器。 (操作码 0B9h
在非 16 位模式下为 mov ecx, imm32
,并写入完整的 ecx
或 rcx
寄存器。)
;THROW-Geschichten... [english: THROW stories...]
SetErr18:
mov al,18
db 0B9h ;mov cx,nnnn
SetErr5:
mov al,5
db 0B9h ;mov cx,nnnn
SetErr3:
mov al,3
db 0B9h ;mov cx,nnnn
SetErr2:
mov al,2
SetError:
在 x86 程序集的例程中,如果代码包含指向两个有效地址之间的有效地址的跳转,会发生什么情况?这是一个人为的例子:
0x0001: mov ...
0x0005: add ...
0x0009: jmp 0x0003
此外,我怎样才能在本地机器或在线上试验这样的东西?我检查了像https://defuse.ca/online-x86-assembler.htm#disassembly这样的在线x86编辑器,但它不允许我输入像“0x0001”这样的指令地址。
没有所谓的“有效”或“无效”地址。每一个地址都可以跳转到,如果对应的页被映射,则执行。
那么当你在指令“之间”跳转时会发生什么?好吧,处理器不知道你打算从哪里开始和结束指令。它只是执行它看到的字节。此代码将与您预期的不同,因为 CPU 试图将其他一些指令的中间部分解析为操作码。
您的具体示例不够具体,我无法说明指令的结果。也许你可以提供一个完整的示例(包括机器代码),以便我给出更好的解释。
CPU会在目标地址开始解码指令。
您在反汇编中看到的指令流(当使用像 objdump
这样的工具时)只是对程序可执行字节的一种解释,假定给定的起点。
碰巧“跳入指令的中间”是一种混淆技术,有时恶意软件会使用它来对线性扫描反汇编程序(如 objdump
)隐藏程序语义。更复杂的反汇编程序将尝试遵循这些“未对齐”的跳转,但这可能是不可能的,具体取决于 can/can未确定的内容 statically/dynamically。
论文 "Obfuscation of executable code to improve resistance to static disassembly" by Linn and Debray 对此进行了更详细的讨论。
参见第 3.2 节“垃圾插入”。您描述的场景是他们所说的“部分或完全重叠指令”,即字节流的不同解释可以为重叠地址范围提供不同的汇编指令。
我最近在 codegolf 的“使用 x86/x64 机器代码打高尔夫球的技巧”中添加了一个关于 skipping instructions 的技巧。你会发现那些是跳入先前指令的一部分的有意应用。而且不仅仅是为了混淆。这是该答案的完整文本:
跳过指令
跳过指令是与一个或多个后续操作码组合的操作码片段。后续操作码可以与不同的入口点一起使用,而不是预先设置的跳过指令。使用跳过指令代替无条件短跳转可以节省代码space,速度更快,并设置附带状态如NC
(No Carry)。
我的示例都是针对 16 位 Real/Virtual 86 模式的,但是很多这些技术可以类似地用于 16 位保护模式,或者 32 位或 64 位模式。
11: Skipping instructions
The constants __TEST_IMM8, __TEST_IMM16, and __TEST_OFS16_IMM8 are defined to the respective byte strings for these instructions. They can be used to skip subsequent instructions that fit into the following 1, 2, or 3 bytes. However, note that they modify the flags register, including always setting NC. The 16-bit offset plus 16-bit immediate test instruction is not included for these purposes because it might access a word at offset 0FFFFh in a segment. Also, the __TEST_OFS16_IMM8 as provided should only be used in 86M, to avoid accessing data beyond a segment limit. After the db instruction using one of these constants, a parenthetical remark should list which instructions are skipped.
86 模式定义 in lmacros1.mac 323cc150061e (2021-08-29 21:45:54 +0200):
%define __TEST_IMM8 0A8h ; changes flags, NC
%define __TEST_IMM16 0A9h ; changes flags, NC
; Longer NOPs require two bytes, like a short jump does.
; However they execute faster than unconditional jumps.
; This one reads random data in the stack segment.
; (Search for better ones.)
%define __TEST_OFS16_IMM8 0F6h,86h ; changes flags, NC
16 位模式下的 0F6h,86h
操作码是 test byte [bp + disp16], imm8
指令。我相信我实际上并没有在任何地方使用这个。 (事实上,堆栈内存访问实际上可能比无条件短跳转慢。)
0A8h
是任何模式下 test al, imm8
的操作码。 0A9h
操作码在 32 位和 64 位模式下变为 test eax, imm32
形式的指令。
两个用例in ldosboot boot32.asm 07f4ba0ef8cd (2021-09-10 22:45:32 +0200):
首先,为一个公共函数链接两个不同的入口点,这两个入口点都需要初始化一个字节大小的寄存器。 mov al, X
指令每条占用 2 个字节,因此 __TEST_IMM16
可用于跳过一条这样的指令。 (如果有两个以上的入口点,可以重复此模式。)
error_fsiboot:
mov al,'I'
db __TEST_IMM16 ; (skip mov)
read_sector.err:
mov al, 'R' ; Disk 'R'ead error
error:
其次,某个入口点需要两个字节的额外拆解,但可以与后面代码部分的 fallthrough case 共享。
mov bx, [VAR(para_per_sector)]
sub word [VAR(paras_left)], bx
jbe @F ; read enough -->
loop @BB
pop bx
pop cx
call clust_next
jnc next_load_cluster
inc ax
inc ax
test al, 8 ; set in 0FFF_FFF8h--0FFF_FFFFh,
; clear in 0, 1, and 0FFF_FFF7h
jz fsiboot_error_badchain
db __TEST_IMM16
@@:
pop bx
pop cx
call check_enough
jmp near word [VAR(fsiboot_table.success)]
这是一个用例 in inicomp lz4.asm 4d568330924c (2021-09-03 16:59:42 +0200),我们依赖 test al, X
指令清除进位标志:
.success:
db __TEST_IMM8 ; (NC)
.error:
stc
retn
最后,这里是 DOSLFN Version 0.41c (11/2012) 中跳转指令的非常相似的用法。他们使用 mov cx, imm16
而不是 test ax, imm16
,它对状态标志没有影响,但会破坏 cx
寄存器。 (操作码 0B9h
在非 16 位模式下为 mov ecx, imm32
,并写入完整的 ecx
或 rcx
寄存器。)
;THROW-Geschichten... [english: THROW stories...]
SetErr18:
mov al,18
db 0B9h ;mov cx,nnnn
SetErr5:
mov al,5
db 0B9h ;mov cx,nnnn
SetErr3:
mov al,3
db 0B9h ;mov cx,nnnn
SetErr2:
mov al,2
SetError: