本文翻译自:Hack the virtual memory: the stack, registers and assembly code
Hack the virtual memory: the stack, registers and assembly code
栈
正如我们在第2章中看到的,栈位于内存的高端并向下增长。但它如何正常工作?它如何转换为汇编代码?使用的寄存器是什么?在本章中,我们将详细介绍栈的工作原理,以及程序如何自动分配和释放局部变量。
一旦我们理解了这一点,我们就可以尝试劫持程序的执行流程。
*注意:我们将仅讨论用户栈,而不是内核栈
前提
为了完全理解本文,你需要知道:
- C语言的基础知识(特别是指针部分)
环境
所有脚本和程序都已经在以下系统上进行过测试:
- Ubuntu
- Linux ubuntu 4.4.0-31-generic #50~14.04.1-Ubuntu SMP Wed Jul 13 01:07:32 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
- 使用的工具:
- gcc
- gcc (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4
- objdump
- GNU objdump (GNU Binutils for Ubuntu) 2.2
- gcc
下文讨论的所有内容都适用于此系统/环境,但在其他系统上可能会有所不同
自动分配
首先让我们看一个非常简单的程序,只有一个函数,并在函数中使用了单个变量(0-main.c):
#include <stdio.h>
int main(void)
{
int a;
a = 972;
printf("a = %d\n", a);
return (0);
}
让我们编译这个程序,并使用objdump反汇编它:
holberton$ gcc 0-main.c
holberton$ objdump -d -j .text -M intel
main函数反汇编生成的代码如下:
000000000040052d <main>:
40052d: 55 push rbp
40052e: 48 89 e5 mov rbp,rsp
400531: 48 83 ec 10 sub rsp,0x10
400535: c7 45 fc cc 03 00 00 mov DWORD PTR [rbp-0x4],0x3cc
40053c: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
40053f: 89 c6 mov esi,eax
400541: bf e4 05 40 00 mov edi,0x4005e4
400546: b8 00 00 00 00 mov eax,0x0
40054b: e8 c0 fe ff ff call 400410 <printf@plt>
400550: b8 00 00 00 00 mov eax,0x0
400555: c9 leave
400556: c3 ret
400557: 66 0f 1f 84 00 00 00 nop WORD PTR [rax+rax*1+0x0]
40055e: 00 00
现在先关注前三行:
000000000040052d <main>:
40052d: 55 push rbp
40052e: 48 89 e5 mov rbp,rsp
400531: 48 83 ec 10 sub rsp,0x10
main函数的第一行引用了rbp和rsp;这些是有特殊用途的寄存器。rbp是基址指针寄存器,指向当前栈帧的底部,rsp是栈指针寄存器,指向当前栈帧的顶部。
让我们逐步分析这句汇编语句。这是main函数第一条指令运行之前栈的状态:
push rbp指令将寄存器rbp的值压入栈中。因为它“压入”到栈上,所以现在rsp的值是栈新的顶部的内存地址。栈和寄存器看起来像这样:
mov rbp,rsp将rsp的值复制到rbp中 -> rpb和rsp现在都指向栈的顶部
sub rsp,0x10预留一些空间(rbp和rsp之间的空间)来存储局部变量。请注意,此空间足以存储我们的整型变量
我们刚刚在内存中为局部变量创建了一个空间 —— 在栈中。这个空间被称为栈帧。每个具有局部变量的函数都将使用栈帧来存储自己的局部变量。
使用局部变量
main函数的第四行汇编代码如下:
400535: c7 45 fc cc 03 00 00 mov DWORD PTR [rbp-0x4],0x3cc
0x3cc的十进制值为972。这一行对应于我们的C代码:
a = 972;
mov DWORD PTR [rbp-0x4],0x3cc 将rbp-4地址处的内存赋值为972。[rbp - 4]是我们的局部变量a。计算机实际上并不知道我们在代码中使用的变量名称,它只是引用栈上的内存地址。
执行此指令后的栈和寄存器状态:
函数返回,自动释放
如果查看一下函数的末尾代码,我们会发现:
400555: c9 leave
此指令将rsp设置为rbp,然后将栈顶弹出赋值给rbp。
因为我们在函数的开头处将rbp的先前值压入栈,所以rbp现在设置为rbp的先前值。这就是:
- 局部变量被“释放”
- 在离开当前函数之前,先恢复上一个函数的栈帧。
栈和寄存器rbp和rsp恢复到刚进入main函数时的状态。
Playing with the stack
当变量从栈中自动释放时,它们不会被完全“销毁”。它们的值仍然存在内存中,并且这个空间可能会被其他函数使用。
所以在编写代码时要初始化变量,否则,它们将在程序运行时获取栈中的任何值。
让我们考虑以下C代码(1-main.c):
#include <stdio.h>
void func1(void)
{
int a;
int b;
int c;
a = 98;
b = 972;
c = a + b;
printf("a = %d, b = %d, c = %d\n", a, b, c);
}
void func2(void)
{
int a;
int b;
int c;
printf("a = %d, b = %d, c = %d\n", a, b, c);
}
int main(void)
{
func1();
func2();
return (0);
}
正如所见,func2函数并没有初始化局部变量a,b和c,如果我们编译并运行此程序,它将打印...
holberton$ gcc 1-main.c && ./a.out
a = 98, b = 972, c = 1070
a = 98, b = 972, c = 1070
holberton$
...与func1函数输出的变量值相同!这是因为栈的工作原理。这两个函数以相同的顺序声明了相同数量的变量(具有相同的类型)。他们的栈帧完全相同。当func1结束时,其局部变量所在的内存不会被清除 —— 只有rsp递增。
因此,当我们调用func2时,它的栈帧位于与前一个func1栈帧完全相同的位置,当我们离开func1时,func2的局部变量具有与func1的局部变量相同的值。
让我们检查反汇编代码来证明上述:
```