shellcoders 手册中不一致的汇编指令

Inconsistent assembly instructions in shellcoders handbook

两年后,我又回来了。试图再次处理 shellcoders 手册,但我继续发现不一致之处。本书提供以下功能:

int triangle (int width, in height){
   int array[5] = {0,1,2,3,4};
   int area;
   area = width * height/2;
   return (area);
}

以及函数的以下反汇编:

0x8048430 <triangle>: push %ebp
0x8048431 <triangle+1>: mov %esp, %ebp
0x8048433 <triangle+3>: push %edi
0x8048434 <triangle+4>: push %esi
0x8048435 <triangle+5>: sub [=11=]x30,%esp
0x8048438 <triangle+8>: lea 0xffffffd8(%ebp), %edi
0x804843b <triangle+11>: mov [=11=]x8049508,%esi
0x8048440 <triangle+16>: cld
0x8048441 <triangle+17>: mov [=11=]x30,%esp
0x8048446 <triangle+22>: repz movsl %ds:( %esi), %es:( %edi)
0x8048448 <triangle+24>: mov 0x8(%ebp),%eax
0x804844b <triangle+27>: mov %eax,%edx
0x804844d <triangle+29>: imul 0xc(%ebp),%edx
0x8048451 <triangle+33>: mov %edx,%eax
0x8048453 <triangle+35>: sar [=11=]x1f,%eax
0x8048456 <triangle+38>: shr [=11=]x1f,%eax
0x8048459 <triangle+41>: lea (%eax, %edx, 1), %eax
0x804845c <triangle+44>: sar %eax
0x804845e <triangle+46>: mov %eax,0xffffffd4(%ebp)
0x8048461 <triangle+49>: mov 0xffffffd4(%ebp),%eax
0x8048464 <triangle+52>: mov %eax,%eax
0x8048466 <triangle+54>: add [=11=]x30,%esp
0x8048469 <triangle+57>: pop %esi
0x804846a <triangle+58>: pop %edi
0x804846b <triangle+59> pop %ebp
0x804846c <triangle+60>: ret

出于学术原因,我试图分解并解释程序集的每一行。但是有几处感觉不对,例如:lea 0xffffffd8(%ebp), %edi,我的理解是第一部分意味着将基指针乘以0xffffffd8,这似乎不正确。另一个例子是mov [=13=]x30, $esp,你为什么要把一个文字值移动到堆栈指针寄存器中。如果是mov [=14=]x30, (%ebp)我能理解,但好像不是这样。是我错了,还是这一切看起来都错了?

But several things just feel wrong

是的,他们是。一本书出现印刷错误并不罕见。通常,当您看到让您摸不着头脑的东西时,您应该查找已发布的勘误列表。出版商的网站和作者的网站一样,是一个不错的查看地点。我不知道这本书的确切书名是什么,所以我不能自己搜索,但你应该可以很容易地找到它。

当然,可能没有那么简单。信誉较差的出版商的书籍通常不会提供勘误表,而不太受欢迎的书籍通常没有足够的读者来发现错误。您可以通过找到作者的电子邮件地址并通知他们您发现的错误来尽自己的一份力量。或者,如果您不确定它们是否有误,作者澄清。 (您不想指望作者为您提供个人教程,但关于他们书中发表的内容的具体问题总是公平的游戏。)

lea 0xffffffd8(%ebp), %edi, it is my understanding that the first part means multiply the base pointer by 0xffffffd8, that seems incorrect

在这种情况下,您对代码功能的理解不正确。我责怪这种疯狂的 AT&T 语法。翻译成英特尔语法是:

lea edi, DWORD [ebp + ffffffd8h]

相当于:

lea edi, DWORD [ebp - 28h]

所以这实际上等同于:

mov  edi, ebp
sub  edi, 28h

现在,您是对的,LEA 指令 可以 进行乘法运算。好吧,有点。它可以按某些常数缩放,例如 2、4 和 8,这与乘法具有相同的效果。但是这种形式并没有对乘法进行编码(或者,更准确地说,它是乘以 1 的比例)。

mov [=20=]x30, $esp, why would you move a literal value into the stack pointer register. I could understand if it was mov [=21=]x30, (%ebp), but that does not seem to be the case.

