你的代码反汇编成这样的:
00000000 31C0 xor eax,eax
00000002 50 push eax
00000003 682F2F7368 push dword 0x68732f2f
00000008 682F62696E push dword 0x6e69622f
0000000D 89E3 mov ebx,esp
0000000F 50 push eax
00000010 53 push ebx
00000011 89E1 mov ecx,esp
00000013 99 cdq
00000014 B00B mov al,0xb
00000016 CD80 int 0x80
礼貌ndisasm
。让我们一步一步地看一遍这些指令,并在途中分析堆栈帧。
xor eax,eax
将eax
寄存器清零,因为操作数与自身的 XOR 操作将始终产生零作为结果。push eax
然后将值压入堆栈。因此,堆栈当前或多或少看起来像这样(相对于esp
代码开头的值显示的偏移量,esp
表示esp
当前指向的堆栈单元):
+----------+
0 | 00000000 |
esp -4 | xxxxxxxx |
+----------+
接下来,我们有两条push dword
指令,它们将一些立即值压入堆栈,在执行它们之后 - 看起来像这样:
+----------+
0 | 00000000 |
-4 | 68732f2f |
-8 | 6e69622f |
esp -12| xxxxxxxx |
+----------+
esp
当前指向被压入堆栈的第二个立即数的最后一个字节。让我们尝试将推送的值解释为 ASCII,如果我们从esp
. 我们得到 的字节序列2f62696e2f2f7368
,在 ASCII 中等于/bin//sh
。另外,该序列以 0 结尾,因此它是一个有效的 C 字符串。
esp
这是将当前值保存到寄存器中的主要原因ebx
。它包含将要运行的可执行文件的路径。双斜杠对操作系统来说不是问题,因为 POSIX 只是忽略了多次出现的斜杠并将它们视为一个斜杠。
接下来,我们将 的当前值eax
推ebx
入堆栈。我们知道eax
包含零,并且ebx
包含指向 C-string 的指针"/bin//sh"
。堆栈当前看起来像这样:
+----------+
0 | 00000000 |
-4 | 68732f2f |
-8 | 6e69622f |
ebx -12| 00000000 |
-16| (ebxVal) |
ecx esp -20| xxxxxxxx |
+----------+
将寄存器的值压入堆栈后,当前指向的指针esp
保存在ecx
.
cdq
是在这种情况下执行一个非常巧妙的技巧的指令:它将当前值符号扩展eax
到edx:eax
寄存器对中。因此,在这种情况下,它会将 中的值edx
清零,因为零的符号扩展为零。当然,我们可以清除edx
with中的值xor edx, edx
,但是该指令用两个字节编码 - 并且cdq
只占用一个字节。
下一条指令将值0xb
(11) 放入 的低字节寄存器eax
。与前一种情况类似,我们可以这样做mov eax, 0xb
,但这将导致 5 字节指令,因为立即数必须编码为完整的 32 位值。
int 0x80
在 Linux 上调用系统调用调用程序。它需要系统调用的数量eax
(现在等于0xb
,因此将调用该函数),以及, , , ,和sys_execve
中的附加参数。ebx
ecx
edx
esi
edi
ebp
现在,让我们看一下该系统调用的原型:
int execve(const char *filename, char *const argv[], char *const envp[]);
因此,filename
参数放在ebx
- 它指向/bin//sh
。argv
,放置在 中ecx
,是要执行的可执行文件的参数数组,必须以一个NULL
值终止。在 Intel 架构上,NULL
等于0
,并且ecx
仅指向 :一个指向 的指针/bin//sh
,然后是一个NULL
值。envp
,即NULL
指向一个环境值数组,它必须表示为char*
形式的值key=value
。
成功执行execve
导致当前进程映像替换为指向的可执行文件的映像,并使用提供的参数执行。在这种情况下,/bin/sh
将使用 的参数执行(如果存在)/bin//sh
。
Michael 对于为什么这不起作用可能是正确的:最近的 Linux 内核将数据页标记为不可执行,并且尝试执行它们将导致分段错误。