为什么在shellcode中使用Push/Pop而不是Mov将数字放入寄存器?

7

我有一些来自shell代码有效载荷的示例代码,其中展示了一个for循环,使用push/pop设置计数器:

push 9
pop ecx

为什么不能只使用mov指令?
mov ecx, 9

2
你有没有在漏洞或者shellcode中看到过这个技巧?我问这个问题是因为这种技巧的优点是不会在编码中添加NUL(0)字节。mov ecx, 9 在它的编码中确实有零。我可以想到几个原因:a)程序员刚开始学汇编语言,写的代码不好;b)这种编码比mov更短;c)在push和pop之间有一个标签,并且pop在循环的顶部;d)有人试图将循环的顶部对齐到16字节边界上;e)有人编写代码以避免编码中的NUL字节(shell exploits)。 - Michael Petch
(f)这是编译器生成的代码,可能是代码生成和有限/无优化(错过的优化)的结果。 - Michael Petch
1
这也可能是需要避免NUL字节的shell代码。 - fuz
1
谢谢,这是来自恶意代码样本的。 - Hawke
1
没问题。我有预感可能是一个漏洞;-)。这就是为什么我在我的第一条评论中感到有必要问的原因。这是实际上最有意义的地方。我会更新标签和问题。 - Michael Petch
3个回答

10
通常出于性能原因,应该总是使用mov ecx, 9。它比push/pop更高效,因为它是一个单uop指令,可以在任何端口上运行。(这对Agner Fog测试的所有现有CPU都有效: https://agner.org/optimize/)
push imm8/pop r32的正常原因是机器码中没有零字节。这对于必须通过strcpy或任何将其视为隐式长度C字符串的一部分并以0字节终止的方法溢出缓冲区的shellcode非常重要。 mov ecx,immediate仅适用于32位立即数,因此机器码看起来像B9 09 00 00 00。与6a 09 push 9; 59 pop ecx相比。
(ECX是寄存器号码1,这就是B959的来源:指令的低3位=001)


另一个用例纯粹是代码大小: mov r32,imm32是5个字节(使用无ModRM编码,在操作码的低3位中放置寄存器号码),因为x86不幸地缺乏用于mov的符号扩展imm8操作码(没有mov r/m32,imm8)。几乎所有的ALU指令都可以追溯到8086年。
在16位8086中,该编码不会节省任何空间:mov r16,imm16的3字节短格式与假设的mov r/m16,imm8一样好,几乎适用于所有情况,除了将立即数移动到需要ModRM字节的内存中的情况下使用mov r/m16,imm16形式。

由于386的32位模式没有增加特定于该模式的新操作码,只改变了默认的操作数大小和立即数宽度,因此32位模式中ISA中的这个“缺失优化”始于386。由于全宽立即数比原来多2个字节,add r32,imm32现在比add r/m32,imm8更长。详见x86 assembly 16 bit vs 8 bit immediate operand encoding。但是我们不能为mov选择这个选项,因为没有MOV操作码可以符号扩展(或零扩展)其立即数。

有趣的事实: clang -Oz(即以牺牲速度为代价优化大小)会编译 int foo(){return 9;} 成为 push 9 ; pop rax。 GCC12也支持类似的-Oz

另请参阅Codegolf.SE上的x86/x64机器码高尔夫技巧(这是一个关于通常为了乐趣而优化大小,而不是将代码适应小ROM或引导扇区的网站。但对于机器码,有时优化大小确实有实际应用,即使牺牲性能。)

如果您已经有另一个含有已知内容的寄存器,则可以使用3字节的lea ecx, [eax-0 + 9](如果EAX包含0)在另一个寄存器中创建9。只需Opcode + ModRM + disp8。因此,如果您已经打算对其他寄存器进行xor-zero,则可以避免push/pop hack。lea几乎与mov一样有效,当优化速度时,您可以考虑它,因为在大规模上,较小的代码大小具有轻微的速度优劣:L1i缓存命中,有时解码(如果uop缓存尚未热)。


2

这可能有不同的原因。

在这种情况下,这似乎是因为代码更小:

使用pushpop组合的变体长度为3字节,而mov指令长度为5字节。

然而,我猜测mov变体更快...

最初的回答:

可能有不同的原因。

在这种情况下,这样做似乎是因为代码更小:

使用pushpop组合的变体长度为3字节,而mov指令长度为5字节。

然而,我猜测mov变体更快...


这是可信的。 - Al Kepp

-2
基本上是完全一样的事情。将9推入堆栈,然后将其弹出到ecx寄存器,这与mov ecx, 9基本相同。个人认为将9传递给ecx可能比将9推入堆栈然后弹出到ecx更高效,但我认为处理时间不是问题,因此无论哪种方式,它们都同样快速考虑到代码的规模非常小。

这不是答案,您可以在问题下发表评论。 - Hien Nguyen
它们的速度不同。在性能不重要的情况下,并不意味着所有事情都一样快!而你是如何决定性能不重要的呢? - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接