是的,将文字移动到堆栈指针中是一件非常奇怪的事情。永远不要说永远,但这应该尖叫 "bug"(或 "typo")。

但是请看下一条指令:

repz movsl  %ds:(%esi), %es:(%edi)

repeat string operation prefixes 导致字符串指令(在本例中为 MOVSL)重复 ECX 寄存器中指定的次数,因此前一条指令是 可能 应该初始化 ECX。将 ECX 初始化为 30h 是有意义的,因为这是先前在堆栈 (subl [=26=]x30, %esp) 上分配的 space 的数量。

但是这里还有另一个错误:REPZ(或者等效的 REPE)前缀对于 MOVS 指令没有意义! [N]Z/[N]E 通常表示零标志用作次要终止条件,但移动不会设置任何标志,因此写 REPZ MOVS 没有意义!那应该只是 REP MOVS.


坦率地说,就我而言,整个反汇编过程都是可疑的。我开始怀疑这本书是否值得它所写的纸。为什么要显示 未优化的 汇编代码?如果您正在尝试学习汇编语言,那么您不想学习如何编写次优代码。如果你正在尝试学习逆向工程,那么研究未优化的代码是没有意义的,因为那不是编译器会生成的。漏洞利用也是如此。我想不出一个很好的理由来解释为什么你会想要浪费时间查看未优化的代码。只是一堆杂音,不会教给您任何有用的东西。

例如,您在一开始就看到了未优化代码的迹象:未省略基指针的初始化 (EBP)。

REPZ MOVS 指令(以及相关的必需指令)的用途对我来说也是一个完全的谜。我什至看不出为什么编译器会在禁用优化的情况下生成这些。

我猜作者 不得不 关闭优化,否则整个 "array" allocation/initialization 将被省略。不是最好的例子。

这个序列也错误:

sar [=14=]x1f, %eax
shr [=14=]x1f, %eax

无符号右移 31 位是有意义的(将符号位隔离为优化的有符号除以 2 的一部分),但在有符号右移之后立即进行则没有意义。 (作为此优化除法的一部分的预期 sar %eax 稍后出现,在典型的 GAS 格式中省略了立即数 $1。)

如果所有(甚至大部分)代码都是这样,我的建议是要么放弃那本书,另找一本,要么自己编译和反汇编 C 函数。

未损坏的 C 编译器将为该 C 函数生成以下代码:

    ; Load second parameter from stack into EAX.
    movl    8(%esp), %eax

    ; Multiply that second parameter by the first parameter.
    ; (Could just as well have used a second movl, and then done a reg-reg imull.)
    imull   4(%esp), %eax

    ; Make a copy of that result in EDX.
    movl    %eax, %edx

    ; Optimized signed divide-by-2:
    shrl    , %eax
    addl    %edx, %eax
    sarl    , %eax    ; GAS encodes this as 'sarl %eax', making the  implicit

    ret

或者,如果优化被禁用(这在不同的编译器之间有更多的差异,这是查看未优化的代码很愚蠢的另一个原因,但你可以得到基本的想法):

    ; Set up a stack frame
    pushl   %ebp
    movl    %esp, %ebp

    ; Allocate space on the stack for the pointless "array" array,
    ; and store the values in that space.
    ; (Why 32 bytes instead of only 30? To keep the stack pointer aligned.)
    subl    , %esp
    movl    [=16=], -24(%ebp)
    movl    , -20(%ebp)
    movl    , -16(%ebp)
    movl    , -12(%ebp)
    movl    , -8(%ebp)

    ; Get first parameter from the stack.
    movl    8(%ebp), %eax

    ; Multiply it by the second parameter.
    imull   12(%ebp), %eax

    ; Make a copy of the result.
    movl    %eax, %edx

    ; Optimized signed divide-by-2 (some compilers will always apply this
    ; strength-reduction, even when optimizations are disabled; others won't
    ; and will go ahead and emit the IDIV instruction you might expect):
    shrl    , %edx
    addl    %edx, %eax
    sarl    , %eax

    ; Store the result in a temporary location in memory.
    movl    %eax, -4(%ebp)

    ; Then load it back into EAX so it can be returned.
    movl    -4(%ebp), %eax

    ; Tear down the stack frame and deallocate stack space.
    leave

    ret